<?php
/**
* SeekQuarry/Yioop --
* Open Source Pure PHP Search Engine, Crawler, and Indexer
*
* Copyright (C) 2009 - 2026 Chris Pollett chris@pollett.org
*
* LICENSE:
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* END LICENSE
*
* @author Chris Pollett chris@pollett.org
* @license https://www.gnu.org/licenses/ GPL3
* @link https://www.seekquarry.com/
* @copyright 2009 - 2026
* @filesource
*/
namespace seekquarry\yioop\tests;
use seekquarry\yioop\library\av_processing\AacBands;
use seekquarry\yioop\library\av_processing\AacEncoder;
use seekquarry\yioop\library\av_processing\AacFrame;
use seekquarry\yioop\library\av_processing\AacQuantizer;
use seekquarry\yioop\library\av_processing\AacTables;
use seekquarry\yioop\library\av_processing\BitWriter;
use seekquarry\yioop\library\av_processing\Mdct;
use seekquarry\yioop\library\UnitTest;
/**
* AacEncoderTest checks the pieces that compress sound into a
* frame of Advanced Audio Coding, which is what AAC stands for.
*
* The one thing these cases cannot do is play the result, so they
* check what can be checked without a player: that rounding a tone and
* undoing it gives the tone back, that a frame's opening reads back as
* what was written into it, that the size loop lands near the size it
* was asked for, and that the code tables are indexed the way the
* writer assumes. Whether a real decoder accepts the result is checked
* by hand against ffmpeg and noted in the README, since it needs a
* program these cases cannot call.
*
* @author Chris Pollett
*/
class AacEncoderTest extends UnitTest
{
/**
* Samples a second the encoder works at
*/
const SAMPLE_RATE = 48000;
/**
* How many samples a frame advances by
*/
const HOP = 1024;
/**
* A starting point for the made up numbers
*/
const SEED = 20260803;
/**
* setUp sets the made up numbers off from the same place every
* run
*/
public function setUp()
{
mt_srand(self::SEED);
}
/**
* tearDown has nothing to clear away after these cases
*/
public function tearDown()
{
}
/**
* takeBits reads a value of a given width back out of a run of
* bits
*
* @param string $bits the run, as ones and zeroes
* @param int $at where in it to read, moved past what was read
* @param int $width how many bits to read
* @return int the value read
*/
public function takeBits($bits, &$at, $width)
{
$value = bindec(substr($bits, $at, $width));
$at += $width;
return $value;
}
/**
* asBits turns a run of bytes into ones and zeroes
*
* @param string $bytes the bytes
* @return string the bits
*/
public function asBits($bytes)
{
$bits = "";
for ($i = 0; $i < strlen($bytes); $i++) {
$bits .= sprintf("%08b", ord($bytes[$i]));
}
return $bits;
}
/**
* bitsComeBackInOrderTestCase checks that values of any width
* come back out of a run of bits in the
* order they went in
*/
public function bitsComeBackInOrderTestCase()
{
$wanted = [[5, 3], [0x2A, 7], [1, 1], [0, 4], [255, 8], [3, 2]];
$writer = new BitWriter();
$total = 0;
foreach ($wanted as $one) {
$writer->add($one[0], $one[1]);
$total += $one[1];
}
$this->assertEqual($writer->length(), $total,
"the run is as long as what was put in it");
$bits = $this->asBits($writer->finish());
$at = 0;
$wrong = 0;
foreach ($wanted as $one) {
if ($this->takeBits($bits, $at, $one[1]) != $one[0]) {
$wrong++;
}
}
$this->assertEqual($wrong, 0, "every value came back as it went in");
$this->assertTrue(strlen($bits) >= $total,
"the last byte is padded rather than cut short");
}
/**
* smallestStepFollowsTheBandTestCase checks that the smallest
* step a band may use keeps its loudest tone
* within what can be written, and should follow the loudness of
* the band rather than sitting at a fixed place
*/
public function smallestStepFollowsTheBandTestCase()
{
$steps = [];
foreach ([1000000.0, 1000.0, 1.0, 0.001] as $size) {
$tones = [$size, -$size / 2];
$step = AacQuantizer::smallestStepFor($tones, 0, 2);
$steps[] = $step;
$rounded = AacQuantizer::roundBand($tones, 0, 2, $step);
$this->assertTrue(abs($rounded[0]) <=
AacQuantizer::LARGEST_PLAIN,
"the loudest tone of a band fits what may be written");
$this->assertTrue(abs($rounded[0]) >
AacQuantizer::LARGEST_PLAIN / 4,
"and it uses most of the room it is given");
}
$falling = true;
for ($i = 1; $i < count($steps); $i++) {
if ($steps[$i] >= $steps[$i - 1]) {
$falling = false;
}
}
$this->assertTrue($falling,
"a quieter band is given a smaller step than a louder one");
}
/**
* frameOpeningReadsBackTestCase checks that the opening of a
* frame reads back as what was written into
* it, in the order the standard sets out
*/
public function frameOpeningReadsBackTestCase()
{
$tones = [];
for ($i = 0; $i < self::HOP; $i++) {
$tones[] = mt_rand(-1000, 1000) / 10.0;
}
$made = AacFrame::write($tones, 2000);
$bits = $this->asBits($made["bytes"]);
$at = 0;
$this->assertEqual($this->takeBits($bits, $at, 3),
AacFrame::ONE_CHANNEL, "the frame names one run of sound");
$this->assertEqual($this->takeBits($bits, $at, 4), 0,
"which run it is");
$base = $this->takeBits($bits, $at, 8);
$this->assertTrue($base >= 0 && $base <= AacFrame::BASE_LIMIT,
"the loudness it counts from is one that may be written");
$this->assertEqual($this->takeBits($bits, $at, 1), 0,
"the bit that must be nothing is nothing");
$this->assertEqual($this->takeBits($bits, $at, 2),
AacFrame::LONG_SHAPE, "the stretch is one long stretch");
$this->takeBits($bits, $at, 1);
$this->assertEqual($this->takeBits($bits, $at, 6),
AacBands::longBandCount(), "every band may carry sound");
$this->assertEqual($this->takeBits($bits, $at, 1), 0,
"nothing is predicted from the stretch before");
}
/**
* silenceMakesATinyFrameTestCase checks that a frame of silence
* is tiny and is still a whole
* frame, ending where it says it does
*/
public function silenceMakesATinyFrameTestCase()
{
$made = AacFrame::write(array_fill(0, self::HOP, 0.0), 2000);
$this->assertTrue(strlen($made["bytes"]) < 12,
"a silent frame is a handful of bytes");
$bits = $this->asBits($made["bytes"]);
$at = $made["bits"] - 3;
$this->assertEqual($this->takeBits($bits, $at, 3),
AacFrame::END_OF_FRAME, "it ends where it says it does");
}
/**
* sizeLoopFillsWhatItIsGivenTestCase checks that the size loop
* comes close to the size it was asked for on
* sound dense enough to fill it, and should never run past it
*/
public function sizeLoopFillsWhatItIsGivenTestCase()
{
$sound = [];
for ($i = 0; $i < 2 * self::HOP; $i++) {
$sound[] = mt_rand(-1000, 1000) / 2500.0;
}
$fade = AacBands::fadeFor(AacBands::LONG);
for ($i = 0; $i < 2 * self::HOP; $i++) {
$sound[$i] *= $fade[$i];
}
$tones = Mdct::forSize(self::HOP)->forward($sound);
$over = 0;
$sizes = [];
foreach ([768, 1365, 2048] as $room) {
$made = AacFrame::write($tones, $room);
if (strlen($made["bytes"]) * 8 > $room) {
$over++;
}
$sizes[] = strlen($made["bytes"]) * 8;
}
$this->assertEqual($over, 0, "no frame runs past the room allowed");
$shrank = 0;
for ($i = 1; $i < count($sizes); $i++) {
if ($sizes[$i] < $sizes[$i - 1]) {
$shrank++;
}
}
$this->assertEqual($shrank, 0,
"more room allowed never gives a smaller frame");
$this->assertTrue($sizes[1] > $sizes[0],
"and more room is actually taken up while there is use for it");
}
/**
* everyStepCanBeNamedTestCase checks that every step a band takes
* from the one before it is one the
* table of steps can name
*/
public function everyStepCanBeNamedTestCase()
{
$tones = [];
for ($i = 0; $i < self::HOP; $i++) {
/* Loud at the bottom and almost silent at the top, which is
what pulls the steps of neighboring bands apart. */
$tones[] = ($i < 40 ? 100000.0 : 0.0001) *
(mt_rand(0, 1) ? 1 : -1);
}
$bands = AacBands::longBandCount();
$floor = [];
for ($band = 0; $band < $bands; $band++) {
$floor[$band] = AacQuantizer::smallestStepFor($tones,
AacBands::LONG_EDGES[$band], AacBands::LONG_EDGES[$band + 1]);
}
$carries = array_fill(0, $bands, true);
$steps = AacFrame::clampSteps($floor, $carries);
$wrong = 0;
$lowered = 0;
for ($band = 0; $band < $bands; $band++) {
if ($steps[$band] < $floor[$band]) {
$lowered++;
}
if ($band > 0 && abs($steps[$band] - $steps[$band - 1]) >
AacTables::LOUDNESS_SPAN) {
$wrong++;
}
}
$this->assertEqual($wrong, 0,
"every step is within reach of the one before it");
$this->assertEqual($lowered, 0,
"no band was made finer than its own floor");
}
/**
* runLengthsDoNotMatterTestCase checks that the encoder takes
* samples a run at a time and makes the
* same frames whatever lengths those runs come in. This case runs
* for about a hundredth of a second, since it compresses the same
* sound several times over.
*/
public function runLengthsDoNotMatterTestCase()
{
$wave = [];
for ($i = 0; $i < 2 * self::HOP; $i++) {
$wave[] = 0.3 * sin(2 * M_PI * 440 * $i / self::SAMPLE_RATE);
}
$whole = new AacEncoder();
$whole->take($wave);
$whole->finish();
$piecemeal = new AacEncoder();
/* Runs of an awkward length, so that no run lines up with the
length a frame advances by. */
foreach (array_chunk($wave, 683) as $run) {
$piecemeal->take($run);
}
$piecemeal->finish();
$this->assertEqual(count($whole->frames), count($piecemeal->frames),
"the same number of frames either way");
$this->assertEqual($whole->frames, $piecemeal->frames,
"and the very same frames");
$this->assertTrue(count($whole->frames) >= 2,
"a run of samples makes a frame for each hop it covers");
}
/**
* tablesAreIndexedAsAssumedTestCase checks that the code
* tables are indexed the way the frame writer
* assumes, since it looks a pair up by working out where it sits
*/
public function tablesAreIndexedAsAssumedTestCase()
{
$this->assertEqual(count(AacTables::SOUND_CODES),
AacTables::PAIR_RANGE * AacTables::PAIR_RANGE,
"the sound table holds every pair it should");
$this->assertEqual(count(AacTables::SOUND_CODES),
count(AacTables::SOUND_CODE_BITS),
"every code has a length");
$this->assertEqual(0 * AacTables::PAIR_RANGE + 0, 0,
"the pair of nothing and nothing sits first");
$this->assertEqual(AacTables::ESCAPE_AT * AacTables::PAIR_RANGE +
AacTables::ESCAPE_AT, count(AacTables::SOUND_CODES) - 1,
"the pair of the largest and the largest sits last");
$wrong = 0;
foreach (AacTables::SOUND_CODES as $where => $code) {
if ($code >= (1 << AacTables::SOUND_CODE_BITS[$where])) {
$wrong++;
}
}
foreach (AacTables::LOUDNESS_CODES as $where => $code) {
if ($code >= (1 << AacTables::LOUDNESS_CODE_BITS[$where])) {
$wrong++;
}
}
$this->assertEqual($wrong, 0,
"every code fits the length it claims");
$shortest = min(AacTables::LOUDNESS_CODE_BITS);
$this->assertEqual(array_search($shortest,
AacTables::LOUDNESS_CODE_BITS), AacTables::LOUDNESS_SPAN,
"a step of nothing is the shortest to write");
}
}