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