/ src / library / av_processing / CeltStereoBands.php
<?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);
        }
    }
}
X