<?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;
/**
* CeltSynthesis turns the tones of a stretch back into samples, and joins each
* stretch to the one before it. The transform that does the turning has already
* been written and checked. What is left is the joining, and the shape of the
* fade the joining uses. That fade is unusual. An ordinary one rises across a
* whole stretch and falls across the next, so neighboring stretches overlap by
* their whole length and half the sound is being faded at any moment. Opus
* cannot afford that, because the overlap is delay: a player cannot give out a
* sample until both stretches covering it have arrived. So the fade here is
* flat across nearly all of a stretch and tapers only over a short run at each
* end, which is two and a half milliseconds rather than twenty. The stretches
* still cancel each other's errors exactly, because the taper satisfies the
* same condition an ordinary fade does. The last step undoes a tilt the
* recording was given before it was compressed. Sound has more of its energy at
* low pitches than high, and compressing it evenly would spend too much on the
* low end, so the high end is lifted before compressing and let back down here.
* This follows RFC 6716, the specification of the Opus audio codec, in its
* section on the transform layer.
*/
class CeltSynthesis
{
/**
* TAPER is how long the taper at each end of a stretch is, in samples at
* the shortest stretch's rate.
*/
const TAPER = 120;
/**
* TILT_BACK is how much of the tilt is let back down at each sample.
*/
const TILT_BACK = 0.8500061035;
/**
* fades stores the fade used to join stretches, worked out once for each
* length and kept
*
* @var array
*/
public static $fades = [];
/**
* fadeFor the fade for a stretch of a given length. It is nothing at all
* outside a middle region, rises across a short taper, holds flat, and
* falls again, so that only the tapers overlap between one stretch and the
* next. half how many samples the fade covers
*
* @param int $tones how many tones the stretch holds, which is
* @param int $taper how long each taper is
* @return array the fade, one value per sample
*/
public static function fadeFor($tones, $taper)
{
$key = $tones . ":" . $taper;
if (isset(self::$fades[$key])) {
return self::$fades[$key];
}
$length = 2 * $tones;
$fade = array_fill(0, $length, 0.0);
$begins = intdiv($tones - $taper, 2);
for ($i = 0; $i < $tones; $i++) {
if ($i < $begins) {
$value = 0.0;
} else if ($i < $begins + $taper) {
$value = self::taperAt($i - $begins, $taper);
} else {
$value = 1.0;
}
$fade[$i] = $value;
/* The second half is the first half backwards, which is
what makes neighboring stretches cancel. */
$fade[$length - 1 - $i] = $value;
}
self::$fades[$key] = $fade;
return $fade;
}
/**
* taperAt how far up the taper has risen at a given point
*
* @param int $at how far along the taper
* @param int $taper how long the taper is
* @return float how far up it has risen, from nothing to one
*/
public static function taperAt($at, $taper)
{
$along = sin(0.5 * M_PI * ($at + 0.5) / $taper);
return sin(0.5 * M_PI * $along * $along);
}
/**
* toSamples turns a stretch of tones into samples and joins it to what came
* before which is more than one where the sound changed suddenly before,
* replaced by what this one leaves hanging
*
* @param array $tones the stretch's tones
* @param int $blocks how many blocks the stretch is split into,
* @param array $tail what was left hanging over from the stretch
* @return array the samples this stretch settles
*/
public static function toSamples($tones, $blocks, &$tail)
{
$length = count($tones);
$each = intdiv($length, $blocks);
$taper = self::TAPER;
$half = intdiv($taper, 2);
$running = array_fill(0, $length + $taper, 0.0);
for ($i = 0; $i < $half; $i++) {
$running[$i] = $tail[$i] ?? 0.0;
}
$rise = [];
for ($i = 0; $i < $taper; $i++) {
$rise[] = self::taperAt($i, $taper);
}
$middle = intdiv($each, 2);
$scale = $each / 2.0;
for ($block = 0; $block < $blocks; $block++) {
/* A stretch split into blocks keeps its tones interleaved,
so a block's own tones are every so many along. */
$mine = [];
for ($tone = 0; $tone < $each; $tone++) {
$mine[] = $tones[$tone * $blocks + $block];
}
$samples = Mdct::forSize($each)->inverse($mine);
$at = $block * $each;
/* The middle of the transform's result is laid down as it
is, reaching a little past this block's end. */
for ($i = 0; $i < $each; $i++) {
$running[$at + $half + $i] = $samples[$middle + $i] *
$scale;
}
/* The start is folded against the raw end the block before
left lying there, which is what makes the two cancel
each other's aliasing exactly. */
for ($i = 0; $i < $half; $i++) {
$near = $running[$at + $i];
$far = $running[$at + $taper - 1 - $i];
$low = $rise[$i];
$high = $rise[$taper - 1 - $i];
$running[$at + $i] = $high * $near - $low * $far;
$running[$at + $taper - 1 - $i] = $low * $near +
$high * $far;
}
}
$settled = array_slice($running, 0, $length);
/* The raw end of the last block is carried to the next
stretch, which folds its own start against it. */
$tail = [];
for ($i = 0; $i < $half; $i++) {
$tail[] = $running[$length + $i];
}
return $settled;
}
/**
* letDownTilt lets back down the tilt the recording was given before it was
* compressed stretch before, replaced by what it holds after
*
* @param array $samples the samples to let down
* @param float $carried what the filter was holding from the
* @return array the samples with the tilt let down
*/
public static function letDownTilt($samples, &$carried)
{
$out = [];
$held = $carried;
foreach ($samples as $sample) {
$now = $sample + $held;
$held = self::TILT_BACK * $now;
$out[] = $now;
}
$carried = $held;
return $out;
}
}