<?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
*
* VorbisSetup reads the tables a Vorbis file carries before its sound.
*/
namespace seekquarry\yioop\library\av_processing;
/**
* VorbisSetup reads the third description of a Vorbis stream, which
* holds every table the rest of the file is written with: the code
* books, the shapes the loudness curve is drawn from, the way the
* leftover detail is stored, how channels are paired, and which of
* those a packet uses.
*
* Vorbis differs from AAC and MP3 in carrying its own tables rather
* than taking them from a standard, so an encoder chooses tables to
* suit the sound at hand. Nothing in a Vorbis packet can be read until
* this description has been.
*
* The codes a book uses are not written down. A book gives only the
* length of each of its codes, and both sides work the codes out from
* those lengths the same way. That way is not the usual one, and
* getting it wrong cost several rounds: see codesFor.
*
* @author Chris Pollett
*/
class VorbisSetup
{
/**
* BOOK_MARK is the number a code book begins with, which a reader
* checks so that a description read out of step is noticed at once.
* @var int
*/
const BOOK_MARK = 0x564342;
/**
* $books stores the code books, each with the length of every one
* of its codes and, where it has them, the numbers those codes
* stand for.
* @var array
*/
public $books = [];
/**
* $floors stores the shapes a loudness curve is drawn from, one for
* each the file carries.
* @var array
*/
public $floors = [];
/**
* $residues stores how the leftover detail of each channel is
* written, one for each way the file carries.
* @var array
*/
public $residues = [];
/**
* $mappings stores which floor and which residue each channel uses,
* and which channels are paired.
* @var array
*/
public $mappings = [];
/**
* $modes stores the ways a packet may be written: which stretch
* length it uses and which mapping it follows.
* @var array
*/
public $modes = [];
/**
* fromString reads the whole of the third description. A caller
* hands it the packet and the number of channels the first
* description gave, since a mapping is written per channel.
*
* @param string $data The third packet of the stream.
* @param int $channels How many channels the sound carries.
* @return VorbisSetup The tables the file carries.
*/
public static function fromString($data, $channels)
{
if (strlen($data) < 8 || ord($data[0]) !== VorbisHeader::TABLES
|| substr($data, 1, 6) !== VorbisHeader::MARK) {
throw new \RuntimeException("this packet is not the table "
. "description a Vorbis stream carries");
}
$reader = new LowBitReader(substr($data, 7));
$said = new self();
$said->books = self::readBooks($reader);
$time_shapes = $reader->readBits(6) + 1;
for ($at = 0; $at < $time_shapes; $at++) {
$reader->readBits(16);
}
$said->floors = self::readFloors($reader, count($said->books));
$said->residues = self::readResidues($reader);
$said->mappings = self::readMappings($reader, $channels);
$said->modes = self::readModes($reader);
return $said;
}
/**
* readBooks reads every code book the file carries. A book says how
* long each of its codes is, and may carry a list of numbers those
* codes stand for, which is how Vorbis writes a run of values as a
* single code.
*
* @param LowBitReader $reader The description's bits.
* @return array The books, each with its code lengths and numbers.
*/
public static function readBooks($reader)
{
$count = $reader->readBits(8) + 1;
$books = [];
for ($at = 0; $at < $count; $at++) {
$books[] = self::readBook($reader);
}
return $books;
}
/**
* readBook reads one code book: how many values a code stands for,
* how many codes there are, the length of each, and where the book
* carries them, the numbers themselves.
*
* @param LowBitReader $reader The description's bits.
* @return array The book.
*/
public static function readBook($reader)
{
$mark = $reader->readBits(24);
if ($mark !== self::BOOK_MARK) {
throw new \RuntimeException("a code book in this file does "
. "not begin the way the format says");
}
$holds = $reader->readBits(16);
$entries = $reader->readBits(24);
$ordered = $reader->readBit();
$lengths = [];
if ($ordered === 0) {
$sparse = $reader->readBit();
for ($at = 0; $at < $entries; $at++) {
if ($sparse === 1 && $reader->readBit() === 0) {
$lengths[$at] = 0;
continue;
}
$lengths[$at] = $reader->readBits(5) + 1;
}
} else {
/* An ordered book writes how many codes share each length
rather than a length for every code. */
$length = $reader->readBits(5) + 1;
$at = 0;
while ($at < $entries) {
$room = LowBitReader::bitsFor($entries - $at);
$share = $reader->readBits($room);
for ($seen = 0; $seen < $share && $at < $entries;
$seen++) {
$lengths[$at] = $length;
$at++;
}
$length++;
}
}
$book = ["holds" => $holds, "entries" => $entries,
"lengths" => $lengths, "codes" => self::codesFor($lengths),
"numbers" => []];
$lookup = $reader->readBits(4);
if ($lookup === 1 || $lookup === 2) {
$book["numbers"] = self::readNumbers($reader, $book,
$lookup);
$book["lookup"] = $lookup;
} else {
$book["lookup"] = 0;
}
return $book;
}
/**
* codesFor works out the code that stands for each entry of a book
* from the lengths alone. The format fixes how, and the way it
* fixes is not the usual one: rather than handing out codes
* shortest first, it walks the entries in the order the file wrote
* them and gives each the lowest code of its length that no earlier
* entry has taken or ruled out.
*
* A code is kept here as a whole number whose highest bits are the
* code itself, so that codes of different lengths can be compared
* and split without knowing the length in advance. Taking a code of
* one length frees the codes below it, which is what lets a later
* entry of a longer code sit under an earlier short one.
*
* @param array $lengths How long each entry's code is.
* @return array The code for each entry, as the bits a reader will
* see, highest bit first.
*/
public static function codesFor($lengths)
{
$codes = [];
$free = array_fill(0, 34, 0);
$started = false;
foreach ($lengths as $at => $length) {
if ($length <= 0) {
continue;
}
if (!$started) {
$codes[$at] = 0;
for ($which = 1; $which <= $length; $which++) {
$free[$which] = 1 << (32 - $which);
}
$started = true;
continue;
}
$room = $length;
while ($room > 0 && $free[$room] === 0) {
$room--;
}
if ($room === 0) {
throw new \RuntimeException("a code book in this file "
. "has no room left for one of its codes");
}
$taken = $free[$room];
$free[$room] = 0;
$codes[$at] = $taken >> (32 - $length);
if ($room !== $length) {
for ($which = $length; $which > $room; $which--) {
$free[$which] = $taken + (1 << (32 - $which));
}
}
}
return $codes;
}
/**
* readNumbers reads the list of numbers a book's codes stand for. A
* book stores a few values and builds the rest by multiplying them
* together, which is how a run of several numbers costs one code.
*
* @param LowBitReader $reader The description's bits.
* @param array $book The book so far.
* @param int $lookup Which of the two ways the book stores them.
* @return array The numbers each entry stands for.
*/
public static function readNumbers($reader, $book, $lookup)
{
$least = $reader->readFloat();
$step = $reader->readFloat();
$room = $reader->readBits(4) + 1;
$signed = $reader->readBit();
$held = ($lookup === 1)
? self::rootOf($book["entries"], $book["holds"])
: $book["entries"] * $book["holds"];
$values = [];
for ($at = 0; $at < $held; $at++) {
$values[$at] = $reader->readBits($room);
}
$numbers = [];
for ($entry = 0; $entry < $book["entries"]; $entry++) {
$numbers[$entry] = self::numbersOf($entry, $book, $values,
$least, $step, $signed, $lookup, $held);
}
return $numbers;
}
/**
* numbersOf builds the run of numbers one entry of a book stands
* for. Where the book multiplies its stored values together, the
* entry names which of them to use for each place in the run.
*
* @param int $entry Which entry of the book.
* @param array $book The book so far.
* @param array $values The values the book stored.
* @param float $least The number every value counts from.
* @param float $step How much one step of a value is worth.
* @param int $signed Whether each number carries the one before it.
* @param int $lookup Which of the two ways the book stores them.
* @param int $held How many values the book stored.
* @return array The run of numbers.
*/
public static function numbersOf($entry, $book, $values, $least,
$step, $signed, $lookup, $held)
{
$run = [];
$carried = 0.0;
if ($lookup === 1) {
$divide = 1;
for ($place = 0; $place < $book["holds"]; $place++) {
$which = intdiv($entry, $divide) % $held;
$one = $values[$which] * $step + $least + $carried;
if ($signed === 1) {
$carried = $one;
}
$run[$place] = $one;
$divide *= $held;
}
return $run;
}
$at = $entry * $book["holds"];
for ($place = 0; $place < $book["holds"]; $place++) {
$one = ($values[$at + $place] ?? 0) * $step + $least
+ $carried;
if ($signed === 1) {
$carried = $one;
}
$run[$place] = $one;
}
return $run;
}
/**
* rootOf works out how many values a book stores where its entries
* are every way of choosing one value for each place in a run. That
* count is the root of the number of entries, taken to the power of
* how many places there are.
*
* @param int $entries How many entries the book has.
* @param int $holds How many numbers an entry stands for.
* @return int How many values the book stores.
*/
public static function rootOf($entries, $holds)
{
if ($holds <= 0) {
return 0;
}
$root = (int)round(pow($entries, 1.0 / $holds));
while (pow($root + 1, $holds) <= $entries) {
$root++;
}
while ($root > 0 && pow($root, $holds) > $entries) {
$root--;
}
return $root;
}
/**
* readFloors reads the shapes a loudness curve is drawn from. A
* Vorbis frame writes the shape of its sound as a curve and the
* detail left over once that curve is taken out, so the curve's
* shape has to be read first.
*
* @param LowBitReader $reader The description's bits.
* @param int $books How many code books the file carries.
* @return array The shapes.
*/
public static function readFloors($reader, $books)
{
$count = $reader->readBits(6) + 1;
$floors = [];
for ($at = 0; $at < $count; $at++) {
$kind = $reader->readBits(16);
if ($kind !== 1) {
throw new \RuntimeException("this file draws its "
. "loudness curve a way that is not read yet");
}
$floors[] = self::readCurveShape($reader);
}
return $floors;
}
/**
* readCurveShape reads one shape a loudness curve is drawn from:
* the places along the sound where the curve may bend, gathered
* into classes that share a code book.
*
* @param LowBitReader $reader The description's bits.
* @return array The shape.
*/
public static function readCurveShape($reader)
{
$parts = $reader->readBits(5);
$classes = [];
$highest = -1;
$of_part = [];
for ($at = 0; $at < $parts; $at++) {
$of_part[$at] = $reader->readBits(4);
$highest = max($highest, $of_part[$at]);
}
for ($at = 0; $at <= $highest; $at++) {
$holds = $reader->readBits(3) + 1;
$spread = $reader->readBits(2);
$book = -1;
if ($spread > 0) {
$book = $reader->readBits(8);
}
$books = [];
for ($which = 0; $which < (1 << $spread); $which++) {
$books[$which] = $reader->readBits(8) - 1;
}
$classes[$at] = ["holds" => $holds, "spread" => $spread,
"book" => $book, "books" => $books];
}
$step = $reader->readBits(2) + 1;
$room = $reader->readBits(4);
$places = [0, 1 << $room];
for ($at = 0; $at < $parts; $at++) {
$holds = $classes[$of_part[$at]]["holds"];
for ($which = 0; $which < $holds; $which++) {
$places[] = $reader->readBits($room);
}
}
return ["parts" => $of_part, "classes" => $classes,
"step" => $step, "room" => $room, "places" => $places];
}
/**
* readResidues reads how the detail left over once the loudness
* curve is taken out is written. A file may carry several such
* ways, and a mapping says which one each channel uses.
*
* @param LowBitReader $reader The description's bits.
* @return array The ways.
*/
public static function readResidues($reader)
{
$count = $reader->readBits(6) + 1;
$residues = [];
for ($at = 0; $at < $count; $at++) {
$kind = $reader->readBits(16);
if ($kind > 2) {
throw new \RuntimeException("this file writes its "
. "leftover detail a way that is not read yet");
}
$begin = $reader->readBits(24);
$end = $reader->readBits(24);
$part_size = $reader->readBits(24) + 1;
$classes = $reader->readBits(6) + 1;
$classbook = $reader->readBits(8);
$cascade = [];
for ($which = 0; $which < $classes; $which++) {
$low = $reader->readBits(3);
$high = 0;
if ($reader->readBit() === 1) {
$high = $reader->readBits(5);
}
$cascade[$which] = $low | ($high << 3);
}
$books = [];
for ($which = 0; $which < $classes; $which++) {
for ($pass = 0; $pass < 8; $pass++) {
$books[$which][$pass] =
(($cascade[$which] >> $pass) & 1)
? $reader->readBits(8) : -1;
}
}
$residues[] = ["kind" => $kind, "begin" => $begin,
"end" => $end, "part_size" => $part_size,
"classes" => $classes, "classbook" => $classbook,
"books" => $books];
}
return $residues;
}
/**
* readMappings reads which curve shape and which leftover detail
* each channel uses, and which channels are written as a pair, one
* carrying the sound and the other how far apart the two are.
*
* @param LowBitReader $reader The description's bits.
* @param int $channels How many channels the sound carries.
* @return array The mappings.
*/
public static function readMappings($reader, $channels)
{
$count = $reader->readBits(6) + 1;
$mappings = [];
for ($at = 0; $at < $count; $at++) {
$kind = $reader->readBits(16);
if ($kind !== 0) {
throw new \RuntimeException("this file lays out its "
. "channels a way that is not read yet");
}
$submaps = 1;
if ($reader->readBit() === 1) {
$submaps = $reader->readBits(4) + 1;
}
$pairs = [];
if ($reader->readBit() === 1) {
$steps = $reader->readBits(8) + 1;
$room = LowBitReader::bitsFor($channels - 1);
for ($which = 0; $which < $steps; $which++) {
$pairs[] = ["sound" => $reader->readBits($room),
"apart" => $reader->readBits($room)];
}
}
if ($reader->readBits(2) !== 0) {
throw new \RuntimeException("this file carries a "
. "mapping field the format leaves empty");
}
$which_submap = array_fill(0, $channels, 0);
if ($submaps > 1) {
for ($channel = 0; $channel < $channels; $channel++) {
$which_submap[$channel] = $reader->readBits(4);
}
}
$floors = [];
$residues = [];
for ($which = 0; $which < $submaps; $which++) {
$reader->readBits(8);
$floors[$which] = $reader->readBits(8);
$residues[$which] = $reader->readBits(8);
}
$mappings[] = ["pairs" => $pairs, "submap" => $which_submap,
"floors" => $floors, "residues" => $residues];
}
return $mappings;
}
/**
* readModes reads the ways a packet may be written. A packet names
* one of these in its first bits, and the mode says whether the
* packet covers a long stretch or a short one and which mapping it
* follows.
*
* @param LowBitReader $reader The description's bits.
* @return array The modes.
*/
public static function readModes($reader)
{
$count = $reader->readBits(6) + 1;
$modes = [];
for ($at = 0; $at < $count; $at++) {
$long = $reader->readBit();
$reader->readBits(16);
$reader->readBits(16);
$mapping = $reader->readBits(8);
$modes[] = ["long" => $long, "mapping" => $mapping];
}
return $modes;
}
}