cc-libs/docs/audio_guide.md
2026-06-20 14:15:54 +02:00

261 lines
8.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# ComputerCraft Audio Guide
Consolidated reference for playing audio on CC: Tweaked. Covers the `cc.audio.dfpwm`
library, the `speaker` peripheral, and the practical guide to streaming PCM audio.
Sources:
- <https://tweaked.cc/library/cc.audio.dfpwm.html>
- <https://tweaked.cc/peripheral/speaker.html>
- <https://tweaked.cc/guide/speaker_audio.html>
---
## 1. Audio fundamentals
### PCM (Pulse-code Modulation)
Sound is an analog, continuous signal. Computers need discrete data, so audio is
**sampled** at a fixed rate and each sample **quantized** to a representable value.
A long, uniformly sampled list of amplitudes is called **PCM**.
### Speaker specifications
CC: Tweaked speakers play back:
- **48,000 samples per second** (48 kHz)
- each sample is an **integer between -128 and 127** (8-bit resolution)
So one second of audio = 48,000 samples. One minute ≈ 2.88 million samples — which is
why streaming (processing in chunks) is preferred over building one giant buffer.
### DFPWM
**DFPWM (Dynamic Filter Pulse Width Modulation)** is a codec by GreaseMonkey, the de
facto standard audio format in the ComputerCraft / OpenComputers world. It encodes at
**1 bit per sample**, making it compact for storage while remaining simple enough for
real-time processing. CC: Tweaked ships the built-in `cc.audio.dfpwm` module to
encode/decode it. You convert DFPWM → PCM amplitudes, then feed those to the speaker.
---
## 2. `cc.audio.dfpwm` library
`require("cc.audio.dfpwm")` — converts between DFPWM streams and PCM amplitude tables.
> **Stateful warning:** Encoders and decoders keep internal state about the current
> stream. Reusing one encoder/decoder across multiple streams — or using different
> ones on the same stream — produces corrupt audio. One stream = one
> encoder/decoder.
### `make_encoder()`
Creates a new encoder for converting PCM audio to DFPWM.
- **Returns:** a function. Call it with a table of amplitude values (-128 to 127);
it returns encoded DFPWM data as a string.
- Use this (not `encode`) when writing multiple chunks to the same location.
### `encode(input)`
Convenience function to encode a whole file at once.
- **`input`** — table of amplitude numbers.
- **Returns:** string of encoded DFPWM data.
- Prefer `make_encoder()` for chunked/multi-write output.
### `make_decoder()`
Creates a new decoder for converting DFPWM data to PCM amplitudes.
- **Returns:** a function. Call it with a DFPWM string; it returns a table of
amplitude values (-128 to 127).
### `decode(input)`
Convenience function to decode a whole file at once.
- **`input`** — string of DFPWM data.
- **Returns:** table of amplitude values.
- For larger files, use `make_decoder()` with chunked reading instead.
---
## 3. `speaker` peripheral
Three ways to produce audio, increasing in complexity: note block notes, Minecraft
sounds, and arbitrary PCM streaming.
### `playNote(instrument [, volume [, pitch]])` — *New in 1.80pr1*
Plays a note block note.
- **`instrument`** (string, required): `"harp"`, `"basedrum"`, `"snare"`, `"hat"`,
`"bass"`, `"flute"`, `"bell"`, `"guitar"`, `"chime"`, `"xylophone"`,
`"iron_xylophone"`, `"cow_bell"`, `"didgeridoo"`, `"bit"`, `"banjo"`, `"pling"`.
- **`volume`** (number, optional): `0.0``3.0`, default `1.0`.
- **`pitch`** (number, optional): semitones `0``24`, default `12`.
- **Returns:** boolean — `false` if the 8-notes-per-tick limit was reached.
- **Throws:** if the instrument doesn't exist.
### `playSound(name [, volume [, pitch]])` — *New in 1.80pr1*
Plays a Minecraft sound effect (vanilla or modded).
- **`name`** (string, required): sound id, e.g. `"entity.creeper.primed"`.
- **`volume`** (number, optional): `0.0``3.0`, default `1.0`.
- **`pitch`** (number, optional): speed multiplier `0.5``2.0`, default `1.0`.
- **Returns:** boolean — `false` if another sound already started this tick or audio
is currently playing.
- **Throws:** if the sound name is invalid.
```lua
local speaker = peripheral.find("speaker")
speaker.playSound("entity.creeper.primed")
```
### `playAudio(audio [, volume])` — *New in 1.100*
Streams arbitrary audio as 8-bit PCM samples at 48 kHz.
- **`audio`** (table, required): list of amplitude values between -128 and 127.
- **`volume`** (number, optional): if omitted, reuses the previous volume.
- **Returns:** boolean — `true` if the buffer accepted the data, `false` if the
speaker's backlog is full.
- **Throws:** if the audio data is malformed.
- **Notes:**
- Keep chunks to a max of **128 × 1024 (128k) samples** per call to minimize
stuttering.
- When it returns `false`, wait for the **`speaker_audio_empty`** event before
retrying.
- Samples are re-encoded before playback.
```lua
local dfpwm = require("cc.audio.dfpwm")
local speaker = peripheral.find("speaker")
local decoder = dfpwm.make_decoder()
for chunk in io.lines("data/example.dfpwm", 16 * 1024) do
local buffer = decoder(chunk)
while not speaker.playAudio(buffer) do
os.pullEvent("speaker_audio_empty")
end
end
```
### `stop()` — *New in 1.100*
Stops all playback from this speaker: clears the queued audio and halts any active
sound. Returns nothing.
---
## 4. The core streaming pattern
This is the loop you'll reuse for almost all PCM playback:
```lua
while not speaker.playAudio(buffer) do
os.pullEvent("speaker_audio_empty")
end
```
The speaker rejects new chunks when its backlog is too large — `playAudio` returns
`false`. Wait for `speaker_audio_empty`, then retry the same buffer.
---
## 5. Examples
### Generate a sine wave (in-memory buffer)
220 Hz hum, ~2.7 seconds:
```lua
local speaker = peripheral.find("speaker")
local buffer = {}
local t, dt = 0, 2 * math.pi * 220 / 48000
for i = 1, 128 * 1024 do
buffer[i] = math.floor(math.sin(t) * 127)
t = (t + dt) % (math.pi * 2)
end
speaker.playAudio(buffer)
```
### Stream an endless sine wave (chunked)
Avoids allocating millions of samples up front:
```lua
local speaker = peripheral.find("speaker")
local t, dt = 0, 2 * math.pi * 220 / 48000
while true do
local buffer = {}
for i = 1, 16 * 1024 * 8 do
buffer[i] = math.floor(math.sin(t) * 127)
t = (t + dt) % (math.pi * 2)
end
while not speaker.playAudio(buffer) do
os.pullEvent("speaker_audio_empty")
end
end
```
### Play a DFPWM file
Read in 16 KiB chunks, decode, play:
```lua
local dfpwm = require("cc.audio.dfpwm")
local speaker = peripheral.find("speaker")
local decoder = dfpwm.make_decoder()
for chunk in io.lines("data/example.dfpwm", 16 * 1024) do
local buffer = decoder(chunk)
while not speaker.playAudio(buffer) do
os.pullEvent("speaker_audio_empty")
end
end
```
### Delay / echo effect (ring buffer)
PCM's simplicity lets you mix and manipulate samples directly. Mix streams by adding
amplitudes; change playback rate by dropping samples; etc. This adds a 1.5-second
delay. Scale values (0.6 original + 0.4 delayed) to avoid clipping past -128..127:
```lua
local dfpwm = require("cc.audio.dfpwm")
local speaker = peripheral.find("speaker")
local samples_i, samples_n = 1, 48000 * 1.5
local samples = {}
for i = 1, samples_n do samples[i] = 0 end
local decoder = dfpwm.make_decoder()
for chunk in io.lines("data/example.dfpwm", 16 * 1024) do
local buffer = decoder(chunk)
for i = 1, #buffer do
local original_value = buffer[i]
buffer[i] = original_value * 0.6 + samples[samples_i] * 0.4
samples[samples_i] = original_value
samples_i = samples_i + 1
if samples_i > samples_n then samples_i = 1 end
end
while not speaker.playAudio(buffer) do
os.pullEvent("speaker_audio_empty")
end
sleep(0.05)
end
```
---
## 6. Quick reference
| Need | Use |
| --- | --- |
| Note block tone | `speaker.playNote(instrument, vol, pitch)` |
| Minecraft sound effect | `speaker.playSound(name, vol, pitch)` |
| Arbitrary PCM playback | `speaker.playAudio(buffer, vol)` |
| Stop everything | `speaker.stop()` |
| Decode DFPWM → PCM | `dfpwm.make_decoder()` / `dfpwm.decode(s)` |
| Encode PCM → DFPWM | `dfpwm.make_encoder()` / `dfpwm.encode(t)` |
| Backpressure signal | `os.pullEvent("speaker_audio_empty")` |
**Key constants:** 48 kHz sample rate · 8-bit samples (-128..127) · ≤128k samples per
`playAudio` call · 16 KiB is a common DFPWM chunk size.