/ src / library / av_processing / CeltShape.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;

/**
 * CeltShape the steps that turn a band's stored pattern of pulses into the
 * tones that band actually holds. A pattern of pulses is a coarse thing. Only
 * so many slots carry a pulse and the rest carry nothing, so played as it
 * stands a band would sound thin and metallic, with the silence between the
 * pulses audible as a kind of ringing. The cure is to smear the pattern across
 * the band before using it, by turning every neighboring pair of slots slightly
 * into each other. That leaves the band's overall loudness untouched, since
 * turning a pair does not change how long the pair is, but it fills the gaps.
 * How far to turn depends on how many pulses the band was given. A band with
 * plenty of pulses has few gaps and is left nearly alone; a band with two or
 * three is smeared hard. How far the writer turned is not stored, because both
 * sides can work it out from the pulse count and the one setting the stretch
 * does store. The other steps here are the pairing that lets a band be looked
 * at as either finer in time or finer in pitch, the two approximations used
 * when a split band says which way it leaned, and the last step of all, where a
 * band's shape and its loudness are put back together into the tones the
 * transform will turn into sound. This follows RFC 6716, the specification of
 * the Opus audio codec, in its sections on the band shapes.
 */
class CeltShape
{
    /**
     * SMEAR_FACTORS is how hard to smear the pattern, for each of the three
     * settings that do any smearing at all.
     */
    const SMEAR_FACTORS = [15, 10, 5];
    /**
     * NO_SMEAR is the setting that leaves the pattern alone.
     */
    const NO_SMEAR = 0;
    /**
     * PAIR_SCALE is what a pair of slots is multiplied by when it is paired up,
     * so that pairing leaves the loudness alone.
     */
    const PAIR_SCALE = 0.70710678;
    /**
     * WHOLE is the whole the two approximations below count against.
     */
    const WHOLE = 32768;
    /**
     * QUARTER_TURN is half that whole, which stands for a quarter turn.
     */
    const QUARTER_TURN = 16384;
    /**
     * Smears a band's pattern of pulses across its slots, or undoes
     * the smearing
     *
     * @param array $shape the band's slots, changed in place
     * @param int $length how many slots there are
     * @param int $undo minus one to smear as a reader does, one to
     *      undo it
     * @param int $groups how many groups the band is looked at in
     * @param int $pulses how many pulses the band was given
     * @param int $setting how hard the stretch said to smear
     */
    public static function smear(&$shape, $length, $undo, $groups, $pulses,
        $setting)
    {
        /* A band with a pulse in half its slots or more has no gaps
           worth filling, and one setting says to leave it alone. */
        if (2 * $pulses >= $length || $setting == self::NO_SMEAR) {
            return;
        }
        $factor = self::SMEAR_FACTORS[$setting - 1];
        $gain = $length / ($length + $factor * $pulses);
        $turn = 0.5 * $gain * $gain;
        $across = cos(0.5 * M_PI * $turn);
        $up = cos(0.5 * M_PI * (1.0 - $turn));
        $wide = 0;
        /* Where a band is much longer than its groups, a second
           turning is made across a wider spacing, so that the smearing
           reaches further than neighboring slots. */
        if ($length >= 8 * $groups) {
            $wide = 1;
            while (($wide * $wide + $wide) * $groups + ($groups >> 2) <
                $length) {
                $wide++;
            }
        }
        $each = intdiv($length, $groups);
        for ($group = 0; $group < $groups; $group++) {
            $at = $group * $each;
            if ($undo < 0) {
                if ($wide) {
                    self::turnPairs($shape, $at, $each, $wide, $up, $across);
                }
                self::turnPairs($shape, $at, $each, 1, $across, $up);
            } else {
                self::turnPairs($shape, $at, $each, 1, $across, -$up);
                if ($wide) {
                    self::turnPairs($shape, $at, $each, $wide, $up, -$across);
                }
            }
        }
    }
    /**
     * turnPairs turns every pair of slots a fixed distance apart slightly into
     * each other, first walking up the band and then back down it
     *
     * @param array $shape the band's slots, changed in place
     * @param int $at where in the array the band begins
     * @param int $length how many slots the band has
     * @param int $apart how far apart the slots of a pair sit
     * @param float $across how much of each slot stays where it is
     * @param float $up how much of it moves to its partner
     */
    public static function turnPairs(&$shape, $at, $length, $apart, $across,
        $up)
    {
        for ($i = 0; $i < $length - $apart; $i++) {
            $first = $shape[$at + $i];
            $second = $shape[$at + $i + $apart];
            $shape[$at + $i + $apart] = $across * $second + $up * $first;
            $shape[$at + $i] = $across * $first - $up * $second;
        }
        /* The second walk goes back down the band, so that a slot near
           the start is reached by the smearing as well as one near the
           end. */
        for ($i = $length - 2 * $apart - 1; $i >= 0; $i--) {
            $first = $shape[$at + $i];
            $second = $shape[$at + $i + $apart];
            $shape[$at + $i + $apart] = $across * $second + $up * $first;
            $shape[$at + $i] = $across * $first - $up * $second;
        }
    }
    /**
     * pairUp pairs up the slots of a band, replacing each pair by its sum and
     * its difference. This is how a band is looked at as either finer in time
     * or finer in pitch. Doing it twice gives back what was started with, so
     * the same step serves in both directions.
     *
     * @param array $shape the band's slots, changed in place
     * @param int $at where in the array the band begins
     * @param int $length how many slots to pair up
     * @param int $apart how far apart the slots of a pair sit
     */
    public static function pairUp(&$shape, $at, $length, $apart)
    {
        $pairs = $length >> 1;
        for ($group = 0; $group < $apart; $group++) {
            for ($pair = 0; $pair < $pairs; $pair++) {
                $first = self::PAIR_SCALE *
                    $shape[$at + $apart * 2 * $pair + $group];
                $second = self::PAIR_SCALE *
                    $shape[$at + $apart * (2 * $pair + 1) + $group];
                $shape[$at + $apart * 2 * $pair + $group] = $first + $second;
                $shape[$at + $apart * (2 * $pair + 1) + $group] =
                    $first - $second;
            }
        }
    }
    /**
     * steadyCos a rounding of the cosine that comes out the same on every
     * machine. Where a band is split, how far the split leaned is worked out
     * rather than stored, and the working feeds back into how the room is
     * shared. So it has to give the same answer everywhere, which an ordinary
     * cosine cannot promise. This uses whole numbers only.
     *
     * @param int $turn how far round, where a quarter turn is 16384
     * @return int the cosine, where one is 32768
     */
    public static function steadyCos($turn)
    {
        /* The working is kept in a sixteen bit signed value
           throughout, and at the far end of the range it wraps round.
           That wrapping is part of the answer rather than a fault, so
           it is done here as well. */
        $squared = self::asSigned((4096 + $turn * $turn) >> 13);
        $value = self::asSigned((self::WHOLE - 1 - $squared) +
            self::fracTimes($squared, -7651 + self::fracTimes($squared,
            8277 + self::fracTimes(-626, $squared))));
        return 1 + $value;
    }
    /**
     * leanLog says how far a split leaned, as a logarithm, worked out
     * the wayay
     * on every machine
     *
     * @param int $up how much of the split went one way
     * @param int $across how much went the other
     * @return int the logarithm of their ratio
     */
    public static function leanLog($up, $across)
    {
        $across_width = RangeDecoder::bitCount($across);
        $up_width = RangeDecoder::bitCount($up);
        $across <<= 15 - $across_width;
        $up <<= 15 - $up_width;
        return ($up_width - $across_width) * (1 << 11) +
            self::fracTimes($up, self::fracTimes($up, -2597) + 7932) -
            self::fracTimes($across, self::fracTimes($across, -2597) + 7932);
    }
    /**
     * fracTimes multiplies two numbers that both stand for fractions, keeping
     * the answer as a fraction of the same size
     *
     * @param int $first the first number
     * @param int $second the second
     * @return int their product, as a fraction of the same size
     */
    public static function fracTimes($first, $second)
    {
        $first = self::asSigned($first);
        $second = self::asSigned($second);
        return (16384 + $first * $second) >> 15;
    }
    /**
     * asSigned reads a number as a signed one of the width these workings use
     *
     * @param int $value the number to read
     * @return int the same number, read as a signed one
     */
    public static function asSigned($value)
    {
        $value &= 0xFFFF;
        return ($value >= 0x8000) ? $value - 0x10000 : $value;
    }
    /**
     * scaleTo scales a band's slots so the whole band comes to a given loudness
     *
     * @param array $shape the band's slots, changed in place
     * @param int $at where in the array the band begins
     * @param int $length how many slots the band has
     * @param float $loudness what the band should come to
     */
    public static function scaleTo(&$shape, $at, $length, $loudness)
    {
        $total = 0.0;
        for ($i = 0; $i < $length; $i++) {
            $total += $shape[$at + $i] * $shape[$at + $i];
        }
        if ($total <= 0.0) {
            return;
        }
        $scale = $loudness / sqrt($total);
        for ($i = 0; $i < $length; $i++) {
            $shape[$at + $i] *= $scale;
        }
    }
    /**
     * readBand reads one band's shape out of a stretch and smears it
     *
     * @param object $reader the reader partway through a stretch
     * @param int $length how many slots the band has
     * @param int $pulses how many pulses the band was given
     * @param int $setting how hard the stretch said to smear
     * @param int $groups how many groups the band is looked at in
     * @param float $loudness what the band should come to
     * @return array the band's slots, and which groups hold anything
     */
    public static function readBand($reader, $length, $pulses, $setting,
        $groups, $loudness)
    {
        $pattern = Pvq::readFrom($reader, $length, $pulses);
        $total = 0.0;
        foreach ($pattern as $slot) {
            $total += $slot * $slot;
        }
        $scale = ($total > 0.0) ? $loudness / sqrt($total) : 0.0;
        $shape = $pattern;
        foreach ($shape as $slot => $value) {
            $shape[$slot] = $value * $scale;
        }
        self::smear($shape, $length, -1, $groups, $pulses, $setting);
        return ["shape" => $shape,
            "filled" => self::filledGroups($pattern, $length, $groups)];
    }
    /**
     * filledGroups says which groups of a band ended up holding anything at
     * all. A group left empty is a group the sound will fall silent in, and a
     * later step fills those back in with noise rather than letting them ring.
     * something
     *
     * @param array $pattern how many pulses fall in each slot
     * @param int $length how many slots the band has
     * @param int $groups how many groups the band is looked at in
     * @return int a bit for each group, set where the group holds
     */
    public static function filledGroups($pattern, $length, $groups)
    {
        if ($groups <= 1) {
            return 1;
        }
        $each = intdiv($length, $groups);
        $filled = 0;
        for ($group = 0; $group < $groups; $group++) {
            $any = 0;
            for ($slot = 0; $slot < $each; $slot++) {
                $any |= $pattern[$group * $each + $slot];
            }
            if ($any != 0) {
                $filled |= 1 << $group;
            }
        }
        return $filled;
    }
    /**
     * toTones puts each band's shape back together with its loudness, giving
     * the tones the transform turns into sound been doubled
     *
     * @param array $shape every band's slots, laid end to end
     * @param array $loudness how loud each band is
     * @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
     * @param int $length how many tones the stretch has in all
     * @return array the tones
     */
    public static function toTones($shape, $loudness, $doublings,
        $first_band, $past_last, $length)
    {
        $tones = array_fill(0, $length, 0.0);
        $edges = CeltBands::edgesFor($doublings);
        for ($band = $first_band; $band < $past_last; $band++) {
            $scale = CeltEnergy::scaleFor($loudness[$band], $band);
            for ($slot = $edges[$band]; $slot < $edges[$band + 1]; $slot++) {
                if ($slot < $length) {
                    $tones[$slot] = $shape[$slot] * $scale;
                }
            }
        }
        return $tones;
    }
}
X