<?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\CeltBands;
use seekquarry\yioop\library\av_processing\CeltEnergy;
use seekquarry\yioop\library\av_processing\RangeDecoder;
use seekquarry\yioop\configs as C;
use seekquarry\yioop\library\UnitTest;
/**
* Checks how Opus divides sound into bands and how each band's
* loudness is read back.
*
* A loudness is never stored as it is. What is stored is how far it
* fell from what the decoder already expected, and the expectation is
* built from the same band in the stretch before and from how wrong
* the bands before it in this stretch turned out to be. So a fault
* here does not spoil one band; it spoils that band and then leans on
* every band after it and every stretch after that. The cases below
* therefore check the leaning as well as the reading: that a run of
* differences of nothing leaves everything where it was, that one
* difference moves the bands after it by the right amount, and that a
* stretch standing on its own ignores what came before.
*
* @author Chris Pollett
*/
class CeltEnergyTest extends UnitTest
{
/**
* How much room to give a written piece in these cases
*/
const ROOM = 2000;
/**
* How close worked out loudnesses have to come to each other
*/
const CLOSE_ENOUGH = 1e-9;
/**
* The longest stretch, which nearly every recording uses
*/
const USUAL_DOUBLINGS = 3;
/**
* A starting point for the made up numbers
*/
const SEED = 20260803;
/**
* Brings in the writers these cases check against
*/
public function setUp()
{
if (!class_exists("seekquarry\\yioop\\tests\\RangeEncoder")) {
require_once C\PARENT_DIR . "/tests/test_files/RangeEncoder.php";
}
if (!class_exists("seekquarry\\yioop\\tests\\LaplaceWriter")) {
require_once C\PARENT_DIR . "/tests/test_files/LaplaceWriter.php";
}
mt_srand(self::SEED);
}
/**
* Nothing needs clearing away after these cases
*/
public function tearDown()
{
}
/**
* Writes a run of differences, one per band, and reads the
* loudnesses back
*
* @param array $steps the difference to store for each band
* @param array $before what each band was in the stretch before
* @param bool $alone whether the stretch stands on its own
* @return array the loudnesses read back
*/
public function storeAndRead($steps, $before, $alone)
{
$shapes = CeltBands::shapesFor(self::USUAL_DOUBLINGS, $alone);
$writer = new RangeEncoder(self::ROOM);
$channels = count($before);
foreach ($steps as $band => $step) {
for ($channel = 0; $channel < $channels; $channel++) {
LaplaceWriter::writeTo($writer, $step, $shapes[$band][0],
$shapes[$band][1]);
}
}
$reader = new RangeDecoder($writer->finish());
return CeltEnergy::readRough($reader, $before, $alone,
self::USUAL_DOUBLINGS, 0, count($steps));
}
/**
* The bands should get wider as they go up, cover the whole of the
* sound, and widen by doubling as the stretch gets longer
*/
public function bandLayoutTestCase()
{
$edges = CeltBands::edgesFor(0);
$this->assertEqual(count($edges), CeltBands::BAND_COUNT + 1,
"there is one more edge than there are bands");
$rising = true;
$widening = true;
$narrowest = CeltBands::widthOf(0, 0);
for ($band = 0; $band < CeltBands::BAND_COUNT; $band++) {
if ($edges[$band + 1] <= $edges[$band]) {
$rising = false;
}
if (CeltBands::widthOf($band, 0) < $narrowest) {
$widening = false;
}
}
$this->assertTrue($rising, "every band begins after the last ended");
$this->assertTrue($widening,
"no band is narrower than the lowest one");
$this->assertTrue(CeltBands::widthOf(20, 0) >
CeltBands::widthOf(0, 0) * 8,
"the top band is far wider than the bottom one");
$wide = CeltBands::edgesFor(3);
$this->assertEqual($wide[21], $edges[21] * 8,
"the longest stretch spreads the bands eight times as wide");
$this->assertEqual(CeltBands::widthOf(5, 3),
CeltBands::widthOf(5, 0) * 8, "each band widens to match");
}
/**
* A stretch of length Opus does not have should be refused
*/
public function impossibleStretchIsRefusedTestCase()
{
$refused = 0;
foreach ([-1, 4, 9] as $doublings) {
try {
CeltBands::edgesFor($doublings);
} catch (\Exception $problem) {
$refused++;
}
}
$this->assertEqual($refused, 3, "every impossible length refused");
}
/**
* There should be a pair of likelihoods for every band at every
* length of stretch, and each should sit inside what the reader
* can work with
*/
public function likelihoodsAreWholeTestCase()
{
$wrong = 0;
for ($doublings = 0; $doublings <= 3; $doublings++) {
foreach ([false, true] as $alone) {
$shapes = CeltBands::shapesFor($doublings, $alone);
if (count($shapes) != CeltBands::BAND_COUNT) {
$wrong++;
}
foreach ($shapes as $shape) {
if ($shape[0] < 1 || $shape[0] >= 32768 ||
$shape[1] < 0 || $shape[1] >= 16384) {
$wrong++;
}
}
}
}
$this->assertEqual($wrong, 0,
"every band at every length has likelihoods the reader can use");
$following = CeltBands::shapesFor(3, false);
$standing = CeltBands::shapesFor(3, true);
$this->assertTrue($standing[0][0] < $following[0][0],
"a stretch standing alone expects larger differences");
}
/**
* Where every stored difference is nothing, a stretch standing on
* its own should leave every band at nothing
*/
public function noDifferencesLeaveNothingTestCase()
{
$steps = array_fill(0, CeltBands::BAND_COUNT, 0);
$before = CeltEnergy::nothingYet(1);
$read = $this->storeAndRead($steps, $before, true);
$largest = 0.0;
foreach ($read[0] as $loudness) {
$largest = max($largest, abs($loudness));
}
$this->assertTrue($largest < self::CLOSE_ENOUGH,
"nothing stored leaves every band at nothing");
}
/**
* One difference should move its own band by that much, and should
* lean on the bands after it by a smaller amount that does not
* fade
*/
public function oneDifferenceLeansOnTheRestTestCase()
{
$steps = array_fill(0, CeltBands::BAND_COUNT, 0);
$steps[0] = 4;
$before = CeltEnergy::nothingYet(1);
$read = $this->storeAndRead($steps, $before, true);
$this->assertTrue(abs($read[0][0] - 4.0) < self::CLOSE_ENOUGH,
"the band the difference belongs to moves by all of it");
$lean = 4.0 * (1.0 - CeltBands::HOLD_BACK_ALONE);
$this->assertTrue(abs($read[0][1] - $lean) < self::CLOSE_ENOUGH,
"the next band up leans by what was not held back");
$this->assertTrue(abs($read[0][20] - $lean) < self::CLOSE_ENOUGH,
"the lean carries all the way up without fading");
}
/**
* A stretch that follows on should take part of what each band was
* in the stretch before, and one standing alone should ignore it
*/
public function followingOnUsesTheStretchBeforeTestCase()
{
$steps = array_fill(0, CeltBands::BAND_COUNT, 0);
$before = CeltEnergy::nothingYet(1);
for ($band = 0; $band < CeltBands::BAND_COUNT; $band++) {
$before[0][$band] = 3.0;
}
$following = $this->storeAndRead($steps, $before, false);
$carry = CeltBands::CARRY_ACROSS[self::USUAL_DOUBLINGS];
$this->assertTrue(abs($following[0][0] - 3.0 * $carry) <
self::CLOSE_ENOUGH,
"a stretch that follows on keeps part of what was before");
$standing = $this->storeAndRead($steps, $before, true);
$largest = 0.0;
foreach ($standing[0] as $loudness) {
$largest = max($largest, abs($loudness));
}
$this->assertTrue($largest < self::CLOSE_ENOUGH,
"a stretch standing alone ignores what was before");
}
/**
* A band that was silent in the stretch before should not be
* allowed to drag the next stretch down without limit
*/
public function silentBandsAreFlooredTestCase()
{
$steps = array_fill(0, CeltBands::BAND_COUNT, 0);
$before = CeltEnergy::nothingYet(1);
for ($band = 0; $band < CeltBands::BAND_COUNT; $band++) {
$before[0][$band] = -100.0;
}
$read = $this->storeAndRead($steps, $before, false);
$carry = CeltBands::CARRY_ACROSS[self::USUAL_DOUBLINGS];
$this->assertTrue(abs($read[0][0] - CeltBands::FLOOR * $carry) <
self::CLOSE_ENOUGH,
"a silent band is treated as no quieter than the floor");
}
/**
* Both channels of two channel sound should be read, each keeping
* its own leaning on the bands after it
*/
public function bothChannelsAreReadTestCase()
{
$steps = array_fill(0, CeltBands::BAND_COUNT, 0);
$steps[0] = 2;
$before = CeltEnergy::nothingYet(2);
$read = $this->storeAndRead($steps, $before, true);
$this->assertEqual(count($read), 2, "two channels come back");
$this->assertTrue(abs($read[0][0] - $read[1][0]) <
self::CLOSE_ENOUGH,
"both channels read the same stored difference");
$this->assertTrue(abs($read[0][20] - $read[1][20]) <
self::CLOSE_ENOUGH, "both channels lean the same way");
}
/**
* The finer pass should move each loudness by less than half a
* step, and more bits should let it land closer
*/
public function finerPassNarrowsTheStepTestCase()
{
$spend = array_fill(0, CeltBands::BAND_COUNT, 3);
$writer = new RangeEncoder(self::ROOM);
for ($band = 0; $band < CeltBands::BAND_COUNT; $band++) {
$writer->encodeRawBits($band % 8, 3);
}
$reader = new RangeDecoder($writer->finish());
$rough = CeltEnergy::nothingYet(1);
$finer = CeltEnergy::readFiner($reader, $rough, $spend, 0,
CeltBands::BAND_COUNT);
$largest = 0.0;
foreach ($finer[0] as $loudness) {
$largest = max($largest, abs($loudness));
}
$this->assertTrue($largest < 0.5,
"the finer pass never moves a loudness half a step or more");
$this->assertTrue($largest > 0.0,
"the finer pass does move the loudnesses");
$this->assertTrue(abs($finer[0][0] + 0.5 - 1.0 / 16) <
self::CLOSE_ENOUGH,
"the lowest reading lands in the middle of the lowest part");
}
/**
* A band the recording set no room aside for should be left where
* the rough pass put it
*/
public function bandsWithNoRoomAreLeftAloneTestCase()
{
$spend = array_fill(0, CeltBands::BAND_COUNT, 0);
$writer = new RangeEncoder(self::ROOM);
$writer->encodeRawBits(1, 1);
$reader = new RangeDecoder($writer->finish());
$rough = CeltEnergy::nothingYet(1);
$rough[0][4] = 2.5;
$finer = CeltEnergy::readFiner($reader, $rough, $spend, 0,
CeltBands::BAND_COUNT);
$this->assertEqual($finer[0][4], 2.5,
"a band with no room set aside is left where it was");
}
/**
* Bits left over at the end should go to the bands that asked
* first, and should run out rather than being spent twice
*/
public function leftoverBitsGoToTheKeenestTestCase()
{
$spent = array_fill(0, CeltBands::BAND_COUNT, 2);
$wanting = array_fill(0, CeltBands::BAND_COUNT, 1);
$wanting[7] = 0;
$wanting[9] = 0;
$writer = new RangeEncoder(self::ROOM);
$writer->encodeRawBits(1, 1);
$writer->encodeRawBits(0, 1);
$reader = new RangeDecoder($writer->finish());
$loudness = CeltEnergy::nothingYet(1);
$finished = CeltEnergy::readLeftovers($reader, $loudness, $spent,
$wanting, 2, 0, CeltBands::BAND_COUNT);
$moved = 0;
foreach ($finished[0] as $band => $loud) {
if (abs($loud) > self::CLOSE_ENOUGH) {
$moved++;
}
}
$this->assertEqual($moved, 2, "only two bands were given a bit");
$this->assertTrue(abs($finished[0][7] - 0.5 / 8) <
self::CLOSE_ENOUGH, "the first keen band was given its bit");
$this->assertTrue(abs($finished[0][9] + 0.5 / 8) <
self::CLOSE_ENOUGH, "the second keen band was given the next");
}
/**
* A loudness in steps should turn into a scale where each whole
* step is a fourfold change in power
*/
public function loudnessBecomesAScaleTestCase()
{
$quiet = CeltEnergy::scaleFor(0.0, 0);
$louder = CeltEnergy::scaleFor(1.0, 0);
$this->assertTrue(abs($louder / $quiet - 2.0) < self::CLOSE_ENOUGH,
"one whole step doubles the scale");
$this->assertTrue($quiet > 0.0, "a scale is never nothing");
}
}