{"id":45439205,"url":"https://github.com/bseverns/horizon","last_synced_at":"2026-02-22T03:37:58.556Z","repository":{"id":325537486,"uuid":"1099352876","full_name":"bseverns/horizon","owner":"bseverns","description":"Mid/Side Spatial \u0026 Dynamics Station","archived":false,"fork":false,"pushed_at":"2026-01-10T22:13:28.000Z","size":208197,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-01-11T06:49:09.402Z","etag":null,"topics":["midi","sound","teensy"],"latest_commit_sha":null,"homepage":"","language":"C++","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/bseverns.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null}},"created_at":"2025-11-18T22:15:51.000Z","updated_at":"2026-01-10T22:13:31.000Z","dependencies_parsed_at":null,"dependency_job_id":"66e47519-2985-4fdb-bb12-d498f8fcfc93","html_url":"https://github.com/bseverns/horizon","commit_stats":null,"previous_names":["bseverns/horizon"],"tags_count":2,"template":false,"template_full_name":null,"purl":"pkg:github/bseverns/horizon","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bseverns%2Fhorizon","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bseverns%2Fhorizon/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bseverns%2Fhorizon/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bseverns%2Fhorizon/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/bseverns","download_url":"https://codeload.github.com/bseverns/horizon/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bseverns%2Fhorizon/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29704417,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-22T03:17:42.375Z","status":"ssl_error","status_checked_at":"2026-02-22T03:17:31.622Z","response_time":110,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["midi","sound","teensy"],"created_at":"2026-02-22T03:37:57.949Z","updated_at":"2026-02-22T03:37:58.549Z","avatar_url":"https://github.com/bseverns.png","language":"C++","funding_links":[],"categories":[],"sub_categories":[],"readme":"# HORIZON — Mid/Side Spatial \u0026 Dynamics Station\n\nA mastering‑style “space shaper”: mid/side encode → tone shaping → **dynamic width** via transient detection → soft clip → lookahead limiter → width/mix.\nMakes dense mixes breathe (tails wider, hits focused). Drop it after any instrument.\n\n## Signal flow (ASCII cheat sheet)\n```\n[I2S In] → MS encode\n          → Mid tilt (pivot ~1 kHz) → Side air shelf → DynWidth (transient-following)\n          → MS decode → Lookahead limiter (tilted detector, adaptive release)\n          → SoftSat (mild post safety) → Output trim → [I2S Out]\n```\n- Block-by-block intent + clamp/smoothing cheats live in [`docs/block_notes.md`](docs/block_notes.md).\n- The limiter runs its own delay line so dry/wet and bypass crossfades stay phase-honest. Detector tilt is detector-only.\n\n## How to navigate this notebook\n- **Instant context** → start with a platform quick start (Teensy or laptop) to hear the chain.\n- **Deep dive** → skim the “Why it sounds this way” notebook chunks near the bottom, then hop into `docs/block_notes.md`.\n- **Host curious** → the desktop quick start below mirrors the Teensy flow so you can keep the embedded mindset while in DAW land.\n\n## Where to start\n- **Teensy / hardware:** flash the minimal sketch, twist pots, watch the serial scope breathe.\n- **Desktop / host:** build the CMake preset, render a file through `horizon-cli`, then open the plugin to see every knob → setter map.\n\n## Teensy quick start (hardware-first)\n- Open `examples/minimal/minimal.ino` in Arduino + TeensyDuino.\n- Select Teensy 4.0/4.1 and upload.\n- Feed stereo program and try presets (see CSV + JSON).\n- Feeling adventurous? Jump straight to `examples/preset_morph/preset_morph.ino` to hear slow-motion morphs between a cinema-wide wash and a gentle bus chain.\n- Need visuals? Flash `examples/horizon_scope/horizon_scope.ino` and watch width/transient/GR scroll by at 115200 baud.\n\n## Desktop quick start (CLI + presets + plugin map)\nThis is the laptop twin of the Teensy quick start—same smoothing and guardrails, just wrapped for CMake instead of Arduino. Treat it like a studio notebook page that shows how the embedded discipline survives on the host.\n\n1) **Build the host preset**\n- CMake presets bake in sane defaults so you can configure + build without spelunking flags:\n  ```bash\n  cmake --preset linux-clang\n  cmake --build --preset linux-clang\n  cmake --install cmake-out/linux-clang --prefix /where/you/want/it\n  ```\n  - macOS Universal 2: `cmake --preset macos-universal-release` → `cmake --build --preset macos-universal-release`\n  - Windows 11 + MSVC: `cmake --preset windows-msvc-release` → `cmake --build --preset windows-msvc-release`\n- Prefer raw CMake? `cmake -S . -B cmake-build -DCMAKE_BUILD_TYPE=Release` still works. `sync_compile_commands` rides along as an always-on helper target for clangd/IntelliSense vibes.\n- Build trees stay out of the repo on purpose: `cmake-out/` and any `cmake-build*/` scratch pads are ignored alongside the usual `CMakeFiles/`, `CMakeCache.txt`, `CMakeUserPresets.json`, installer manifests, and generated `compile_commands.json`. Toss them at will; the tracked presets remain the source of truth.\n\n2) **Run the CLI like you would a Teensy preset**\n- Render a file with a preset baked in:\n  ```bash\n  ./cmake-out/linux-clang/horizon_cli input.wav output.flac --preset bus_glue\n  ./cmake-out/linux-clang/horizon_cli --list-presets\n  ```\n- Want the serial-scope feel while you bounce? Add `--scope` to log the block-level width, transient pulse, and limiter GR. Example line:\n  ```\n  [scope] f0 | W 0.64 [=============....] | T 0.21 [++........] | GR -2.3 dB [#####.......]\n  ```\n  Same telemetry as the Teensy serial scope, just breathing through stdio instead of USB.\n\n3) **Map the plugin knobs (DAW view)**\n- Build the JUCE target (VST3 + standalone):\n  ```bash\n  cmake -S plugins -B plugins/build\n  cmake --build plugins/build\n  ```\n- Every control in the editor forwards directly to a `HostHorizonProcessor` setter:\n  - Width → `setWidth` (0 = mono, 1 = max)\n  - Dyn Width → `setDynWidth` (transients tug sides in before tails reopen)\n  - Transient → `setTransientSens`\n  - Tilt → `setMidTilt`\n  - Air Freq/Gain → `setSideAir(freq, gain)`\n  - Low Anchor → `setLowAnchor`\n  - Dirt → `setDirt`\n  - Ceiling / Release / Lookahead / Detector Tilt / Limit Mix / Link → limiter setters (`setCeiling`, `setLimiterReleaseMs`, `setLimiterLookaheadMs`, `setLimiterDetectorTilt`, `setLimiterMix`, `setLimiterLinkMode`)\n  - Mix → `setMix`, Out Trim → `setOutputTrim`\n- Hover text in the plugin matches these names so students can trace knob → setter → mix move without mystery glue. Full map + build quirks live in [`plugins/README.md`](plugins/README.md).\n\n### Where builds land (so you can find the bits you just compiled)\n- **CMake presets** drop CLI + library binaries under `cmake-out/\u003cpreset\u003e/` (e.g. `cmake-out/linux-clang/`).\n- **Raw `cmake -B cmake-build`** keeps everything in the `cmake-build/` tree if you’d rather skip presets.\n- **JUCE plugin builds** live in `plugins/build`, with JUCE’s defaults giving you `plugins/build/VST3/` and `plugins/build/Standalone/` drop points. Need deeper path spelunking? Peek at [`plugins/README.md`](plugins/README.md) for the nitty-gritty.\n\n## Platform\nTeensy 4.x + SGTL5000 (Teensy Audio Library), 44.1 kHz / 128‑sample blocks. Host builds reuse the exact DSP core via `HostHorizonProcessor` so demos, CI, and DAWs all share the same brain.\n\n## PlatformIO, CI, and host tricks\nQuick map (details live in the sections below):\n\n- **Environments**\n  - `main_teensy40` / `main_teensy41` — stage-ready firmware; flash with `pio run -e main_teensy41 -t upload`.\n  - `scope_teensy40` / `scope_teensy41` — telemetry-on-Serial builds for block meters; upload then watch at 115200.\n  - `native_dsp` — host-only math bench; run `pio test -e native_dsp` (see [Native DSP test bench](#native-dsp-test-bench-no-hardware-required)).\n\n- **Core commands**\n  - Build/upload: `pio run -e main_teensy41 -t upload` (swap envs as needed).\n  - Serial scope: `pio run -e scope_teensy41 -t upload` then crack open a terminal at 115200.\n  - Tests (no hardware): `pio test -e native_dsp`.\n  - Lint/compile DB: `HORIZON_LINT_STUBS=1 pio run -e main_teensy41 -t compiledb` (see [Host-side IntelliSense / clangd cheat codes](#host-side-intellisense--clangd-cheat-codes)).\n\n- **CI coverage**\n  - GitHub Actions installs PlatformIO, builds every Teensy env, and runs `native_dsp` tests so broken flags or platform-only includes get caught early.\n\n### Native DSP test bench (no hardware required)\n- Want to bash on the DSP math without a Teensy plugged in? Run `pio test -e native_dsp` to spin up a host-only build that links tiny Arduino/Audio stubs and exercises the limiter, smoother, and width logic.\n- The env doesn’t inherit any Teensy/Arduino scaffolding, so the native toolchain stays lean and never nags for a board definition—perfect for CI runners and students poking around on a laptop.\n- The project-level `test_dir = test/native_dsp` forces PlatformIO to scoop up the host bench directly, and `test_build_src = yes` keeps the DSP implementation compiled alongside the tests even when firmware entry points are filtered out. Great for CI, teaching, or proving a refactor didn’t sandbag the groove.\n- There’s also a tiny WAV harness (`test/native_dsp/process_wav.cpp`) with a callable `horizon_wav_driver` so you can bounce audio through Horizon on the host. The default `native_dsp` run sticks to the Unity test main to avoid dueling entry points, but you can flip on `HORIZON_WAV_STANDALONE` if you want a quick command-line renderer instead of tests.\n\n### Host-side IntelliSense / clangd cheat codes\n- Linting your editor without dragging in the whole Teensy core? The `patches/cores/teensy4/lint_stubs.h` shim is opt-in so firmware builds always pull `F_CPU_ACTUAL`, `NVIC_SET_PENDING`, etc. from the real PJRC headers. Flip `HORIZON_LINT_STUBS=1` when generating editor metadata and the shim injects no-op definitions for the usual suspects (`__disable_irq`, `Serial`, ...), keeping clangd/IntelliSense chill even if the PlatformIO download cache is missing.\n- Preferred compile database: `HORIZON_LINT_STUBS=1 pio run -e main_teensy41 -t compiledb` (drops `.pio/build/main_teensy41/compile_commands.json`).\n- Offline/editor-only fallback: `python tools/gen_compile_commands.py` (writes `compile_commands.json` at the repo root with the same `-I patches/cores/teensy4` + `-include patches/cores/teensy4/lint_stubs.h` flags baked in).\n- Point your editor at that database (VS Code already ships a `.vscode/c_cpp_properties.json` that hooks `compile_commands.json` and forces the lint shim include path) and re-index so host-side scanning stops flagging `F_CPU_ACTUAL`, `IRQ_SOFTWARE`, and `NVIC_SET_PENDING` as undefined.\n\n## Control ranges (cheat sheet)\n- Width lives in **0.0..1.0**. Static width is clamped there, and the dynamic width block only breathes inside that window so pots/encoders don’t promise \"1.5x\" magic that never actually happens.\n- Limiter: ceiling clamps to **-12..-0.1 dBFS** across code, CSV, and this cheat sheet (no fake \"0 dB\" promises—leave a whisper of headroom), release 20..200 ms (adapts shorter on transient hits), lookahead 1..8 ms, detector tilt -3..+3 dB/oct, mix 0..1, link mode = Linked or Mid/Side, bypass is a 5 ms crossfade.\n\n## Folders\n- `src/` — core classes (matrix, EQs, detector, limiter, smoothing).\n- `examples/` — minimal wiring sketch.\n- `presets/` — starter presets JSON.\n- `docs/` — control map CSV.\n\n## License\nMIT — see `LICENSE`.\n\n## Examples\n- `examples/minimal/minimal.ino` — bare wiring: I2S in → Horizon → I2S out.\n- `examples/horizon_scope/horizon_scope.ino` — ASCII \"scope\" showing block width, transient activity and limiter gain.\n- `examples/preset_morph/preset_morph.ino` — hands-free tour of two contrasting presets: a slow morph to cartoonishly wide stages, then a pillow-soft mastering chain.\n\n## Limiter telemetry + LED ladder\n- Gain reduction (dB) maps to an 8-step ladder at: **−1, −2, −3, −4, −6, −8, −10, −12 dB** (higher index = deeper clamp).\n- Telemetry from `LimiterLookahead::Telemetry` gives per-block peak in/out plus GR dB via `getLimiterGRdB()` for quick logging or UI.\n- Example serial line (0.5 s cadence): `GR(dB): -2.3 | Pin: 0.89 Pout: 0.79 | LEDs: 4 | Clip: no`\n\n## Latency notes\n- Audio path latency = **lookahead delay + I2S buffer**. Default ~5–6 ms lookahead keeps the limiter transparent; parallel mix and bypass are already delay-compensated inside the limiter.\n\n### Making the limiter actually look ahead\n- **Prime the delay**: call `LimiterLookahead::setup()` once at boot so the circular buffer is zeroed and the crossfade math is warmed up instead of spewing whatever was on the stack.\n- **Pick a lead time**: set `setLookaheadMs(1..8)` to taste (5–6 ms is the sweet “clairvoyant fader ride” default). The setter converts ms → samples and clamps so we never outrun the buffer.\n- **Stay on the delayed rails**: process audio through `processStereo`, which writes the live sample to the buffer, reads the delayed copy for output, and drives the detector off the undelayed feed so gain reduction lands before the delayed program hits your ears. Wet/dry and bypass already ride the same delay so phase stays honest.\n\n## Why it sounds this way (studio notebook addenda)\n- **Limiter lookahead math** — The limiter runs a short delay line for the program path but drives its envelope from a tilted detector so bright stuff pops the gain computer faster. Lookahead is set in milliseconds and turned into samples for a circular buffer; the detector uses linked peaks (or mid/side maxima) and a smoothed gain request that can only clamp faster than it releases. Release adapts: a transient average steers the time constant between a 10 ms-ish fast lane and a slower base release so cymbal sustains float while snares snap back. Wet/dry stays phase-aligned because both paths ride the same delay. Safety clipping sits after gain to catch rogue peaks but never replaces the envelope.\n- **Transient activity curve** — The transient meter is a two-time-constant envelope follower (2 ms attack, 80 ms release) with a sliding threshold. Sensitivity 0..1 moves that threshold from about 0.05 to 0.5 of full-scale, and anything above it maps linearly to a 0..1 “activity” pulse. That pulse feeds dynamic width and limiter release decisions, meaning the harder the stick hit, the quicker the release rebounds and the more the stage narrows before blooming back out.\n- **Width-to-mid/side mapping** — Dynamic width isn’t a random chorus trick: the side channel is split by a gentle low-pass anchored around 40–250 Hz so sub energy stays centered. Transient activity crossfades between two static widths: a narrowed-on-hit value and a widened tail. High band uses the chosen width directly; lows get an extra mono pull (25% of the chosen width) to keep kick/bass glued. Mid passes through untouched here, but the width fader swings are logged per sample so you can meter how the stage breathes.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbseverns%2Fhorizon","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fbseverns%2Fhorizon","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbseverns%2Fhorizon/lists"}