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

/**
 * 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];
                }
            }
        }
    }
}
X