<?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;
}
}