/ src / library / av_processing / CeltEnergy.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;

/**
 * CeltEnergy reads how loud each band of a stretch of sound is. The loudness of
 * a band is not stored as it is. The decoder already has an expectation for
 * each band, built from what that band usually sounds like and from what the
 * band before it and the same band in the stretch before it turned out to be.
 * Only the difference from that expectation is stored, and the differences are
 * usually tiny, so they cost very little. The reading happens in three passes
 * over the bands. The first gets each loudness roughly, to the nearest whole
 * step. The second spends whatever room the recording set aside on making each
 * one finer. The third hands out any bits still unspent at the end, a single
 * bit at a time, to whichever bands were marked as wanting them most. Splitting
 * it this way lets the rough pass happen before the decoder knows how the
 * remaining room will be shared out, which it needs to know in order to share
 * it. A loudness here is counted in steps where each whole step is a fourfold
 * change in power, which is how Opus carries them throughout. This follows RFC
 * 6716, the specification of the Opus audio codec, in its section on the band
 * loudnesses.
 */
class CeltEnergy
{
    /**
     * FULL_ROOM is room needed before a loudness is read the ordinary way.
     */
    const FULL_ROOM = 15;
    /**
     * SOME_ROOM is room needed before a loudness is read the cheaper way.
     */
    const SOME_ROOM = 2;
    /**
     * LAST_ROOM is room needed before a loudness is read the cheapest way of
     * all.
     */
    const LAST_ROOM = 1;
    /**
     * CRAMPED_SHAPE is the likelihoods used when there is too little room to
     * use the usual ones, counting down from the whole.
     */
    const CRAMPED_SHAPE = [2, 1, 0];
    /**
     * CRAMPED_BITS is how many bits the likelihoods above add up to.
     */
    const CRAMPED_BITS = 2;
    /**
     * LAST_OWN_SHAPE is the highest band whose own likelihoods are used; bands
     * above it share the last set.
     */
    const LAST_OWN_SHAPE = 20;
    /**
     * MAX_FINE_BITS is most bits the finer pass may spend on one band.
     */
    const MAX_FINE_BITS = 8;
    /**
     * CEILING is the largest loudness a band may reach, which stops a damaged
     * recording driving one away without limit.
     */
    const CEILING = 28.0;
    /**
     * readRough reads each band's loudness roughly, to the nearest whole step.
     * Each band's loudness is built from three things: what the same band was
     * in the stretch before, how much the bands before this one in the same
     * stretch were off by, and the difference stored in the recording. Where a
     * stretch stands on its own, the first of those is dropped. one run per
     * channel been doubled to reach this one
     *
     * @param object $reader the reader partway through a piece
     * @param array $before what each band was in the stretch before,
     * @param bool $alone whether this stretch stands on its own
     * @param int $doublings how many times the shortest stretch has
     * @param int $first_band the lowest band to read
     * @param int $past_last one past the highest band to read
     * @return array each band's rough loudness, one run per channel
     */
    public static function readRough($reader, $before, $alone, $doublings,
        $first_band, $past_last)
    {
        $shapes = CeltBands::shapesFor($doublings, $alone);
        $carry = $alone ? 0.0 : CeltBands::CARRY_ACROSS[$doublings];
        $held_back = $alone ? CeltBands::HOLD_BACK_ALONE :
            CeltBands::HOLD_BACK[$doublings];
        $channels = count($before);
        $loudness = $before;
        $running = array_fill(0, $channels, 0.0);
        $room = $reader->size * 8;
        for ($band = $first_band; $band < $past_last; $band++) {
            for ($channel = 0; $channel < $channels; $channel++) {
                $left = $room - $reader->bitsUsed();
                if ($left >= self::FULL_ROOM) {
                    $which = min($band, self::LAST_OWN_SHAPE);
                    $steps = Laplace::readFrom($reader,
                        $shapes[$which][0], $shapes[$which][1]);
                } else if ($left >= self::SOME_ROOM) {
                    $packed = $reader->decodeFromTable(self::CRAMPED_SHAPE,
                        self::CRAMPED_BITS);
                    /* The cramped likelihoods hold zero, one and minus
                       one in that order, so the value read is opened
                       back out into a difference here. */
                    $steps = ($packed >> 1) ^ -($packed & 1);
                } else if ($left >= self::LAST_ROOM) {
                    $steps = -$reader->decodeBit(1);
                } else {
                    $steps = -1;
                }
                $was = max(CeltBands::FLOOR, $loudness[$channel][$band]);
                $now = $carry * $was + $running[$channel] + $steps;
                $loudness[$channel][$band] = max(-self::CEILING,
                    min(self::CEILING, $now));
                /* Part of this band's difference is carried into the
                   bands after it, since a recording that is loud in
                   one band is usually loud in the next. */
                $running[$channel] += $steps - $held_back * $steps;
            }
        }
        return $loudness;
    }
    /**
     * readFiner spends the room set aside for it on making each loudness finer
     * channel
     *
     * @param object $reader the reader partway through a piece
     * @param array $loudness each band's rough loudness, one run per
     * @param array $spend how many bits to spend on each band
     * @param int $first_band the lowest band to read
     * @param int $past_last one past the highest band to read
     * @return array each band's finer loudness
     */
    public static function readFiner($reader, $loudness, $spend, $first_band,
        $past_last)
    {
        $channels = count($loudness);
        $room = $reader->size * 8;
        for ($band = $first_band; $band < $past_last; $band++) {
            $bits = $spend[$band];
            if ($bits <= 0) {
                continue;
            }
            if ($reader->bitsUsed() + $channels * $bits > $room) {
                continue;
            }
            for ($channel = 0; $channel < $channels; $channel++) {
                $within = $reader->decodeRawBits($bits);
                /* The bits say where in the step the true loudness
                   sat, so the middle of that part of the step is
                   taken and the step's own middle subtracted. */
                $offset = ($within + 0.5) / (1 << $bits) - 0.5;
                $loudness[$channel][$band] += $offset;
            }
        }
        return $loudness;
    }
    /**
     * readLeftovers hands out any bits still unspent at the end of a stretch,
     * one at a time, to the bands that were marked as wanting them most band
     *
     * @param object $reader the reader partway through a piece
     * @param array $loudness each band's loudness so far
     * @param array $spent how many bits the finer pass spent on each
     * @param array $wanting which bands asked first and which second
     * @param int $left how many bits are still unspent
     * @param int $first_band the lowest band to read
     * @param int $past_last one past the highest band to read
     * @return array each band's finished loudness
     */
    public static function readLeftovers($reader, $loudness, $spent,
        $wanting, $left, $first_band, $past_last)
    {
        $channels = count($loudness);
        for ($round = 0; $round < 2; $round++) {
            for ($band = $first_band; $band < $past_last && $left >= $channels;
                $band++) {
                if ($spent[$band] >= self::MAX_FINE_BITS ||
                    $wanting[$band] != $round) {
                    continue;
                }
                for ($channel = 0; $channel < $channels; $channel++) {
                    $bit = $reader->decodeRawBits(1);
                    $loudness[$channel][$band] += ($bit - 0.5) /
                        (1 << ($spent[$band] + 1));
                    $left--;
                }
            }
        }
        return $loudness;
    }
    /**
     * nothingYet a run of loudnesses for a stretch that has nothing before it,
     * every band starting from nothing
     *
     * @param int $channels how many channels the sound has
     * @return array a run of loudnesses per channel, all at nothing
     */
    public static function nothingYet($channels)
    {
        $loudness = [];
        for ($channel = 0; $channel < $channels; $channel++) {
            $loudness[] = array_fill(0, CeltBands::BAND_COUNT, 0.0);
        }
        return $loudness;
    }
    /**
     * scaleFor turns a band's stored loudness into the scale the band's slots
     * are multiplied by, which is where the loudness stops being a count of
     * steps and becomes a plain number
     *
     * @param float $loudness the band's loudness in steps
     * @param int $band which band it is
     * @return float what the band's shape should be multiplied by
     */
    public static function scaleFor($loudness, $band)
    {
        return pow(2.0, $loudness + CeltBands::EXPECTED[$band]);
    }
}
X