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