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

/**
 * CeltPitchFilter sharpens a stretch of sound by adding a quieter copy of
 * itself from one cycle of its own pitch earlier. Sound with a clear pitch, a
 * voice or a played note, repeats itself every cycle. Compressing it loses some
 * of that repetition, and what is lost is heard as roughness sitting on top of
 * an otherwise clean note. Adding back a quiet copy of the sound from exactly
 * one cycle ago restores it, because the copy agrees with the sound wherever
 * the sound is truly repeating and disagrees where it is not. How far back to
 * reach and how loud a copy to add are stored at the opening of each stretch.
 * They may change from one stretch to the next, and a sudden change would be
 * heard, so the first short run of each stretch fades from the old settings to
 * the new rather than switching. Rather than take one sample from a cycle ago,
 * five neighboring samples are taken and weighted, which lets the reach fall
 * between whole samples and softens what the copy adds. Three sets of weights
 * are offered and the stretch says which it used. This follows RFC 6716, the
 * specification of the Opus audio codec, in its section on the filter that
 * follows the transform.
 */
class CeltPitchFilter
{
    /**
     * WEIGHTS is the three sets of weights, each spreading the copy across up
     * to five neighboring samples.
     */
    const WEIGHTS = [[0.3066406250, 0.2170410156, 0.1296386719],
        [0.4638671875, 0.2680664062, 0.0], [0.7998046875, 0.1000976562, 0.0]];
    /**
     * SHORTEST_REACH is the shortest reach back the filter may use, since a
     * shorter one would reach into samples this stretch has only just made.
     */
    const SHORTEST_REACH = 15;
    /**
     * LONGEST_REACH is the longest reach back the filter may use, which is how
     * much of the sound before must be kept.
     */
    const LONGEST_REACH = 1024;
    /**
     * FADE_LENGTH is how long the fade from the old settings to the new is, in
     * samples.
     */
    const FADE_LENGTH = 120;
    /**
     * apply adds the quieter copy across a whole stretch, fading from the
     * settings the stretch before used to this stretch's own samples before it
     * in front so the filter can reach back front before used uses
     *
     * @param array $sound the samples of this stretch, with the
     * @param int $before how many samples of the stretch before sit in
     * @param int $length how many samples this stretch has
     * @param array $was the reach, loudness and weights the stretch
     * @param array $now the reach, loudness and weights this stretch
     * @return array this stretch's samples with the copy added
     */
    public static function apply($sound, $before, $length, $was, $now)
    {
        $out = array_slice($sound, $before, $length);
        if ($was["loudness"] == 0.0 && $now["loudness"] == 0.0) {
            return $out;
        }
        $old_reach = max($was["reach"], self::SHORTEST_REACH);
        $new_reach = max($now["reach"], self::SHORTEST_REACH);
        $old_weights = self::weightedBy($was["loudness"], $was["shape"]);
        $new_weights = self::weightedBy($now["loudness"], $now["shape"]);
        $fade = self::FADE_LENGTH;
        /* Where nothing about the filter changed there is nothing to
           fade between, so the whole stretch uses the one setting. */
        if ($was["loudness"] == $now["loudness"] &&
            $old_reach == $new_reach && $was["shape"] == $now["shape"]) {
            $fade = 0;
        }
        for ($at = 0; $at < $fade && $at < $length; $at++) {
            $along = sin(0.5 * M_PI * ($at + 0.5) / self::FADE_LENGTH);
            $across = sin(0.5 * M_PI * $along * $along);
            $part = $across * $across;
            $out[$at] = $sound[$before + $at] +
                (1.0 - $part) * self::copyAt($sound, $before + $at,
                    $old_reach, $old_weights) +
                $part * self::copyAt($sound, $before + $at, $new_reach,
                    $new_weights);
        }
        if ($now["loudness"] == 0.0) {
            return $out;
        }
        for ($at = $fade; $at < $length; $at++) {
            $out[$at] = $sound[$before + $at] +
                self::copyAt($sound, $before + $at, $new_reach,
                    $new_weights);
        }
        return $out;
    }
    /**
     * copyAt the quieter copy taken from one cycle earlier, spread across five
     * neighboring samples
     *
     * @param array $sound the samples, with the ones before in front
     * @param int $at where in that run the sample sits
     * @param int $reach how far back one cycle is, in samples
     * @param array $weights how much of each neighbor to take
     * @return float what to add to this sample
     */
    public static function copyAt($sound, $at, $reach, $weights)
    {
        $from = $at - $reach;
        return $weights[0] * self::valueAt($sound, $from) +
            $weights[1] * (self::valueAt($sound, $from + 1) +
            self::valueAt($sound, $from - 1)) +
            $weights[2] * (self::valueAt($sound, $from + 2) +
            self::valueAt($sound, $from - 2));
    }
    /**
     * valueAt one sample of the run, giving nothing where the reach goes back
     * further than there is sound
     *
     * @param array $sound the samples
     * @param int $at which one
     * @return float that sample, or nothing
     */
    public static function valueAt($sound, $at)
    {
        return ($at < 0 || $at >= count($sound)) ? 0.0 : $sound[$at];
    }
    /**
     * weightedBy the three weights scaled by how loud a copy the stretch asked
     * for
     *
     * @param float $loudness how loud a copy to add
     * @param int $shape which of the three sets of weights
     * @return array the scaled weights
     */
    public static function weightedBy($loudness, $shape)
    {
        $set = self::WEIGHTS[$shape];
        return [$loudness * $set[0], $loudness * $set[1],
            $loudness * $set[2]];
    }
}
