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