<?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\OpusPacket;
use seekquarry\yioop\library\UnitTest;
/**
* Checks that a piece of Opus sound is taken apart correctly: that the
* first byte is read as the settings it stands for, that all four ways
* of packing several stretches into one piece are unpacked, and that a
* piece saying something impossible about itself is refused rather
* than read as though it made sense.
*
* The settings table is worth checking closely. Its thirty two entries
* are the whole of what a decoder may be asked to do, and the boundary
* between one way of compressing and the next falls in the middle of
* the numbering, so an off by one there would send a piece to the
* wrong decoder and be hard to see afterwards.
*
* @author Chris Pollett
*/
class OpusPacketTest extends UnitTest
{
/**
* Nothing needs setting up for these cases
*/
public function setUp()
{
}
/**
* Nothing needs clearing away after these cases
*/
public function tearDown()
{
}
/**
* Builds the first byte of a piece out of what it should say
*
* @param int $setting which of the thirty two settings
* @param bool $paired whether the sound is two channels
* @param int $arrangement how the stretches are packed
* @return string the byte, ready to put in front of the sound
*/
public function buildFirstByte($setting, $paired, $arrangement)
{
return chr(($setting << OpusPacket::SETTING_SHIFT) |
($paired ? OpusPacket::PAIRED_BIT : 0) | $arrangement);
}
/**
* The settings table should cover all thirty two numbers, and the
* boundaries between the ways of compressing should fall where the
* specification puts them
*/
public function settingsTableTestCase()
{
$settings = OpusPacket::settings();
$this->assertEqual(count($settings), 32,
"every setting number is covered");
$this->assertEqual($settings[0][0], OpusPacket::SPEECH_METHOD,
"the lowest setting is the way built for speech");
$this->assertEqual($settings[11][0], OpusPacket::SPEECH_METHOD,
"the way built for speech runs to eleven");
$this->assertEqual($settings[12][0], OpusPacket::BOTH_METHODS,
"both ways at once begin at twelve");
$this->assertEqual($settings[15][0], OpusPacket::BOTH_METHODS,
"both ways at once run to fifteen");
$this->assertEqual($settings[16][0], OpusPacket::MUSIC_METHOD,
"the way built for music begins at sixteen");
$this->assertEqual($settings[31][0], OpusPacket::MUSIC_METHOD,
"the way built for music runs to thirty one");
$this->assertEqual($settings[31][1], OpusPacket::FULL_SPECTRUM,
"the highest setting keeps the whole spectrum");
$this->assertEqual($settings[31][2], 20.0,
"the highest setting covers twenty milliseconds");
$this->assertEqual($settings[16][2], 2.5,
"the shortest stretch is two and a half milliseconds");
}
/**
* A piece holding one stretch should give back that stretch and
* report the settings its first byte named
*/
public function oneStretchTestCase()
{
$data = $this->buildFirstByte(20, false, OpusPacket::ONE_STRETCH) .
"SOUND";
$piece = OpusPacket::fromString($data);
$this->assertEqual(count($piece->stretches), 1,
"one stretch comes back");
$this->assertEqual($piece->stretches[0], "SOUND",
"the stretch is the rest of the piece");
$this->assertEqual($piece->method, OpusPacket::MUSIC_METHOD,
"setting twenty is the way built for music");
$this->assertEqual($piece->spectrum, OpusPacket::WIDE_SPECTRUM,
"setting twenty keeps the wide spectrum");
$this->assertFalse($piece->is_paired, "the sound is one channel");
$this->assertEqual($piece->stretch_duration, 2.5,
"setting twenty covers two and a half milliseconds");
}
/**
* A piece holding two stretches of the same length should split
* evenly down the middle
*/
public function twoEvenStretchesTestCase()
{
$data = $this->buildFirstByte(11, true,
OpusPacket::TWO_EVEN_STRETCHES) . "AAAABBBB";
$piece = OpusPacket::fromString($data);
$this->assertEqual(count($piece->stretches), 2,
"two stretches come back");
$this->assertEqual($piece->stretches[0], "AAAA", "first half");
$this->assertEqual($piece->stretches[1], "BBBB", "second half");
$this->assertTrue($piece->is_paired, "the sound is two channels");
$this->assertEqual($piece->duration(), 120.0,
"two sixty millisecond stretches run two minutes of a second");
}
/**
* A piece holding two stretches of different lengths writes the
* first length in front, and the second stretch is whatever is
* left
*/
public function twoUnevenStretchesTestCase()
{
$data = $this->buildFirstByte(0, false,
OpusPacket::TWO_UNEVEN_STRETCHES) . chr(3) . "AAABBBBB";
$piece = OpusPacket::fromString($data);
$this->assertEqual(count($piece->stretches), 2,
"two stretches come back");
$this->assertEqual($piece->stretches[0], "AAA",
"the first is as long as the piece said");
$this->assertEqual($piece->stretches[1], "BBBBB",
"the second is what was left");
}
/**
* A length of 252 or more takes two bytes rather than one, and the
* second byte counts in fours
*/
public function longStretchLengthTestCase()
{
$long = str_repeat("A", 300);
$first = OpusPacket::SHORT_LENGTH_LIMIT;
$second = intdiv(300 - $first, OpusPacket::LENGTH_STEP);
$data = $this->buildFirstByte(0, false,
OpusPacket::TWO_UNEVEN_STRETCHES) . chr($first) . chr($second) .
$long . "TAIL";
$piece = OpusPacket::fromString($data);
$this->assertEqual(strlen($piece->stretches[0]), 300,
"a length past what one byte holds is read from two");
$this->assertEqual($piece->stretches[1], "TAIL",
"the stretch after it begins in the right place");
}
/**
* A piece may say how many stretches it holds, all the same
* length, and they should divide evenly
*/
public function manyEvenStretchesTestCase()
{
$data = $this->buildFirstByte(0, false, OpusPacket::MANY_STRETCHES) .
chr(3) . "AAABBBCCC";
$piece = OpusPacket::fromString($data);
$this->assertEqual(count($piece->stretches), 3,
"three stretches come back");
$this->assertEqual($piece->stretches[2], "CCC", "the last is right");
$this->assertEqual($piece->duration(), 30.0,
"three ten millisecond stretches run thirty milliseconds");
}
/**
* A piece may say how many stretches it holds and write out the
* length of each but the last
*/
public function manyUnevenStretchesTestCase()
{
$count_byte = chr(OpusPacket::UNEVEN_BIT | 3);
$data = $this->buildFirstByte(0, false, OpusPacket::MANY_STRETCHES) .
$count_byte . chr(2) . chr(4) . "AABBBBCCCCCC";
$piece = OpusPacket::fromString($data);
$this->assertEqual(count($piece->stretches), 3,
"three stretches come back");
$this->assertEqual($piece->stretches[0], "AA", "first as stated");
$this->assertEqual($piece->stretches[1], "BBBB", "second as stated");
$this->assertEqual($piece->stretches[2], "CCCCCC",
"the last is what was left");
}
/**
* Filler at the end of a piece is not sound and should be left out
* of the stretches
*/
public function fillerIsNotSoundTestCase()
{
$count_byte = chr(OpusPacket::FILLER_BIT | 2);
$data = $this->buildFirstByte(0, false, OpusPacket::MANY_STRETCHES) .
$count_byte . chr(3) . "AAABBB" . "\0\0\0";
$piece = OpusPacket::fromString($data);
$this->assertEqual($piece->filler, 3, "the filler was counted");
$this->assertEqual(count($piece->stretches), 2,
"two stretches come back");
$this->assertEqual($piece->stretches[1], "BBB",
"the filler was left out of the sound");
}
/**
* A piece claiming more sound than it holds, or a length no piece
* may have, should be refused rather than read as though it made
* sense
*/
public function impossiblePiecesAreRefusedTestCase()
{
$refused = 0;
$tries = [
"" ,
$this->buildFirstByte(0, false, OpusPacket::TWO_EVEN_STRETCHES) .
"ABC",
$this->buildFirstByte(0, false, OpusPacket::MANY_STRETCHES) .
chr(0),
$this->buildFirstByte(3, false, OpusPacket::MANY_STRETCHES) .
chr(5) . "AAAAA",
$this->buildFirstByte(0, false, OpusPacket::MANY_STRETCHES) .
chr(OpusPacket::FILLER_BIT | 1) . chr(200) . "AA"];
foreach ($tries as $try) {
try {
OpusPacket::fromString($try);
} catch (\Exception $problem) {
$refused++;
}
}
$this->assertEqual($refused, count($tries),
"every impossible piece was refused");
}
/**
* A stretch that says it is 60 milliseconds long may not be
* repeated past the longest a piece may run
*/
public function overlongPieceIsRefusedTestCase()
{
$data = $this->buildFirstByte(3, false, OpusPacket::MANY_STRETCHES) .
chr(3) . "AAABBBCCC";
$refused = false;
try {
OpusPacket::fromString($data);
} catch (\Exception $problem) {
$refused = true;
}
$this->assertTrue($refused,
"three sixty millisecond stretches is past the limit");
}
}