Audio
MyPHP\Audio plays sound from PHP: effects decoded into memory, music
streamed as it plays, and sound an app makes itself. It mixes everything
playing in PHP and sends it to the system's audio output, in apps of
native UI or web pages.
use MyPHP\Audio\Audio;
$hoe = Audio::decodeSound(file_get_contents(__DIR__ . '/sounds/hoe.wav')); // decoded once
$hoe->play(); // as often as it sounds, over itself
$music = Audio::openStream(__DIR__ . '/sounds/spring.ogg'); // decoded as it plays
$music->setLoop(true)->setVolume(0.6)->play();
Sounds and streams
A Sound holds decoded samples for effects that play often: play()
starts a new player each time, so a sound plays over itself, and
newPlayer() returns one paused, to set up before it plays:
$hoe->newPlayer()->setPan(-0.4)->setSpeed(0.95 + mt_rand() / mt_getrandmax() * 0.1)->play();
Audio::openStream decodes a file, or a seekable stream, about a second
ahead as it plays, for music and other long audio, and
Audio::newPlayer does the same with any Decoder. Close a stream's
player when you are done with it: it closes the file. A sound's players
need no closing.
Players
A Player plays, pauses (pause()), and goes back to the start
(stop()); play() after its end plays it again from the start.
seek() moves it, position() and duration() say where it is and how
long it is, in seconds, and playing() turns false at its end, so a
game can wait for a sound.
position() is where the player is heard. The output mixes ahead of what
the speakers play (see the output), and position() lags
what it mixed by that much, so lyrics, a progress bar or a visualizer
drawn from it keep time with the sound: after pause() it goes on while
the output plays what it held, and after play() it waits until the
player is heard. seek() and stop() move it at once.
onEnd($fn) calls $fn each time the player plays to its end, as a
playlist plays the next track then (a looping player never ends), on the
main thread once its last frames are mixed; it returns a function that
removes it.
| setting | effect |
|---|---|
| setVolume($v) | a gain: 1 as decoded, 0 silent |
| setPan($p) | from the left speaker (-1) to the right (1): mono keeps its power, stereo both channels, as Web Audio's StereoPannerNode |
| setSpeed($s) | speed and pitch together, as a record's: 2 is twice as fast and an octave higher |
| setLoop(true) | starts again at the end, without a gap |
| setLoopRange($start, $end) | loops from $end back to $start, in seconds: a tune's intro plays once, then its loop ($end 0 is the end) |
The setters return the player, to chain them. Volume changes, pauses and
resumes ramp over 5 ms, so none clicks. Audio::setVolume sets the
volume of everything.
Media keys and Now Playing
NowPlaying::control() lets the media keys and Control Center's media
controls play, pause, stop and seek a player, and shows its state there:
$release = NowPlaying::control($music);
NowPlaying::set(new NowPlayingInfo(title: 'Spring', artist: 'Vivaldi', commands: MediaCommand::NEXT));
$music->play();
NowPlaying::sync(); // after playing it from the app
NowPlaying::onCommand(function (int $cmd, float $position): void { /* MediaCommand::NEXT: the next track */ });
NowPlaying::set(), setPlayback() and onCommand() serve an app that
plays by other means; clear() takes the app out of the controls.
Formats
| format | decoder |
|---|---|
| WAV (integer PCM of 8 to 32 bits, 32 or 64 bit floats) | PHP |
| Ogg Vorbis | PHP |
| MP3, AAC and ALAC in M4A, FLAC, AIFF, CAF | the system's (AudioToolbox) |
Prefer Ogg Vorbis for music that loops: MP3 files begin and end with the
encoder's padding, a few milliseconds of silence that a loop would play
too, unless setLoopRange leaves it out.
// 12.5 s of intro, then bars from 12.5 s to 1 min 40 s, again and again.
$music->setLoopRange(12.5, 100)->play();
Audio::registerFormat adds a format of your own, and a Decoder you
write plays sound an app makes as it plays, such as a synthesizer:
final class Tone implements Decoder
{
private int $pos = 0;
public function sampleRate(): int { return 48000; }
public function channels(): int { return 1; }
public function len(): int { return -1; } // endless
public function setPosition(int $frame): void { $this->pos = $frame; }
public function read(int $frames): array
{
$out = [];
for ($i = 0; $i < $frames; $i++, $this->pos++) { $out[] = 0.2 * sin(2 * M_PI * 440 * $this->pos / 48000); }
return $out;
}
}
Audio::newPlayer(new Tone())->play();
Audio::newSound takes samples an app computed once.
The output
The output opens when something first plays and closes five seconds
after everything stopped, so an app that plays nothing keeps no audio
device busy. It plays at 48 kHz in stereo, about 85 ms ahead, through an
Audio Queue on the default output, which follows it; players of other
rates are resampled as they play. PHP mixes on the main thread, as the
app runs (App::run): work that keeps the main thread busy for longer
than that makes the sound break up.
Audio::setLatency sets how far ahead it mixes, as Web Audio's
latencyHint (mygo's SetLatency):
| latency | ahead | for |
|---|---|---|
| Latency::Interactive (the default) | about 85 ms | games and interfaces: sounds start, stop and change as soon as asked |
| Latency::Playback | about 256 ms | music players: playback rides out the moments the system keeps the app waiting, as macOS does for up to 0.2 s while its appearance changes |
With Latency::Playback, plays, pauses, seeks and volume changes are
heard that much later; players' position() follows what is heard. Set it before playing: an output open opens again,
which drops what it held.
Where no output opens, as on a machine with no audio device, players
still play on time, unheard, and Audio::err() says why.
examples/piano is a piano of synthesized
keys that plays from the pointer and the keyboard, with a sustain pedal,
dampers that fade its strings out, and a few tunes it plays by itself.