Building the app
Audio
Sound effects, streamed music and sound an app makes itself, mixed in Go, with WAV, Ogg Vorbis and MP3.
Package audio plays sound from Go: effects decoded into memory, music
streamed as it plays, and sound an app makes itself. It mixes everything
playing in Go and sends it to the system's audio output, with no cgo, in
apps of native UI or web pages, on every platform MyGo runs on, browsers
included.
import (
"github.com/egoist/mygo/audio"
_ "github.com/egoist/mygo/audio/vorbis" // Ogg Vorbis
)
//go:embed sounds
var sounds embed.FS
data, _ := sounds.ReadFile("sounds/hoe.wav")
hoe, err := audio.DecodeSound(data) // decoded once
hoe.Play() // as often as it sounds, over itself
f, _ := sounds.Open("sounds/spring.ogg")
music, err := audio.OpenStream(f.(io.ReadSeeker)) // 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 + rand.Float64()*0.1).Play()OpenStream decodes a file about a second ahead as it plays, for music
and other long audio, and 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, and
Playing turns false at its end, so a game can wait for a sound;
OnEnd calls a function then, on a goroutine of its own, as a playlist
plays the next track (a looping player never ends).
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.
| 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 |
SetPreservesPitch(true) |
keeps the pitch at any speed, as HTML media's preservesPitch: speech at 1.5× in the voice it was recorded in |
SetLoop(true) |
starts again at the end, without a gap |
SetLoopRange(start, end) |
loops from end back to start: 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.
SetSpeed resamples, so the pitch changes with the speed, which games
rely on for effects that play often. A podcast, lecture or voice-message
player at 1.25×, 1.5× or 2× wants the voice to stay where it is: with
SetPreservesPitch(true) the player time-stretches instead (WSOLA, in Go),
playing segments of about 30 ms crossfaded where their waveforms match, so
whole periods are skipped or repeated and the pitch stays:
podcast, err := audio.OpenStream(f)
podcast.SetPreservesPitch(true).SetSpeed(1.5).Play()It suits speech and other pitched sound from about 0.5× to 4×, where sharp
attacks such as drums' come out a little smeared, and it costs about 1% of
a core while a player stretches; players that don't preserve pitch cost
nothing more. Speed changes, seeks, loops and turning it on or off go on
without a gap, and Position and OnEnd keep the source's time as with
resampling.
Media keys and Now Playing
mygo.NowPlaying.Control lets the media keys and the system's media
controls (Control Center on macOS, the volume flyout on Windows, MPRIS
clients on Linux) play, pause, stop and seek a player, and shows its state
there; see Now Playing:
release := mygo.NowPlaying.Control(music)
mygo.NowPlaying.Set(mygo.NowPlayingInfo{Title: "Spring", Artist: "Vivaldi", Commands: mygo.MediaNext})
music.Play()
mygo.NowPlaying.Sync() // after playing it from the appFormats
WAV (integer PCM of 8 to 32 bits, 32 or 64 bit floats) decodes out of the
box. Import a format's package for its side effect, as with package
image:
| package | format | decoder |
|---|---|---|
audio |
WAV | built in |
audio/vorbis |
Ogg Vorbis | jfreymuth/oggvorbis |
audio/mp3 |
MP3 | hajimehoshi/go-mp3 |
audio/flac |
FLAC, in MP4 too | mewkiz/flac |
audio/aac |
AAC-LC: in MP4 (.m4a), and ADTS (.aac) | tphakala/go-aac, under the LGPL |
audio/mp4 |
MP4 (.m4a, .mp4, .m4s), with Apple Lossless (ALAC) | built in |
A file that starts with an ID3 tag, as MP3s do and some FLAC and AAC files, is of the format that follows it.
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 plays 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(12500*time.Millisecond, 100*time.Second).Play()RegisterFormat adds a format of your own, and a Decoder you write plays
sound an app makes as it plays, such as a synthesizer:
type tone struct{ pos int64 }
func (t *tone) SampleRate() int { return 48000 }
func (t *tone) Channels() int { return 1 }
func (t *tone) Len() int64 { return -1 } // endless
func (t *tone) SetPosition(f int64) error { t.pos = f; return nil }
func (t *tone) Read(buf []float32) (int, error) {
for i := range buf {
buf[i] = 0.2 * float32(math.Sin(2*math.Pi*440*float64(t.pos)/48000))
t.pos++
}
return len(buf), nil
}
audio.NewPlayer(&tone{}).Play()NewSound takes samples an app computed once.
MP4
Package audio/mp4 reads the container: a whole .m4a, or one cut into
fragments, as a stream for DASH is (.m4s), whose packets are in a header
of their own each, after the movie header. Codecs decode the packets:
Apple Lossless is built in, and audio/aac and audio/flac add AAC and
FLAC in MP4, importing audio/mp4 themselves:
import _ "github.com/egoist/mygo/audio/aac" // .m4a of AAC, and .aac
f, _ := os.Open("song.m4a")
music, err := audio.OpenStream(f)It reads the packets as they play, so a file still coming in, as a
stream from the network, plays as it comes when its reader waits for the
bytes asked of it: a fragmented one plays from its first fragment, its
length from its segment index (sidx), and one with no index is read
through for its length as it opens. The encoder's priming, which an edit
list says to skip, is skipped, and the padding of the last packet, so a
track lasts what it was encoded from. A seek lands on the frame asked
for; AAC's decodes the packet before too, whose sound overlaps it.
mp4.ReadTrack describes a file's sound track from its movie header,
without decoding it, for a music library scanning files: its codec, its
configuration, how long it plays, and whether a codec registered decodes
it (Decodable). mp4.RegisterCodec adds a codec of your own, by the
type of its sample entry:
mp4.RegisterCodec("Opus", func(config []byte) (mp4.Codec, error) {
return newOpusCodec(config) // config is the boxes within the sample entry: dOps
})HE-AAC (AAC+, with SBR) does not decode: its files fail to open, and
Decodable says so. go-aac is under the LGPL 2.1 or later, unlike MyGo:
an app that imports audio/aac links it, and takes on the LGPL's terms
for it; apps that don't, don't.
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 40 ms ahead; players of other rates are resampled as they play. Those that play faster than 48 kHz, a sound of 88.2, 96 or 192 kHz or one sped up, go through a low-pass first, flat to 20 kHz and 80 dB down from 24 kHz, so that what they have above 24 kHz does not fold back as a tone you hear; it costs about 1% of a core for each such player at 96 kHz.
audio.SetLatency sets how far ahead it mixes, as Web Audio's
latencyHint:
| latency | ahead | for |
|---|---|---|
LatencyInteractive (the default) |
about 40 ms | games and interfaces: sounds start, stop and change as soon as asked |
LatencyPlayback |
about 250 ms | music players: playback rides out the moments the system keeps an app waiting, as macOS does for up to 0.2 s while its appearance changes, where 40 ms runs dry and is heard to stop |
With LatencyPlayback, plays, pauses, seeks and volume changes are heard
that much later; players' Position follows what is heard. Set it before playing: an output already open opens
again, which drops what it held.
| platform | output |
|---|---|
| macOS | an Audio Queue on the default output, which follows it |
| Linux | PulseAudio's simple API (which PipeWire serves as well), else ALSA's default device |
| Windows | WASAPI in shared mode, opened again when the default output changes |
| browsers | an AudioWorklet; a page may start audio only after the user interacted with it, so it starts on the first click or key |
Where no output opens, as on a machine with no audio device, players still
play on time, unheard, and audio.Err says why. Every function and method
is safe to call from any goroutine.
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.