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

/**
 * SpeechLoudness turns the loudness numbers a stretch of speech carries
 * into the scales its four quarters are played at.
 *
 * A stretch writes its loudness as a step on a scale of decibels, since
 * that is how loudness is heard and it costs fewer bits than a plain
 * number. The steps are counted up, held within the range the standard
 * allows, and then raised out of the decibel scale into the plain
 * multiplier each quarter's pulses are scaled by.
 *
 * A caller uses this after reading a stretch's opening and before
 * turning its pulses into sound.
 *
 * @author Chris Pollett
 */
class SpeechLoudness
{
    /**
     * LEVELS is how many steps of loudness a stretch may name. The steps
     * run evenly on a scale of decibels.
     */
    const LEVELS = 64;
    /**
     * SMALLEST_STEP is the smallest step a quarter may name after the
     * first, as an offset added before the steps are counted up.
     */
    const SMALLEST_STEP = -4;
    /**
     * LARGEST_STEP is the largest step a quarter may name after the
     * first. A step past a point counts double, so that a stretch can
     * rise quickly without the table needing more entries.
     */
    const LARGEST_STEP = 36;
    /**
     * FALL_LIMIT is how many steps the first quarter of a stretch may
     * fall below the last quarter of the stretch before it. A greater
     * fall is held at this, which keeps a lost packet from silencing the
     * one that follows.
     */
    const FALL_LIMIT = 16;
    /**
     * DECIBEL_OFFSET is added to a step before it is raised out of the
     * decibel scale, and sets where the quietest step sits.
     */
    const DECIBEL_OFFSET = 2090;
    /**
     * DECIBELS_PER_STEP is how far apart two steps sit on the decibel
     * scale, written so that a whole step of loudness is one unit of
     * this over 65536.
     */
    const DECIBELS_PER_STEP = 1907825;
    /**
     * LOUDEST is the largest number the raising out of the decibel scale
     * will take. Past this the answer is the largest a whole number
     * holds, which no recording reaches.
     */
    const LOUDEST = 3967;
    /**
     * scalesFor gives the scale each quarter of a stretch is played at,
     * from the numbers the stretch wrote for them.
     *
     * The first quarter of a stretch that stands on its own names its
     * step outright; every other quarter names a step up or down from
     * the quarter before it. A caller hands in the last step of the
     * stretch before this one so that a stretch leaning on its
     * neighbor is counted from the right place.
     *
     * @param array $written the numbers the stretch wrote, one for each
     *     quarter, as SpeechFrameHeader read them
     * @param bool $independent whether the first quarter names its step
     *     outright rather than as a move from the stretch before
     * @param int $carried the step the stretch before this one ended at
     * @return array the scale of each quarter, and the step it ended at,
     *     as scales and ended_at
     */
    public static function scalesFor($written, $independent = true,
        $carried = 0)
    {
        $step = $carried;
        $scales = [];
        foreach ($written as $at => $one) {
            if ($at == 0 && $independent) {
                $step = max($one, $step - self::FALL_LIMIT);
            } else {
                $moved = $one + self::SMALLEST_STEP;
                /* Past a point a step counts double, so a stretch can
                   rise quickly without a longer table. */
                $doubling = 2 * self::LARGEST_STEP - self::LEVELS + $step;
                if ($moved > $doubling) {
                    $step += $moved * 2 - $doubling;
                } else {
                    $step += $moved;
                }
            }
            $step = max(0, min($step, self::LEVELS - 1));
            $scales[] = self::outOfDecibels(min(
                self::timesFraction(self::DECIBELS_PER_STEP, $step) +
                self::DECIBEL_OFFSET, self::LOUDEST));
        }
        return ["scales" => $scales, "ended_at" => $step];
    }
    /**
     * timesFraction multiplies a whole number by a fraction written out
     * of 65536, which is how the standard keeps fractions without using
     * decimals.
     *
     * @param int $number the number to multiply
     * @param int $fraction how many sixty-five thousand five hundred and
     *     thirty-sixths to take of it
     * @return int the answer, rounded down
     */
    public static function timesFraction($number, $fraction)
    {
        return ($number * $fraction) >> 16;
    }
    /**
     * outOfDecibels raises a loudness written on the decibel scale into
     * the plain multiplier that pulses are scaled by. The answer is
     * written out of 65536, so a multiplier of one comes back as 65536.
     *
     * The whole part of the input says which power of two the answer
     * sits near, and the fraction is filled in by a curve close enough
     * to the true one that the difference cannot be heard.
     *
     * @param int $on_scale the loudness on the decibel scale, in
     *     hundred-and-twenty-eighths
     * @return int the multiplier, written out of 65536
     */
    public static function outOfDecibels($on_scale)
    {
        if ($on_scale < 0) {
            return 0;
        }
        $out = 1 << ($on_scale >> 7);
        $fraction = $on_scale & 0x7F;
        $curve = $fraction + self::timesFraction(
            $fraction * (128 - $fraction), -174);
        if ($on_scale < 2048) {
            return $out + (($out * $curve) >> 7);
        }
        return $out + ($out >> 7) * $curve;
    }
}
X