<?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;
/**
* CeltStereoBands reads a band that carries two channels. Two channels of the
* same moment are nearly the same sound, so storing them separately would store
* almost everything twice. Instead the two are turned into their average and
* their difference before being stored. The average carries nearly all of the
* sound and the difference carries only what the two channels disagree about,
* which for most recordings is little, so the difference costs far less than a
* second channel would. How the loudness divided between the average and the
* difference is stored for each band, in the same way a split band's lean is
* stored, and the two are put back into left and right afterwards. High up the
* range the ear stops telling the two channels apart by their difference and
* hears only their relative loudness. Above the band where that begins, the
* difference is not stored at all and the two channels are given the same sound
* at different loudnesses. A recording may also say that a stretch was stored
* as two plain channels side by side rather than as an average and a
* difference, which suits sound where the two channels have little in common.
* This follows RFC 6716, the specification of the Opus audio codec.
*/
class CeltStereoBands
{
/**
* BIT_PARTS is how finely room is counted, as parts of a bit.
*/
const BIT_PARTS = 3;
/**
* SPLIT_OFFSET is how much a split is favored in the usual case.
*/
const SPLIT_OFFSET = 4;
/**
* SPLIT_OFFSET_PAIR is the same where a band has only two slots, which is
* stored a different way.
*/
const SPLIT_OFFSET_PAIR = 16;
/**
* QUARTER_TURN is a quarter turn, which is what a lean of everything to one
* side comes to.
*/
const QUARTER_TURN = 16384;
/**
* EVEN_LEAN is half a quarter turn, the lean at which the two sides are
* even.
*/
const EVEN_LEAN = 8192;
/**
* LOWER_WEIGHT is how much likelier the lower half of the lean range is
* than the upper, since two channels usually agree more than they differ.
*/
const LOWER_WEIGHT = 3;
/**
* OPPOSED_CHANCE is how unlikely it is that the two channels are opposed
* rather than alike.
*/
const OPPOSED_CHANCE = 2;
/**
* TOO_QUIET is below this the two channels are taken as the same sound
* rather than merged, since merging them would divide by almost nothing.
*/
const TOO_QUIET = 0.0006;
/**
* readPair reads one band of two channels been doubled sound at different
* loudnesses
*
* @param object $reader the band reader walking this stretch
* @param array $left the first channel's slots, changed in place
* @param array $right the second channel's slots, changed in place
* @param int $at where in those the band begins
* @param int $width how many slots the band has
* @param int $room how much room the band gets
* @param int $groups how many groups the band is looked at in
* @param mixed $below the band to copy from, or null
* @param int $doublings how many times the shortest stretch has
* @param int $filled which groups of the band below hold something
* @param bool $joined whether the two channels are given the same
* @return int which groups this band holds
*/
public static function readPair($reader, &$left, &$right, $at, $width,
$room, $groups, $below, $doublings, $filled, $joined)
{
$lean = self::readLean($reader, $width, $room, $groups, $doublings,
$filled, $joined);
$room = $lean["room"];
$filled = $lean["filled"];
if ($width == 2) {
return self::readNarrowPair($reader, $left, $right, $at, $room,
$groups, $below, $doublings, $lean, $filled);
}
$middle_room = max(0, min($room, intdiv($room - $lean["gap"], 2)));
$side_room = $room - $middle_room;
$marks = 0;
if ($middle_room >= $side_room) {
$before = $reader->room_left;
$read = $reader->readBand($left, $at, $width, $middle_room,
$groups, $below, $doublings, $filled);
$marks = $read["marks"];
$spare = $middle_room - ($before - $reader->room_left);
if ($spare > 3 << self::BIT_PARTS && $lean["turn"] != 0) {
$side_room += $spare - (3 << self::BIT_PARTS);
}
$read = $reader->readBand($right, $at, $width, $side_room,
$groups, null, $doublings, $filled >> $groups,
$lean["side"]);
$marks |= $read["marks"];
} else {
$before = $reader->room_left;
$read = $reader->readBand($right, $at, $width, $side_room,
$groups, null, $doublings, $filled >> $groups,
$lean["side"]);
$marks = $read["marks"];
$spare = $side_room - ($before - $reader->room_left);
if ($spare > 3 << self::BIT_PARTS &&
$lean["turn"] != self::QUARTER_TURN) {
$middle_room += $spare - (3 << self::BIT_PARTS);
}
$read = $reader->readBand($left, $at, $width, $middle_room,
$groups, $below, $doublings, $filled);
$marks |= $read["marks"];
}
/* What lower bands are copied from is the average channel as
it stood here, before it was turned back into left and
right, at the loudness the split gave it. */
$mid_kept = [];
for ($slot = 0; $slot < $width; $slot++) {
$mid_kept[] = $lean["middle"] * $left[$at + $slot];
}
self::merge($left, $right, $at, $width, $lean["middle"]);
if ($lean["opposed"]) {
for ($slot = 0; $slot < $width; $slot++) {
$right[$at + $slot] = -$right[$at + $slot];
}
}
return ["marks" => $marks, "mid" => $mid_kept];
}
/**
* readNarrowPair reads a band of two slots, where the difference between
* the channels needs only a single bit because it can only point one of two
* ways been doubled was stored, which lower bands copy from
*
* @param object $reader the band reader walking this stretch
* @param array $left the first channel's slots, changed in place
* @param array $right the second channel's slots, changed in place
* @param int $at where in those the band begins
* @param int $room how much room the band gets
* @param int $groups how many groups the band is looked at in
* @param mixed $below the band to copy from, or null
* @param int $doublings how many times the shortest stretch has
* @param array $lean how the loudness divided
* @param int $filled which groups of the band below hold something
* @return array which groups this band holds and the channel that
*/
public static function readNarrowPair($reader, &$left, &$right, $at,
$room, $groups, $below, $doublings, $lean, $filled)
{
$side_room = 0;
if ($lean["turn"] != 0 && $lean["turn"] != self::QUARTER_TURN) {
$side_room = 1 << self::BIT_PARTS;
}
$middle_room = $room - $side_room;
$swapped = ($lean["turn"] > self::EVEN_LEAN);
$reader->room_left -= $side_room;
$sign = 1.0;
if ($side_room > 0) {
$sign = ($reader->reader->decodeRawBits(1) != 0) ? -1.0 : 1.0;
}
/* The louder of the two is read and the quieter follows from
it, turned a quarter way round, since the two are at right
angles to each other. */
$first = $swapped ? $right : $left;
$read = $reader->readBand($first, $at, 2, $middle_room, $groups,
$below, $doublings, $filled);
$marks = $read["marks"];
$second = [$at => -$sign * $first[$at + 1],
$at + 1 => $sign * $first[$at]];
if ($swapped) {
$right = $first;
$left[$at] = $second[$at];
$left[$at + 1] = $second[$at + 1];
} else {
$left = $first;
$right[$at] = $second[$at];
$right[$at + 1] = $second[$at + 1];
}
/* For a band this narrow the channel that was stored is the
one lower bands copy from, as it was read. */
$mid_kept = [$first[$at], $first[$at + 1]];
$middle = $lean["middle"];
$side = $lean["side"];
for ($slot = 0; $slot < 2; $slot++) {
$one = $middle * $left[$at + $slot];
$other = $side * $right[$at + $slot];
$left[$at + $slot] = $one - $other;
$right[$at + $slot] = $one + $other;
}
return ["marks" => $marks, "mid" => $mid_kept];
}
/**
* readLean reads how the loudness divided between the average of the two
* channels and their difference been doubled room
*
* @param object $reader the band reader walking this stretch
* @param int $width how many slots the band has
* @param int $room how much room the band gets
* @param int $groups how many groups the band is looked at in
* @param int $doublings how many times the shortest stretch has
* @param int $filled which groups of the band below hold something
* @param bool $joined whether the difference is not stored at all
* @return array how the loudness divided and what is left of the
*/
public static function readLean($reader, $width, $room, $groups,
$doublings, $filled, $joined)
{
$before = $reader->reader->bitsUsedFinely();
$ceiling = CeltPulseCache::widthLog($reader->band) +
$doublings * (1 << self::BIT_PARTS);
$offset = ($ceiling >> 1) -
(($width == 2) ? self::SPLIT_OFFSET_PAIR : self::SPLIT_OFFSET);
$steps = CeltBandReader::leanSteps($width, $room, $offset, $ceiling);
if ($joined) {
$steps = 1;
}
$turn = 0;
$opposed = false;
if ($steps != 1) {
if ($width > 2) {
$turn = self::readWeighted($reader, $steps);
} else {
$turn = $reader->reader->decodeNumber($steps + 1);
}
$turn = intdiv($turn * self::QUARTER_TURN, $steps);
} else {
/* Where the difference is not stored, one bit may still say
that the two channels are opposed rather than alike. */
if ($room > (2 << self::BIT_PARTS) &&
$reader->room_left > (2 << self::BIT_PARTS)) {
$opposed = ($reader->reader->decodeBit(
self::OPPOSED_CHANCE) != 0);
}
}
$spent = $reader->reader->bitsUsedFinely() - $before;
$reader->room_left -= $spent;
$room -= $spent;
if ($turn == 0) {
$middle = 32767 / 32768.0;
$side = 0.0;
$filled &= (1 << $groups) - 1;
$gap = -self::QUARTER_TURN;
} else if ($turn == self::QUARTER_TURN) {
$middle = 0.0;
$side = 32767 / 32768.0;
$filled &= ((1 << $groups) - 1) << $groups;
$gap = self::QUARTER_TURN;
} else {
$across = CeltShape::steadyCos($turn);
$up = CeltShape::steadyCos(self::QUARTER_TURN - $turn);
$middle = $across / 32768.0;
$side = $up / 32768.0;
$gap = CeltShape::fracTimes(($width - 1) << 7,
CeltShape::leanLog($up, $across));
}
return ["turn" => $turn, "middle" => $middle, "side" => $side,
"gap" => $gap, "room" => $room, "filled" => $filled,
"opposed" => $opposed];
}
/**
* readWeighted reads a lean where the lower half of the range is likelier
* than the upper, which is how a two channel split is stored because two
* channels usually agree more than they differ
*
* @param object $reader the band reader walking this stretch
* @param int $steps how many steps the lean is stored in
* @return int which step the lean fell on
*/
public static function readWeighted($reader, $steps)
{
$half = intdiv($steps, 2);
$whole = self::LOWER_WEIGHT * ($half + 1) + $half;
$found = $reader->reader->decode($whole);
if ($found < ($half + 1) * self::LOWER_WEIGHT) {
$turn = intdiv($found, self::LOWER_WEIGHT);
} else {
$turn = $half + 1 + ($found - ($half + 1) * self::LOWER_WEIGHT);
}
if ($turn <= $half) {
$below = self::LOWER_WEIGHT * $turn;
$above = self::LOWER_WEIGHT * ($turn + 1);
} else {
$below = ($turn - 1 - $half) + ($half + 1) * self::LOWER_WEIGHT;
$above = ($turn - $half) + ($half + 1) * self::LOWER_WEIGHT;
}
$reader->reader->update($below, $above, $whole);
return $turn;
}
/**
* merge turns the average of the two channels and their difference back
* into left and right, each brought to the loudness it should have
*
* @param array $left the average, replaced by the left channel
* @param array $right the difference, replaced by the right
* @param int $at where in those the band begins
* @param int $width how many slots the band has
* @param float $middle how loud the average is
*/
public static function merge(&$left, &$right, $at, $width, $middle)
{
$crossed = 0.0;
$difference = 0.0;
for ($slot = 0; $slot < $width; $slot++) {
$crossed += $right[$at + $slot] * $left[$at + $slot];
$difference += $right[$at + $slot] * $right[$at + $slot];
}
$crossed *= $middle;
$one = $middle * $middle + $difference - 2 * $crossed;
$other = $middle * $middle + $difference + 2 * $crossed;
/* Where either side comes to almost nothing, dividing by it
would run away, so the two channels are given the same
sound. */
if ($one < self::TOO_QUIET || $other < self::TOO_QUIET) {
for ($slot = 0; $slot < $width; $slot++) {
$right[$at + $slot] = $left[$at + $slot];
}
return;
}
$one_gain = 1.0 / sqrt($one);
$other_gain = 1.0 / sqrt($other);
for ($slot = 0; $slot < $width; $slot++) {
$average = $middle * $left[$at + $slot];
$differs = $right[$at + $slot];
$left[$at + $slot] = $one_gain * ($average - $differs);
$right[$at + $slot] = $other_gain * ($average + $differs);
}
}
}