<?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;
/**
* AacFrame writes one stretch of sound as an AAC frame. A frame says how it was
* made, how loud each band was kept, and then the rounded tones themselves.
* Most of it is bookkeeping the reader needs before it can make sense of the
* sound: which stretch shape was used, how many bands carry anything, which
* code table each run of bands was written with, and the step size each band
* was rounded at. The step sizes are what decide both the size of the frame and
* how it sounds. A smaller step keeps a band more finely and costs more bits.
* There is no room to keep every band finely, so a frame is written over and
* over with the steps pushed up or down together until it fits the room
* allowed. That is a crude way to choose: a real encoder models what a listener
* can and cannot hear and spends its bits where they are noticed. This spends
* them evenly, which costs quality at a given size and is the obvious next
* improvement. Bands whose tones all round to nothing are named as empty and
* cost almost nothing, which is where most of the saving comes from at the top
* of the range where there is little sound. This follows the AAC standard,
* ISO/IEC 14496-3.
*/
class AacFrame
{
/**
* ONE_CHANNEL is names a run of sound for one channel.
*/
const ONE_CHANNEL = 0;
/**
* END_OF_FRAME is the element name that closes a frame. A reader
* stops at it rather than counting elements.
*/
const END_OF_FRAME = 7;
/**
* NAME_BITS is how many bits an element's name takes.
*/
const NAME_BITS = 3;
/**
* EMPTY_TABLE is the code table meaning a band holds nothing.
*/
const EMPTY_TABLE = 0;
/**
* TABLE_BITS is how many bits the name of a code table takes.
*/
const TABLE_BITS = 4;
/**
* RUN_BITS is how many bits the length of a run of bands takes.
*/
const RUN_BITS = 5;
/**
* RUN_CARRIES_ON is the run length that means the run carries on into
* another length.
*/
const RUN_CARRIES_ON = 31;
/**
* BASE_BITS is how many bits the loudness a frame counts from takes.
*/
const BASE_BITS = 8;
/**
* BASE_LIMIT is the largest loudness a frame may record. Anything
* louder is held down to it, since the field carries eight bits.
*/
const BASE_LIMIT = 255;
/**
* BAND_COUNT_BITS is how many bits say how many bands carry anything.
*/
const BAND_COUNT_BITS = 6;
/**
* LONG_SHAPE is the stretch shape meaning one long stretch.
*/
const LONG_SHAPE = 0;
/**
* SHAPE_BITS is how many bits the stretch shape takes.
*/
const SHAPE_BITS = 2;
/**
* ESCAPE_AT is the value at which a rounded number stops being written
* whole.
*/
const ESCAPE_AT = 16;
/**
* ESCAPE_BASE is how many bits of a number the escape leaves out of its
* count.
*/
const ESCAPE_BASE = 4;
/**
* DECODER_SCALE is how much larger the tones this transform gives are than
* the ones the standard's decoder works in. The two differ by a fixed
* factor of two to the sixteenth, measured by writing tones of known size
* and reading back what a decoder makes of them. Applying it here rather
* than changing the transform keeps the transform the same one the reading
* side uses.
*/
const DECODER_SCALE = 65536.0;
/**
* FINENESS_SPREAD is how much finer than the loudest band of a frame any
* other band may be kept. A band far quieter than the rest holds little but
* the numerical dust left by the transform, and keeping that finely both
* wastes room and turns the dust into something audible. Each step is a
* quarter of a power of two, so this allows a spread of about thirty seven
* decibels, which is more than a listener will follow between one band and
* another of the same moment.
*/
const FINENESS_SPREAD = 25;
/**
* write writes one stretch of tones as a frame of a given size bits it came
* to
*
* @param array $tones the stretch's tones
* @param int $room how many bits the frame may take
* @param bool $smooth whether the softer fade was used
* @return array the frame's bytes, the steps chosen, and how many
*/
public static function write($tones, $room, $smooth = false)
{
$bands = AacBands::longBandCount();
$scaled = [];
foreach ($tones as $tone) {
$scaled[] = $tone * self::DECODER_SCALE;
}
$tones = $scaled;
$squashed = [];
foreach ($tones as $tone) {
$squashed[] = pow(abs($tone), AacQuantizer::SQUASH);
}
$floor = [];
$coarsest = 0;
for ($band = 0; $band < $bands; $band++) {
$floor[$band] = AacQuantizer::smallestStepFor($tones,
AacBands::LONG_EDGES[$band], AacBands::LONG_EDGES[$band + 1]);
$coarsest = max($coarsest, $floor[$band]);
}
for ($band = 0; $band < $bands; $band++) {
$floor[$band] = max($floor[$band],
$coarsest - self::FINENESS_SPREAD);
}
/* The steps are pushed up together until the frame fits, and
the smallest push that fits is the one kept. */
$low = 0;
$high = 120;
$keep = null;
while ($low <= $high) {
$middle = ($low + $high) >> 1;
/* The tries only need their length, so they are counted
rather than built. */
$tried = self::build($tones, $floor, $middle, $smooth,
$squashed, true);
if ($tried["bits"] <= $room) {
$keep = $middle;
$high = $middle - 1;
} else {
$low = $middle + 1;
}
}
if ($keep === null) {
$keep = $high + 1;
}
return self::build($tones, $floor, $keep, $smooth, $squashed);
}
/**
* build writes one frame at a given push of the steps
*
* @param array $tones the stretch's tones
* @param array $floor the smallest step each band may use
* @param int $push how much to push every step up by
* @param bool $smooth whether the softer fade was used
* @param array $squashed the tones already squashed, or null
* @param bool $counting whether only the length is wanted
* @return array the frame's bytes and the steps chosen
*/
public static function build($tones, $floor, $push, $smooth,
$squashed = null, $counting = false)
{
$bands = AacBands::longBandCount();
$steps = [];
$rounded = [];
$carries = [];
for ($band = 0; $band < $bands; $band++) {
$step = min(self::BASE_LIMIT, $floor[$band] + $push);
$from = AacBands::LONG_EDGES[$band];
$past = AacBands::LONG_EDGES[$band + 1];
$numbers = AacQuantizer::roundBand($tones, $from, $past, $step,
$squashed);
$any = false;
foreach ($numbers as $number) {
if ($number != 0) {
$any = true;
break;
}
}
$steps[$band] = $step;
$rounded[$band] = $numbers;
$carries[$band] = $any;
}
$steps = self::clampSteps($steps, $carries);
for ($band = 0; $band < $bands; $band++) {
if ($carries[$band]) {
$rounded[$band] = AacQuantizer::roundBand($tones,
AacBands::LONG_EDGES[$band],
AacBands::LONG_EDGES[$band + 1], $steps[$band],
$squashed);
}
}
$writer = $counting ? new BitCounter() : new BitWriter();
$writer->add(self::ONE_CHANNEL, self::NAME_BITS);
$writer->add(0, 4);
$base = self::baseLoudness($steps, $carries);
$writer->add($base, self::BASE_BITS);
self::writeShape($writer, $bands, $smooth);
self::writeRuns($writer, $bands, $carries);
self::writeSteps($writer, $bands, $steps, $carries, $base);
$writer->add(0, 1);
$writer->add(0, 1);
$writer->add(0, 1);
self::writeTones($writer, $bands, $rounded, $carries);
$writer->add(self::END_OF_FRAME, self::NAME_BITS);
return ["bytes" => $writer->finish(), "steps" => $steps,
"bits" => $writer->length()];
}
/**
* clampSteps brings each band's step within reach of the band before it. A
* step is written as how far it sits from the step before, and the table of
* those distances only reaches so far. A quiet band next to a loud one can
* want a step further away than that, so such a step is pulled in until it
* is reachable. Pulling it in costs that band some fineness, which is the
* price of being able to say where it sits at all.
*
* @param array $steps the step each band would like
* @param array $carries whether each band carries anything
* @return array the steps once each is within reach of the last
*/
public static function clampSteps($steps, $carries)
{
$which = [];
foreach ($carries as $band => $any) {
if ($any) {
$which[] = $band;
}
}
$span = AacTables::LOUDNESS_SPAN;
/* A step may only ever be raised, never lowered, because a
step below a band's own floor lets that band's tones grow
past what may be written and they would be cut short. Two
passes each way settle it, since raising a step only makes
its neighbors easier to reach. */
for ($turn = 0; $turn < 3; $turn++) {
$moved = false;
for ($i = 1; $i < count($which); $i++) {
$want = $steps[$which[$i - 1]] - $span;
if ($steps[$which[$i]] < $want) {
$steps[$which[$i]] = $want;
$moved = true;
}
}
for ($i = count($which) - 2; $i >= 0; $i--) {
$want = $steps[$which[$i + 1]] - $span;
if ($steps[$which[$i]] < $want) {
$steps[$which[$i]] = $want;
$moved = true;
}
}
if (!$moved) {
break;
}
}
foreach ($which as $band) {
$steps[$band] = max(0,
min(AacQuantizer::LARGEST_STEP, $steps[$band]));
}
return $steps;
}
/**
* baseLoudness the loudness the frame counts its steps from, which is the
* step of the first band that carries anything
*
* @param array $steps the step each band uses
* @param array $carries whether each band carries anything
* @return int the loudness to count from
*/
public static function baseLoudness($steps, $carries)
{
foreach ($carries as $band => $any) {
if ($any) {
return max(0, min(self::BASE_LIMIT, $steps[$band]));
}
}
return AacQuantizer::MIDDLE_STEP;
}
/**
* writeShape writes how the stretch was shaped and how many bands may carry
* sound
*
* @param object $writer the run of bits being built
* @param int $bands how many bands there are
* @param bool $smooth whether the softer fade was used
*/
public static function writeShape($writer, $bands, $smooth)
{
$writer->add(0, 1);
$writer->add(self::LONG_SHAPE, self::SHAPE_BITS);
$writer->add($smooth ? 1 : 0, 1);
$writer->add($bands, self::BAND_COUNT_BITS);
$writer->add(0, 1);
}
/**
* writeRuns writes which code table each run of bands was written with.
* Bands are gathered into runs that share a table, and only the table and
* the length of the run are written, since a run of empty bands at the top
* of the range is common and costs almost nothing said that way.
*
* @param object $writer the run of bits being built
* @param int $bands how many bands there are
* @param array $carries whether each band carries anything
*/
public static function writeRuns($writer, $bands, $carries)
{
$at = 0;
while ($at < $bands) {
$table = $carries[$at] ? AacTables::SOUND_TABLE :
self::EMPTY_TABLE;
$length = 1;
while ($at + $length < $bands &&
($carries[$at + $length] ? AacTables::SOUND_TABLE :
self::EMPTY_TABLE) == $table) {
$length++;
}
$writer->add($table, self::TABLE_BITS);
$left = $length;
while ($left >= self::RUN_CARRIES_ON) {
$writer->add(self::RUN_CARRIES_ON, self::RUN_BITS);
$left -= self::RUN_CARRIES_ON;
}
$writer->add($left, self::RUN_BITS);
$at += $length;
}
}
/**
* writeSteps writes each band's step size, as a step from the band before
* it
*
* @param object $writer the run of bits being built
* @param int $bands how many bands there are
* @param array $steps the step each band uses
* @param array $carries whether each band carries anything
* @param int $base the loudness the frame counts from
*/
public static function writeSteps($writer, $bands, $steps, $carries,
$base)
{
$last = $base;
for ($band = 0; $band < $bands; $band++) {
if (!$carries[$band]) {
continue;
}
/* A step is written for every band that carries anything,
the first one included. The loudness the frame counts
from is only where the running total starts, not the
first band's step, so leaving the first one out would
put every step after it out of place. */
$gap = $steps[$band] - $last;
$which = $gap + AacTables::LOUDNESS_SPAN;
if ($which < 0) {
$which = 0;
}
if ($which >= count(AacTables::LOUDNESS_CODES)) {
$which = count(AacTables::LOUDNESS_CODES) - 1;
}
$writer->add(AacTables::LOUDNESS_CODES[$which],
AacTables::LOUDNESS_CODE_BITS[$which]);
$last += $which - AacTables::LOUDNESS_SPAN;
}
}
/**
* writeTones writes the rounded tones themselves, a pair at a time
*
* @param object $writer the run of bits being built
* @param int $bands how many bands there are
* @param array $rounded the rounded tones of each band
* @param array $carries whether each band carries anything
*/
public static function writeTones($writer, $bands, $rounded, $carries)
{
/* The tables are held in locals so the busiest loop in the
encoder is not fetching class constants pair by pair. */
$codes = AacTables::SOUND_CODES;
$code_bits = AacTables::SOUND_CODE_BITS;
$range = AacTables::PAIR_RANGE;
$escape_at = self::ESCAPE_AT;
for ($band = 0; $band < $bands; $band++) {
if (!$carries[$band]) {
continue;
}
$numbers = $rounded[$band];
$count = count($numbers);
for ($at = 0; $at + 1 < $count; $at += 2) {
$first = $numbers[$at];
$second = $numbers[$at + 1];
$big_one = abs($first);
$big_two = abs($second);
$one = ($big_one < $escape_at) ? $big_one : $escape_at;
$two = ($big_two < $escape_at) ? $big_two : $escape_at;
$where = $one * $range + $two;
$joined = $codes[$where];
$bits = $code_bits[$where];
if ($first != 0) {
$joined = ($joined << 1) | ($first < 0 ? 1 : 0);
$bits++;
}
if ($second != 0) {
$joined = ($joined << 1) | ($second < 0 ? 1 : 0);
$bits++;
}
$writer->add($joined, $bits);
if ($big_one >= $escape_at) {
self::writeRest($writer, $big_one);
}
if ($big_two >= $escape_at) {
self::writeRest($writer, $big_two);
}
}
}
}
/**
* writeRest writes the rest of a number too large to fit the code table, as
* a count of bits followed by the number itself
*
* @param object $writer the run of bits being built
* @param int $size how large the number is
*/
public static function writeRest($writer, $size)
{
$width = 0;
while ((1 << ($width + 1)) <= $size) {
$width++;
}
for ($i = 0; $i < $width - self::ESCAPE_BASE; $i++) {
$writer->add(1, 1);
}
$writer->add(0, 1);
$writer->add($size - (1 << $width), $width);
}
}