# 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.
