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

8.2 KiB
Raw Blame History

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:


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.03.0, default 1.0.
  • pitch (number, optional): semitones 024, 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.03.0, default 1.0.
  • pitch (number, optional): speed multiplier 0.52.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.
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.
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:

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:

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:

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:

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:

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.