/ src / library / av_processing / CeltStretch.php
<?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;

/**
 * CeltStretch reads one whole stretch of sound compressed the way built for
 * music. This puts the parts together in the one order they will work in. A
 * stretch opens by saying what it is, then how loud each band is roughly, then
 * how much extra room any band asked for and which way it leaned, then the
 * sharing out of the room, then each loudness more finely, then every band's
 * shape, and finally whatever bits were still unspent. Nothing in that order
 * can be moved, because each step decides how much room the next has, and
 * reading one step out of turn puts everything after it out of step. How
 * closely the reading lands on the end of a stretch is the best check there is
 * on all of it. Whatever wrote the recording filled the stretch to the bit, so
 * a reading that agrees will finish with almost nothing left over. A reading
 * that has gone wrong anywhere will finish hundreds or thousands of bits out,
 * since every step after the fault reads the wrong things. What is not here
 * yet: sound of two channels, the step that fills a band back in where it fell
 * silent across a sudden change, and turning the tones back into samples. The
 * first two are read past rather than acted on, so a stretch still comes out at
 * the right length. This follows RFC 6716, the specification of the Opus audio
 * codec.
 */
class CeltStretch
{
    /**
     * fillCollapsed fills the pieces of a band that fell silent with faint
     * noise. A stretch whose sound changed suddenly is read as eight short
     * pieces, and a band can come out with nothing in some of them. If the
     * recording asked for it, each such piece is filled with noise whose
     * loudness follows how the band now compares with its two stretches before:
     * a band no louder than it was gets filled near its old level, and one that
     * just jumped gets almost nothing, so the filling never smears a real
     * attack. The band is then brought back to unit length. doubled
     *
     * @param array $slots each channel's slots, changed in place
     * @param array $marks which pieces of each band hold something
     * @param array $shape_room how much room each band's shape got
     * @param array $loudness each channel's loudness now
     * @param array $previous_one each channel's loudness one stretch ago
     * @param array $previous_two the same two stretches ago
     * @param int $doublings how many times the shortest stretch was
     * @param int $channels how many channels there are
     * @param int $seed where the noise numbers start
     * @param int $first_band the lowest band the stretch carries
     * @param int $past_last one past the highest band it carries
     */
    public static function fillCollapsed(&$slots, $marks, $shape_room,
        $loudness, $previous_one, $previous_two, $doublings, $channels, $seed,
        $first_band, $past_last)
    {
        $edges = CeltBands::EDGES;
        $pieces = 1 << $doublings;
        foreach ($slots as $channel => $unused) {
            for ($band = $first_band; $band < $past_last; $band++) {
                $width = $edges[$band + 1] - $edges[$band];
                $depth = intdiv(1 + $shape_room[$band], $width) >>
                    $doublings;
                $ceiling = 0.5 * pow(2.0, -0.125 * $depth);
                $one = $previous_one[$channel][$band] ?? $previous_one[0]
                    [$band];
                $two = $previous_two[$channel][$band] ?? $previous_two[0]
                    [$band];
                $jump = max(0.0,
                    $loudness[$channel][$band] - min($one, $two));
                $faint = 2.0 * pow(2.0, -$jump);
                if ($doublings == 3) {
                    $faint *= M_SQRT2;
                }
                $faint = min($ceiling, $faint) /
                    sqrt($width << $doublings);
                $begin = $edges[$band] << $doublings;
                $filled = false;
                for ($piece = 0; $piece < $pieces; $piece++) {
                    if (($marks[$band] >> $piece) & 1) {
                        continue;
                    }
                    for ($slot = 0; $slot < $width; $slot++) {
                        $seed = (CeltBandReader::SEED_STEP * $seed +
                            CeltBandReader::SEED_ADD) % 4294967296;
                        $slots[$channel][$begin + ($slot << $doublings) +
                            $piece] = ($seed & 0x8000) ? $faint : -$faint;
                    }
                    $filled = true;
                }
                if ($filled) {
                    $total = 0.0;
                    $count = $width << $doublings;
                    for ($slot = 0; $slot < $count; $slot++) {
                        $total += $slots[$channel][$begin + $slot] ** 2;
                    }
                    if ($total > 0.0) {
                        $scale = 1.0 / sqrt($total);
                        for ($slot = 0; $slot < $count; $slot++) {
                            $slots[$channel][$begin + $slot] *= $scale;
                        }
                    }
                }
            }
        }
    }
    /**
     * BIT_PARTS is how finely room is counted, as parts of a bit.
     */
    const BIT_PARTS = 3;
    /**
     * WHOLE_BIT is how many of those eighth parts make one whole
     * bit, since the format counts bits in eighths.
     */
    const WHOLE_BIT = 8;
    /**
     * LEAST_DOUBLINGS_FOR_FILLING is how many times the shortest stretch has
     * been doubled to reach a stretch that could hold a sudden change worth
     * filling back in.
     */
    const LEAST_DOUBLINGS_FOR_FILLING = 2;
    /**
     * readFrom reads one whole stretch been doubled before, one run per channel
     * stands or null to skip filling bands back in
     *
     * @param object $reader a reader over the stretch
     * @param int $doublings how many times the shortest stretch has
     * @param array $before how loud each band was in the stretch
     * @param int $seed where the wandering number used for filling
     * @param int $first_band the lowest band the stretch carries
     * @param int $past_last one past the highest band it carries
     * @param array $previous_one each channel's loudness one stretch ago,
     * @param array $previous_two the same two stretches ago, or null
     * @return array everything read from the stretch
     */
    public static function readFrom($reader, $doublings, $before, $seed,
        $first_band, $past_last, $previous_one = null, $previous_two = null)
    {
        $channels = count($before);
        $whole = $reader->size * 8 << self::BIT_PARTS;
        $header = CeltFrameHeader::readFrom($reader, $doublings, $before,
            $first_band, $past_last);
        /* A stretch that says it is silent is not skipped. Its room is
           already marked as used up, so every band is given nothing and
           ends up filled with faint noise at whatever loudness was
           read, which is what a player expects rather than digital
           silence. */
        $asked = CeltAllocation::readBoosts($reader, $doublings, $channels,
            $first_band, $past_last, $whole);
        $tilt = CeltAllocation::readTilt($reader, $asked["room"]);
        $room = $whole - $reader->bitsUsedFinely() - 1;
        /* Where the sound changed suddenly, one bit is held back to say
           whether a band that fell silent should be filled back in. */
        $filling_held = 0;
        if ($header->sudden &&
            $doublings >= self::LEAST_DOUBLINGS_FOR_FILLING &&
            $whole >= ($doublings + 2) * self::WHOLE_BIT) {
            $filling_held = self::WHOLE_BIT;
        }
        $room -= $filling_held;
        $shared = CeltAllocation::shareOut($reader, $asked["boosts"], $tilt,
            $room, $doublings, $channels, $first_band, $past_last);
        $loudness = CeltEnergy::readFiner($reader, $header->loudness,
            $shared["fine"], $first_band, $past_last);
        $bands = new CeltBandReader($reader, $doublings, $header->spread,
            $seed);
        /* The bit held back for the sudden change step is not there for
           the bands to spend, so it comes off the room they are told
           they have. */
        $bands->whole_room -= $filling_held;
        if ($channels > 1) {
            $read = $bands->readAllPairs($shared["shape"],
                $header->length_changes, $shared["kept"], $header->sudden,
                $shared["spare"], $first_band, $past_last,
                $shared["joins_from"], $shared["side_by_side"]);
        } else {
            $read = $bands->readAll($shared["shape"],
                $header->length_changes, $shared["kept"], $header->sudden,
                $shared["spare"], $first_band, $past_last);
            $read["slots"] = [$read["slots"]];
        }
        $fill_back = 0;
        if ($filling_held > 0) {
            $fill_back = $reader->decodeRawBits(1);
        }
        $unspent = $reader->size * 8 - $reader->bitsUsed();
        $loudness = CeltEnergy::readLeftovers($reader, $loudness,
            $shared["fine"], $shared["keenest"], $unspent, $first_band,
            $past_last);
        if ($fill_back && $previous_one !== null && $previous_two !== null) {
            self::fillCollapsed($read["slots"], $read["marks"],
                $shared["shape"], $loudness, $previous_one, $previous_two,
                $doublings, $channels, $bands->seed, $first_band,
                $past_last);
        }
        /* Only one channel is written out, so where there are two they
           are averaged. Writing both is work for the day the encoder
           takes two. */
        $mixed = $read["slots"][0];
        if ($channels > 1) {
            $count = count($mixed);
            for ($slot = 0; $slot < $count; $slot++) {
                $mixed[$slot] = 0.5 * ($mixed[$slot] +
                    $read["slots"][1][$slot]);
            }
        }
        $read["slots"] = $mixed;
        return ["header" => $header, "loudness" => $loudness,
            "slots" => $read["slots"], "marks" => $read["marks"],
            "shared" => $shared, "fill_back" => $fill_back,
            "seed" => $bands->seed,
            "left" => $whole - $reader->bitsUsedFinely()];
    }
}
X