Skip to content

Latest commit

 

History

40 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

harmonic-shaper

The canonical additive harmonic synthesizer of the Harmonic Beacon ecosystem: 32 voices + waveshaper + per-voice LFO + sidechain.

This package is the reconciled standalone extraction of the evolved Shaper in digital-beacon. The older NaturalHarmony/harmonic_shaper implementation is historical; its Minilab3 controller was recovered here because it had been lost from the fork.

Install

python -m venv .venv
source .venv/bin/activate
pip install -e .

PortAudio must be available on the host for real-time output through sounddevice.

Run

python -m harmonic_shaper
# or, after installation:
harmonic-shaper

The default process starts the audio engine, native OSC listener, MIDI controllers when present (Launchpad, Minilab3, and a native keyboard note source), and the FastAPI state service.

Standalone mode defaults to sequential natural-harmonic banks: adjacent keys select n=1..32 at exact f1*n frequencies. The lower configured bank is momentary; the upper bank is toggle/sustain. No 12-TET pitch adaptation is applied. See Native MIDI Harmonic Banks for configuration and controller calibration. No NaturalHarmony beacon is required. Useful flags:

python -m harmonic_shaper --slave            # opt in to NH /beacon/* broadcasts
python -m harmonic_shaper --f1 40.40 --anchor 24
python -m harmonic_shaper --native-midi-momentary-start 24 --native-midi-toggle-start 72
python -m harmonic_shaper --native-midi-mode legacy_hybrid
python -m harmonic_shaper --no-native-midi   # keyboards off; pads/CC still on
python -m harmonic_shaper --no-audio         # headless control/API process
python -m harmonic_shaper --no-midi
python -m harmonic_shaper --no-api
python -m harmonic_shaper --help

Default bindings:

  • UDP :9002: current v1 wire protocol under /digital/*.
  • UDP :9001: optional /beacon/* slave input, only with --slave.
  • HTTP 127.0.0.1:8080: GET /api/state, POST /api/shaper/*, and WebSocket /ws.

/shaper/* is the planned native namespace. It is intentionally not mapped on the wire yet because renaming /digital/* requires a contract version bump.

Pure reference renderer

The extracted offline reference is also installable:

harmonic-shaper-synth-pure input.wav --out output.wav

It retains the fork's voice-analysis and NumPy rendering path for the clipping work tracked after this extraction.

Test

pytest -q

See the extraction report for the module map, fork reconciliation, dependency audit, and clipping notes.

License

MIT — see LICENSE.

The movement laboratory can render PCM without opening an audio device using AudioEngine.render_block(now=<logical_seconds>). It runs the same kernel as the production callback, with the configured block size and a caller-owned monotonic clock. Submit LaboratoryInput controls with that same logical time; leases and envelopes retain their production behavior. Each offline run owns a fresh store and engine. Rendering on an engine that is running a stream is rejected. Audio remains stereo float32 after shaping/master/soft limiting; voice_frame() describes oscillators before shaping/limiting, not the PCM.

Optional laboratory PCM capture (development)

The capture tap is after the soft limiter and copies the exact float32 samples assigned to PortAudio. It is separate from the legacy pre-limiter attach_recorder hook. Nothing is recorded until an explicit start.

  • POST /api/audio/capture/start: {"max_seconds":120,"queue_blocks":128}. Requires a running audio engine; rejects overlapping starts and invalid limits. Optional owner is an opaque 1..80-character alphanumeric/underscore/hyphen nonce. Repeating that owner with the same settings returns its existing capture (also after completion), allowing recovery of a lost start acknowledgement. Reusing it with different settings is rejected; it is not authentication.
  • GET /api/audio/capture: progress/error, sample bounds, queue size and output directory. POST /api/audio/capture/stop: optionally {"id":"<capture-id>"}; a stale identifier cannot stop a newer capture.
  • Default private output: ~/.local/share/harmonic-shaper/laboratory-captures/<id>/. audio.wav is stereo float after shape/master/limiter; blocks.jsonl preserves sample index, callback monotonic/DAC timestamps, pre-shape voices and crop size. manifest.json is written atomically after the files close. Capture state and manifest include module hashes and Python/numpy/soundfile versions observed when recording starts (files on disk, not an attestation of loaded modules).

A bounded single-producer/single-consumer deque holds copied blocks. The callback never waits for disk or queue capacity; the writer runs separately. Queue overflow, clock discontinuity, sample-rate changes, callback status or stream failure mark capture failed. Overflow/disk failure do not stop synthesis. Duration ends exactly at the requested sample count; early stop closes the current contiguous interval. The RIFF size limit is validated before starting. Start/stop ownership is serialized outside the callback. This is Python software, not a hard realtime deadline guarantee.

Captured PCM is Shaper's digital output, excluding downstream mixer/device gain or other programs. This first foundation does not capture video or laboratory configuration changes; the Weaver UI/session collector and audiovisual alignment are subsequent LAB-09 work. No camera capture or retrospective buffer is enabled. Abrupt process termination can leave an interrupted file: this version does not claim crash recovery. The running laboratory is not upgraded by checking out this branch. A change in AudioEngine also changes the strict offline engine hash; use the original pinned checkout for an old frozen PCM request, or create a new run.

Verification: pytest tests/test_capture.py tests/test_offline_render.py tests/test_laboratory.py tests/test_audio_smoke.py tests/test_pads_v2_audio.py -q: 32 tests passed. Capture tests cover exact post-limiter samples versus the separate pre-limiter tap, sample limits/crop, early stop, bounded overflow, disk/clock/stream errors, stale stop, concurrent starts and validation/API. These are hardware-free checks; real device latency, audiovisual synchronization and listening remain open.

Interrupted capture recovery (POSIX): POST /api/audio/capture/recover with {"id":"<32-character capture id>"}. An advisory writer lock rejects active recordings. New captures preserve capture.json and periodically flush audio/journal outside the callback. Recovery parses the float WAV header even when length fields are stale and writes only complete, contiguous, journal-confirmed blocks to a new recovered/<id>/ folder. Raw WAV/journal/manifests remain unchanged. Result status is recovered, never complete; truncated or unconfirmed tails are excluded. This handles process interruption, not a guarantee against power loss or disk corruption. Legacy captures without the lock/metadata contract are rejected. API accepts IDs under its configured capture root, not caller-supplied paths.

Recovery retries are idempotent for unchanged raw hashes: a previously recovered prefix is returned with reused=true after verifying its WAV/journal hashes. A modified recovered artifact is rejected rather than silently copied again. Raw changes allow a new result; every result retains its source hashes. GET /api/audio/capture/recovery-contract advertises schema 1 and idempotent_source_hashes=true and pollable_jobs=true. The legacy synchronous route remains available. For resumable recovery, POST /api/audio/capture/recovery-jobs with {"id":"<capture-id>","job_id":"<client-uuid-hex>"}. GET /api/audio/capture/recovery-jobs/<job-id> observes queued/running/recovered/ failed/interrupted without starting work. Same job ID/capture recovers its receipt; another capture with that ID is rejected. Receipts persist under recovery-jobs; writer locks distinguish live jobs from interrupted ones across service restart. An interrupted/failed receipt does not restart automatically; a new explicit attempt needs a new job ID. Completed receipts describe that attempt, not a fresh verification of current raw files; legacy recovery/exports verify artifacts.

Output controls for the laboratory

GET /api/audio/output lists stereo outputs visible to this process and reports requested/effective sample rate, block size and output revision. POST accepts expected_revision, device (index/name/null), sample_rate and block_size (128/256/512/1024/2048). Release voices and stop captures first. Settings are checked before closing the stream; a failed open attempts to restore the previous output. Identical settings do not reopen it. These settings are session-local; startup flags still configure the next session. JACK uses the graph sample rate, so requesting 96 kHz does not change a 48 kHz PipeWire server.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages