<?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\CeltAllocation;
use seekquarry\yioop\library\av_processing\CeltBands;
use seekquarry\yioop\library\av_processing\CeltEnergy;
use seekquarry\yioop\library\av_processing\CeltFrameHeader;
use seekquarry\yioop\library\av_processing\CeltPulseCache;
use seekquarry\yioop\library\av_processing\OpusPacket;
use seekquarry\yioop\library\av_processing\RangeDecoder;
use seekquarry\yioop\library\av_processing\WebmDemuxer;
use seekquarry\yioop\configs as C;
use seekquarry\yioop\library\UnitTest;
/**
* Checks how a stretch's room is shared out between its bands.
*
* Almost nothing about the sharing is stored in a recording. The
* writer and the reader run the same arithmetic from the same starting
* point and are expected to arrive at the same answer, so there is
* nothing to compare a wrong answer against. That makes it the hardest
* part to check and the worst part to get wrong: a fault does not spoil
* one value, it hands every band the wrong amount of room.
*
* What can be checked is that the answer holds together. Everything
* handed out has to add back up to what there was to hand out, and the
* only room unaccounted for should be the bit held back to say where
* the dropping of bands stops, plus one bit for each band the
* recording was actually asked about. No band may be given more than
* it can use. The finer loudness pass may not be given more bits than
* it can spend. Bands dropped entirely must be left with nothing.
*
* Those hold across every stretch of four real recordings made by
* other software. A sharing that had gone wrong would fail them almost
* at once, since the amounts would stop adding up.
*
* @author Chris Pollett
*/
class CeltAllocationTest extends UnitTest
{
/**
* Recordings to share out the room of
*/
const RECORDINGS = ["/test_files/tiny_tone_webm.txt",
"/test_files/tiny_noise_webm.txt",
"/test_files/tiny_recording_webm.txt"];
/**
* The longest stretch, which nearly every recording uses
*/
const DOUBLINGS = 3;
/**
* How many bands the recordings carry
*/
const BANDS = 21;
/**
* How finely room is counted, as parts of a bit
*/
const BIT_PARTS = 3;
/**
* How many stretches of each recording to share out. Sharing is
* real work, so the cases do enough to be convincing rather than
* every stretch there is.
*/
const STRETCHES_SHARED = 6;
/**
* A whole bit, in those parts
*/
const WHOLE_BIT = 8;
/**
* Most bits the finer loudness pass may spend on one band
*/
const MOST_FINE_BITS = 8;
/**
* Nothing needs setting up for these cases
*/
public function setUp()
{
}
/**
* Nothing needs clearing away after these cases
*/
public function tearDown()
{
}
/**
* Shares out the room of every stretch of one recording
*
* @param string $where which recording to read
* @return array what the sharing gave for each stretch
*/
public function shareEverything($where)
{
$stored = file_get_contents(C\PARENT_DIR . "/tests" . $where);
$reader = new WebmDemuxer(base64_decode($stored));
$before = CeltEnergy::nothingYet(1);
$shared = [];
foreach ($reader->packets() as $piece) {
$sound = OpusPacket::fromString($piece->data);
if ($sound->method != OpusPacket::MUSIC_METHOD) {
continue;
}
foreach ($sound->stretches as $stretch) {
if (strlen($stretch) < 2) {
continue;
}
$entropy = new RangeDecoder($stretch);
$header = CeltFrameHeader::readFrom($entropy, self::DOUBLINGS,
$before, 0, self::BANDS);
$before = $header->loudness;
if ($header->silent) {
continue;
}
$shared[] = $this->shareOne($entropy, $header,
strlen($stretch));
if (count($shared) >= self::STRETCHES_SHARED) {
break 2;
}
}
}
return $shared;
}
/**
* Shares out the room of one stretch
*
* @param object $entropy the reader partway through the stretch
* @param object $header what the stretch said about itself
* @param int $size how long the stretch is, in bytes
* @return array what the sharing gave, and what it had to give
*/
public function shareOne($entropy, $header, $size)
{
$whole = $size * 8 << self::BIT_PARTS;
$asked = CeltAllocation::readBoosts($entropy, self::DOUBLINGS, 1, 0,
self::BANDS, $whole);
$tilt = CeltAllocation::readTilt($entropy, $asked["room"]);
$left = $whole - $entropy->bitsUsedFinely() - 1;
/* A stretch where the sound changed suddenly holds back one bit
to say whether a band that fell silent should be filled back
in. */
if ($header->sudden && $left >= (self::DOUBLINGS + 2) *
self::WHOLE_BIT) {
$left -= self::WHOLE_BIT;
}
$given = CeltAllocation::shareOut($entropy, $asked["boosts"], $tilt,
$left, self::DOUBLINGS, 1, 0, self::BANDS);
$given["had"] = $left;
$given["size"] = $size;
$given["used"] = $entropy->bitsUsedFinely();
$given["tilt"] = $tilt;
$given["boosts"] = $asked["boosts"];
return $given;
}
/**
* What sharing every recording gave, kept so that the work is done
* once for the whole file rather than once for each case
* @var array
*/
public static $shared_already = [];
/**
* Shares out every stretch of every recording
*
* @return array what the sharing gave for each stretch
*/
public function shareAll()
{
if (self::$shared_already != []) {
return self::$shared_already;
}
$shared = [];
foreach (self::RECORDINGS as $where) {
foreach ($this->shareEverything($where) as $one) {
$shared[] = $one;
}
}
self::$shared_already = $shared;
return $shared;
}
/**
* Everything handed out should add back up to what there was to
* hand out, apart from the bit held back to say where dropping
* stops and one bit for each band the recording was asked about
*/
public function everythingAddsUpTestCase()
{
$wrong = 0;
$ragged = 0;
$checked = 0;
foreach ($this->shareAll() as $one) {
$handed = array_sum($one["shape"]) +
array_sum($one["fine"]) * self::WHOLE_BIT + $one["spare"];
$gap = $one["had"] - $handed;
if ($gap % self::WHOLE_BIT != 0) {
$ragged++;
}
$dropped = self::BANDS - $one["kept"];
if ($gap < self::WHOLE_BIT ||
$gap > self::WHOLE_BIT * (1 + $dropped)) {
$wrong++;
}
$checked++;
}
$this->assertTrue($checked > 8, "many stretches were shared out");
$this->assertEqual($ragged, 0,
"what is left over is always a whole number of bits");
$this->assertEqual($wrong, 0,
"what is left over is the bit held back plus the bands asked " .
"about, $checked stretches");
}
/**
* No band should be given more room than it can use, since room
* beyond that is wasted while another band goes short
*/
public function noBandGetsMoreThanItCanUseTestCase()
{
$over = 0;
$checked = 0;
foreach ($this->shareAll() as $one) {
for ($band = 0; $band < $one["kept"]; $band++) {
$ceiling = CeltPulseCache::ceilingRoom($band,
self::DOUBLINGS, 1);
if ($one["shape"][$band] > $ceiling) {
$over++;
}
$checked++;
}
}
$this->assertTrue($checked > 150, "many bands were checked");
$this->assertEqual($over, 0,
"no band was given more room than it can use");
}
/**
* The finer loudness pass should never be given more bits than it
* can spend, nor a negative number of them
*/
public function finerPassStaysInItsLimitsTestCase()
{
$wrong = 0;
$any = 0;
foreach ($this->shareAll() as $one) {
foreach ($one["fine"] as $bits) {
if ($bits < 0 || $bits > self::MOST_FINE_BITS) {
$wrong++;
}
if ($bits > 0) {
$any++;
}
}
}
$this->assertEqual($wrong, 0,
"every band's finer pass sits within its limits");
$this->assertTrue($any > 50,
"the finer pass is given something to do");
}
/**
* A band dropped entirely should be left with no room for a shape
*/
public function droppedBandsGetNoShapeTestCase()
{
$wrong = 0;
$dropped = 0;
foreach ($this->shareAll() as $one) {
for ($band = $one["kept"]; $band < self::BANDS; $band++) {
if ($one["shape"][$band] != 0) {
$wrong++;
}
$dropped++;
}
}
$this->assertTrue($dropped > 0, "some bands were dropped");
$this->assertEqual($wrong, 0, "a dropped band gets no shape room");
}
/**
* At least one band should always be kept, since a stretch that
* kept none would be silence and would have said so
*/
public function somethingIsAlwaysKeptTestCase()
{
$empty = 0;
$checked = 0;
foreach ($this->shareAll() as $one) {
if ($one["kept"] < 1) {
$empty++;
}
$checked++;
}
$this->assertTrue($checked > 8, "many stretches were checked");
$this->assertEqual($empty, 0, "every stretch keeps some bands");
}
/**
* Sharing should never account for more of a stretch than there is
*/
public function readingStaysInsideTheStretchTestCase()
{
$over = 0;
$checked = 0;
foreach ($this->shareAll() as $one) {
if ($one["used"] > $one["size"] * 8 * self::WHOLE_BIT) {
$over++;
}
$checked++;
}
$this->assertEqual($over, 0,
"no stretch was read past its end, $checked read");
}
/**
* The lean and the extra room asked for should both name something
* the stretch could really have said
*/
public function whatWasReadIsBelievableTestCase()
{
$wrong = 0;
$any_asked = 0;
foreach ($this->shareAll() as $one) {
if ($one["tilt"] < 0 || $one["tilt"] > 10) {
$wrong++;
}
foreach ($one["boosts"] as $band => $boost) {
if ($boost < 0 || $boost > CeltPulseCache::ceilingRoom($band,
self::DOUBLINGS, 1) * 2) {
$wrong++;
}
if ($boost > 0) {
$any_asked++;
}
}
}
$this->assertEqual($wrong, 0,
"the lean and the extra asked for are both believable");
$this->assertTrue($any_asked > 0,
"some band somewhere asked for extra room");
}
/**
* A recording given more room overall should end up with more room
* for its shapes
*/
public function moreRoomOverallMeansMoreForShapesTestCase()
{
$spare = $this->shareEverything(self::RECORDINGS[1]);
$plenty = 0;
$stretches = 0;
foreach ($spare as $one) {
$plenty += array_sum($one["shape"]);
$stretches++;
}
$tight = $this->shareEverything(self::RECORDINGS[0]);
$little = 0;
$fewer = 0;
foreach ($tight as $one) {
$little += array_sum($one["shape"]);
$fewer++;
}
$this->assertTrue($stretches > 0 && $fewer > 0,
"both recordings had stretches");
$this->assertTrue($plenty / $stretches > $little / $fewer,
"the recording with more room gives its shapes more");
}
}