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

/**
 * OpusHeader the description of an Opus recording that sits in front of the
 * sound itself and says what a player needs to know before it can play it.
 * Every Opus recording begins with a short run of bytes giving how many
 * channels it has, how much of the start is only there to let the decoder
 * settle, how loud to play it, and how its channels are laid out. In an .opus
 * file this is the first piece in the file; in a .webm file the same bytes sit
 * in the box describing the sound. The same reader handles both. One thing
 * worth knowing: the samples a second written here is what the sound was
 * recorded at, not what it will be played at. Opus always gives back 48000
 * samples a second whatever it was fed, so this figure is a note about where
 * the sound came from and nothing more. The layout is set out in RFC 7845, the
 * specification for carrying Opus sound inside an Ogg file.
 */
class OpusHeader
{
    /**
     * MAGIC is the eight letters this description always begins with.
     */
    const MAGIC = "OpusHead";
    /**
     * MIN_SIZE is the fewest bytes an Opus description can take. A shorter
     * one is a damaged file rather than a description.
     */
    const MIN_SIZE = 19;
    /**
     * SAMPLE_RATE is samples a second Opus always gives back, whatever it was
     * fed.
     */
    const SAMPLE_RATE = 48000;
    /**
     * SIMPLE_LAYOUT is how the channels are laid out when there is nothing
     * unusual about them, which covers ordinary one and two channel sound.
     */
    const SIMPLE_LAYOUT = 0;
    /**
     * MAX_SIMPLE_CHANNELS is most channels the simple layout above allows.
     */
    const MAX_SIMPLE_CHANNELS = 2;
    /**
     * GAIN_STEPS_PER_DECIBEL is how much the loudness figure is scaled by
     * before it is written, so that fractions of a decibel can be stored as a
     * whole number.
     */
    const GAIN_STEPS_PER_DECIBEL = 256;
    /**
     * channel_count stores how many channels the recording has.
     * @var int
     */
    public $channel_count;
    /**
     * lead_in stores how much of the start of the decoded sound to throw away
     * because it is only there to let the decoder settle, counted in samples at
     * 48000 a second.
     * @var int
     */
    public $lead_in;
    /**
     * original_rate stores how many samples a second the sound was recorded at,
     * before it was compressed.
     * @var int
     */
    public $original_rate;
    /**
     * gain stores how much to raise or lower the sound on playing, in decibels.
     * @var float
     */
    public $gain;
    /**
     * layout stores which way the channels are laid out.
     * @var int
     */
    public $layout;
    /**
     * stream_count stores how many separate Opus streams the channels are
     * carried in.
     * @var int
     */
    public $stream_count;
    /**
     * paired_count stores how many of those streams carry two channels rather
     * than one.
     * @var int
     */
    public $paired_count;
    /**
     * channel_order stores which stream and channel each output channel comes
     * from.
     * @var array
     */
    public $channel_order;
    /**
     * fromString reads the description out of the bytes it was stored as
     *
     * @param string $data the bytes the description was stored as
     * @return object the description read
     */
    public static function fromString($data)
    {
        if (strncmp($data, self::MAGIC, strlen(self::MAGIC)) != 0) {
            throw new \Exception("Not an Opus description");
        }
        if (strlen($data) < self::MIN_SIZE) {
            throw new \Exception("Opus description is too short");
        }
        $header = new self();
        $version = ord($data[8]);
        /* The top half of the version says which readers can cope; a
           reader may go on with a version whose top half it knows. */
        if (($version >> 4) != 0) {
            throw new \Exception("Opus description is too new to read");
        }
        $header->channel_count = ord($data[9]);
        $header->lead_in = ord($data[10]) | (ord($data[11]) << 8);
        $header->original_rate = ord($data[12]) | (ord($data[13]) << 8) |
            (ord($data[14]) << 16) | (ord($data[15]) << 24);
        $stored_gain = ord($data[16]) | (ord($data[17]) << 8);
        if ($stored_gain >= 0x8000) {
            $stored_gain -= 0x10000;
        }
        $header->gain = $stored_gain / self::GAIN_STEPS_PER_DECIBEL;
        $header->layout = ord($data[18]);
        if ($header->layout == self::SIMPLE_LAYOUT) {
            if ($header->channel_count < 1 ||
                $header->channel_count > self::MAX_SIMPLE_CHANNELS) {
                throw new \Exception("Opus description has an impossible " .
                    "channel count");
            }
            $header->stream_count = 1;
            $header->paired_count = $header->channel_count - 1;
            $header->channel_order =
                ($header->channel_count == 1) ? [0] : [0, 1];
        } else {
            if (strlen($data) < 21 + $header->channel_count) {
                throw new \Exception("Opus description ends part way " .
                    "through its channel layout");
            }
            $header->stream_count = ord($data[19]);
            $header->paired_count = ord($data[20]);
            $header->channel_order = [];
            for ($i = 0; $i < $header->channel_count; $i++) {
                $header->channel_order[] = ord($data[21 + $i]);
            }
        }
        return $header;
    }
}
X