<?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
*
* VorbisDecoder turns the packets of a Vorbis stream into samples.
*/
namespace seekquarry\yioop\library\av_processing;
/**
* VorbisDecoder turns the packets of a Vorbis stream into samples of
* sound. A packet holds two things: the shape of the sound as a curve
* across the tones, and the detail left over once that curve is taken
* out. The two are multiplied together, turned from tones back into
* sound, faded, and added to the tail of the packet before.
*
* A packet may cover a long stretch or a short one, and says which. The
* fade between two packets has to match at their join, so a long packet
* next to a short one is faded with a curve that is flat in the middle
* and falls over the short one's length.
*
* A file ffmpeg wrote decodes to the sound it was made from. For six
* tones from a hundred to eight thousand cycles a second, each comes
* back at the cycle it went in at. Against ffmpeg's own decoding of a
* tone the samples differ by two ten-thousandths on average, and the
* loudest sample is 0.1298 where ffmpeg gives 0.1301. A file of two
* channels comes back the same way, one ten-thousandth a sample apart.
*
* Music comes back the same way. Twenty seconds of a song differs from
* ffmpeg's decoding by one ten-thousandth a sample. That needed the
* joins between a long stretch and a short one to be right: a packet
* hands back the sound between the middle of the packet before it and
* its own middle, which is half of each rather than half of one.
*/
class VorbisDecoder
{
/**
* CURVE_STEPS is how many steps the loudness curve is written in.
* Each step is a fixed fraction of the one below it, so the curve
* covers a wide range of loudness in few bits.
* @var int
*/
const CURVE_STEPS = 256;
/**
* CURVE_STEP_SIZE is how much one step of the loudness curve is
* worth, as a power of the natural number. A step of zero is the
* quietest the curve reaches and a step of two hundred and
* fifty-five is full loudness.
* @var float
*/
const CURVE_STEP_SIZE = 0.0629829;
/**
* CURVE_RANGES are the heights a loudness curve may be written in,
* one for each of the four steps a file may choose.
* @var array
*/
const CURVE_RANGES = [256, 128, 86, 64];
/**
* TRANSFORM_SHARE is what a stretch's length is multiplied by to
* undo the division the shared transform makes on the way back.
* That transform divides by half the length, so multiplying by half
* the length puts the strengths back at the size Vorbis wrote them.
* @var float
*/
const TRANSFORM_SHARE = 0.5;
/**
* $header stores what the stream said about itself: the rate, the
* channels, and the two stretch lengths.
* @var VorbisHeader
*/
public $header = null;
/**
* $setup stores the tables the file carries, which every packet is
* read with.
* @var VorbisSetup
*/
public $setup = null;
/**
* $carried stores the tail of the packet decoded before this one,
* one run of samples for each channel. Packets overlap, so the tail
* of one is added to the head of the next.
* @var array
*/
public $carried = [];
/**
* $carried_length stores how long that tail is, which is half the
* length of the packet it came from.
* @var int
*/
public $carried_length = 0;
/**
* __construct sets a decoder up from the two descriptions a stream
* begins with.
*
* @param VorbisHeader $header What the stream says about itself.
* @param VorbisSetup $setup The tables the file carries.
*/
public function __construct($header, $setup)
{
$this->header = $header;
$this->setup = $setup;
$this->carried = [];
for ($at = 0; $at < $header->channels; $at++) {
$this->carried[$at] = [];
}
}
/**
* fromFile reads an Ogg file holding Vorbis and hands back a
* decoder ready for its packets, along with those packets.
*
* @param string $path The file to read.
* @return array The decoder under the key decoder and the packets
* of sound under the key pieces.
*/
public static function fromFile($path)
{
$reader = OggDemuxer::fromName($path);
$packets = [];
foreach ($reader->packets() as $packet) {
$packets[] = $packet->data;
}
if (count($packets) < 3) {
throw new \RuntimeException("this file is too short to hold "
. "a Vorbis stream");
}
$header = VorbisHeader::fromString($packets[0]);
$setup = VorbisSetup::fromString($packets[2],
$header->channels);
return ["decoder" => new self($header, $setup),
"pieces" => array_slice($packets, 3)];
}
/**
* eachPacket hands back the samples of each packet in turn, already
* faded into the packet before it and averaged into one channel,
* which is what this folder writes.
*
* @param array $packets The packets of sound, in order.
* @return Generator Runs of samples, each a fraction of one.
*/
public function eachPacket($packets)
{
foreach ($packets as $packet) {
$given = $this->decodePacket($packet);
if ($given !== []) {
yield $given;
}
}
}
/**
* decodePacket turns one packet into samples. The curve and the
* leftover detail are read, multiplied together, turned back into
* sound, faded, and added to the tail the packet before it left.
*
* @param string $packet One packet of sound.
* @return array The samples this packet gives, averaged into one
* channel.
*/
public function decodePacket($packet)
{
$reader = new LowBitReader($packet);
if ($reader->readBit() !== 0) {
return [];
}
$modes = count($this->setup->modes);
$which = $reader->readBits(LowBitReader::bitsFor($modes - 1));
$mode = $this->setup->modes[$which] ?? null;
if ($mode === null) {
throw new \RuntimeException("this packet names a way of "
. "writing that the file does not carry");
}
$long = ($mode["long"] === 1);
$length = $long ? $this->header->long_block
: $this->header->short_block;
$before = true;
$after = true;
if ($long) {
$before = ($reader->readBit() === 1);
$after = ($reader->readBit() === 1);
}
$mapping = $this->setup->mappings[$mode["mapping"]];
$tones = $this->readTones($reader, $mapping, $length);
return $this->soundFrom($tones, $length, $long, $before, $after);
}
/**
* readTones reads the tone strengths of every channel of one
* packet: the loudness curve for each, the leftover detail, the
* putting back of any paired channels, and the multiplying of the
* two together.
*
* @param LowBitReader $reader The packet's bits.
* @param array $mapping Which curve and which leftover each channel
* uses.
* @param int $length How many samples the packet covers.
* @return array One run of strengths for each channel.
*/
public function readTones($reader, $mapping, $length)
{
$half = intdiv($length, 2);
$channels = $this->header->channels;
$curves = [];
$silent = [];
for ($channel = 0; $channel < $channels; $channel++) {
$which = $mapping["floors"][$mapping["submap"][$channel]];
$shape = $this->setup->floors[$which];
$said = $this->readCurve($reader, $shape, $half);
$curves[$channel] = $said;
$silent[$channel] = ($said === null);
}
/* A pair is written as the sound of one channel and how far the
other sits from it, so neither is silent unless both are. */
foreach ($mapping["pairs"] as $pair) {
if (!$silent[$pair["sound"]] || !$silent[$pair["apart"]]) {
$silent[$pair["sound"]] = false;
$silent[$pair["apart"]] = false;
}
}
$left = $this->readLeftovers($reader, $mapping, $half, $silent);
foreach (array_reverse($mapping["pairs"]) as $pair) {
$left = self::unpairChannels($left, $pair, $half);
}
$tones = [];
for ($channel = 0; $channel < $channels; $channel++) {
$run = array_fill(0, $half, 0.0);
if ($curves[$channel] !== null) {
for ($at = 0; $at < $half; $at++) {
$run[$at] = $curves[$channel][$at] *
($left[$channel][$at] ?? 0.0);
}
}
$tones[$channel] = $run;
}
return $tones;
}
/**
* unpairChannels puts a pair of channels back. A stream may write
* one channel's sound and how far the other sits from it, since two
* channels of music are usually close; this turns those two back
* into a left and a right.
*
* @param array $left The runs of leftover detail.
* @param array $pair Which channel carries the sound and which
* carries the distance.
* @param int $half How many strengths a channel holds.
* @return array The runs with that pair put back.
*/
public static function unpairChannels($left, $pair, $half)
{
for ($at = 0; $at < $half; $at++) {
$sound = $left[$pair["sound"]][$at] ?? 0.0;
$apart = $left[$pair["apart"]][$at] ?? 0.0;
/* Which of the two channels keeps the value it was given
depends on the signs of both. Where the sound is loud and
the two channels lean the same way, the first keeps its
value and the second is the difference; where they lean
apart, the second keeps it and the first is the sum. */
if ($sound > 0.0) {
if ($apart > 0.0) {
$one = $sound;
$two = $sound - $apart;
} else {
$two = $sound;
$one = $sound + $apart;
}
} else {
if ($apart > 0.0) {
$one = $sound;
$two = $sound + $apart;
} else {
$two = $sound;
$one = $sound - $apart;
}
}
$left[$pair["sound"]][$at] = $one;
$left[$pair["apart"]][$at] = $two;
}
return $left;
}
/**
* readCurve reads the loudness curve of one channel and draws it
* across every tone. The curve is written as a few points, and the
* places between them are filled by drawing a straight line from
* one point to the next.
*
* @param LowBitReader $reader The packet's bits.
* @param array $shape The shape the curve is drawn from.
* @param int $half How many strengths a channel holds.
* @return array The curve, or nothing where the channel is silent.
*/
public function readCurve($reader, $shape, $half)
{
if ($reader->readBit() === 0) {
return null;
}
$unread = $reader->bitsLeft();
$range = self::CURVE_RANGES[$shape["step"] - 1];
$room = LowBitReader::bitsFor($range - 1);
$heights = [$reader->readBits($room), $reader->readBits($room)];
foreach ($shape["parts"] as $part) {
$class = $shape["classes"][$part];
$held = 0;
if ($class["spread"] > 0) {
$held = max(0, $this->readCode($reader,
$this->setup->books[$class["book"]]));
}
for ($which = 0; $which < $class["holds"]; $which++) {
$book = $class["books"][$held & ((1 << $class["spread"])
- 1)];
$held >>= $class["spread"];
$read = ($book >= 0) ? $this->readCode($reader,
$this->setup->books[$book]) : 0;
$heights[] = max(0, $read);
}
}
return self::drawCurve($heights, $shape, $range, $half);
}
/**
* drawCurve turns the points of a loudness curve into a value for
* every tone. The points are put in the order of the places they
* sit at, each point is nudged by the ones already drawn, and a
* straight line is drawn from each point to the next.
*
* @param array $heights The heights the packet wrote.
* @param array $shape The shape the curve is drawn from.
* @param int $range How tall the curve may be.
* @param int $half How many strengths a channel holds.
* @return array The curve, one value for each tone.
*/
public static function drawCurve($heights, $shape, $range, $half)
{
$places = $shape["places"];
$count = min(count($heights), count($places));
$final = array_fill(0, $count, 0);
$used = array_fill(0, $count, false);
$final[0] = $heights[0];
$final[1] = $heights[1];
$used[0] = true;
$used[1] = true;
for ($at = 2; $at < $count; $at++) {
$low = self::nearestBelow($places, $used, $at);
$high = self::nearestAbove($places, $used, $at);
$guess = self::lineAt($places[$low], $final[$low],
$places[$high], $final[$high], $places[$at]);
/* A height is written as how far it sits from that guess,
in a form that spends fewer bits where the guess leaves
little room above or below it. */
$below = $guess;
$above = $range - $guess;
$room = 2 * min($below, $above);
$held = $heights[$at];
if ($held === 0) {
$used[$at] = false;
$final[$at] = $guess;
continue;
}
/* A point that was written brings its two neighbors into
the drawing with it, even where those neighbors were
left at their guessed heights. Without them the line
runs from the wrong place and the curve bends in the
wrong spot. */
$used[$low] = true;
$used[$high] = true;
$used[$at] = true;
if ($held >= $room) {
$final[$at] = ($above > $below)
? $held - $below + $guess
: $guess - $held + $above - 1;
} else {
$final[$at] = ($held % 2 === 1)
? $guess - intdiv($held + 1, 2)
: $guess + intdiv($held, 2);
}
}
return self::curveAcross($places, $final, $used, $count,
$shape["step"], $half);
}
/**
* curveAcross draws the straight lines between the points of a
* curve and turns each into the loudness it stands for.
*
* @param array $places Where each point sits along the tones.
* @param array $final The height of each point.
* @param array $used Whether each point was written.
* @param int $count How many points there are.
* @param int $step How much one step of height is worth.
* @param int $half How many strengths a channel holds.
* @return array The curve, one value for each tone.
*/
public static function curveAcross($places, $final, $used, $count,
$step, $half)
{
$order = [];
for ($at = 0; $at < $count; $at++) {
if ($used[$at]) {
$order[] = ["at" => $places[$at], "height" =>
$final[$at] * $step];
}
}
usort($order, function ($one, $two) {
return $one["at"] - $two["at"];
});
$curve = array_fill(0, $half, 0.0);
$last_at = 0;
$last_height = $order[0]["height"] ?? 0;
foreach ($order as $point) {
self::drawLine($curve, $last_at, $last_height, $point["at"],
$point["height"], $half);
$last_at = $point["at"];
$last_height = $point["height"];
}
for ($at = $last_at; $at < $half; $at++) {
$curve[$at] = self::loudnessOf($last_height);
}
return $curve;
}
/**
* drawLine fills the tones between two points of the curve, moving
* from the height of one to the height of the other a step at a
* time.
*
* @param array $curve The curve being drawn, changed in place.
* @param int $from Where the line starts.
* @param int $from_height How high it starts.
* @param int $to Where the line ends.
* @param int $to_height How high it ends.
* @param int $half How many strengths a channel holds.
*/
public static function drawLine(&$curve, $from, $from_height, $to,
$to_height, $half)
{
$across = $to - $from;
if ($across <= 0) {
return;
}
$rise = $to_height - $from_height;
for ($at = $from; $at < $to && $at < $half; $at++) {
$height = $from_height +
intdiv($rise * ($at - $from), $across);
$curve[$at] = self::loudnessOf($height);
}
}
/**
* loudnessOf turns a height on the curve into the loudness it
* stands for. The curve counts in steps, each a fixed fraction
* louder than the one below, so the loudness rises steeply with the
* height.
*
* @param int $height The height on the curve.
* @return float The loudness it stands for.
*/
public static function loudnessOf($height)
{
if ($height <= 0) {
return 0.0;
}
if ($height >= self::CURVE_STEPS) {
$height = self::CURVE_STEPS - 1;
}
return exp(($height - (self::CURVE_STEPS - 1)) *
self::CURVE_STEP_SIZE);
}
/**
* nearestBelow finds the point already drawn that sits closest
* below a given place along the tones.
*
* @param array $places Where each point sits.
* @param array $used Whether each point counts.
* @param int $at Which point is being drawn.
* @return int Which point sits closest below it.
*/
public static function nearestBelow($places, $used, $at)
{
$best = 0;
for ($which = 0; $which < $at; $which++) {
if ($places[$which] < $places[$at]
&& $places[$which] >= $places[$best]) {
$best = $which;
}
}
return $best;
}
/**
* nearestAbove finds the point already drawn that sits closest
* above a given place along the tones.
*
* @param array $places Where each point sits.
* @param array $used Whether each point counts.
* @param int $at Which point is being drawn.
* @return int Which point sits closest above it.
*/
public static function nearestAbove($places, $used, $at)
{
$best = 1;
for ($which = 0; $which < $at; $which++) {
if ($places[$which] > $places[$at]
&& $places[$which] <= $places[$best]) {
$best = $which;
}
}
return $best;
}
/**
* lineAt says how high a straight line between two points stands at
* a place between them, which is the height a point is nudged from.
*
* @param int $from Where the line starts.
* @param int $from_height How high it starts.
* @param int $to Where it ends.
* @param int $to_height How high it ends.
* @param int $at The place to measure at.
* @return int The height there.
*/
public static function lineAt($from, $from_height, $to, $to_height,
$at)
{
$across = $to - $from;
if ($across === 0) {
return $from_height;
}
return $from_height + intdiv(($to_height - $from_height) *
($at - $from), $across);
}
/**
* readLeftovers reads the detail left over once the loudness curve
* is taken out. A file may write it channel by channel or with the
* channels woven together, and the way is named in the tables.
*
* @param LowBitReader $reader The packet's bits.
* @param array $mapping Which leftover each channel uses.
* @param int $half How many strengths a channel holds.
* @param array $silent Whether each channel is silent.
* @return array One run of leftover detail for each channel.
*/
public function readLeftovers($reader, $mapping, $half, $silent)
{
$channels = $this->header->channels;
$left = [];
for ($channel = 0; $channel < $channels; $channel++) {
$left[$channel] = array_fill(0, $half, 0.0);
}
$which = $mapping["residues"][0];
$said = $this->setup->residues[$which];
$wanted = [];
for ($channel = 0; $channel < $channels; $channel++) {
if (!$silent[$channel]) {
$wanted[] = $channel;
}
}
if ($wanted === []) {
return $left;
}
if ($said["kind"] === 2) {
$woven = $this->readOneResidue($reader, $said,
$half * count($wanted), 1);
foreach ($wanted as $place => $channel) {
for ($at = 0; $at < $half; $at++) {
$left[$channel][$at] =
$woven[0][$at * count($wanted) + $place] ?? 0.0;
}
}
return $left;
}
$runs = $this->readOneResidue($reader, $said, $half,
count($wanted));
foreach ($wanted as $place => $channel) {
$left[$channel] = $runs[$place];
}
return $left;
}
/**
* readOneResidue reads one run of leftover detail. The run is cut
* into parts, each part is told which book to read it with, and the
* parts are read in several passes so that a part may be written
* coarsely first and refined afterwards.
*
* @param LowBitReader $reader The packet's bits.
* @param array $said How this leftover detail is written.
* @param int $length How many values one run holds.
* @param int $runs How many runs to read.
* @return array The runs.
*/
public function readOneResidue($reader, $said, $length, $runs)
{
$out = [];
for ($at = 0; $at < $runs; $at++) {
$out[$at] = array_fill(0, $length, 0.0);
}
$begin = min($said["begin"], $length);
$end = min($said["end"], $length);
$part_size = $said["part_size"];
$parts = intdiv($end - $begin, $part_size);
if ($parts <= 0) {
return $out;
}
$classbook = $this->setup->books[$said["classbook"]];
$per_code = max(1, $classbook["holds"]);
$classes = [];
/* The format walks the parts eight times over. The first walk
reads which class each part belongs to, a whole group of
parts to a code. Every walk then reads the parts whose class
has a book for that walk, and a later walk refines what an
earlier one wrote. */
for ($pass = 0; $pass < 8; $pass++) {
$part = 0;
while ($part < $parts) {
if ($pass === 0) {
for ($run = 0; $run < $runs; $run++) {
$held = $this->readCode($reader, $classbook);
if ($held < 0) {
return $out;
}
for ($which = $per_code - 1; $which >= 0;
$which--) {
$classes[$run][$part + $which] =
$held % $said["classes"];
$held = intdiv($held, $said["classes"]);
}
}
}
for ($which = 0; $which < $per_code && $part < $parts;
$which++, $part++) {
for ($run = 0; $run < $runs; $run++) {
$class = $classes[$run][$part] ?? 0;
$book = $said["books"][$class][$pass] ?? -1;
if ($book < 0) {
continue;
}
if ($reader->bitsLeft() <= 0) {
return $out;
}
$this->readPart($reader,
$this->setup->books[$book], $out[$run],
$begin + $part * $part_size, $part_size,
$said["kind"]);
}
}
}
}
return $out;
}
/**
* readPart reads one part of a run of leftover detail and adds what
* it holds to what is there already. A later pass over the same
* part refines what an earlier pass wrote, so the values are added
* rather than replaced.
*
* @param LowBitReader $reader The packet's bits.
* @param array $book The book this part is written with.
* @param array $run The run being filled, changed in place.
* @param int $from Where this part begins in the run.
* @param int $size How many values the part holds.
* @param int $kind Which of the three ways this leftover detail is
* written, since the first lays its numbers out differently.
*/
public function readPart($reader, $book, &$run, $from, $size,
$kind = 1)
{
$holds = max(1, $book["holds"]);
if ($kind === 0) {
/* The first way spreads the numbers of one code across the
part rather than laying them side by side. */
$steps = intdiv($size, $holds);
for ($step = 0; $step < $steps; $step++) {
$entry = $this->readCode($reader, $book);
if ($entry < 0) {
return;
}
$numbers = $book["numbers"][$entry] ?? [];
foreach ($numbers as $which => $value) {
$spot = $from + $step + $which * $steps;
if ($spot >= count($run)) {
break;
}
$run[$spot] += $value;
}
}
return;
}
for ($at = 0; $at < $size; $at += $holds) {
$entry = $this->readCode($reader, $book);
if ($entry < 0) {
return;
}
$numbers = $book["numbers"][$entry] ?? [];
foreach ($numbers as $which => $value) {
if ($from + $at + $which >= count($run)) {
break;
}
$run[$from + $at + $which] += $value;
}
}
}
/**
* readCode reads one code from a book and hands back which entry it
* names. The codes are of different lengths, so bits are taken one
* at a time until they match an entry.
*
* @param LowBitReader $reader The packet's bits.
* @param array $book The book to read with.
* @return int Which entry the code names.
*/
public function readCode($reader, $book)
{
if ($reader->bitsLeft() <= 0) {
return -1;
}
$held = 0;
$length = 0;
while ($length < 32) {
$held = ($held << 1) | $reader->readBit();
$length++;
foreach ($book["lengths"] as $entry => $entry_length) {
if ($entry_length === $length
&& ($book["codes"][$entry] ?? -1) === $held) {
return $entry;
}
}
if ($reader->bitsLeft() <= 0) {
/* A packet may end in the middle of a code. The format
says a decoder keeps what it has read and stops
there, rather than treating the end as damage. */
return -1;
}
}
throw new \RuntimeException("a code in this packet is not in "
. "the book it was written with");
}
/**
* soundFrom turns the tone strengths of a packet back into samples:
* the transform back into sound, the fade, and the adding of the
* tail the packet before it left. The channels are averaged into
* one, which is what this folder writes.
*
* @param array $tones One run of strengths for each channel.
* @param int $length How many samples the packet covers.
* @param bool $long Whether this is a long stretch.
* @param bool $before Whether the packet before was long.
* @param bool $after Whether the packet after is long.
* @return array The samples this packet gives.
*/
public function soundFrom($tones, $length, $long, $before, $after)
{
$half = intdiv($length, 2);
$transform = Mdct::forSize($half);
$fade = self::fadeFor($length, $long, $before, $after,
$this->header->short_block);
$channels = count($tones);
$given = [];
$tail = [];
/* A packet hands back the sound between the middle of the
packet before it and its own middle. Where the two are the
same length that is half a packet, and where one is short and
the other long it is half of each, which is what a join
between the two lengths needs. */
$before_half = $this->carried_length;
$count = intdiv($before_half, 2) + intdiv($half, 2);
$lead = intdiv($before_half, 2) - intdiv($half, 2);
for ($channel = 0; $channel < $channels; $channel++) {
$sound = $transform->inverse($tones[$channel]);
/* The transform this folder shares with the other
formats divides by half the length of a stretch on the
way back, which the formats that use it expect. Vorbis
does not: its strengths are written for a transform that
divides by nothing, so the division is undone here. */
$fit = $half * self::TRANSFORM_SHARE;
$faded = [];
for ($at = 0; $at < $length; $at++) {
$faded[$at] = $sound[$at] * $fade[$at] * $fit;
}
$carried = $this->carried[$channel] ?? [];
$run = [];
for ($at = 0; $at < $count; $at++) {
$from_before = $carried[$at] ?? 0.0;
$here = ($at - $lead >= 0) ? ($faded[$at - $lead] ?? 0.0)
: 0.0;
$run[$at] = $from_before + $here;
}
$given[$channel] = $run;
$keep = [];
for ($at = 0; $at < $half; $at++) {
$keep[$at] = $faded[$half + $at];
}
$tail[$channel] = $keep;
}
$this->carried = $tail;
$this->carried_length = $half;
return self::averageChannels($given, $count);
}
/**
* averageChannels turns the runs of every channel into one run, so
* that what comes out matches the single channel this folder
* writes.
*
* @param array $given One run for each channel.
* @param int $count How many samples each run holds.
* @return array The one run.
*/
public static function averageChannels($given, $count)
{
$channels = count($given);
if ($channels === 0 || $count <= 0) {
return [];
}
$run = [];
for ($at = 0; $at < $count; $at++) {
$total = 0.0;
foreach ($given as $one) {
$total += $one[$at] ?? 0.0;
}
$run[$at] = $total / $channels;
}
return $run;
}
/**
* fadeFor builds the curve a packet is faded in and out with. Two
* packets fade into one another over the place where they overlap,
* and their fades must add to one there, so a long packet beside a
* short one holds flat and then falls over the short one's length.
*
* @param int $length How many samples the packet covers.
* @param bool $long Whether this is a long stretch.
* @param bool $before Whether the packet before was long.
* @param bool $after Whether the packet after is long.
* @param int $short How many samples a short stretch covers.
* @return array The fade, one value for each sample.
*/
public static function fadeFor($length, $long, $before, $after,
$short)
{
$half = intdiv($length, 2);
$fade = array_fill(0, $length, 1.0);
$rise_length = ($long && !$before) ? $short : $length;
$rise_from = ($long && !$before)
? intdiv($length, 4) - intdiv($short, 4) : 0;
for ($at = 0; $at < $rise_from; $at++) {
$fade[$at] = 0.0;
}
for ($at = 0; $at < intdiv($rise_length, 2); $at++) {
$fade[$rise_from + $at] = self::risingAt($at,
intdiv($rise_length, 2));
}
for ($at = $rise_from + intdiv($rise_length, 2); $at < $half;
$at++) {
$fade[$at] = 1.0;
}
$fall_length = ($long && !$after) ? $short : $length;
$fall_from = ($long && !$after)
? intdiv(3 * $length, 4) - intdiv($short, 4) : $half;
for ($at = $half; $at < $fall_from; $at++) {
$fade[$at] = 1.0;
}
for ($at = 0; $at < intdiv($fall_length, 2); $at++) {
$fade[$fall_from + $at] = self::risingAt(
intdiv($fall_length, 2) - 1 - $at,
intdiv($fall_length, 2));
}
for ($at = $fall_from + intdiv($fall_length, 2); $at < $length;
$at++) {
$fade[$at] = 0.0;
}
return $fade;
}
/**
* risingAt says how far up the rising half of a fade one place
* sits. The curve is a sine of a sine, which is the shape that lets
* two overlapping fades add to one.
*
* @param int $at Which place along the rise.
* @param int $length How long the rise is.
* @return float How far up the fade that place sits.
*/
public static function risingAt($at, $length)
{
if ($length <= 0) {
return 1.0;
}
$along = ($at + 0.5) / $length * M_PI / 2.0;
return sin(M_PI / 2.0 * sin($along) * sin($along));
}
}