<?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;
}
}