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