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

/**
 * CeltFrameHeader reads what a stretch of sound says about itself before it
 * says anything about the sound. A stretch opens with a short run of flags and
 * settings, and every one of them changes how the rest of the stretch is read.
 * Whether the stretch is silent decides whether there is anything after it at
 * all. Whether the sound changed suddenly partway through decides whether the
 * stretch is treated as one long piece or several short ones, and that in turn
 * changes how the tones are laid out. How much room is left decides which of
 * the flags are present at all: a stretch too short to hold one simply leaves
 * it out, and both the writer and the reader have to leave it out at exactly
 * the same point. That last part is what makes this worth its own file. Nothing
 * here is difficult on its own, but the order is fixed, several of the flags
 * are conditional on how much room remains, and reading one flag that was never
 * written puts every later reading out of step and turns the rest of the
 * stretch into noise. This follows RFC 6716, the specification of the Opus
 * audio codec, in its section on the transform layer.
 */
class CeltFrameHeader
{
    /**
     * SILENCE_CHANCE is room needed before the silence flag is stored.
     */
    const SILENCE_CHANCE = 15;
    /**
     * JUST_BEGUN is how much the reader has accounted for when a stretch has
     * only just begun, which is how a stretch of sound alone is told from one
     * that follows speech in the same piece.
     */
    const JUST_BEGUN = 1;
    /**
     * FILTER_ROOM is room needed before the filter settings are stored.
     */
    const FILTER_ROOM = 16;
    /**
     * FILTER_OCTAVES is how many different lengths of pitch the filter may be
     * set to.
     */
    const FILTER_OCTAVES = 6;
    /**
     * FILTER_SHORTEST is the shortest pitch the filter may be set to.
     */
    const FILTER_SHORTEST = 16;
    /**
     * FILTER_STRENGTH_BITS is how many bits the filter's strength takes.
     */
    const FILTER_STRENGTH_BITS = 3;
    /**
     * FILTER_STRENGTH_STEP is how much each step of the filter's strength is
     * worth.
     */
    const FILTER_STRENGTH_STEP = 0.09375;
    /**
     * FILTER_SHAPE_CHANCES is the likelihoods for which of the filter's three
     * shapes was chosen, counting down from the whole.
     */
    const FILTER_SHAPE_CHANCES = [2, 1, 0];
    /**
     * FILTER_SHAPE_BITS is how many bits those likelihoods add up to.
     */
    const FILTER_SHAPE_BITS = 2;
    /**
     * SUDDEN_ROOM is room needed before the sudden change flag is stored.
     */
    const SUDDEN_ROOM = 3;
    /**
     * ALONE_ROOM is room needed before the flag saying the stretch stands alone
     * is stored.
     */
    const ALONE_ROOM = 3;
    /**
     * SPREAD_ROOM is room needed before the spreading setting is stored.
     */
    const SPREAD_ROOM = 4;
    /**
     * SPREAD_CHANCES is the likelihoods for how much the shapes were spread
     * out, counting down from the whole.
     */
    const SPREAD_CHANCES = [25, 23, 2, 0];
    /**
     * SPREAD_BITS is how many bits those likelihoods add up to.
     */
    const SPREAD_BITS = 5;
    /**
     * SPREAD_USUAL is the spreading used when there was no room to store which
     * was chosen.
     */
    const SPREAD_USUAL = 2;
    /**
     * LENGTH_CHANGES is how the stored change of length is turned into a real
     * one, given how long the stretch is and whether it changed suddenly.
     */
    const LENGTH_CHANGES = [[0, -1, 0, -1, 0, -1, 0, -1],
        [0, -1, 0, -2, 1, 0, 1, -1], [0, -2, 0, -3, 2, 0, 1, -1],
        [0, -2, 0, -3, 3, 0, 1, -1]];
    /**
     * silent stores whether the stretch holds no sound at all.
     * @var bool
     */
    public $silent;
    /**
     * filter_on stores whether the filter that follows the sound is switched
     * on.
     * @var bool
     */
    public $filter_on;
    /**
     * filter_pitch stores what pitch that filter is set to, in samples.
     * @var int
     */
    public $filter_pitch;
    /**
     * filter_strength stores how strongly that filter acts.
     * @var float
     */
    public $filter_strength;
    /**
     * filter_shape stores which of the filter's three shapes was chosen.
     * @var int
     */
    public $filter_shape;
    /**
     * Whether the sound changed suddenly partway through the stretch
     * @var bool
     */
    public $sudden;
    /**
     * alone stores whether the stretch stands on its own rather than following
     * on from the one before.
     * @var bool
     */
    public $alone;
    /**
     * loudness stores how loud each band is, one run per channel.
     * @var array
     */
    public $loudness;
    /**
     * length_changes stores for each band, how its length was changed from the
     * stretch's own.
     * @var array
     */
    public $length_changes;
    /**
     * spread stores how widely the shapes read for each band were spread
     * before they were written, which the decoder undoes
     * @var int
     */
    public $spread;
    /**
     * readFrom reads everything a stretch says about itself, up to the point
     * where it starts saying how the bits were shared out been doubled to reach
     * this one before, one run per channel
     *
     * @param object $reader the reader at the start of a stretch
     * @param int $doublings how many times the shortest stretch has
     * @param array $before how loud each band was in the stretch
     * @param int $first_band the lowest band the stretch carries
     * @param int $past_last one past the highest band it carries
     * @return object what the stretch says about itself
     */
    public static function readFrom($reader, $doublings, $before,
        $first_band, $past_last)
    {
        $header = new self();
        $room = $reader->size * 8;
        $used = $reader->bitsUsed();
        /* Only a stretch that opens a piece says whether it is silent.
           Where speech has already been read from the same piece there
           is no such flag, and a stretch with no room left is taken as
           silent without being asked. */
        if ($used >= $room) {
            $header->silent = true;
        } else if ($used == self::JUST_BEGUN) {
            $header->silent =
                ($reader->decodeBit(self::SILENCE_CHANCE) != 0);
        } else {
            $header->silent = false;
        }
        if ($header->silent) {
            $reader->bits_used += $reader->size * 8 - $reader->bitsUsed();
        }
        $header->readFilter($reader, $room, $first_band);
        $used = $reader->bitsUsed();
        $header->sudden = false;
        if ($doublings > 0 && $used + self::SUDDEN_ROOM <= $room) {
            $header->sudden = ($reader->decodeBit(self::SUDDEN_ROOM) != 0);
            $used = $reader->bitsUsed();
        }
        $header->alone = ($used + self::ALONE_ROOM <= $room) &&
            ($reader->decodeBit(self::ALONE_ROOM) != 0);
        $header->loudness = CeltEnergy::readRough($reader, $before,
            $header->alone, $doublings, $first_band, $past_last);
        $header->readLengthChanges($reader, $doublings, $first_band,
            $past_last);
        $header->spread = self::SPREAD_USUAL;
        if ($reader->bitsUsed() + self::SPREAD_ROOM <= $room) {
            $header->spread = $reader->decodeFromTable(self::SPREAD_CHANCES,
                self::SPREAD_BITS);
        }
        return $header;
    }
    /**
     * readFilter reads the settings for the filter that runs over the sound
     * after it has been put back together
     *
     * @param object $reader the reader partway through a stretch
     * @param int $room how many bits the whole stretch holds
     * @param int $first_band the lowest band the stretch carries
     */
    public function readFilter($reader, $room, $first_band)
    {
        $this->filter_on = false;
        $this->filter_pitch = 0;
        $this->filter_strength = 0.0;
        $this->filter_shape = 0;
        if ($first_band != 0 ||
            $reader->bitsUsed() + self::FILTER_ROOM > $room) {
            return;
        }
        if ($reader->decodeBit(1) == 0) {
            return;
        }
        $this->filter_on = true;
        /* The pitch is stored as a rough range and then a position
           within it, so that low pitches are given finer steps than
           high ones. */
        $range = $reader->decodeNumber(self::FILTER_OCTAVES);
        $this->filter_pitch = (self::FILTER_SHORTEST << $range) +
            $reader->decodeRawBits(4 + $range) - 1;
        $steps = $reader->decodeRawBits(self::FILTER_STRENGTH_BITS);
        $this->filter_strength = self::FILTER_STRENGTH_STEP * ($steps + 1);
        if ($reader->bitsUsed() + self::FILTER_SHAPE_BITS <= $room) {
            $this->filter_shape = $reader->decodeFromTable(
                self::FILTER_SHAPE_CHANCES, self::FILTER_SHAPE_BITS);
        }
    }
    /**
     * readLengthChanges reads which bands were given a different length from
     * the stretch's own. A stretch can afford finer detail in time for some
     * bands and finer detail in pitch for others, so each band may be split
     * differently. What is stored is only where the setting changes from one
     * band to the next, since it usually holds across runs of bands, and one
     * further choice covering the whole stretch. been doubled to reach this one
     *
     * @param object $reader the reader partway through a stretch
     * @param int $doublings how many times the shortest stretch has
     * @param int $first_band the lowest band the stretch carries
     * @param int $past_last one past the highest band it carries
     */
    public function readLengthChanges($reader, $doublings, $first_band,
        $past_last)
    {
        $room = $reader->size * 8;
        $used = $reader->bitsUsed();
        $chance = $this->sudden ? 2 : 4;
        $has_choice = ($doublings > 0 && $used + $chance + 1 <= $room);
        if ($has_choice) {
            $room--;
        }
        $running = 0;
        $any_changed = 0;
        $stored = array_fill(0, CeltBands::BAND_COUNT, 0);
        for ($band = $first_band; $band < $past_last; $band++) {
            if ($used + $chance <= $room) {
                $running ^= $reader->decodeBit($chance);
                $used = $reader->bitsUsed();
                $any_changed |= $running;
            }
            $stored[$band] = $running;
            $chance = $this->sudden ? 4 : 5;
        }
        $sudden_part = $this->sudden ? 4 : 0;
        $choice = 0;
        /* The further choice is only stored where it would make a
           difference, so the reader has to work out whether it would
           before deciding to look for it. */
        if ($has_choice &&
            self::LENGTH_CHANGES[$doublings][$sudden_part + $any_changed] !=
            self::LENGTH_CHANGES[$doublings][$sudden_part + 2 + $any_changed]) {
            $choice = $reader->decodeBit(1);
        }
        $this->length_changes = array_fill(0, CeltBands::BAND_COUNT, 0);
        for ($band = $first_band; $band < $past_last; $band++) {
            $this->length_changes[$band] = self::LENGTH_CHANGES[$doublings][
                $sudden_part + 2 * $choice + $stored[$band]];
        }
    }
}
