261 lines
8.2 KiB
Markdown
261 lines
8.2 KiB
Markdown
# 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.
|