/ src / library / av_processing / OpusPacket.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\library\av_processing;

/**
 * OpusPacket one piece of Opus sound, taken apart into the separate stretches
 * it is built from. A piece of Opus sound begins with a single byte saying how
 * the rest was compressed: which of the two methods inside Opus was used, how
 * much of the sound spectrum was kept, how long a stretch it covers, whether it
 * is one channel or two, and how many stretches follow. The stretches
 * themselves may be all the same length, or their lengths may be written out in
 * front of them, so getting at them means reading those lengths first. Opus is
 * really two ways of compressing sound in one wrapper: one built for speech,
 * one built for music, and a middle setting that runs both at once and splits
 * the spectrum between them. Which one a recording used decides how much work
 * playing it back takes, and the first byte of every piece is where that is
 * written down. Counting those bytes across a whole file says which of the two
 * a given recorder actually reaches for. The layout is set out in RFC 6716, the
 * specification of the Opus audio codec.
 */
class OpusPacket
{
    /**
     * SPEECH_METHOD is the way of compressing built for speech.
     */
    const SPEECH_METHOD = "speech";
    /**
     * BOTH_METHODS is both ways at once, splitting the spectrum between them.
     */
    const BOTH_METHODS = "both";
    /**
     * MUSIC_METHOD is the way of compressing built for music.
     */
    const MUSIC_METHOD = "music";
    /**
     * NARROW_SPECTRUM is only the lowest part of the spectrum was kept, to
     * about 4000 cycles a second.
     */
    const NARROW_SPECTRUM = "narrow";
    /**
     * MEDIUM_SPECTRUM is a little more was kept, to about 6000 cycles a second.
     */
    const MEDIUM_SPECTRUM = "medium";
    /**
     * WIDE_SPECTRUM is more still, to about 8000 cycles a second.
     */
    const WIDE_SPECTRUM = "wide";
    /**
     * WIDER_SPECTRUM is most of it, to about 12000 cycles a second.
     */
    const WIDER_SPECTRUM = "wider";
    /**
     * FULL_SPECTRUM is all a person can hear, to about 20000 cycles a second.
     */
    const FULL_SPECTRUM = "full";
    /**
     * SAMPLE_RATE is samples a second Opus gives back, whatever it was fed.
     */
    const SAMPLE_RATE = 48000;
    /**
     * MILLISECONDS_PER_SECOND is milliseconds in a second, used to turn a
     * stretch's length into a count of samples.
     */
    const MILLISECONDS_PER_SECOND = 1000;
    /**
     * SETTING_SHIFT is how far to shift the first byte to reach the setting
     * number.
     */
    const SETTING_SHIFT = 3;
    /**
     * SETTING_MASK is how much of the shifted first byte the setting number
     * takes.
     */
    const SETTING_MASK = 0x1F;
    /**
     * PAIRED_BIT is which bit of the first byte says the sound has two
     * channels.
     */
    const PAIRED_BIT = 0x04;
    /**
     * ARRANGEMENT_MASK is how much of the first byte says how many stretches
     * follow.
     */
    const ARRANGEMENT_MASK = 0x03;
    /**
     * ONE_STRETCH is one stretch, taking up the rest of the piece.
     */
    const ONE_STRETCH = 0;
    /**
     * TWO_EVEN_STRETCHES is the packing where a packet carries two
     * stretches of sound of the same length.
     */
    const TWO_EVEN_STRETCHES = 1;
    /**
     * TWO_UNEVEN_STRETCHES is the packing where a packet carries two
     * stretches of different lengths, with the first one's length
     * written out.
     */
    const TWO_UNEVEN_STRETCHES = 2;
    /**
     * MANY_STRETCHES is any number of stretches, counted in a byte of its own.
     */
    const MANY_STRETCHES = 3;
    /**
     * UNEVEN_BIT is which bit of the count byte says the stretches differ in
     * length.
     */
    const UNEVEN_BIT = 0x80;
    /**
     * FILLER_BIT is which bit of the count byte says filler was added at the
     * end.
     */
    const FILLER_BIT = 0x40;
    /**
     * COUNT_MASK is how much of the count byte holds the count itself.
     */
    const COUNT_MASK = 0x3F;
    /**
     * MAX_STRETCHES is the most stretches of sound one packet may carry,
     * which bounds how long a packet can run.
     */
    const MAX_STRETCHES = 48;
    /**
     * MAX_DURATION is longest a whole piece may run, in milliseconds.
     */
    const MAX_DURATION = 120;
    /**
     * SHORT_LENGTH_LIMIT is largest length that fits in a single byte; past
     * this a second byte is used.
     */
    const SHORT_LENGTH_LIMIT = 252;
    /**
     * LENGTH_STEP is what the second length byte counts in.
     */
    const LENGTH_STEP = 4;
    /**
     * FILLER_CONTINUES is a run of filler bytes of this value means more filler
     * follows.
     */
    const FILLER_CONTINUES = 255;
    /**
     * FILLER_PER_BYTE is how much filler a byte of the value above stands for.
     */
    const FILLER_PER_BYTE = 254;
    /**
     * Which of the thirty two settings the first byte named
     * @var int
     */
    public $setting;
    /**
     * is_paired stores whether the sound is two channels rather than one.
     * @var bool
     */
    public $is_paired;
    /**
     * method stores which of the two ways of compressing was used.
     * @var string
     */
    public $method;
    /**
     * How much of the sound spectrum was kept
     * @var string
     */
    public $spectrum;
    /**
     * stretch_duration stores how long one stretch covers, in milliseconds.
     * @var float
     */
    public $stretch_duration;
    /**
     * stretches stores the compressed pieces of sound this packet
     * carries, each covering the same fraction of a second.
     * @var array
     */
    public $stretches;
    /**
     * How much filler was added at the end of the piece, in bytes
     * @var int
     */
    public $filler;
    /**
     * The thirty two settings the first byte of a piece may name, each
     * giving the way of compressing, how much spectrum was kept, and
     * how long one stretch runs in milliseconds
     *
     * @return array the settings, in the order they are numbered
     */
    public static function settings()
    {
        static $settings = null;
        if ($settings !== null) {
            return $settings;
        }
        $settings = [];
        $speech_durations = [10.0, 20.0, 40.0, 60.0];
        $speech_spectra = [self::NARROW_SPECTRUM, self::MEDIUM_SPECTRUM,
            self::WIDE_SPECTRUM];
        foreach ($speech_spectra as $i => $spectrum) {
            foreach ($speech_durations as $j => $duration) {
                $settings[$i * 4 + $j] = [self::SPEECH_METHOD, $spectrum,
                    $duration];
            }
        }
        $both_durations = [10.0, 20.0];
        $both_spectra = [self::WIDER_SPECTRUM, self::FULL_SPECTRUM];
        foreach ($both_spectra as $i => $spectrum) {
            foreach ($both_durations as $j => $duration) {
                $settings[12 + $i * 2 + $j] = [self::BOTH_METHODS, $spectrum,
                    $duration];
            }
        }
        $music_durations = [2.5, 5.0, 10.0, 20.0];
        $music_spectra = [self::NARROW_SPECTRUM, self::WIDE_SPECTRUM,
            self::WIDER_SPECTRUM, self::FULL_SPECTRUM];
        foreach ($music_spectra as $i => $spectrum) {
            foreach ($music_durations as $j => $duration) {
                $settings[16 + $i * 4 + $j] = [self::MUSIC_METHOD, $spectrum,
                    $duration];
            }
        }
        ksort($settings);
        return $settings;
    }
    /**
     * fromString takes a piece of Opus sound apart into the stretches it is
     * built from
     *
     * @param string $data the piece as it was stored
     * @return object the piece taken apart
     */
    public static function fromString($data)
    {
        $length = strlen($data);
        if ($length < 1) {
            throw new \Exception("Opus piece is empty");
        }
        $packet = new self();
        $first = ord($data[0]);
        $packet->setting = ($first >> self::SETTING_SHIFT) &
            self::SETTING_MASK;
        $packet->is_paired = ($first & self::PAIRED_BIT) != 0;
        $arrangement = $first & self::ARRANGEMENT_MASK;
        $settings = self::settings();
        $packet->method = $settings[$packet->setting][0];
        $packet->spectrum = $settings[$packet->setting][1];
        $packet->stretch_duration = $settings[$packet->setting][2];
        $packet->filler = 0;
        $at = 1;
        if ($arrangement == self::ONE_STRETCH) {
            $packet->stretches = [substr($data, $at)];
        } else if ($arrangement == self::TWO_EVEN_STRETCHES) {
            $left = $length - $at;
            if ($left % 2 != 0) {
                throw new \Exception("Opus piece says two even stretches " .
                    "but cannot be halved");
            }
            $half = intdiv($left, 2);
            $packet->stretches = [substr($data, $at, $half),
                substr($data, $at + $half, $half)];
        } else if ($arrangement == self::TWO_UNEVEN_STRETCHES) {
            $first_length = self::readLength($data, $at);
            $at = $first_length["next_at"];
            if ($first_length["value"] > $length - $at) {
                throw new \Exception("Opus piece says a first stretch " .
                    "longer than the piece");
            }
            $packet->stretches = [
                substr($data, $at, $first_length["value"]),
                substr($data, $at + $first_length["value"])];
        } else {
            $packet->stretches = self::readManyStretches($data, $at, $packet);
        }
        return $packet;
    }
    /**
     * readManyStretches pulls apart a piece holding any number of stretches,
     * whose count and lengths are written in front of them records
     *
     * @param string $data the piece as it was stored
     * @param int $at where the count byte sits
     * @param object $packet the piece being built, whose filler this
     * @return array the stretches pulled out
     */
    public static function readManyStretches($data, $at, $packet)
    {
        $length = strlen($data);
        if ($at >= $length) {
            throw new \Exception("Opus piece is missing its count of " .
                "stretches");
        }
        $count_byte = ord($data[$at]);
        $at++;
        $uneven = ($count_byte & self::UNEVEN_BIT) != 0;
        $has_filler = ($count_byte & self::FILLER_BIT) != 0;
        $count = $count_byte & self::COUNT_MASK;
        if ($count < 1 || $count > self::MAX_STRETCHES) {
            throw new \Exception("Opus piece says an impossible number " .
                "of stretches");
        }
        if ($count * $packet->stretch_duration > self::MAX_DURATION) {
            throw new \Exception("Opus piece runs longer than a piece may");
        }
        if ($has_filler) {
            while (true) {
                if ($at >= $length) {
                    throw new \Exception("Opus piece ends part way through " .
                        "how much filler it has");
                }
                $part = ord($data[$at]);
                $at++;
                if ($part == self::FILLER_CONTINUES) {
                    $packet->filler += self::FILLER_PER_BYTE;
                } else {
                    $packet->filler += $part;
                    break;
                }
            }
        }
        $lengths = [];
        if ($uneven) {
            for ($i = 0; $i < $count - 1; $i++) {
                $read = self::readLength($data, $at);
                $lengths[] = $read["value"];
                $at = $read["next_at"];
            }
        }
        $left = $length - $at - $packet->filler;
        if ($left < 0) {
            throw new \Exception("Opus piece says more filler than it holds");
        }
        if ($uneven) {
            $used = array_sum($lengths);
            if ($used > $left) {
                throw new \Exception("Opus piece says stretches longer " .
                    "than it holds");
            }
            $lengths[] = $left - $used;
        } else {
            if ($left % $count != 0) {
                throw new \Exception("Opus piece says even stretches but " .
                    "cannot be divided");
            }
            $lengths = array_fill(0, $count, intdiv($left, $count));
        }
        $stretches = [];
        foreach ($lengths as $stretch_length) {
            $stretches[] = substr($data, $at, $stretch_length);
            $at += $stretch_length;
        }
        return $stretches;
    }
    /**
     * readLength reads how long one stretch is, which takes one byte for
     * shorter stretches and two for longer ones
     *
     * @param string $data the piece as it was stored
     * @param int $at where the length sits
     * @return array the length and where it ends
     */
    public static function readLength($data, $at)
    {
        $length = strlen($data);
        if ($at >= $length) {
            throw new \Exception("Opus piece ends where a stretch length " .
                "was expected");
        }
        $first = ord($data[$at]);
        $at++;
        if ($first < self::SHORT_LENGTH_LIMIT) {
            return ["value" => $first, "next_at" => $at];
        }
        if ($at >= $length) {
            throw new \Exception("Opus piece ends part way through a " .
                "stretch length");
        }
        $second = ord($data[$at]);
        $at++;
        return ["value" => $first + $second * self::LENGTH_STEP,
            "next_at" => $at];
    }
    /**
     * duration how long the whole piece covers, in milliseconds
     *
     * @return float the length of the piece in milliseconds
     */
    public function duration()
    {
        return $this->stretch_duration * count($this->stretches);
    }
    /**
     * describe a short phrase naming how this piece was compressed, for
     * counting up what a file is built from kept, how long a stretch runs, and
     * how many channels
     *
     * @return string the way of compressing, how much spectrum was
     */
    public function describe()
    {
        return $this->method . "/" . $this->spectrum . "/" .
            $this->stretch_duration . "ms/" .
            ($this->is_paired ? "two channel" : "one channel");
    }
}
X