# sound

> Load and play WAV, MP3, OGG and QOA sounds, with volume, pitch, pan, pause and seeking.

This is the Markdown version of https://osl-lang.org/packages/sound. Every page of the OSL docs has one: add .md to its path. https://osl-lang.org/llms.txt lists them all.

```osl
import "std:sound"
```

`std:sound` plays audio. Load a sound from a file, bytes or a URL, then `play`, `pause`, `seek` and change its volume, pitch and stereo position while it plays. It works on its own or beside a [raylib](/packages/raylib.md) window, and supports WAV, MP3, OGG Vorbis and QOA.

Playing needs an audio device, so the playing examples are marked compile-only. Loading and inspecting a sound doesn't, so that example runs here. To have a sound to load, it makes a half-second beep in memory with a small helper in `tone.osl`.

## Load a sound

`sound.decode(bytes)` reads a sound from bytes and `sound.decodeFile(path)` from a file. Both return a result. A sound knows how long it is, in milliseconds. A new one has volume 1, pitch 1 (normal speed) and pan 0 (centre), and its setters take effect at once, even while it plays:

File `main.osl`:

```osl
import "std:sound"
import "./tone.osl"

*sound.Sound beep = sound.decode(tone(500)).unwrap()
log beep.duration()
log beep.volume() beep.pitch() beep.pan()

beep.setVolume(0.5)
beep.setPitch(2)
beep.setPan(-1)
log beep.volume() beep.pitch() beep.pan()

log beep.isPlaying() beep.isPaused() beep.position()
beep.seek(250).unwrap()
log beep.position() beep.progress()
```

File `tone.osl`:

```osl
// A square-wave beep as a WAV file in memory: 8000 samples a second, 8 bits.
def le32(int n) byte[] -> [
  (n % 256).as<byte>,
  (floor(n / 256) % 256).as<byte>,
  (floor(n / 65536) % 256).as<byte>,
  (floor(n / 16777216) % 256).as<byte>
]

def tone(int ms) byte[] (
  int count = floor((8000 * ms) / 1000).as<int>
  byte[] samples = []
  for i in 0 to count - 1 (
    samples.append((if floor(i / 9) % 2 == 0 then 200 else 56).as<byte>)
  )
  byte[] header = [0x52, 0x49, 0x46, 0x46] + le32(36 + count)
  header = header + [0x57, 0x41, 0x56, 0x45, 0x66, 0x6D, 0x74, 0x20]
  header = header + le32(16) + [1, 0, 1, 0] + le32(8000) + le32(8000)
  header = header + [1, 0, 8, 0] + [0x64, 0x61, 0x74, 0x61] + le32(count)
  return header + samples
)
```

Output:

```text
500
1 1 0
0.5 2 -1
false false 0
250 0.5
```

Change `tone(500)` to `tone(1200)` and run it again.

## Play it

`play()` starts the sound, or resumes it if it was paused. It fails with an `IOError` when there is no audio device, so it returns a result:

```osl
import "std:sound"

*sound.Sound jump = sound.decodeFile("jump.wav").unwrap()
jump.play().unwrap()
```

The program ends when `main` ends, so a short program that plays a sound needs to stay alive, for instance with `thread.sleep(jump.duration())`. A game keeps running anyway.

## Volume, pitch and pan

Volume is 0 (silent) to 1. Pitch is the playback speed, so 2 is twice as fast and an octave higher, and 0.5 an octave lower. Pan runs from -1 (left) to 1 (right). A value out of range throws a `ValueError`. `sound.setGlobalVolume` scales every sound at once, like a master volume knob.

**Quiz.** You call `beep.setPitch(0.5)`. What do you hear?

- [ ] Half as loud.
- [x] Half as fast and an octave lower.
- [ ] Panned to the left.

Answer: Pitch is a speed multiplier. Volume is the loudness control.

## Pause, stop and seek

A sound has a position in milliseconds. `pause` keeps it, `stop` rewinds to the start, and `seek(ms)` jumps anywhere, playing or not. `progress()` is the position as a fraction from 0 to 1, so a progress bar is `progress()` times the bar's width.

## Sound effects and music

Sound effects are loaded once and played often. Each `Sound` plays one voice, and `play()` on one that is already playing does nothing, so for overlapping effects (many gunshots) load the file more than once. Music is one long sound with `isPlaying()` checked each frame:

```osl
import "std:raylib"
import "std:sound"

*sound.Sound music = sound.decodeFile("theme.ogg").unwrap()
*sound.Sound click = sound.decodeFile("click.wav").unwrap()
music.setVolume(0.4)
music.play().unwrap()

raylib.runWindow(def(ms) (
  if raylib.isKeyPressed("space") (
    click.stop()
    click.play().unwrap()
  )
  if !music.isPlaying() (
    music.seek(0).unwrap()
    music.play().unwrap()
  )
), {background: "#181825"})
```

`click.stop()` rewinds it first, so a quick second press restarts the sound.

## From a URL, and cleaning up

`sound.fetch(url)` downloads and decodes a sound from an `http` or `https` address. `sound.isReady()` opens the audio device and tells you whether there is one, which is how a program checks before it relies on sound. `sound.close()` stops everything and releases the device, and a sound's own `close()` frees it. Using a closed sound throws a `StateError`.

### Exercise: Reject bad data

Decode the bytes `[1, 2, 3, 4]`. They aren't a sound, so the result is an error. Print its kind.

Starting code:

```osl
import "std:sound"

log sound.globalVolume()
```

It should print:

```text
ParseError
```

Hint 1: `sound.decode(...)` returns a result. Use `.unwrapErr().kind`.

A solution:

```osl
import "std:sound"

log sound.decode([1, 2, 3, 4]).unwrapErr().kind
```

## API reference

## API reference

Every member of `std:sound`, generated from the source of OSL.

### Functions

#### `sound.clear(any id) boolean`

**Deprecated.** Deprecated: use snd.close() on a *sound.Sound instead.

clear frees the sound with this ID.

#### `sound.close()`

close stops every sound and releases the audio device until a sound plays
again. Sounds stay loaded.

#### `sound.currentTime(any id) number`

**Deprecated.** Deprecated: use snd.position() on a *sound.Sound instead; it returns milliseconds.

currentTime returns the playback position of the sound with this ID in
seconds, or 0.

#### `sound.decode(byte[] data) result<*sound.Sound, error>`

decode reads a WAV, MP3, OGG Vorbis or QOA sound from bytes, recognising
the format from the data rather than a file name. Fails with ParseError
when the bytes aren't a sound it can decode.

#### `sound.decodeFile(string path) result<*sound.Sound, error>`

decodeFile reads a sound file (WAV, MP3, OGG Vorbis or QOA, up to 64 MiB).
Fails with NotFoundError or IOError when the file can't be read,
UnsupportedError when it is too large, and ParseError when it isn't a
sound it can decode.

#### `sound.duration(any id) number`

**Deprecated.** Deprecated: use snd.duration() on a *sound.Sound instead; it returns milliseconds.

duration returns the length of the sound with this ID in seconds, or 0.

#### `sound.fetch(string url, object? options) result<*sound.Sound, error>`

fetch downloads a sound from an http or https URL and decodes it. Options:
timeout (milliseconds, default 10000). Fails with NetworkError,
TimeoutError, NotFoundError (404 or 410), PermissionError (401 or 403),
UnsupportedError for more than 64 MiB, and ParseError when the response
isn't a sound it can decode.

#### `sound.globalVolume() number`

globalVolume returns the volume applied to every sound, from 0 to 1.

#### `sound.info(string id, string field) number`

**Deprecated.** Deprecated: use the *sound.Sound methods instead.

info returns one numeric field of the sound with this ID ("loaded",
"playing", "volume", "pitch", "pan", "duration" or "current_time"), or 0.

#### `sound.isReady() boolean`

isReady reports whether an audio output device is available, opening it
if needed. Playing a sound opens it too.

#### `sound.load(any url) string`

**Deprecated.** Deprecated: use sound.decodeFile(path) or sound.fetch(url) instead; they return a *sound.Sound.

load loads a sound and returns its ID, or "" when it can't be loaded.

#### `sound.loaded(any id) boolean`

**Deprecated.** Deprecated: use snd.isClosed() on a *sound.Sound instead.

loaded reports whether a sound with this ID is loaded.

#### `sound.masterVolume(number value)`

**Deprecated.** Deprecated: use sound.setGlobalVolume(volume) instead.

masterVolume sets the volume of every sound, clamped to 0 to 1.

#### `sound.new(any url) string`

**Deprecated.** Deprecated: use sound.decodeFile(path) or sound.fetch(url) instead; they return a *sound.Sound.

new loads a sound from a path, file:// URL or http(s) URL and returns its
ID, or "" when it can't be loaded.

#### `sound.pan(any id, number value) boolean`

**Deprecated.** Deprecated: use snd.setPan(pan) on a *sound.Sound instead.

pan sets the stereo position of the sound with this ID, clamped to -1 to 1.

#### `sound.pause(any id) boolean`

**Deprecated.** Deprecated: use snd.pause() on a *sound.Sound instead.

pause pauses the sound with this ID and returns false when it wasn't
playing.

#### `sound.percent(any id) number`

**Deprecated.** Deprecated: use snd.progress() on a *sound.Sound instead.

percent returns how far through the sound with this ID playback is, from 0
to 1, or 0.

#### `sound.pitch(any id, number value) boolean`

**Deprecated.** Deprecated: use snd.setPitch(pitch) on a *sound.Sound instead.

pitch sets the playback speed of the sound with this ID and returns false
for a speed that isn't positive.

#### `sound.play(any id) boolean`

**Deprecated.** Deprecated: use snd.play() on a *sound.Sound instead.

play starts or resumes the sound with this ID and returns false when the ID
is unknown or no audio device is available.

#### `sound.playing(any id) boolean`

**Deprecated.** Deprecated: use snd.isPlaying() or snd.isPaused() on a *sound.Sound instead.

playing reports whether the sound with this ID is playing or paused.

#### `sound.ready() boolean`

**Deprecated.** Deprecated: use sound.isReady() instead.

ready reports whether an audio output device is available.

#### `sound.setGlobalVolume(number volume)`

setGlobalVolume sets the volume applied to every sound, from 0 to 1.
Throws ValueError outside that range.

#### `sound.start(any id) boolean`

**Deprecated.** Deprecated: use snd.play() on a *sound.Sound instead.

start starts or resumes the sound with this ID.

#### `sound.stop(any id) boolean`

**Deprecated.** Deprecated: use snd.stop() on a *sound.Sound instead.

stop stops the sound with this ID and returns false when it wasn't
playing.

#### `sound.unload(any id) boolean`

**Deprecated.** Deprecated: use snd.close() on a *sound.Sound instead.

unload frees the sound with this ID and returns false when the ID is
unknown.

#### `sound.unpause(any id) boolean`

**Deprecated.** Deprecated: use snd.resume() on a *sound.Sound instead.

unpause resumes the sound with this ID and returns false when it wasn't
paused.

#### `sound.volume(any id, number value) boolean`

**Deprecated.** Deprecated: use snd.setVolume(volume) on a *sound.Sound instead.

volume sets the volume of the sound with this ID, clamped to 0 to 1.

### `*sound.Sound`

#### `sound.close()`

close stops the sound and frees it. Closing it again does nothing; any
other use afterwards throws StateError.

#### `sound.duration() number`

duration returns the sound's length in milliseconds.

#### `sound.isClosed() boolean`

isClosed reports whether close has been called on the sound.

#### `sound.isPaused() boolean`

isPaused reports whether the sound was paused part way through.

#### `sound.isPlaying() boolean`

isPlaying reports whether the sound is playing and not paused. A closed
sound isn't playing.

#### `sound.pan() number`

pan returns the sound's stereo position, from -1 (left) to 1 (right).

#### `sound.pause()`

pause pauses the sound where it is, if it is playing. Throws StateError
when the sound is closed.

#### `sound.pitch() number`

pitch returns the sound's playback speed (default 1).

#### `sound.play() result<unit, error>`

play starts the sound from its position, or resumes it when paused.
Playing a sound that is already playing does nothing. Fails with IOError
when no audio output device can be opened; throws StateError when the
sound is closed.

#### `sound.position() number`

position returns how far into the sound playback is, in milliseconds.
Throws StateError when the sound is closed.

#### `sound.progress() number`

progress returns how far through the sound playback is, from 0 to 1.
Throws StateError when the sound is closed.

#### `sound.resume()`

resume continues a paused sound. Throws StateError when the sound is
closed.

#### `sound.seek(number ms) result<unit, error>`

seek moves playback to ms milliseconds from the start, whether or not the
sound is playing. Throws ValueError outside 0 to duration() and
StateError when the sound is closed.

#### `sound.setPan(number pan)`

setPan sets the stereo position from -1 (left) through 0 (centre) to 1
(right). Throws ValueError outside that range and StateError when the
sound is closed.

#### `sound.setPitch(number pitch)`

setPitch sets the playback speed: 2 plays twice as fast and an octave
higher, 0.5 half as fast and an octave lower. Throws ValueError unless it
is more than 0, and StateError when the sound is closed.

#### `sound.setVolume(number volume)`

setVolume sets the sound's volume from 0 (silent) to 1 (full). Throws
ValueError outside that range and StateError when the sound is closed.

#### `sound.stop()`

stop ends playback and rewinds the sound to the start. Throws StateError
when the sound is closed.

#### `sound.volume() number`

volume returns the sound's volume, from 0 to 1 (default 1).
