/ tests / OpusPacketTest.php
<?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");
    }
}
X