Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cli/XAir.c
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ static void handle_fx_type(int slot) {
}

static void handle_fx_par(int slot) {
/* echo the set command back to caller (X32 behaviour) */
/* echo the set command back to caller (X32 behavior) */
memcpy(s_buf, r_buf, r_len);
s_len = r_len;
xsend((struct sockaddr *)&client_ip, client_ip_len);
Expand Down
8 changes: 4 additions & 4 deletions cli/ringout_logic.c
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ RingoutAction ringout_step(RingoutState *st, const float *bins, const float *bas
}

/* A notch placed last step takes priority: back the gain off and re-settle
* before analysing again, so the notch can take effect. */
* before analyzing again, so the notch can take effect. */
if (st->pending_backoff) {
st->pending_backoff = 0;
st->cur_gain_db -= st->cfg.step_db;
Expand All @@ -57,7 +57,7 @@ RingoutAction ringout_step(RingoutState *st, const float *bins, const float *bas
return a;
}

/* Settle: ignore frame content until the system has stabilised. */
/* Settle: ignore frame content until the system has stabilized. */
if (st->phase == RO_PHASE_SETTLE) {
if (st->settle_count > 0) {
st->settle_count--;
Expand All @@ -67,10 +67,10 @@ RingoutAction ringout_step(RingoutState *st, const float *bins, const float *bas
st->confirm_count = 0;
st->confirm_bin = -1;
st->stable_count = 0;
/* fall through and analyse this frame */
/* fall through and analyze this frame */
}

/* Analyse: is there a narrow ring? */
/* Analyze: is there a narrow ring? */
int peak;
int found = detect_peak(bins, baseline, st->cfg.threshold_db, &peak);
int narrow = found && is_narrow_peak(bins, RTA_BINS, peak,
Expand Down
4 changes: 2 additions & 2 deletions cli/ringout_logic.h
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ typedef struct {
int stable_frames; /* consecutive ring-free frames required before raising */
int narrow_skip; /* is_narrow_peak: skip */
int narrow_span; /* is_narrow_peak: span */
float narrow_min_db; /* is_narrow_peak: min advantage over neighbours */
float narrow_min_db; /* is_narrow_peak: min advantage over neighbors */
} RingoutConfig;

typedef struct {
Expand All @@ -73,7 +73,7 @@ typedef struct {
float margin_db; /* gain-before-feedback margin achieved (cur - start) */
} RingoutState;

/* Initialise state from config. cfg is copied; sane fields are required (e.g.
/* Initialize state from config. cfg is copied; sane fields are required (e.g.
* step_db > 0, ceiling_db >= start_gain_db). The bus is assumed to already be at
* cfg.start_gain_db when the loop begins. */
void ringout_init(RingoutState *st, const RingoutConfig *cfg);
Expand Down
2 changes: 1 addition & 1 deletion cli/rta_bins.c
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

/* Log-frequency interpolation of the 31 ISO 1/3-octave GEQ anchors
* (TOAST_GEQ_BIN ↔ ISO centers), 1 kHz pinned to bin 56. Off-anchor bins are
* modelled, not measured — verify with a console sweep via --rta-probe. */
* modeled, not measured — verify with a console sweep via --rta-probe. */
const float RTA_BIN_FREQ[RTA_BIN_COUNT] = {
20.0f, 21.5f, 23.2f, 25.0f, 26.5f, 28.1f, 29.7f, 31.5f,
34.1f, 36.9f, 40.0f, 43.1f, 46.4f, 50.0f, 54.0f, 58.3f,
Expand Down
4 changes: 2 additions & 2 deletions cli/rta_probe.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#!/usr/bin/env python3
"""Live /meters/4 RTA peak-bin probe for X32/XAir — field tool for T-027.

Subscribe to the 100-bin RTA, print the peak bin (and its modelled frequency
Subscribe to the 100-bin RTA, print the peak bin (and its modeled frequency
from cli/rta_bins.c). Use with a known tone to verify the bin->Hz table:
a 1 kHz pistonphone must light bin 56; a console sine sweep verifies the rest.

Expand All @@ -15,7 +15,7 @@
sys.exit("usage: rta_probe.py <xr18_ip> [seconds]")
PORT = 10024

# modelled bin->Hz, parsed from the C table so the two never drift
# modeled bin->Hz, parsed from the C table so the two never drift
try:
txt = pathlib.Path(__file__).with_name("rta_bins.c").read_text()
BIN_FREQ = [float(x) for x in re.findall(r"([0-9]+\.[0-9]+)f", txt)[:100]]
Expand Down
2 changes: 1 addition & 1 deletion cli/test_ringout_emu.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#!/usr/bin/env python3
"""T-025 / T-032 ring-out safety harness: a mock X32 that models the feedback
loop and asserts the ring-out control loop's safety properties (ceiling, notch,
restore, abort). No audio — feedback is modelled as a function of commanded
restore, abort). No audio — feedback is modeled as a function of commanded
bus gain via a pluggable RoomModel.

Run:
Expand Down
2 changes: 1 addition & 1 deletion docs/ringout-design.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ with a handoff:
**static notch profile + the measured gain-before-feedback margin**.
- **Reactive (protect)** — starts *from that profile* (static notches pre-placed)
and dynamically defends the remaining margin during the show. This is the
existing ToastSaver behaviour, plus a new "load a profile at startup" step.
existing ToastSaver behavior, plus a new "load a profile at startup" step.

> The proactive pass *produces* what the reactive pass *consumes*. "React as
> planned" = the planned notches are already in when the show starts; live mode
Expand Down
2 changes: 1 addition & 1 deletion docs/system-tune-design.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ generator and writes the GEQ.
1. **Mains response EQ** — pink noise → measure at a mic → flatten LR GEQ to the
target curve.
2. **Per-monitor** — optionally pink-flatten each wedge, **then ring it out**
(the current work) to maximise gain-before-feedback.
(the current work) to maximize gain-before-feedback.
3. **Report** — final EQ curves + per-monitor headroom margins (and reuse the
ring-out profile format from `T-024`).

Expand Down
4 changes: 2 additions & 2 deletions esp32/X32Link/beat_synth.h
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,14 @@ typedef struct {
uint32_t last_ms; // wall-clock ms of the last emitted beat
} BeatSynth;

// Clear state (last_ms = 0). Behaviour then matches a fresh static counter:
// Clear state (last_ms = 0). Behavior then matches a fresh static counter:
// the first step() with now_ms past one interval fires immediately.
void beat_synth_reset(BeatSynth* s);

// Advance to now_ms and return true if a beat is due at `bpm`. Fires when
// (now_ms - last_ms) has reached the 60000/bpm interval (unsigned/wrap-safe),
// then latches last_ms = now_ms. bpm <= 0 always returns false and leaves state
// untouched (so a resumed BPM fires promptly, matching the old inline behaviour).
// untouched (so a resumed BPM fires promptly, matching the old inline behavior).
bool beat_synth_step(BeatSynth* s, uint32_t now_ms, float bpm);

#ifdef __cplusplus
Expand Down
2 changes: 1 addition & 1 deletion esp32/X32Link/web_config.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ if(curSlot>maxSlot){curSlot=maxSlot;hSlot.value=maxSlot}renderSlots()});
renderSlots();
var bpmEl=document.getElementById('bpm'),beatEl=document.getElementById('beat');
var seedBpm=parseFloat(bpmEl.textContent)||0,beatTimer=null,shownBpm=-1;
// Free-running fallback blink (pre-LNK-022 behaviour) — used whenever the
// Free-running fallback blink (pre-LNK-022 behavior) — used whenever the
// server says phase isn't valid yet (sync gap / no source). Unchanged.
function setBeat(bpm){if(beatTimer){clearInterval(beatTimer);beatTimer=null}
if(bpm>0){beatTimer=setInterval(function(){beatEl.classList.add('on');setTimeout(function(){beatEl.classList.remove('on')},90)},60000/bpm)}}
Expand Down
2 changes: 1 addition & 1 deletion esp32/X32MidiClock/X32MidiClock.ino
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ void setup() {
g_config.fx_slot, g_config.mixer_ip,
config_model_port(g_config.model));

// USB MIDI must be initialised before WiFi to ensure USB enumeration is fast
// USB MIDI must be initialized before WiFi to ensure USB enumeration is fast
// Device name is set in midi_clock.cpp via USBMIDI constructor
USB.manufacturerName("X32Link");
midi_clock_init(); // calls MidiUSB.begin() + spawns poll task
Expand Down
2 changes: 1 addition & 1 deletion esp32/X32MidiClock/midi_bpm.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
#include <Arduino.h>

void midi_bpm_init() {
// nothing to initialise — state lives in midi_clock module
// nothing to initialize — state lives in midi_clock module
}

float midi_bpm_update() {
Expand Down
2 changes: 1 addition & 1 deletion esp32/tasks/FDR-002.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ updated_at: 2026-06-29

## Goal

Implement the FaderDisp behaviour: when a channel fader moves, show its value in
Implement the FaderDisp behavior: when a channel fader moves, show its value in
**dB on that channel's scribble strip**, by writing `/ch/NN/config/name`. On a
clean shutdown / disable, restore the original names.

Expand Down
2 changes: 1 addition & 1 deletion esp32/tasks/ITT-002.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,5 +45,5 @@ send a MIDI message). This is the "If This Then That" core.
and send as the raw OSC float (the mixer applies its own fader taper).
- Keep the engine allocation-free at eval time; build the rule table once at
load / on web save.
- This generalises X32Midi2OSC (MIDI→OSC) and X32Commander (OSC→MIDI/OSC) from
- This generalizes X32Midi2OSC (MIDI→OSC) and X32Commander (OSC→MIDI/OSC) from
the upstream toolset into one bidirectional firmware.
4 changes: 2 additions & 2 deletions esp32/tasks/LNK-001.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ updated_at: 2026-06-26
## Goal

Create the `esp32/X32Link/` Arduino sketch directory with a minimal WiFi
sketch and a `config.h` that centralises all runtime parameters. Verify the
sketch and a `config.h` that centralizes all runtime parameters. Verify the
bare sketch compiles and connects to WiFi before any Link or OSC code is added.

This task targets any ESP32-S3 board (XIAO ESP32-S3, ESP32-S3 Super Mini, etc.).
Expand Down Expand Up @@ -71,7 +71,7 @@ arduino-cli upload \

**Serial quirk**: On ESP32-S3 with USB CDC, open the serial port with
DTR=True, RTS=False BEFORE pressing RST. Hardware reset required after upload
(same behaviour as ESP-001 / XIAO ESP32-S3).
(same behavior as ESP-001 / XIAO ESP32-S3).

**config.h constants** — all values as `#define`:

Expand Down
2 changes: 1 addition & 1 deletion esp32/tasks/LNK-003.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
id: LNK-003
title: WiFi connection and Link session initialisation
title: WiFi connection and Link session initialization
status: done
assignee: null
priority: high
Expand Down
8 changes: 4 additions & 4 deletions esp32/tasks/LNK-005.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ updated_at: 2026-06-26

## Goal

Open a UDP socket to the XR18 and write the current BPM as a normalised float
Open a UDP socket to the XR18 and write the current BPM as a normalized float
to `/fx/{slot}/par/02` whenever `g_bpm_changed` is set. This is the final
output stage — Link tempo → XR18 delay time.

Expand All @@ -23,13 +23,13 @@ output stage — Link tempo → XR18 delay time.
- [ ] `send_bpm_osc()` builds a valid OSC `/fx/{slot}/par/02 ,f {val}` packet
- [ ] OSC packet byte layout verified against `src/X32lib/Xsprint.c` reference
- [ ] XR18 FX slot delay time visibly changes in X-Air Edit when DAW BPM changes
- [ ] BPM range tested: 60, 120, 180 BPM → correct normalised values (see Notes)
- [ ] BPM range tested: 60, 120, 180 BPM → correct normalized values (see Notes)

## Notes

**Firmware scope**: `esp32/X32Link/` only. Do NOT touch `esp32/X32_emulator/`.

**BPM → normalised float** (inline — cannot share `src/x32tap_logic.c`):
**BPM → normalized float** (inline — cannot share `src/x32tap_logic.c`):
```c
// Mirrors src/x32tap_logic.c: ms_to_normalized(), TAP_MAX_MS = 3000
static float bpm_to_normalized(float bpm) {
Expand All @@ -41,7 +41,7 @@ static float bpm_to_normalized(float bpm) {
```

**Expected values**:
| BPM | delay_ms | normalised |
| BPM | delay_ms | normalized |
|------|----------|------------|
| 60 | 1000 ms | 0.3333 |
| 120 | 500 ms | 0.1667 |
Expand Down
2 changes: 1 addition & 1 deletion esp32/tasks/LNK-009.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ same knobs under different prefixes:

## Constraints

- No behaviour change: same values reach the same call sites, just sourced
- No behavior change: same values reach the same call sites, just sourced
per-input instead of via a fixed macro.
- `DEFAULT_WIFI_*` / `DEFAULT_MIXER_IP` / `DEFAULT_MODEL` / `DEFAULT_FX_SLOT`
already share names — leave them.
Expand Down
2 changes: 1 addition & 1 deletion esp32/tasks/LNK-011.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ PublishDecision bpm_publisher_step(float bpm, bool active, uint32_t now_ms);

- Pure C, no Arduino deps — buildable and testable on host (follow the
existing `test/test_link_protocol.c` + weak-`millis` pattern).
- Preserve current behaviour exactly: bpm=0 ⇒ no send (mixer holds last value);
- Preserve current behavior exactly: bpm=0 ⇒ no send (mixer holds last value);
send only on change-past-threshold OR per-bar refresh; respect min interval.

## Acceptance Criteria
Expand Down
2 changes: 1 addition & 1 deletion esp32/tasks/LNK-012.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ no input-source field yet.
`TempoSourceKind` enum from LNK-008. Default = Link.
2. **NVS** (`app_config_nvs.cpp`): load/save the new key `input_src`; default to
Link when absent (first boot / post factory-reset).
3. **Web form** (`web_config.cpp`): add a labelled dropdown
3. **Web form** (`web_config.cpp`): add a labeled dropdown
"Tempo source: Ableton Link / USB MIDI clock"; submit persists it like the
other fields, then reboots (existing save→restart flow).
4. **Boot** (LNK-010 `setup`): `tempo_source_select(cfg.input_source)` before
Expand Down
2 changes: 1 addition & 1 deletion esp32/tasks/LNK-031.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ in `link_measurement.c` stay as-is and are called by the session or the glue.
timeouts emit END_FAIL; different-peer and due-timer both retrigger; trigger
gated by `active`; pong carries prev-ghost / commits at READY.
- [x] `link_measurement_io.cpp` reduced to sockets + timing that translate the
session's actions (`run_action`); behaviour faithfully ported (LNK-026
session's actions (`run_action`); behavior faithfully ported (LNK-026
fixes preserved as session actions).
- [x] Full host suite green (20 suites); firmware compiles 86%; `AGENTS.md` updated.
- [x] **Re-verified on hardware** (flashed; Link peer @ 127 BPM). Steady play:
Expand Down
2 changes: 1 addition & 1 deletion esp32/tasks/MCK-005.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ buffer just stays empty when disconnected; `midi_bpm_update()` returns 0.0 after
the timeout, and OSC stops. On reconnect, Ableton re-sends clock and the
firmware picks it back up within one beat.

**Clock-stop behaviour (design decision)**: When the musician stops the DAW,
**Clock-stop behavior (design decision)**: When the musician stops the DAW,
we intentionally do NOT reset the X32 tempo to zero or a default. The desk
keeps the last known tempo — the band can keep playing in time if the DAW
stops unexpectedly. Tempo only changes when a new valid BPM arrives.
Expand Down
2 changes: 1 addition & 1 deletion esp32/tasks/MCK-006.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ DAW start/stop.
- [ ] Enabling "Clock" output in Ableton and pressing Play causes FX delay time
on XR18 to update within 1–2 beats
- [ ] BPM accuracy: set Ableton to 80, 120, 140, 180 BPM → X-Air Edit delay time
matches expected normalised values (see LNK-005 reference table)
matches expected normalized values (see LNK-005 reference table)
- [ ] Tempo change: drag Ableton BPM from 120 → 140 → X32 follows within 1 beat
- [ ] DAW stop: FX delay time stays at last known value (does not reset)
- [ ] DAW start after stop: tempo resumes correctly within 1 beat
Expand Down
2 changes: 1 addition & 1 deletion esp32/test/test_beat_synth.c
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ void setUp(void) { beat_synth_reset(&s); }
void tearDown(void) {}

void test_fresh_state_fires_on_first_step_past_interval(void) {
// Matches the old inline behaviour: static last=0, a large millis() reading
// Matches the old inline behavior: static last=0, a large millis() reading
// is >= one interval so the first observed beat fires.
TEST_ASSERT_TRUE(beat_synth_step(&s, 1000, 120.0f)); // interval 500, 1000-0 >= 500
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
// Distinguishes a narrow feedback ring from broadband content (e.g. a vocal
// formant): a real ring towers over its neighbours a few bins out. Ported from
// formant): a real ring towers over its neighbors a few bins out. Ported from
// cli/toast_logic.c (is_narrow_peak).

public enum NarrownessFilter {
/// True if `bins[peakBin]` is at least `minDB` above the average of the
/// neighbour bins sampled `skip+1 … skip+span` positions away on each side
/// (clamped to the array). Needs ≥1 neighbour on each side, else false.
/// neighbor bins sampled `skip+1 … skip+span` positions away on each side
/// (clamped to the array). Needs ≥1 neighbor on each side, else false.
public static func isNarrowPeak(bins: [Float], peakBin: Int,
skip: Int = 2, span: Int = 3, minDB: Float = 10.0) -> Bool {
let n = bins.count
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,15 @@ public struct RingoutConfig {
public var cutDB: Float
/// Consecutive frames a ring must persist before a notch fires.
public var confirmFrames: Int
/// Frames to wait at current gain after any raise or notch before analysing.
/// Frames to wait at current gain after any raise or notch before analyzing.
public var settleFrames: Int
/// Consecutive ring-free frames required before raising gain.
public var stableFrames: Int
/// Narrowness gate: bins to skip immediately adjacent to the peak.
public var narrowSkip: Int
/// Narrowness gate: bins to sample beyond the skip zone on each side.
public var narrowSpan: Int
/// Narrowness gate: peak must exceed neighbour average by at least this many dB.
/// Narrowness gate: peak must exceed neighbor average by at least this many dB.
public var narrowMinDB: Float

public init(
Expand Down Expand Up @@ -147,17 +147,17 @@ public final class RingoutController {
return .backOff(to: currentGainDB)
}

// Settling: hold until the system stabilises after a raise or notch.
// Settling: hold until the system stabilizes after a raise or notch.
if phase == .settle {
if settleCount > 0 { settleCount -= 1; return .hold }
// Settle complete — fall through and analyse this frame.
// Settle complete — fall through and analyze this frame.
phase = .analyze
confirmCount = 0
confirmBin = -1
stableCount = 0
}

// Analyse: look for a narrow ring.
// Analyze: look for a narrow ring.
guard let peakIndex = PeakDetector.detectPeak(
bins: geqLevels, baseline: baseline, threshold: config.thresholdDB)
else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ final class FeedbackDetectorTests: XCTestCase {
var b = flat(); b[idx] = spike; return b
}

// narrowSkip=1, narrowSpan=1: a spike with at least 1 clear neighbour each side qualifies.
// narrowSkip=1, narrowSpan=1: a spike with at least 1 clear neighbor each side qualifies.
private func makeDetector(confirm: Int = 3) -> FeedbackDetector {
FeedbackDetector(threshold: 20.0, confirmFrames: confirm,
narrowSkip: 1, narrowSpan: 1, narrowMinDB: 5.0)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ final class RingoutControllerTests: XCTestCase {
_ = ctrl.step(geqLevels: quiet, baseline: baseline) // raise

// Inject a narrow spike on par 10 (index 9).
// With narrowSkip=1, narrowSpan=1: neighbours are at ±2, which exist.
// With narrowSkip=1, narrowSpan=1: neighbors are at ±2, which exist.
let spike = spikedLevels(at: 9, spike: -10.0, floor: -80.0)

var action: RingoutAction = .hold
Expand Down
8 changes: 4 additions & 4 deletions ios/tasks/IOS-003.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,12 @@ final class FFTEngine {
func process(_ samples: [Float]) -> [Float]

/// Map an FFT bin index (0 ..< fftSize/2) to the nearest GEQ par (1–31).
/// Uses the same 1/3-octave centre frequencies as GEQMap.bins,
/// Uses the same 1/3-octave center frequencies as GEQMap.bins,
/// recalculated for this engine's fftSize and sampleRate.
func geqPar(forBin bin: Int) -> Int

/// Precomputed GEQ bin indices for fftSize and sampleRate.
/// geqBins[i] is the FFT bin index closest to GEQ par i+1's centre frequency.
/// geqBins[i] is the FFT bin index closest to GEQ par i+1's center frequency.
private(set) var geqBins: [Int] // length 31
}
```
Expand Down Expand Up @@ -87,7 +87,7 @@ Create `fftSetup` with `vDSP_create_fftsetup(log2n, FFTRadix(kFFTRadix2))` in `i

## geqBins calculation

GEQ centre frequencies in Hz (31 bands, 1/3-octave, ISO):
GEQ center frequencies in Hz (31 bands, 1/3-octave, ISO):
```
[20, 25, 31.5, 40, 50, 63, 80, 100, 125, 160, 200, 250, 315, 400, 500,
630, 800, 1000, 1250, 1600, 2000, 2500, 3150, 4000, 5000, 6300, 8000,
Expand Down Expand Up @@ -115,7 +115,7 @@ private func computeGeqBins() -> [Int] {

## Acceptance Criteria

- [ ] `FFTEngine(fftSize: 2048, sampleRate: 48000)` initialises without crashing.
- [ ] `FFTEngine(fftSize: 2048, sampleRate: 48000)` initializes without crashing.
- [ ] `process(_:)` with a 1 kHz sine wave at 48 kHz, 2048 samples: peak bin is within ±2 bins of `Int(round(1000 * 2048 / 48000))` = bin 43.
- [ ] `geqBins[17]` (1 kHz, 0-based index) ≈ 43 for 2048/48000.
- [ ] `process(_:)` with all-zero input returns all values ≤ -80 dBFS.
Expand Down
Loading
Loading