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

/**
 * AacQuantizer turns the tones of a stretch into small whole numbers, and back.
 * A tone can be any size, and storing it exactly would cost far more than a
 * listener would notice. So each tone is divided by a step and rounded, and
 * only the rounded number is kept. The step is chosen per band: a band the ear
 * attends to gets a small step and is kept finely, one it does not gets a large
 * step and is kept roughly, and a band whose tones all round to nothing costs
 * almost nothing at all. The rounding is not to the nearest whole number. Tones
 * are raised to the three quarter power first, which spends more of the
 * available range on quiet tones than on loud ones, because a given error is
 * more audible against a quiet tone. Undoing that on playback is what the four
 * thirds power below does. The step for a band is named by a whole number, and
 * each step of that number changes the step size by a quarter of a power of
 * two. This follows the AAC standard, ISO/IEC 14496-3.
 */
class AacQuantizer
{
    /**
     * SQUASH is the power tones are raised to before rounding, which spends
     * more of the range on quiet tones than loud ones.
     */
    const SQUASH = 0.75;
    /**
     * UNSQUASH is the power that turns a quantized value back into its
     * own size. The encoder raises each value to three quarters, so
     * the decoder raises it to four thirds.
     */
    const UNSQUASH = 1.3333333333333333;
    /**
     * ROUNDING is what is added before rounding down, which rounds to nearest
     * rather than towards nothing.
     */
    const ROUNDING = 0.4054;
    /**
     * STEP_POWER is how much of a power of two each step of the step number is
     * worth.
     */
    const STEP_POWER = 0.25;
    /**
     * SQUASHED_STEP is how much of that power the squashing leaves, since the
     * squashing happens before the step is applied.
     */
    const SQUASHED_STEP = 0.1875;
    /**
     * MIDDLE_STEP is the step number that means no scaling at all, from which
     * the others are counted.
     */
    const MIDDLE_STEP = 100;
    /**
     * LARGEST_PLAIN is the largest rounded number that may be written without
     * writing the rest of it separately.
     */
    const LARGEST_PLAIN = 8191;
    /**
     * LARGEST_STEP is the largest step number that may be written.
     */
    const LARGEST_STEP = 255;
    /**
     * roundBand turns one band's tones into rounded whole numbers squash them
     * here
     *
     * @param array $tones the stretch's tones
     * @param int $from the band's first tone
     * @param int $past one past the band's last tone
     * @param int $step which step size to use
     * @param array $squashed the tones already squashed, or null to
     * @return array the rounded numbers, one per tone
     */
    public static function roundBand($tones, $from, $past, $step,
        $squashed = null)
    {
        $scale = pow(2.0, -self::SQUASHED_STEP *
            ($step - self::MIDDLE_STEP));
        $rounded = [];
        for ($at = $from; $at < $past; $at++) {
            /* The step is applied after the squashing, not before it,
               or the squashing would be applied to the step as well
               and the result would not grow in step with the tone.
               The squashing does not depend on the step, so a caller
               trying many steps can hand it in ready made. */
            $flat = $squashed[$at] ??
                pow(abs($tones[$at]), self::SQUASH);
            $whole = (int)($flat * $scale + self::ROUNDING);
            if ($whole > self::LARGEST_PLAIN) {
                $whole = self::LARGEST_PLAIN;
            }
            $rounded[] = ($tones[$at] < 0) ? -$whole : $whole;
        }
        return $rounded;
    }
    /**
     * smallestStepFor the smallest step a band may use without any of its tones
     * growing past what may be written. Working it out rather than searching
     * for it matters, because a quiet band wants a step well below the middle
     * of the range and searching upward from the middle would never find one. A
     * band kept no finer than the middle allows wastes whatever room the frame
     * had left.
     *
     * @param array $tones the stretch's tones
     * @param int $from the band's first tone
     * @param int $past one past the band's last tone
     * @return int the smallest step that works
     */
    public static function smallestStepFor($tones, $from, $past)
    {
        $loudest = 0.0;
        for ($at = $from; $at < $past; $at++) {
            $loudest = max($loudest, abs($tones[$at]));
        }
        if ($loudest <= 0.0) {
            return self::MIDDLE_STEP;
        }
        /* The rounded number is the squashed tone scaled by the step,
           and it must stay within what may be written, so the step
           follows from the loudest tone by turning that around. */
        $squashed = pow($loudest, self::SQUASH);
        $room = log(self::LARGEST_PLAIN / $squashed, 2.0);
        $step = (int)ceil(self::MIDDLE_STEP - $room / self::SQUASHED_STEP);
        return max(0, min(self::LARGEST_STEP, $step));
    }
}
