<?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;
/**
* CeltFrameHeader reads what a stretch of sound says about itself before it
* says anything about the sound. A stretch opens with a short run of flags and
* settings, and every one of them changes how the rest of the stretch is read.
* Whether the stretch is silent decides whether there is anything after it at
* all. Whether the sound changed suddenly partway through decides whether the
* stretch is treated as one long piece or several short ones, and that in turn
* changes how the tones are laid out. How much room is left decides which of
* the flags are present at all: a stretch too short to hold one simply leaves
* it out, and both the writer and the reader have to leave it out at exactly
* the same point. That last part is what makes this worth its own file. Nothing
* here is difficult on its own, but the order is fixed, several of the flags
* are conditional on how much room remains, and reading one flag that was never
* written puts every later reading out of step and turns the rest of the
* stretch into noise. This follows RFC 6716, the specification of the Opus
* audio codec, in its section on the transform layer.
*/
class CeltFrameHeader
{
/**
* SILENCE_CHANCE is room needed before the silence flag is stored.
*/
const SILENCE_CHANCE = 15;
/**
* JUST_BEGUN is how much the reader has accounted for when a stretch has
* only just begun, which is how a stretch of sound alone is told from one
* that follows speech in the same piece.
*/
const JUST_BEGUN = 1;
/**
* FILTER_ROOM is room needed before the filter settings are stored.
*/
const FILTER_ROOM = 16;
/**
* FILTER_OCTAVES is how many different lengths of pitch the filter may be
* set to.
*/
const FILTER_OCTAVES = 6;
/**
* FILTER_SHORTEST is the shortest pitch the filter may be set to.
*/
const FILTER_SHORTEST = 16;
/**
* FILTER_STRENGTH_BITS is how many bits the filter's strength takes.
*/
const FILTER_STRENGTH_BITS = 3;
/**
* FILTER_STRENGTH_STEP is how much each step of the filter's strength is
* worth.
*/
const FILTER_STRENGTH_STEP = 0.09375;
/**
* FILTER_SHAPE_CHANCES is the likelihoods for which of the filter's three
* shapes was chosen, counting down from the whole.
*/
const FILTER_SHAPE_CHANCES = [2, 1, 0];
/**
* FILTER_SHAPE_BITS is how many bits those likelihoods add up to.
*/
const FILTER_SHAPE_BITS = 2;
/**
* SUDDEN_ROOM is room needed before the sudden change flag is stored.
*/
const SUDDEN_ROOM = 3;
/**
* ALONE_ROOM is room needed before the flag saying the stretch stands alone
* is stored.
*/
const ALONE_ROOM = 3;
/**
* SPREAD_ROOM is room needed before the spreading setting is stored.
*/
const SPREAD_ROOM = 4;
/**
* SPREAD_CHANCES is the likelihoods for how much the shapes were spread
* out, counting down from the whole.
*/
const SPREAD_CHANCES = [25, 23, 2, 0];
/**
* SPREAD_BITS is how many bits those likelihoods add up to.
*/
const SPREAD_BITS = 5;
/**
* SPREAD_USUAL is the spreading used when there was no room to store which
* was chosen.
*/
const SPREAD_USUAL = 2;
/**
* LENGTH_CHANGES is how the stored change of length is turned into a real
* one, given how long the stretch is and whether it changed suddenly.
*/
const LENGTH_CHANGES = [[0, -1, 0, -1, 0, -1, 0, -1],
[0, -1, 0, -2, 1, 0, 1, -1], [0, -2, 0, -3, 2, 0, 1, -1],
[0, -2, 0, -3, 3, 0, 1, -1]];
/**
* silent stores whether the stretch holds no sound at all.
* @var bool
*/
public $silent;
/**
* filter_on stores whether the filter that follows the sound is switched
* on.
* @var bool
*/
public $filter_on;
/**
* filter_pitch stores what pitch that filter is set to, in samples.
* @var int
*/
public $filter_pitch;
/**
* filter_strength stores how strongly that filter acts.
* @var float
*/
public $filter_strength;
/**
* filter_shape stores which of the filter's three shapes was chosen.
* @var int
*/
public $filter_shape;
/**
* Whether the sound changed suddenly partway through the stretch
* @var bool
*/
public $sudden;
/**
* alone stores whether the stretch stands on its own rather than following
* on from the one before.
* @var bool
*/
public $alone;
/**
* loudness stores how loud each band is, one run per channel.
* @var array
*/
public $loudness;
/**
* length_changes stores for each band, how its length was changed from the
* stretch's own.
* @var array
*/
public $length_changes;
/**
* spread stores how widely the shapes read for each band were spread
* before they were written, which the decoder undoes
* @var int
*/
public $spread;
/**
* readFrom reads everything a stretch says about itself, up to the point
* where it starts saying how the bits were shared out been doubled to reach
* this one before, one run per channel
*
* @param object $reader the reader at the start of a stretch
* @param int $doublings how many times the shortest stretch has
* @param array $before how loud each band was in the stretch
* @param int $first_band the lowest band the stretch carries
* @param int $past_last one past the highest band it carries
* @return object what the stretch says about itself
*/
public static function readFrom($reader, $doublings, $before,
$first_band, $past_last)
{
$header = new self();
$room = $reader->size * 8;
$used = $reader->bitsUsed();
/* Only a stretch that opens a piece says whether it is silent.
Where speech has already been read from the same piece there
is no such flag, and a stretch with no room left is taken as
silent without being asked. */
if ($used >= $room) {
$header->silent = true;
} else if ($used == self::JUST_BEGUN) {
$header->silent =
($reader->decodeBit(self::SILENCE_CHANCE) != 0);
} else {
$header->silent = false;
}
if ($header->silent) {
$reader->bits_used += $reader->size * 8 - $reader->bitsUsed();
}
$header->readFilter($reader, $room, $first_band);
$used = $reader->bitsUsed();
$header->sudden = false;
if ($doublings > 0 && $used + self::SUDDEN_ROOM <= $room) {
$header->sudden = ($reader->decodeBit(self::SUDDEN_ROOM) != 0);
$used = $reader->bitsUsed();
}
$header->alone = ($used + self::ALONE_ROOM <= $room) &&
($reader->decodeBit(self::ALONE_ROOM) != 0);
$header->loudness = CeltEnergy::readRough($reader, $before,
$header->alone, $doublings, $first_band, $past_last);
$header->readLengthChanges($reader, $doublings, $first_band,
$past_last);
$header->spread = self::SPREAD_USUAL;
if ($reader->bitsUsed() + self::SPREAD_ROOM <= $room) {
$header->spread = $reader->decodeFromTable(self::SPREAD_CHANCES,
self::SPREAD_BITS);
}
return $header;
}
/**
* readFilter reads the settings for the filter that runs over the sound
* after it has been put back together
*
* @param object $reader the reader partway through a stretch
* @param int $room how many bits the whole stretch holds
* @param int $first_band the lowest band the stretch carries
*/
public function readFilter($reader, $room, $first_band)
{
$this->filter_on = false;
$this->filter_pitch = 0;
$this->filter_strength = 0.0;
$this->filter_shape = 0;
if ($first_band != 0 ||
$reader->bitsUsed() + self::FILTER_ROOM > $room) {
return;
}
if ($reader->decodeBit(1) == 0) {
return;
}
$this->filter_on = true;
/* The pitch is stored as a rough range and then a position
within it, so that low pitches are given finer steps than
high ones. */
$range = $reader->decodeNumber(self::FILTER_OCTAVES);
$this->filter_pitch = (self::FILTER_SHORTEST << $range) +
$reader->decodeRawBits(4 + $range) - 1;
$steps = $reader->decodeRawBits(self::FILTER_STRENGTH_BITS);
$this->filter_strength = self::FILTER_STRENGTH_STEP * ($steps + 1);
if ($reader->bitsUsed() + self::FILTER_SHAPE_BITS <= $room) {
$this->filter_shape = $reader->decodeFromTable(
self::FILTER_SHAPE_CHANCES, self::FILTER_SHAPE_BITS);
}
}
/**
* readLengthChanges reads which bands were given a different length from
* the stretch's own. A stretch can afford finer detail in time for some
* bands and finer detail in pitch for others, so each band may be split
* differently. What is stored is only where the setting changes from one
* band to the next, since it usually holds across runs of bands, and one
* further choice covering the whole stretch. been doubled to reach this one
*
* @param object $reader the reader partway through a stretch
* @param int $doublings how many times the shortest stretch has
* @param int $first_band the lowest band the stretch carries
* @param int $past_last one past the highest band it carries
*/
public function readLengthChanges($reader, $doublings, $first_band,
$past_last)
{
$room = $reader->size * 8;
$used = $reader->bitsUsed();
$chance = $this->sudden ? 2 : 4;
$has_choice = ($doublings > 0 && $used + $chance + 1 <= $room);
if ($has_choice) {
$room--;
}
$running = 0;
$any_changed = 0;
$stored = array_fill(0, CeltBands::BAND_COUNT, 0);
for ($band = $first_band; $band < $past_last; $band++) {
if ($used + $chance <= $room) {
$running ^= $reader->decodeBit($chance);
$used = $reader->bitsUsed();
$any_changed |= $running;
}
$stored[$band] = $running;
$chance = $this->sudden ? 4 : 5;
}
$sudden_part = $this->sudden ? 4 : 0;
$choice = 0;
/* The further choice is only stored where it would make a
difference, so the reader has to work out whether it would
before deciding to look for it. */
if ($has_choice &&
self::LENGTH_CHANGES[$doublings][$sudden_part + $any_changed] !=
self::LENGTH_CHANGES[$doublings][$sudden_part + 2 + $any_changed]) {
$choice = $reader->decodeBit(1);
}
$this->length_changes = array_fill(0, CeltBands::BAND_COUNT, 0);
for ($band = $first_band; $band < $past_last; $band++) {
$this->length_changes[$band] = self::LENGTH_CHANGES[$doublings][
$sudden_part + 2 * $choice + $stored[$band]];
}
}
}