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

/**
 * SpeechFrameHeader reads the opening of a stretch of sound compressed
 * the way built for speech: which stretches hold speech at all, whether
 * a second copy of the sound was written for a listener who loses a
 * packet, what kind of sound each stretch holds, and how loud each
 * quarter of it is.
 *
 * A browser records speech this way, so every recording a wiki page
 * takes arrives compressed like this. Reading the opening is the first
 * step of decoding one: the kind of sound decides which tables the rest
 * of the stretch is read with, and the loudness of each quarter scales
 * the pulses that follow.
 *
 * The reading order is fixed by the standard. All the speech marks come
 * first, one for each stretch in the packet, then the mark saying
 * whether a second copy follows, and only then the stretches themselves.
 *
 * @author Chris Pollett
 */
class SpeechFrameHeader
{
    /**
     * $has_speech stores, for each stretch in the packet, whether that
     * stretch was marked as holding speech. A stretch holding none is
     * written with its own quieter tables.
     * @var array
     */
    public $has_speech = [];
    /**
     * $has_spare_copy stores whether the packet carries a second, poorer
     * copy of its sound for a listener who lost the packet before it.
     * @var bool
     */
    public $has_spare_copy = false;
    /**
     * $spare_copies stores, for each stretch in the packet, whether a
     * spare copy of that stretch follows. A listener who lost the
     * packet before this one plays those instead of nothing.
     * @var array
     */
    public $spare_copies = [];
    /**
     * $kind stores which kind of sound the stretch holds: no speech,
     * speech made without the voice, or speech made with the voice.
     * @var string
     */
    public $kind = "";
    /**
     * $pulse_writing stores which of the two ways the pulses of the
     * stretch were written, the quieter or the louder.
     * @var string
     */
    public $pulse_writing = "";
    /**
     * $loudness stores the step of loudness of each quarter of the
     * stretch, in the order the quarters are played. A step covers six
     * decibels.
     * @var array
     */
    public $loudness = [];
    /**
     * $loudness_indices stores the numbers read for each quarter's
     * loudness before they are added up, which is what a reference
     * decoding prints and so what a comparison lines up against.
     * @var array
     */
    public $loudness_indices = [];
    /**
     * readMarks reads the speech marks that open a packet, one for each
     * stretch it carries, and the mark saying whether a second copy of
     * the sound follows. A caller reads these before any stretch, since
     * the standard writes them all together at the front.
     *
     * @param object $reader the range decoder reading the packet
     * @param int $stretches how many stretches the packet carries
     * @return object a header holding the marks that were read
     */
    public static function readMarks($reader, $stretches)
    {
        $header = new self();
        $header->has_speech = [];
        for ($at = 0; $at < $stretches; $at++) {
            $header->has_speech[] =
                ($reader->decodeBit(1) == 1);
        }
        $header->has_spare_copy = ($reader->decodeBit(1) == 1);
        $header->spare_copies = array_fill(0, $stretches, false);
        if ($header->has_spare_copy) {
            if ($stretches == 1) {
                $header->spare_copies[0] = true;
            } else {
                /* Where a packet carries more than one stretch, one
                   number says which of them the spare copies cover, a
                   bit to a stretch. */
                $which = $reader->decodeFromTable(
                    SpeechTables::SPARE_COPY_CHANCES,
                    SpeechTables::WHOLE_BITS) + 1;
                for ($at = 0; $at < $stretches; $at++) {
                    $header->spare_copies[$at] =
                        (($which >> $at) & 1) == 1;
                }
            }
        }
        return $header;
    }
    /**
     * readSound reads what one stretch says about the sound it holds:
     * its kind, the way its pulses were written, and how loud each of
     * its quarters is. A caller reads this once the marks at the front
     * of the packet have been read, and before the shape of the sound.
     *
     * The loudness of the first quarter is written outright where the
     * stretch stands on its own. Every later quarter is written as a
     * change from the quarter before it, which costs fewer bits because
     * loudness moves slowly within a stretch.
     *
     * @param object $reader the range decoder reading the packet
     * @param bool $has_speech whether this stretch was marked as holding
     *     speech
     * @param int $carried the loudness step the stretch before this one
     *     ended at, or -1 where this stretch stands on its own
     * @return object a header holding what the stretch says about its
     *     sound
     */
    public static function readSound($reader, $has_speech, $carried = -1)
    {
        $header = new self();
        $header->has_speech = [$has_speech];
        $table = $has_speech ? SpeechTables::SPOKEN_KINDS :
            SpeechTables::QUIET_KINDS;
        $chosen = $reader->decodeFromTable($table,
            SpeechTables::WHOLE_BITS);
        list($header->kind, $header->pulse_writing) =
            SpeechTables::kindOfSound($chosen, $has_speech);
        $header->loudness = self::readLoudness($reader, $header->kind,
            $carried, $header->loudness_indices);
        return $header;
    }
    /**
     * readLoudness reads how loud each quarter of a stretch is. The
     * first quarter is written outright where nothing was carried over
     * from the stretch before, and as a change otherwise; every quarter
     * after the first is written as a change from the one before it.
     *
     * @param object $reader the range decoder reading the packet
     * @param string $kind which kind of sound the stretch holds
     * @param int $carried the loudness step the stretch before this one
     *     ended at, or -1 where this stretch stands on its own
     * @param array $indices filled in with the numbers read before they
     *     are added up, for lining a reading up against another decoder
     * @return array the loudness step of each quarter, in playing order
     */
    public static function readLoudness($reader, $kind, $carried,
        &$indices = [])
    {
        $steps = [];
        $indices = [];
        $so_far = $carried;
        for ($at = 0; $at < SpeechTables::QUARTERS_IN_STRETCH; $at++) {
            if ($at == 0 && $carried < 0) {
                $step = $reader->decodeFromTable(
                    SpeechTables::loudnessTableFor($kind),
                    SpeechTables::WHOLE_BITS);
                $within = $reader->decodeFromTable(
                    SpeechTables::LOUDNESS_WITHIN_STEP,
                    SpeechTables::WHOLE_BITS);
                $so_far = $step * SpeechTables::PLACES_IN_STEP +
                    $within;
                $indices[] = $so_far;
            } else {
                $change = $reader->decodeFromTable(
                    SpeechTables::LOUDNESS_CHANGE,
                    SpeechTables::WHOLE_BITS);
                /* The change is written as a number from zero upward.
                   The standard reads the first four of those as a fall
                   of four to one step, and everything above them as a
                   rise, so the number is moved back to a change around
                   zero before it is added. */
                $moved = ($change < SpeechTables::CHANGES_THAT_FALL) ?
                    $change - SpeechTables::CHANGES_THAT_FALL :
                    $change - SpeechTables::CHANGES_THAT_FALL + 1;
                $indices[] = $change;
                $so_far = self::heldInRange($so_far + $moved);
            }
            $steps[] = $so_far;
        }
        return $steps;
    }
    /**
     * heldInRange keeps a step of loudness within the range a stretch
     * may be written at. A run of rises would otherwise carry the
     * loudness past what the pulses can be scaled by.
     *
     * @param int $step the step of loudness to hold
     * @return int the step, held between the lowest and the highest
     */
    public static function heldInRange($step)
    {
        if ($step < SpeechTables::LEAST_LOUDNESS_STEP) {
            return SpeechTables::LEAST_LOUDNESS_STEP;
        }
        if ($step > SpeechTables::MOST_LOUDNESS_STEP) {
            return SpeechTables::MOST_LOUDNESS_STEP;
        }
        return $step;
    }
}
X