<?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;
/**
* OpusDecoder turns a whole recording into samples, and writes samples out as a
* sound file anything can play. This is what puts every other part together end
* to end: the file is taken apart into pieces, each piece into stretches, each
* stretch read, and each stretch's tones turned into samples and joined to the
* ones before. What comes out is not right yet. A recording of a single 440
* cycle tone comes back as a tone near 510 cycles, which says the shape within
* a band is wrong even though the band itself is right. The fault is in how a
* wide band is cut in half and put back together. This is here so that the
* state of the work can be listened to rather than only read about, and so that
* the next fix has something to be measured against.
*/
class OpusDecoder
{
/**
* SAMPLE_RATE is how many samples a second the decoder hands back.
* Opus always decodes at this rate, whatever rate the sound was
* recorded at.
*/
const SAMPLE_RATE = 48000;
/**
* USUAL_TONES is how many tones a stretch of the usual length holds.
*/
const USUAL_TONES = 960;
/**
* USUAL_DOUBLINGS is how many times the shortest stretch has been doubled
* to reach the usual one.
*/
const USUAL_DOUBLINGS = 3;
/**
* SUDDEN_BLOCKS is how many blocks a stretch is split into where the sound
* changed suddenly.
*/
const SUDDEN_BLOCKS = 8;
/**
* BANDS is how many frequency bands the sound is split into,
* from the lowest to the highest a recording carries.
*/
const BANDS = 21;
/**
* VERY_QUIET is the loudness a band is taken to have had before the
* recording began, quiet enough that nothing is filled back toward it.
*/
const VERY_QUIET = -28.0;
/**
* LOUDEST is how loud a sample may be before it is held back, so that a
* fault cannot produce something painful to listen to.
*/
const LOUDEST = 1.0;
/**
* eachStretch turns every stretch of a recording into samples, handing each
* stretch's worth over as it is made rather than keeping them all. A
* recording of any length has to work, and holding every sample of it at
* once does not: four minutes of sound comes to eleven million of them,
* which is most of a gigabyte once each is a number in memory. So the
* samples are given out a stretch at a time and whatever wants them decides
* what to keep.
*
* @param array $stretches the compressed stretches, in order
* @param int $channels how many channels the recording carries
* @return \Generator each stretch's worth of samples in turn
*/
public static function eachStretch($stretches, $channels = 1)
{
$loudness = CeltEnergy::nothingYet($channels);
$quiet = [];
for ($channel = 0; $channel < $channels; $channel++) {
$quiet[] = array_fill(0, self::BANDS, self::VERY_QUIET);
}
$previous_one = $quiet;
$previous_two = $quiet;
$seed = 0;
$hanging = null;
$carried = 0.0;
$past = array_fill(0, CeltPitchFilter::LONGEST_REACH, 0.0);
$was = (["reach" => 0, "loudness" => 0.0, "shape" => 0]);
foreach ($stretches as $stretch) {
$reader = new RangeDecoder($stretch);
$read = CeltStretch::readFrom($reader, self::USUAL_DOUBLINGS,
$loudness, $seed, 0, self::BANDS, $previous_one, $previous_two);
$loudness = $read["loudness"];
$previous_two = $previous_one;
$previous_one = $loudness;
/* The number used to fill empty bands carries on from where
the reader itself ended, not from nothing. */
$seed = $reader->range;
$blocks = $read["header"]->sudden ? self::SUDDEN_BLOCKS : 1;
if ($read["header"]->silent) {
$tones = array_fill(0, self::USUAL_TONES, 0.0);
} else {
/* Where there are two channels the shapes have been
averaged already, so the louder of the two loudnesses
is the one to bring them up by. */
$level = $read["loudness"][0];
if (count($read["loudness"]) > 1) {
foreach ($level as $band => $one) {
$level[$band] = max($one,
$read["loudness"][1][$band]);
}
}
$tones = CeltShape::toTones($read["slots"], $level,
self::USUAL_DOUBLINGS, 0, self::BANDS,
self::USUAL_TONES);
}
if ($hanging === null) {
$hanging = array_fill(0,
intdiv(CeltSynthesis::TAPER, 2), 0.0);
}
$settled = CeltSynthesis::toSamples($tones, $blocks, $hanging);
/* The filter reaches back as much as a thousand samples, so
the sound before this stretch is kept in front of it. */
$now = ["reach" => $read["header"]->filter_pitch,
"loudness" => $read["header"]->filter_strength,
"shape" => $read["header"]->filter_shape];
$with_past = array_merge($past, $settled);
$settled = CeltPitchFilter::apply($with_past, count($past),
count($settled), $was, $now);
$was = $now;
$past = array_slice(array_merge($past, $settled),
-CeltPitchFilter::LONGEST_REACH);
yield CeltSynthesis::letDownTilt($settled, $carried);
}
}
/**
* toSamples turns a recording into samples and keeps them all, which suits
* a short recording and nothing longer
*
* @param array $stretches the compressed stretches, in order
* @return array the samples, at 48000 a second
*/
public static function toSamples($stretches)
{
$wave = [];
foreach (self::eachStretch($stretches) as $settled) {
foreach ($settled as $sample) {
$wave[] = $sample;
}
}
return $wave;
}
}