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

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