feat(trapos-music): add audio effects and guide

This commit is contained in:
Guillaume ARM 2026-06-20 14:15:28 +02:00
parent 02190abf1e
commit ff4e9e40cd
10 changed files with 654 additions and 12 deletions

260
docs/audio_guide.md Normal file
View File

@ -0,0 +1,260 @@
# 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.

View File

@ -1,6 +1,6 @@
{
"name": "TrapOS",
"version": "0.12.6",
"version": "0.12.7",
"branch": "master",
"packages": [
"trapos"

View File

@ -2,12 +2,12 @@
"packages": {
"trapos-core": "0.9.5",
"trapos-test": "0.2.3",
"trapos-boot": "0.4.4",
"trapos-boot": "0.4.5",
"trapos-ui": "0.2.4",
"trapos-ai": "0.8.0",
"trapos-cloud": "0.4.5",
"trapos-music": "0.1.1",
"trapos-music": "0.2.0",
"trapos-sandbox-legacy": "0.3.2",
"trapos": "0.12.6"
"trapos": "0.12.7"
}
}

View File

@ -1,6 +1,6 @@
{
"name": "trapos-boot",
"version": "0.4.4",
"version": "0.4.5",
"description": "TrapOS boot: startup MOTD and autostart server launcher",
"dependencies": ["trapos-core"],
"files": ["programs/motd.lua", "startup/boot.lua"],

View File

@ -0,0 +1,203 @@
-- libaudio: simple stateful DSP filters for 8-bit PCM audio buffers.
--
-- A factory: `local createAudio = require('/apis/libaudio'); local audio = createAudio();`
--
-- Each constructor returns a *filter instance* operating on a buffer (a table of
-- amplitudes -128..127, the same shape the dfpwm decoder produces and the
-- speaker consumes). Filters are stateful across chunks -- one instance per
-- playback, exactly like a dfpwm decoder. They share a uniform shape so a UI can
-- drive any of them generically:
--
-- filter.process(buffer) -- in-place transform, returns the buffer
-- filter.set(value) -- set the primary tunable param (keeps history)
-- filter.get() -- current param value
-- filter.reset() -- clear internal state (call on track switch)
-- filter.spec -- { name, param, unit, min, max, step, format }
--
-- `sampleRate` is injectable so the delay-based filters are unit-testable with a
-- tiny, exact ring length outside the CC runtime (mirrors libmusic's style).
local DEFAULT_SAMPLE_RATE = 48000;
-- Round to the nearest representable 8-bit sample and clamp to range.
local function clamp(v)
v = math.floor(v + 0.5);
if v > 127 then return 127; end
if v < -128 then return -128; end
return v;
end
local function clampParam(value, spec)
if value < spec.min then return spec.min; end
if value > spec.max then return spec.max; end
return value;
end
local function createAudio(opts)
opts = opts or {};
local sampleRate = opts.sampleRate or DEFAULT_SAMPLE_RATE;
local api = {};
-- One-pole IIR low-pass: y = y_prev + alpha*(x - y_prev). Higher cutoff lets
-- more treble through; very low cutoff muffles the sound to its bass.
function api.lowPass(cutoffHz)
local f = {};
f.spec = { name = 'Low-pass', param = 'Cutoff', unit = 'Hz',
min = 100, max = 12000, step = 100 };
local yPrev = 0; -- float; only the written sample is clamped
local alpha = 0;
local cutoff = 0;
f.set = function(value)
cutoff = clampParam(value, f.spec);
local rc = 1 / (2 * math.pi * cutoff);
local dt = 1 / sampleRate;
alpha = dt / (rc + dt);
end
f.get = function() return cutoff; end
f.reset = function() yPrev = 0; end
f.process = function(buffer)
for i = 1, #buffer do
yPrev = yPrev + alpha * (buffer[i] - yPrev);
buffer[i] = clamp(yPrev);
end
return buffer;
end
f.set(cutoffHz or 800);
return f;
end
-- One-pole IIR high-pass: y = alpha*(y_prev + x - x_prev). Strips bass; high
-- cutoff leaves only a thin, tinny top end.
function api.highPass(cutoffHz)
local f = {};
f.spec = { name = 'High-pass', param = 'Cutoff', unit = 'Hz',
min = 100, max = 12000, step = 100 };
local yPrev = 0;
local xPrev = 0;
local alpha = 0;
local cutoff = 0;
f.set = function(value)
cutoff = clampParam(value, f.spec);
local rc = 1 / (2 * math.pi * cutoff);
local dt = 1 / sampleRate;
alpha = rc / (rc + dt);
end
f.get = function() return cutoff; end
f.reset = function() yPrev = 0; xPrev = 0; end
f.process = function(buffer)
for i = 1, #buffer do
local x = buffer[i];
yPrev = alpha * (yPrev + x - xPrev);
xPrev = x;
buffer[i] = clamp(yPrev);
end
return buffer;
end
f.set(cutoffHz or 2000);
return f;
end
-- Single-tap delay (audio guide section 5): mixes 0.6 of the dry signal with
-- 0.4 of the signal from `time` ms ago, via a ring buffer.
function api.delay(timeMs)
local f = {};
f.spec = { name = 'Delay', param = 'Time', unit = 'ms',
min = 50, max = 1500, step = 50 };
local timeValue = 0;
local ring = {};
local ringLen = 1;
local idx = 1;
local function rebuild()
ringLen = math.max(1, math.floor(timeValue / 1000 * sampleRate + 0.5));
ring = {};
for i = 1, ringLen do ring[i] = 0; end
idx = 1;
end
f.set = function(value)
timeValue = clampParam(value, f.spec);
rebuild();
end
f.get = function() return timeValue; end
f.reset = function() rebuild(); end
f.process = function(buffer)
for i = 1, #buffer do
local original = buffer[i];
local mixed = original * 0.6 + ring[idx] * 0.4;
ring[idx] = original;
idx = idx + 1;
if idx > ringLen then idx = 1; end
buffer[i] = clamp(mixed);
end
return buffer;
end
f.set(timeMs or 300);
return f;
end
-- Schroeder-style reverb: a few parallel feedback comb filters whose decaying
-- echoes are summed into a wet signal, then mixed with the dry. Each comb is
-- y = (1-g)*x + g*y[n-D]: the (1-g) input gain keeps the tap in range whatever
-- the feedback, so `decay` (g) purely controls how long the tail rings out.
-- An impulse leaves echoes at D, 2D, 3D... decaying by g, i.e. an audible tail.
local COMB_MS = { 50, 56, 61, 68 };
function api.reverb(decay)
local f = {};
f.spec = { name = 'Reverb', param = 'Decay', unit = '',
min = 0, max = 0.9, step = 0.1, format = '%.1f' };
local g = 0;
local combs = {};
local function rebuild()
combs = {};
for c = 1, #COMB_MS do
local len = math.max(1, math.floor(COMB_MS[c] / 1000 * sampleRate + 0.5));
local buf = {};
for i = 1, len do buf[i] = 0; end
combs[c] = { buf = buf, len = len, idx = 1 };
end
end
f.set = function(value)
g = clampParam(value, f.spec);
end
f.get = function() return g; end
f.reset = function() rebuild(); end
f.process = function(buffer)
for i = 1, #buffer do
local x = buffer[i];
local wet = 0;
for c = 1, #combs do
local comb = combs[c];
local y = (1 - g) * x + g * comb.buf[comb.idx];
comb.buf[comb.idx] = y;
comb.idx = comb.idx + 1;
if comb.idx > comb.len then comb.idx = 1; end
wet = wet + y;
end
wet = wet / #combs;
buffer[i] = clamp(x * 0.4 + wet * 0.6);
end
return buffer;
end
rebuild();
f.set(decay or 0.5);
return f;
end
return api;
end
return createAudio;

View File

@ -8,7 +8,11 @@
local DEFAULT_DIR = '/data/musics';
local EXTENSION = '.dfpwm';
local CHUNK_SIZE = 16 * 1024;
-- Small chunks keep little audio queued ahead of the speaker, so live effect
-- and parameter changes are heard almost immediately (at the cost of more
-- decode calls per second). One byte of dfpwm decodes to 8 samples, so this is
-- ~0.34s of audio per chunk.
local CHUNK_SIZE = 2 * 1024;
local function createMusic(opts)
opts = opts or {};
@ -75,6 +79,7 @@ local function createMusic(opts)
local pendingBuffer = nil; -- buffer that playAudio refused, replayed first
local currentPath = nil;
local onFinishCb = nil;
local effect = nil; -- optional libaudio filter applied to each decoded buffer
local function getSpeaker()
local speaker = peripheralLib.find('speaker');
@ -129,8 +134,11 @@ local function createMusic(opts)
end
local buffer = decoder(chunk);
if effect then
buffer = effect.process(buffer);
end
if not speaker.playAudio(buffer) then
pendingBuffer = buffer;
pendingBuffer = buffer; -- already processed; not re-filtered on retry
return;
end
end
@ -154,11 +162,20 @@ local function createMusic(opts)
player.stop();
handle = assert(ioLib.open(path, 'rb'), 'cannot open ' .. tostring(path));
decoder = dfpwm.make_decoder();
if effect then
effect.reset(); -- fresh filter state so no tail bleeds across tracks
end
currentPath = path;
status = 'playing';
pump();
end
-- Sets the active audio effect (a libaudio filter instance), or nil for off.
-- Takes effect on the next decoded buffer; safe to swap mid-playback.
function player.setEffect(filter)
effect = filter;
end
-- Gapless pause: stop feeding and let already-queued audio drain. The
-- decoder/handle/pendingBuffer are kept so resume loses no content.
function player.pause()

View File

@ -1,10 +1,11 @@
{
"name": "trapos-music",
"version": "0.1.1",
"version": "0.2.0",
"description": "TrapOS music player: dfpwm playback through a speaker",
"dependencies": ["trapos-core", "trapos-ui"],
"files": [
"apis/libmusic.lua",
"apis/libaudio.lua",
"programs/music.lua",
"data/musics/GrillControl.dfpwm",
"data/musics/trap-dirty-techno.dfpwm"

View File

@ -41,6 +41,8 @@ end
local createEventLoop = require('/apis/eventloop');
local createTui = require('/apis/libtui');
local createAudio = require('/apis/libaudio');
local audio = createAudio();
local eventloop = createEventLoop();
local ui = createTui(eventloop);
@ -52,9 +54,20 @@ local List = ui.List;
local player = music.createPlayer(eventloop);
-- Ordered effect menu; `make` builds a fresh stateful filter (nil = bypass).
local effects = {
{ label = 'Off', make = nil },
{ label = 'Low-pass', make = function() return audio.lowPass(); end },
{ label = 'High-pass', make = function() return audio.highPass(); end },
{ label = 'Delay', make = function() return audio.delay(); end },
{ label = 'Reverb', make = function() return audio.reverb(); end },
};
local effectIndex = 1;
local currentFilter = nil;
-- Rows of UI chrome around the track list (header, now-playing, controls,
-- footer, plus the track box border). Used to size the scroll window.
local LIST_CHROME = 6;
-- effects, footer, plus the track box border). Used to size the scroll window.
local LIST_CHROME = 7;
local selectedIndex = 1; -- list cursor
local scrollOffset = 0; -- index of the first visible row - 1
@ -108,6 +121,30 @@ player.onFinish(function()
ui.rerender();
end);
-- Switches to effect `index`, building a fresh filter and handing it to the
-- player (nil for 'Off' = bypass).
local function applyEffect(index)
effectIndex = (index - 1) % #effects + 1;
local entry = effects[effectIndex];
currentFilter = entry.make and entry.make() or nil;
player.setEffect(currentFilter);
ui.rerender();
end
local function cycleEffect()
applyEffect(effectIndex + 1);
end
-- Steps the active filter's tunable parameter by one `spec.step` (dir = +/-1).
local function adjustParam(dir)
if not currentFilter then
return;
end
local spec = currentFilter.spec;
currentFilter.set(currentFilter.get() + dir * spec.step);
ui.rerender();
end
local function Header()
return Box({
direction = 'row',
@ -222,9 +259,33 @@ local function Controls()
});
end
local function Effects()
local entry = effects[effectIndex];
local children = {
Button('FX: ' .. entry.label, {
flex = 1,
onClick = cycleEffect,
}),
};
if currentFilter then
local spec = currentFilter.spec;
local value = currentFilter.get();
local shown = spec.format and string.format(spec.format, value) or tostring(value);
local unit = spec.unit ~= '' and (' ' .. spec.unit) or '';
children[#children + 1] = Button('-', { onClick = function() adjustParam(-1); end });
children[#children + 1] = Text(spec.param .. ': ' .. shown .. unit);
children[#children + 1] = Button('+', { onClick = function() adjustParam(1); end });
end
return Box({
direction = 'row',
gap = 1,
children = children,
});
end
local function Footer()
return Text(
'up/down select enter play space pause s stop n/p next/prev q quit',
'enter play space pause s stop n/p next/prev f fx [ ] tune q quit',
{ color = colors.lightGray }
);
end
@ -244,6 +305,7 @@ local function App()
}),
NowPlaying(),
Controls(),
Effects(),
Footer(),
},
});
@ -277,6 +339,12 @@ eventloop.register('key', function(key)
playIndex(nextIndex());
elseif key == keys.p then
playIndex(previousIndex());
elseif key == keys.f then
cycleEffect();
elseif key == keys.leftBracket then
adjustParam(-1);
elseif key == keys.rightBracket then
adjustParam(1);
elseif key == keys.q then
ui.exitUI('quit');
end

View File

@ -0,0 +1,93 @@
local createLibTest = require('/apis/libtest');
local createAudio = require('/apis/libaudio');
local testlib = createLibTest({ ... });
local function constant(value, n)
local buf = {};
for i = 1, n do buf[i] = value; end
return buf;
end
local function impulse(value, n)
local buf = constant(0, n);
buf[1] = value;
return buf;
end
local function allInRange(buf)
for i = 1, #buf do
if buf[i] < -128 or buf[i] > 127 or buf[i] ~= math.floor(buf[i]) then
return false;
end
end
return true;
end
testlib.test('low-pass passes DC and stays in range, rising toward the input', function()
local audio = createAudio();
local lp = audio.lowPass();
local out = lp.process(constant(127, 2000));
testlib.assertTrue(allInRange(out), 'samples must be valid 8-bit integers');
testlib.assertTrue(out[#out] > out[1], 'output should rise toward the DC level');
testlib.assertTrue(out[#out] <= 127, 'output must not exceed the DC level');
end);
testlib.test('high-pass blocks DC: a constant decays toward zero', function()
local audio = createAudio();
local hp = audio.highPass();
local out = hp.process(constant(100, 200));
testlib.assertTrue(allInRange(out), 'samples must be valid 8-bit integers');
testlib.assertTrue(math.abs(out[#out]) < math.abs(out[1]), 'DC component should decay');
testlib.assertTrue(math.abs(out[#out]) <= 1, 'DC should fall close to zero');
end);
testlib.test('set changes the response and reset clears state', function()
local audio = createAudio();
local lp = audio.lowPass();
lp.set(200);
local low = lp.process(constant(127, 1))[1];
lp.reset();
lp.set(5000);
local high = lp.process(constant(127, 1))[1];
testlib.assertTrue(low ~= high, 'different cutoffs should give a different first sample');
-- reset must make a repeated run identical
lp.reset();
local a = lp.process(constant(50, 16));
lp.reset();
local b = lp.process(constant(50, 16));
testlib.assertEquals(a[#a], b[#b]);
end);
testlib.test('set clamps the param to the spec range', function()
local audio = createAudio();
local rv = audio.reverb();
rv.set(10);
testlib.assertEquals(rv.get(), 0.9);
rv.set(-5);
testlib.assertEquals(rv.get(), 0);
end);
testlib.test('delay re-emits the dry signal after the ring length', function()
local audio = createAudio({ sampleRate = 8 });
local d = audio.delay(1000); -- 1000ms * 8/s = 8-sample ring
local out = d.process(impulse(100, 10));
testlib.assertTrue(allInRange(out), 'samples must be valid 8-bit integers');
testlib.assertEquals(out[1], 60); -- 100 * 0.6 dry
testlib.assertEquals(out[9], 40); -- 100 * 0.4 delayed by 8 samples
testlib.assertEquals(out[5], 0); -- nothing in between
end);
testlib.test('reverb stays bounded for a loud impulse', function()
local audio = createAudio();
local rv = audio.reverb(0.8);
local buf = constant(0, 4000);
for i = 1, 200 do buf[i] = 127; end
local out = rv.process(buf);
testlib.assertTrue(allInRange(out), 'reverb output must never clip past 8-bit range');
end);
testlib.run();

View File

@ -1,6 +1,6 @@
{
"name": "trapos",
"version": "0.12.6",
"version": "0.12.7",
"description": "TrapOS full install meta-package",
"dependencies": [
"trapos-boot",