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

/**
 * Laplace reads a number that was likely to be small and near zero, and only
 * rarely far from it. Band loudnesses are stored as differences from what the
 * decoder already expects, and those differences are usually tiny. Writing them
 * the ordinary way would need a table saying how likely every possible
 * difference was, and the differences have no fixed range. So instead the
 * likelihoods are described by a shape: a chance of being zero, and a rate at
 * which the chance falls away for each step further out. Two small numbers
 * describe the whole shape, and a difference of any size can still be written.
 * The shape is only followed so far. Past the point where the falling chance
 * would drop below the smallest the writer can express, everything is treated
 * as equally unlikely, which keeps a very large difference storable rather than
 * impossible. This is described in RFC 6716, the specification of the Opus
 * audio codec, in the section on the band loudnesses.
 */
class Laplace
{
    /**
     * WHOLE is the likelihoods are worked out against this whole, which is what
     * the reader is told to divide the range into.
     */
    const WHOLE = 32768;
    /**
     * WHOLE_BITS is how many bits the whole above takes.
     */
    const WHOLE_BITS = 15;
    /**
     * SMALLEST_CHANCE is the smallest chance the writer can express. Nothing is
     * ever given less than this, so that no value becomes impossible.
     */
    const SMALLEST_CHANCE = 1;
    /**
     * RESERVED_STEPS is how many steps out from zero are held back to be shared
     * among the values too far out to follow the shape.
     */
    const RESERVED_STEPS = 16;
    /**
     * firstStepChance how much of the whole is left over for the values beyond
     * the first, once the chance of zero and the reserve have been taken off
     * takes
     *
     * @param int $zero_chance how much of the whole the value zero
     * @param int $fall how fast the chance falls away per step
     * @return int how much of the whole the first step out takes
     */
    public static function firstStepChance($zero_chance, $fall)
    {
        $left = self::WHOLE -
            self::SMALLEST_CHANCE * (2 * self::RESERVED_STEPS) -
            $zero_chance;
        return ($left * (16384 - $fall)) >> 15;
    }
    /**
     * readFrom reads one number written the way this coding writes them,
     * where values near zero cost fewer bits than values far from it
     *
     * @param object $reader the reader partway through a piece of
     * @param int $zero_chance how much of the whole the value zero
     * @param int $fall how fast the chance falls away per step
     * @return int the number read, which may be negative
     */
    public static function readFrom($reader, $zero_chance, $fall)
    {
        $steps = 0;
        $reached = $reader->decodeBinary(self::WHOLE_BITS);
        $below = 0;
        $chance = $zero_chance;
        if ($reached >= $chance) {
            $steps++;
            $below = $chance;
            $chance = self::firstStepChance($chance, $fall) +
                self::SMALLEST_CHANCE;
            /* Walk out from zero while the chance is still falling,
               one step at a time, until the value read is accounted
               for. */
            while ($chance > self::SMALLEST_CHANCE &&
                $reached >= $below + 2 * $chance) {
                $chance *= 2;
                $below += $chance;
                $chance = (($chance - 2 * self::SMALLEST_CHANCE) * $fall)
                    >> 15;
                $chance += self::SMALLEST_CHANCE;
                $steps++;
            }
            /* Past where the shape runs out, every remaining value is
               as likely as every other, so how far out it is can be
               worked out by division rather than by stepping. */
            if ($chance <= self::SMALLEST_CHANCE) {
                $further = ($reached - $below) >> 1;
                $steps += $further;
                $below += 2 * $further * self::SMALLEST_CHANCE;
            }
            if ($reached < $below + $chance) {
                $steps = -$steps;
            } else {
                $below += $chance;
            }
        }
        $above = $below + $chance;
        if ($above > self::WHOLE) {
            $above = self::WHOLE;
        }
        $reader->update($below, $above, self::WHOLE);
        return $steps;
    }
}
X