<?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;
/**
* SpeechPulses reads the pulses of a stretch of speech: the sound left
* over once the shape of the mouth and the pitch of the voice have been
* taken out of it.
*
* The pulses are written a block of sixteen samples at a time. A block
* carries how many pulses it holds altogether, and that count is then
* split in half again and again: first between the two halves of the
* block, then between the halves of those, until each sample has a count
* of its own. A block written loudly carries extra bits after the split,
* one for each doubling of its counts. Last of all, each sample that
* holds any pulses says which way they point.
*
* A caller reads the pulses after the shape and the pitch. What comes
* back is one number for each sample of the stretch, which the next step
* scales by the loudness of its quarter and passes through the two
* filters.
*
* @author Chris Pollett
*/
class SpeechPulses
{
/**
* $density stores which level of pulse density the stretch named,
* counting from zero. The level chooses the likelihoods the counts
* are read with.
* @var int
*/
public $density = 0;
/**
* $block_counts stores how many pulses each block of sixteen samples
* holds, in the order the blocks are played.
* @var array
*/
public $block_counts = [];
/**
* $samples stores the pulses of each sample of the stretch, in
* playing order, each with the sign the stretch gave it.
* @var array
*/
public $samples = [];
/**
* $seed stores the starting number the stretch gives for the noise
* the decoder adds back to the pulses.
* @var int
*/
public $seed = 0;
/**
* SPLITS_IN_BLOCK is how many times a block's count is halved before
* each sample has a count of its own: sixteen samples take four
* halvings.
*/
const SPLITS_IN_BLOCK = 4;
/**
* MOST_EXTRA_BITS is the most doublings a block may carry. Past this
* the writer stops offering the choice, so the table it reads counts
* from is shifted by one.
*/
const MOST_EXTRA_BITS = 10;
/**
* SIGN_SETS_PER_KIND is how many sets of sign likelihoods each kind
* of sound has, one for each band of block loudness.
*/
const SIGN_SETS_PER_KIND = 6;
/**
* read gives the pulses a stretch carries: the density it named, how
* many pulses each block holds, and the pulses of each sample.
*
* @param object $reader the range decoder reading the stretch
* @param string $kind which kind of sound the stretch holds, since
* speech made with the voice uses different likelihoods
* @param string $pulse_writing which of the two ways the pulses were
* written, the quieter or the louder
* @param int $samples how many samples the stretch covers
* @param bool $independent whether the stretch stands on its own
* rather than leaning on the stretch before it, since a stretch
* that stands alone writes one extra thing before its pulses
* @return object the pulses of the stretch
*/
public static function read($reader, $kind, $pulse_writing, $samples,
$independent = true)
{
$pulses = new self();
$voiced = ($kind === SpeechTables::VOICED_KIND);
/* Between the pitch and the pulses a stretch writes a starting
number for the noise the decoder adds back. How much of the
sound before it to lean on is read with the pitch, since only
a voiced stretch writes it. */
$pulses->seed = $reader->decodeFromTable(
SpeechPulseTables::SEED_CHANCES,
SpeechPulseTables::WHOLE_BITS);
$rates = SpeechPulseTables::PULSE_RATE_CHANCES;
$half = intdiv(count($rates), 2);
$table = array_slice($rates, $voiced ? $half : 0, $half);
$pulses->density = $reader->decodeFromTable($table,
SpeechPulseTables::WHOLE_BITS);
$blocks = intdiv($samples, SpeechPulseTables::SAMPLES_IN_BLOCK);
$doublings = [];
$pulses->block_counts = self::readBlockCounts($reader,
$pulses->density, $blocks, $doublings);
$pulses->samples = [];
foreach ($pulses->block_counts as $at => $count) {
$spread = ($count > 0) ? self::splitCount($reader, $count) :
array_fill(0, SpeechPulseTables::SAMPLES_IN_BLOCK, 0);
$spread = self::addExtraBits($reader, $spread,
$doublings[$at]);
foreach ($spread as $one) {
$pulses->samples[] = $one;
}
}
self::addSigns($reader, $pulses, $kind, $pulse_writing,
$doublings);
return $pulses;
}
/**
* readBlockCounts gives how many pulses each block of the stretch
* holds. A count one past the largest allowed says the block was
* written loudly, so its counts are doubled afterwards and a fresh
* count is read; that may happen several times over.
*
* @param object $reader the range decoder reading the stretch
* @param int $density which level of pulse density the stretch named
* @param int $blocks how many blocks the stretch holds
* @param array $doublings filled in with how many doublings each
* block carries
* @return array the count of pulses in each block
*/
public static function readBlockCounts($reader, $density, $blocks,
&$doublings)
{
$chances = SpeechPulseTables::PULSES_IN_BLOCK_CHANCES;
$each = intdiv(count($chances), SpeechPulseTables::RATE_LEVELS);
$table = array_slice($chances, $density * $each, $each);
$loudest = array_slice($chances,
(SpeechPulseTables::RATE_LEVELS - 1) * $each, $each);
$counts = [];
$doublings = [];
for ($at = 0; $at < $blocks; $at++) {
$doubled = 0;
$count = $reader->decodeFromTable($table,
SpeechPulseTables::WHOLE_BITS);
while ($count == SpeechPulseTables::MOST_PULSES_IN_BLOCK + 1) {
$doubled++;
$shifted = ($doubled == self::MOST_EXTRA_BITS) ?
array_slice($loudest, 1) : $loudest;
$count = $reader->decodeFromTable($shifted,
SpeechPulseTables::WHOLE_BITS);
}
$counts[] = $count;
$doublings[] = $doubled;
}
return $counts;
}
/**
* splitCount spreads a block's count of pulses over its sixteen
* samples. The count is split between the two halves of the block,
* then between the halves of those, and so on four times over. Each
* split is read with the likelihoods for the count being split.
*
* @param object $reader the range decoder reading the stretch
* @param int $count how many pulses the block holds altogether
* @return array the count of pulses at each of the sixteen samples
*/
public static function splitCount($reader, $count)
{
return self::splitPart($reader, $count,
self::SPLITS_IN_BLOCK);
}
/**
* splitPart spreads a count over the samples of one part of a block,
* splitting it in half and then working each half all the way down
* before starting on the other. The writer wrote the splits in that
* order, so a reader that took a whole level at a time would take
* the bits of one half for the other.
*
* @param object $reader the range decoder reading the stretch
* @param int $count how many pulses this part holds
* @param int $depth how many halvings are left to do
* @return array the count of pulses at each sample of this part
*/
public static function splitPart($reader, $count, $depth)
{
if ($depth <= 0) {
return [$count];
}
$tables = [SpeechPulseTables::SPLIT_CHANCES_ONE,
SpeechPulseTables::SPLIT_CHANCES_TWO,
SpeechPulseTables::SPLIT_CHANCES_FOUR,
SpeechPulseTables::SPLIT_CHANCES_EIGHT];
$left = self::splitOne($reader, $count, $tables[$depth - 1]);
$first = self::splitPart($reader, $left, $depth - 1);
$second = self::splitPart($reader, $count - $left, $depth - 1);
return array_merge($first, $second);
}
/**
* splitOne says how many of a count of pulses fall in the first of
* two halves. Where the count is nothing, so is the answer, and no
* bits are read for it.
*
* @param object $reader the range decoder reading the stretch
* @param int $count how many pulses are being split
* @param array $table the likelihoods for this depth of splitting
* @return int how many pulses fall in the first half
*/
public static function splitOne($reader, $count, $table)
{
if ($count <= 0) {
return 0;
}
$starts = SpeechPulseTables::SPLIT_TABLE_STARTS;
$where = $starts[min($count, count($starts) - 1)];
$next = ($count + 1 < count($starts)) ? $starts[$count + 1] :
count($table);
$slice = array_slice($table, $where, max(1, $next - $where));
return $reader->decodeFromTable($slice,
SpeechPulseTables::WHOLE_BITS);
}
/**
* addExtraBits doubles the counts of a block that was written loudly
* and reads one extra bit for each doubling. A loud block writes its
* counts small and makes up the difference this way.
*
* @param object $reader the range decoder reading the stretch
* @param array $spread the count of pulses at each sample
* @param int $doublings how many doublings the block carries
* @return array the counts after doubling and adding the extra bits
*/
public static function addExtraBits($reader, $spread, $doublings)
{
for ($round = 0; $round < $doublings; $round++) {
foreach ($spread as $at => $one) {
$spread[$at] = $one * 2 + $reader->decodeFromTable(
SpeechPulseTables::EXTRA_BIT_CHANCES,
SpeechPulseTables::WHOLE_BITS);
}
}
return $spread;
}
/**
* addSigns gives each sample that holds pulses the direction it
* points. A sample holding nothing needs no sign, so none is written
* for it. The likelihoods depend on the kind of sound, on how the
* pulses were written, and on how many pulses the block holds.
*
* @param object $reader the range decoder reading the stretch
* @param object $pulses the pulses read so far, changed in place
* @param string $kind which kind of sound the stretch holds
* @param string $pulse_writing which of the two ways the pulses were
* written
* @param array $doublings how many doublings each block carries
* @return void nothing is handed back; the pulses are changed in
* place
*/
public static function addSigns($reader, $pulses, $kind,
$pulse_writing, $doublings)
{
$chances = SpeechPulseTables::PULSE_SIGN_CHANCES;
$kinds = [SpeechTables::QUIET_KIND, SpeechTables::VOICELESS_KIND,
SpeechTables::VOICED_KIND];
$which_kind = array_search($kind, $kinds);
$louder = ($pulse_writing === SpeechTables::LOUDER_PULSES) ? 1 : 0;
$per_kind = self::SIGN_SETS_PER_KIND + 1;
$in_block = SpeechPulseTables::SAMPLES_IN_BLOCK;
/* The sets run seven apart, and which set a block uses comes
from the kind of sound and the way its pulses were written. */
$start = ($louder + $which_kind * 2) * $per_kind;
foreach ($pulses->block_counts as $at => $count) {
if ($count <= 0) {
continue;
}
$band = min($count, self::SIGN_SETS_PER_KIND);
$where = min($start + $band, count($chances) - 1);
$table = [$chances[$where], 0];
for ($step = 0; $step < $in_block; $step++) {
$sample = $at * $in_block + $step;
if (empty($pulses->samples[$sample])) {
continue;
}
$up = $reader->decodeFromTable($table,
SpeechPulseTables::WHOLE_BITS);
if ($up == 0) {
$pulses->samples[$sample] =
-$pulses->samples[$sample];
}
}
}
}
}