<?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;
/**
* SpeechPitch reads the pitch of a stretch of speech made with the
* voice: how far back in its own recent past the sound repeats, and how
* strongly.
*
* The vocal folds open and close at some rate, so one opening sounds
* much like the one before it. A stretch therefore carries a lag, being
* the distance back to the last opening, and a filter of five taps
* saying how much of the sound found there to add back in. Each of the
* four quarters of a stretch gets its own lag, worked out from the
* stretch's lag and a shape shared across the four, so the pitch may
* drift within the stretch as a speaker's voice does.
*
* A caller reads the pitch after the shape of a stretch and before its
* pulses. A stretch of speech made without the voice, or one holding no
* speech, carries no pitch at all and this is not called for it.
*
* @author Chris Pollett
*/
class SpeechPitch
{
/**
* $lag stores how far back the stretch as a whole looks, in samples
* at the rate the sound was written at.
* @var int
*/
public $lag = 0;
/**
* $quarter_lags stores the lag of each of the four quarters, in the
* order the quarters are played.
* @var array
*/
public $quarter_lags = [];
/**
* $book stores which of the three books of filters the stretch named
* its filter from, counting from zero.
* @var int
*/
public $book = 0;
/**
* $filter stores the five taps of the first quarter's filter, kept
* for a caller that wants one filter for the stretch.
* @var array
*/
public $filter = [];
/**
* $quarter_filters stores the five taps of each quarter's filter, in
* playing order. How strongly the voice repeats changes within a
* stretch, so each quarter names its own.
* @var array
*/
public $quarter_filters = [];
/**
* $lean stores how much of the sound before this stretch is leaned
* on when its pitch is added back, written out of 16384.
* @var int
*/
public $lean = 0;
/**
* TAPS_IN_FILTER is how many taps one filter holds. The middle tap
* sits at the lag itself and the other four at the two samples
* either side of it.
*/
const TAPS_IN_FILTER = 5;
/**
* TAP_BITS_ADDED is how many bits of fraction are added to a tap
* taken from the book, which holds seven, to bring it to the
* fourteen the pitch filter works with.
*/
const TAP_BITS_ADDED = 7;
/**
* COARSE_LAG_STEPS is how many coarse steps of lag a stretch may
* name when it writes its pitch outright.
*/
const COARSE_LAG_STEPS = 32;
/**
* LAG_CHANGE_MIDDLE is the place in the table of changes that means
* no change at all. A number below it is a fall in pitch and one
* above it a rise.
*/
const LAG_CHANGE_MIDDLE = 9;
/**
* read gives the pitch a stretch of voiced speech carries: the lag
* of the stretch, the lag of each quarter, and the filter that says
* how strongly the sound repeats.
*
* @param object $reader the range decoder reading the stretch
* @param int $carried the lag the stretch before this one used, or
* -1 where this stretch stands on its own
* @param bool $independent whether the stretch stands on its own, in
* which case it also says how much of the sound before it to
* lean on
* @return object a pitch holding the lags, the book and the filter
*/
public static function read($reader, $carried = -1,
$independent = true)
{
$pitch = new self();
$pitch->lag = self::readLag($reader, $carried);
$pitch->quarter_lags = self::quarterLags($reader, $pitch->lag);
$pitch->book = $reader->decodeFromTable(
SpeechPitchTables::REPEAT_BOOK_CHANCES,
SpeechPitchTables::WHOLE_BITS);
/* Each quarter names its own filter out of the one book, since
how strongly the voice repeats changes within a stretch. */
$pitch->quarter_filters = [];
for ($at = 0; $at < SpeechPitchTables::QUARTERS_IN_STRETCH;
$at++) {
$pitch->quarter_filters[] = self::readFilter($reader,
$pitch->book);
}
$pitch->filter = $pitch->quarter_filters[0];
if ($independent) {
$which = $reader->decodeFromTable(
SpeechPulseTables::LEAN_CHANCES,
SpeechPitchTables::WHOLE_BITS);
$pitch->lean = SpeechPitchTables::REPEAT_LEAN_SCALES[
$which % count(SpeechPitchTables::REPEAT_LEAN_SCALES)];
}
return $pitch;
}
/**
* readLag gives how far back the stretch as a whole looks. A stretch
* standing on its own writes the lag outright, as a coarse step and
* a place within that step; a stretch following a voiced one writes
* it as a change from the lag before, which costs fewer bits because
* pitch moves slowly.
*
* @param object $reader the range decoder reading the stretch
* @param int $carried the lag the stretch before this one used, or
* -1 where this stretch stands on its own
* @return int the lag, in samples
*/
public static function readLag($reader, $carried)
{
if ($carried > 0) {
$step = $reader->decodeFromTable(
SpeechPitchTables::PITCH_LAG_CHANGE,
SpeechPitchTables::WHOLE_BITS);
/* A change of zero is the writer saying the lag follows
outright rather than as a move, so the reading goes on to
the coarse and fine parts below. */
if ($step > 0) {
return self::heldInRange($carried + $step -
self::LAG_CHANGE_MIDDLE);
}
}
$coarse = $reader->decodeFromTable(
SpeechPitchTables::PITCH_FIRST_LAG,
SpeechPitchTables::WHOLE_BITS);
$fine = $reader->decodeFromTable(
SpeechPitchTables::PITCH_FINE_LAG,
SpeechPitchTables::WHOLE_BITS);
$steps = SpeechPitchTables::LAG_STEPS_WIDE;
return self::heldInRange(SpeechPitchTables::LEAST_LAG_WIDE +
$coarse * $steps + $fine);
}
/**
* quarterLags gives the lag of each quarter of a stretch. The four
* share one shape, read once, and each quarter's lag is the
* stretch's lag plus that shape's step for the quarter.
*
* @param object $reader the range decoder reading the stretch
* @param int $lag the lag the stretch as a whole uses
* @return array the lag of each quarter, in playing order
*/
public static function quarterLags($reader, $lag)
{
$shape = $reader->decodeFromTable(
SpeechPitchTables::PITCH_SHAPE_WIDE,
SpeechPitchTables::WHOLE_BITS);
$steps = SpeechPitchTables::PITCH_SHAPE_WIDE_STEPS;
$quarters = SpeechPitchTables::QUARTERS_IN_STRETCH;
$shapes = intdiv(count($steps), $quarters);
$shape = $shape % $shapes;
$lags = [];
for ($at = 0; $at < $quarters; $at++) {
/* The table holds a row for each quarter and a column for
each shape, so the quarter picks the row. Reading it the
other way about gave each quarter the wrong step. */
$lags[] = self::heldInRange($lag +
$steps[$at * $shapes + $shape]);
}
return $lags;
}
/**
* readFilter gives the five taps of the filter a stretch names. The
* stretch names a book first, and the book fixes both how likely
* each filter within it was and where its taps are kept.
*
* @param object $reader the range decoder reading the stretch
* @param int $book which of the three books the stretch named
* @return array the five taps, each a fraction written out of 128
*/
public static function readFilter($reader, $book)
{
$chances = [SpeechPitchTables::REPEAT_STRENGTH_CHANCES_0,
SpeechPitchTables::REPEAT_STRENGTH_CHANCES_1,
SpeechPitchTables::REPEAT_STRENGTH_CHANCES_2];
$books = [SpeechPitchTables::REPEAT_FILTERS_0,
SpeechPitchTables::REPEAT_FILTERS_1,
SpeechPitchTables::REPEAT_FILTERS_2];
$book = $book % count($books);
$which = $reader->decodeFromTable($chances[$book],
SpeechPitchTables::WHOLE_BITS);
$taps = $books[$book];
$count = intdiv(count($taps), self::TAPS_IN_FILTER);
$which = $which % $count;
/* The book holds its taps with seven bits of fraction, and the
filter works with fourteen, so each is shifted up. */
$out = [];
foreach (array_slice($taps, $which * self::TAPS_IN_FILTER,
self::TAPS_IN_FILTER) as $one) {
$out[] = $one << self::TAP_BITS_ADDED;
}
return $out;
}
/**
* heldInRange keeps a lag within the distance a stretch of wide
* sound may look back. A lag outside that range would reach into
* sound the decoder does not hold.
*
* @param int $lag the lag to hold
* @return int the lag, held between the shortest and the longest
*/
public static function heldInRange($lag)
{
if ($lag < SpeechPitchTables::LEAST_LAG_WIDE) {
return SpeechPitchTables::LEAST_LAG_WIDE;
}
if ($lag > SpeechPitchTables::MOST_LAG_WIDE) {
return SpeechPitchTables::MOST_LAG_WIDE;
}
return $lag;
}
}