<?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;
}
}