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

/**
 * SpeechFilter turns the sixteen frequencies that describe the shape of
 * a stretch of speech into the sixteen terms of the filter that plays
 * it back.
 *
 * The frequencies say where the mouth and throat let sound through. The
 * filter says, for each sample, how much of the sixteen samples before
 * it to add in. The two describe the same thing, and going from one to
 * the other is a fixed piece of arithmetic: each frequency becomes twice
 * its cosine, those are multiplied together into two runs of terms, and
 * the two runs are added and subtracted to give the filter.
 *
 * A caller uses this once the frequencies of a stretch are read, and
 * before any sample of it is worked out.
 *
 * @author Chris Pollett
 */
class SpeechFilter
{
    /**
     * TERMS_IN_FILTER is how many terms the filter for a stretch of wide
     * sound holds, one for each of the sixteen frequencies.
     */
    const TERMS_IN_FILTER = 16;
    /**
     * WORKING_BITS is how many bits of fraction the arithmetic keeps
     * while the terms are being built. More bits are kept here than in
     * the answer, so that rounding along the way does not show.
     */
    const WORKING_BITS = 16;
    /**
     * ANSWER_BITS is how many bits of fraction each term of the finished
     * filter is written with. A term of one comes back as 4096.
     */
    const ANSWER_BITS = 12;
    /**
     * COSINE_STEPS is how many angles the cosine table holds for the
     * half turn a frequency can name, not counting the closing entry.
     */
    const COSINE_STEPS = 128;
    /**
     * FREQUENCY_BITS is how many bits of fraction a frequency is written
     * with, so that half the sampling rate comes out as 32768.
     */
    const FREQUENCY_BITS = 15;
    /**
     * BUILD_ORDER is the order the frequencies are taken in while the
     * terms are built. Taking them in this order keeps the numbers in
     * the middle of the arithmetic small enough that rounding does not
     * bite, and every decoder uses it, so the answers agree.
     */
    const BUILD_ORDER = [0, 15, 8, 7, 4, 11, 12, 3, 2, 13, 10, 5, 6, 9,
        14, 1];
    /**
     * WHOLE_BLEND is the blend number that means take all of this
     * stretch and none of the stretch before it.
     */
    const WHOLE_BLEND = 4;
    /**
     * termsFor gives the sixteen terms of the filter that plays back a
     * stretch, from the sixteen frequencies that describe its shape.
     *
     * @param array $frequencies the sixteen frequencies of the stretch,
     *     each written out of 32768
     * @return array the sixteen terms of the filter, each written out
     *     of 4096
     */
    public static function termsFor($frequencies)
    {
        $cosines = self::cosinesOf($frequencies);
        $half = intdiv(self::TERMS_IN_FILTER, 2);
        $even = self::runOfTerms($cosines, 0, $half);
        $odd = self::runOfTerms($cosines, 1, $half);
        $wide = array_fill(0, self::TERMS_IN_FILTER, 0);
        for ($at = 0; $at < $half; $at++) {
            $sum = $even[$at + 1] + $even[$at];
            $difference = $odd[$at + 1] - $odd[$at];
            $wide[$at] = -$difference - $sum;
            $wide[self::TERMS_IN_FILTER - $at - 1] = $difference - $sum;
        }
        return self::heldWithinRange($wide);
    }
    /**
     * blendedWith gives the frequencies the first half of a stretch is
     * played with, which are part way between the stretch before and
     * this one. A speaker's mouth moves through a stretch rather than
     * jumping at its start, so the first two quarters are played with a
     * shape between the two and the last two with this stretch's own.
     *
     * Where the blend says to take all of this stretch, or where there
     * is no stretch before, this stretch's own frequencies come back
     * unchanged.
     *
     * @param array $frequencies the sixteen frequencies of this stretch
     * @param array $before the sixteen frequencies of the stretch before
     *     it, or an empty array where there was none
     * @param int $blend how much of this stretch to take, from none at
     *     zero to all of it at four
     * @return array the sixteen frequencies for the first half
     */
    public static function blendedWith($frequencies, $before, $blend)
    {
        if ($blend >= self::WHOLE_BLEND || count($before) !=
            count($frequencies)) {
            return $frequencies;
        }
        $out = [];
        foreach ($frequencies as $at => $one) {
            $out[] = $before[$at] +
                (($blend * ($one - $before[$at])) >> 2);
        }
        return $out;
    }
    /**
     * cosinesOf gives twice the cosine of each frequency, in the order
     * the terms are built in. The cosine is read from a table of angles
     * with a straight line drawn between the two entries either side of
     * the frequency, which is quick and is what every decoder does.
     *
     * @param array $frequencies the sixteen frequencies of the stretch
     * @return array twice the cosine of each, in building order
     */
    public static function cosinesOf($frequencies)
    {
        $table = SpeechShapeTables::COSINE_TABLE;
        $cosines = array_fill(0, self::TERMS_IN_FILTER, 0);
        $shift = self::FREQUENCY_BITS - 7;
        foreach ($frequencies as $at => $one) {
            $step = $one >> $shift;
            $part = $one - ($step << $shift);
            $step = max(0, min($step, self::COSINE_STEPS - 1));
            $here = $table[$step];
            $rise = $table[$step + 1] - $here;
            $cosines[self::BUILD_ORDER[$at]] = self::roundedShift(
                ($here << 8) + $rise * $part,
                20 - self::WORKING_BITS);
        }
        return $cosines;
    }
    /**
     * runOfTerms multiplies together the pairs of cosines that start at
     * one place and step two apart, giving one of the two runs of terms
     * the filter is built from. The two runs stand for the two ways a
     * sound can come back at a frequency, one adding to what came before
     * and one taking away from it.
     *
     * @param array $cosines twice the cosine of each frequency, in
     *     building order
     * @param int $from which cosine the run starts at, zero or one
     * @param int $length how many pairs the run covers
     * @return array the run of terms, one longer than the run of pairs
     */
    public static function runOfTerms($cosines, $from, $length)
    {
        $out = array_fill(0, $length + 1, 0);
        $out[0] = 1 << self::WORKING_BITS;
        $out[1] = -$cosines[$from];
        for ($at = 1; $at < $length; $at++) {
            $one = $cosines[$from + 2 * $at];
            $out[$at + 1] = ($out[$at - 1] << 1) - self::roundedShift(
                $one * $out[$at], self::WORKING_BITS);
            for ($back = $at; $back > 1; $back--) {
                $out[$back] += $out[$back - 2] - self::roundedShift(
                    $one * $out[$back - 1], self::WORKING_BITS);
            }
            $out[1] -= $one;
        }
        return $out;
    }
    /**
     * heldWithinRange writes the terms with the number of fraction bits
     * the answer uses, and shrinks them where any one is too large to
     * fit. Shrinking pulls every term toward zero by the same fraction,
     * which moves the filter's frequencies inward a little rather than
     * breaking its shape.
     *
     * @param array $wide the terms as the building left them
     * @return array the terms written out of 4096
     */
    public static function heldWithinRange($wide)
    {
        $shift = self::WORKING_BITS + 1 - self::ANSWER_BITS;
        for ($round = 0; $round < 10; $round++) {
            $largest = 0;
            $where = 0;
            foreach ($wide as $at => $one) {
                if (abs($one) > $largest) {
                    $largest = abs($one);
                    $where = $at;
                }
            }
            $largest = self::roundedShift($largest, $shift);
            if ($largest <= 32767) {
                break;
            }
            $largest = min($largest, 163838);
            $pull = 65505 - intdiv(($largest - 32767) << 14,
                ($largest * ($where + 1)) >> 2);
            $wide = self::pulledIn($wide, $pull);
        }
        $terms = [];
        foreach ($wide as $one) {
            $terms[] = max(-32768, min(32767,
                self::roundedShift($one, $shift)));
        }
        return $terms;
    }
    /**
     * pulledIn pulls every term toward zero, each one further than the
     * one before it. This is how a filter that would ring is tamed: the
     * frequencies it stands on move inward and the sound stays bounded.
     *
     * @param array $wide the terms to pull in
     * @param int $pull how much of each term to keep, written out of
     *     65536
     * @return array the terms after pulling
     */
    public static function pulledIn($wide, $pull)
    {
        $count = count($wide);
        $so_far = $pull;
        for ($at = 0; $at < $count - 1; $at++) {
            $wide[$at] = self::timesFraction($wide[$at], $so_far);
            $so_far = self::timesFraction($so_far, $pull);
        }
        $wide[$count - 1] = self::timesFraction($wide[$count - 1],
            $so_far);
        return $wide;
    }
    /**
     * timesFraction multiplies a number by a fraction written out of
     * 65536, rounding to the nearest whole number.
     *
     * @param int $number the number to multiply
     * @param int $fraction how many sixty-five thousand five hundred and
     *     thirty-sixths to take of it
     * @return int the answer
     */
    public static function timesFraction($number, $fraction)
    {
        return self::roundedShift($number * $fraction, 16);
    }
    /**
     * roundedShift divides a number by a power of two, rounding to the
     * nearest whole number rather than toward zero. The arithmetic of
     * this filter rounds that way throughout, and rounding the other way
     * moves a term by one, which is enough to make a decoder disagree
     * with every other one.
     *
     * @param int $number the number to divide
     * @param int $bits which power of two to divide by
     * @return int the answer, rounded to nearest
     */
    public static function roundedShift($number, $bits)
    {
        if ($bits < 1) {
            return $number;
        }
        return ($number + (1 << ($bits - 1))) >> $bits;
    }
}
