Changelog
CHANGELOG
[0.6.0-M2] — 2026-08-02
🐛 Bug fixes
- Fixed missing
register_analog_builtinsfunction inrill-analog-effects/src/lang.rs— the function was called fromregister.rsbut did not exist, causing a compilation error. Added the function withCassetteDeckBuiltinblock registration. - Fixed
rill-adrift/src/modular/mod.rs—enginevariable neededmutforallocate_probe_slots(1)call under thedebugfeature.
📚 Documentation
- Updated
docs/src/index.md: removed "audio" from the umbrella description (now "signal processing framework"), added missing crates to the layer table (rill-adrift,rill-sampler,rill-analyzer), fixedrill-lofidescription (replaced "console emulation" with "vintage DAC, tape effects, chip emulation"), fixedrill-langtype reference (RillGraphEngine→CompiledGraphEngine). - Updated
docs/src/reference/crates.md: fixedrill-analog-effectsdescription (replaced "op-amp, tape deck, preamps" with "cassette deck, tape bridge/delay"), fixedrill-langtype reference (RillGraphEngine→CompiledGraphEngine), corrected all feature flag rows forrill-core-model,rill-fft,rill-graph,rill-lang,rill-patchbay,rill-io,rill-sampler, andrill-adrift. - Updated
docs/src/architecture/overview.md: "outside audio" → "in any signal domain". - Fixed
rill-analog-effects/src/lib.rsandregister.rscrate doc comments. - Updated
AGENTS.md: crate count (19 → 20),rill-analog-effectsdescription. - Updated
README.md: library crate count (18 → 19),rill-analog-effectsdescription, test count badge.
🧹 Housekeeping
- All 20 crates bumped to
0.6.0-M2(synchronous versioning).
[0.6.0-M1] — 2026-07-11
🐛 New crate: rill-analyzer — interactive gdb-style debugger
CLI tool for debugging Rill signal processing applications. Three modes:
run graph.json— local debugging with embedded REPLattach <pid>— connect to a running process via shared memorylaunch <target>— start a process and connect immediately
Supports signal probes (break, print, step), command tracing (SetParameter
flow), Lua scripting via mlua, and JSON output for automation. Connects to
running ModularSystem processes through /dev/shm/rill-debug-<pid> with
lock-free ring buffers and SIGUSR1 notifications.
🧪 Diagnostic and debugging infrastructure (debug feature)
rill-lang:
ProbePointIR instruction — lock-free signal sampling (zero overhead when disabled)ProbeSlotwith atomic flags (enabled,break_flag,paused_flag) and SPSC queueDebugControl— inter-block pause/resume atomics for engine controlCommandFrame— fixed-size copy-compatible frame for actor mailbox tracing
rill-graph:
build_ir()now compiles graph nodes to completerill_lang::Irwith builtins, parameters, and instructions — mirroring the rill-lang DSL compilation path- Automatic
ProbePointinsertion at each node's output underdebugfeature
rill-telemetry:
CollectorThread— background thread drains probe queues + command log, formats viaTextFormatter(colored terminal) orJsonFormatter(JSON lines)ProbeStateManager— handles breakpoints, continue/step/pause, probe enable/disableShmemRegion—/dev/shm/rill-debug-<pid>mmap region with two lock-free SPSC ring buffers forAnalyzerCommand/AnalyzerResponseviaserde_cborAnalyzerCommand/AnalyzerResponseprotocol types with automaton/sensor/queue inspection variants
rill-patchbay:
PatchbayInspector— collects automaton/sensor snapshots for control-path debuggingServo::inspector()— automaton state snapshot viaArc<Mutex<>>OscSensor::inspect()/MidiHub::inspect()— sensor status snapshotsModuleFactory::construct()accepts an inspector parameter for auto-registration
rill-adrift:
debug_initmodule —init_shmem()/init_shmem_from_env()for IPC setup- Lifecycle logging in
ModularSystem::launch()— rack creation, engine build, backend connection, shutdown (vialogcrate) - Auto-probes enabled for each graph node;
CollectorThreadspawned with shmem andPatchbayInspector
⚡ Execution model unification
compile_graph()andgraph_lower::lower()now use identical buffer numbering (output_bufs = [0],output_mapping = [0],buffers = 1). Both paths converge on the sameRillProgram::new_with()→ScheduledGraph→RillGraphEnginepipeline.build_ir()produces completeIrwithbuiltins,params, andinstrs— no more stub IRs.GraphDef-based graphs and rill-lang DSL programs share the same execution mechanism.
❌ Removals
rill-oscillatorscrate removed from workspace (obsolete Port-based nodes, replaced by rill-lang builtins).rill/inputandrill/outputbuilt-in identity pass-through nodes removed.ProgramRunnerhandles I/O directly; graphs no longer need explicit Source/Sink nodes for signal routing.
📋 Breaking changes
GraphBuilder::build_ir()now returns completeIr— may change behavior for existing graphs that depended on stub IRs.RillProgram::new_with()madepub— previouslypub(crate).- Graphs using
SinkDefwithtype_name: "rill/output"require updating to remove the sink node (output routing is handled byProgramRunner). SourceDef.backend: Nonegraphs now produce output through graph-leveloutputscomputation (leaf node arity sum), not through explicit sink passthrough.rill-oscillatorsdirect dependencies broken — userill-oscillatorsbuiltins viarill-langregistry orrill-adrift.
📚 Documentation
- Debugging guide (
docs/src/guides/debugging.md) — probe architecture, command logging, shmem IPC, RT safety, lifecycle logging - rill-analyzer guide (
docs/src/guides/rill-analyzer.md) — REPL commands, Lua scripting, JSON output, attach/launch flows - Updated root
architecture.mdwith debug infrastructure and rill-analyzer - All crate-level READMEs updated with debug features and architecture changes
AGENTS.md: naming conventions (Automaton), debugging priority, RT safety rules, warnings policy, forbiddeneprintln!in signal path
🧹 Housekeeping
AGENTS.md— strengthened Automaton naming rule with rationale and scope; debugging priority rule; RT safety rules for logging; warnings policy- Fixed 3 pre-existing clippy warnings (GraphBuilder Default impl, probe struct init)
CmdStr<N>— fixed-size Copy-compatible string buffer forSpscQueuein RT pathchiptune_stcexample: added--no-waitflag; removed SinkDef; compiles to identical IR aslang_chiptunelang_chiptuneexample: added--no-waitflag
[0.5.0] — 2026-07-06
🧬 New crate: rill-lang — Faust-style signal DSL
A new workspace crate that compiles a small, functional block-diagram language
into a rill_core::Algorithm<T>. Programs describe the internal math of a graph
node as source text; the compiler runs a hand-written lexer + Pratt parser, a
Hindley-Milner type checker (scalar unification + let-polymorphism, with
bottom-up arity synthesis), lowers to a flat linear IR, and runs it on a safe,
allocation-free sample-by-sample interpreter.
- Combinators
:(sequential),,(parallel),<:(split),:>(merge),~(feedback),@(integer delay); arithmetic, math builtins, named functions, and top-levelprocesswith arity(0|1) → 1. compile::<T>(src)→RillProgram<T>: Algorithm<T>.serdefeature —RillLangDef { source }+compile_def(the source string is the canonical serialized form).rill-adriftlangfeature — re-exportsrill-langand registers arill/langfactory node (reads asourceparameter; recompiles onset_parameter).- Backend is trait-based; a Cranelift JIT backend is planned behind a future
jitfeature and will reuse the same IR. - Hybrid block processing. The interpreter compiles the IR into an execution
schedule via SCC analysis: feedforward regions run whole-buffer through the
rill_core::math::vectorSIMD eDSL, while feedback/delay recurrences run per-sample. The block path computes inT. The per-sample interpreter is retained as a reference oracle (RillProgram::process_reference). Foundation for whole-graph-as-one-program lowering and the future JIT. - DSP/model built-ins (FFI registry). rill-lang programs can call stateful
built-ins from
rill-core-dsp/rill-core-modelviacompile_with(src, ®istry, sample_rate): per-sample built-ins (onepole,moog— feedback- legal) and whole-buffer built-ins (lowpass,highpass,analog_moog). Params are constants (_ : lowpass(1000.0, 0.7)); signals flow via combinators. Bindings live inrill-adrift(lang_builtins::full_registry,analog_moogbehind theanalogfeature). rill-lang core staysrill-core-only. - Named parameters + smoothing.
param("cutoff", 1000.0)exposes RT-safe control-rate parameter slots (settable viaRillProgram::set_paramand, on therill/langgraph node, by name — so servos/LFO/MIDI automate them for free);smooth(x, ms)is a native one-pole for zipper-free changes; built-in args may beparam(...)for dynamic parameterization (lowpass(param("cutoff"), 0.7)).
⏱️ Sample-accurate parameter automation (rill-core, rill-graph, rill-io, rill-patchbay)
Fixes tick-driven control (sequencers, servos) collapsing under backends that
batch many block_size chunks into a single I/O callback (e.g. PipeWire's
12288-frame buffer = 48 × 256 chunks). Previously all parameter writes for a
callback were applied at the first chunk, so the AY chip in chiptune_stc
rendered ~4 register states/s instead of ~48.8 in release builds — the melody
dragged. Correct playback on ALSA / debug PipeWire was incidental timing.
SetParameter.sample_pos: Option<u64>— optional absolute sample position at which a parameter change should take effect.None= apply on drain (legacy behaviour, unchanged for UI/MIDI-driven writes). Builder:SetParameter::new(...).with_sample_pos(pos).ClockTick.io_quantum: u32— frames the backend processes per I/O callback (its quantum). Defaults tosamples_since_last; chunking backends set the whole callback size. Builder:ClockTick::with_io_quantum(n).- Graph applies parameters per block — the graph actor now queues writes
that carry a
sample_posand applies each during the 256-sample block whose range contains it (ProcessingState::process_block/Graph::process_block), instead of flushing everything at drain time. Writes withoutsample_posstill apply immediately (preserves duplex/legacy paths). - Producers look ahead by one quantum — because an asynchronous control
module reacting to a tick in callback N can only be rendered in callback
N+1, producers stamp
sample_pos = tick.sample_pos + tick.io_quantum. Thechiptune_stcexample and the ClockTick-drivenServowrites do this; MIDI/UI-drivenServowrites stay immediate (no latency on live input). - Cost: ~one I/O quantum of control latency, i.e. the negotiated buffer
duration. Both the PipeWire and PortAudio backends now bound this to
buffer_size × AudioConfig::buffer_blocks(default 16 × 256 = 4096 frames ≈ 93 ms at 44.1 kHz); ALSA ≈ 5.8 ms (one period). Tunable viabuffer_blocks.
🎛️ Graph adopts the backend's hardware sample rate
The graph has no clock of its own — it runs inside the backend process callback
and now adopts the rate carried by each ClockTick.
ProcessingStatere-initialises nodes on rate change — when the drivingClockTick.sample_ratediffers from the rate the nodes were built with (e.g. JACK locked to 48 kHz while the graph was configured for 44.1 kHz), every node is re-inited so chip clocks and filter coefficients match the real rate.- JACK backend — the
ClockTicknow carries the actual JACK hardware rate (wasconfig_rate), fixing playback runninghw_rate / config_ratetoo fast (e.g. +8.8 % at 48 kHz vs 44.1 kHz) with no resampling. - PortAudio backend — request a large DMA buffer
(
buffer_size × AudioConfig::buffer_blocks, default 16 × 256 = 4096 frames) instead of a single 256-frame period, then chunk it back intoblock_sizepieces in the callback, sending oneClockTickper rill block. A single 256-frame period was unstable through the PipeWire ALSA plugin (crackling); a large buffer fixes stability but, when driven as one tick, starved the sequencer (~6× slow). The chunk loop gives the sequencer the correct ~172 ticks/s and a stable buffer; the size also sets the control look-ahead latency (buffer_size × buffer_blocks / sample_rate≈ 93 ms at 16). The old forced-duplex workaround is removed. - PipeWire backend — negotiate a bounded DMA buffer via a
SPA_PARAM_Buffersobject on stream connect (buffer_size × buffer_blocks= 16 × 256 = 4096 frames by default) instead of accepting PipeWire's large default (~12288 frames). The per-chunk loop still emits oneClockTickper 256-frame block, so tempo is unchanged while the async-control look-ahead latency drops from ~278 ms to ~93 ms. AudioConfig::buffer_blocks— new field (default 16) exposing the DMA buffer size as a multiple ofbuffer_sizefor callback-driven backends (PipeWire, PortAudio). Set viawith_buffer_blocks()or the"buffer_blocks"backend param. Larger = more robust on constrained/untuned systems, higher control latency; the stable minimum is hardware/config dependent. ALSA (period fixed tobuffer_size) and JACK (buffer size set by the JACK server) ignore it.- ALSA backend — callback-driven capture and playback. The audio thread
now fires the rill process callbacks per period — the capture chain
(
set_input_process_callback) then the playback chain (set_process_callback) — matching the split-chain model of PipeWire/JACK, and implements a realread_input(previously stubbed to silence, so capture never reached the graph) by publishing each just-read period as an input window. The backend now advertisesIoCapturewheninput_channels > 0, so full-duplex graphs work. Still event-driven viasnd_pcm_wait(nothread::sleep).
📦 Version bump and cleanup
- All 18 crates bumped to
0.5.0-beta.7. - Documentation updated:
SensorDef::Oscdescribed in architecture docs,rill-oscREADME cross-referencesOscSensor, patchbay README coversmidi/oscfeature flags, stale0.5.0-beta.2references fixed throughout docs.
🔌 I/O Backend Extraction (rill-core, rill-io, rill-graph)
Major architecture change: backends extracted from graph nodes to the
orchestrator layer. Signal graph is now pure DSP (no I/O knowledge), all
hardware interaction lives in ProcessingState + backend traits.
rill-core (io.rs):
IoBackend→IoDriver+IoCapture+IoPlayback— single monolithic trait split into three orthogonal capabilities. One struct can implement any combination:IoDriverruns the clock loop,IoCapturereads input samples,IoPlaybackwrites output samples. Mirrors theMidiInput/MidiOutputsplit on the audio side.BufferViewtrait — zero-copy DMA access during I/O callback:read_input(channel, dst)andwrite_output(channel, src). Nodes holdArc<dyn BufferView>and read/write directly without intermediate ring buffers.ProcessingState— new: owns graph runtime parts (actor mailbox, node storage, parent rack ref). Created viagraph.into_processing_state(). Wired with backends viawire_backends(capture, playback). Drives processing loop:process_block(&ClockTick)→ DSP →send_clock_tick().ParameterWritetrait — polymorphic parameter injection into the graph mid-cycle (used by PipeWire per-chunk params).- Removed:
IoNode,ActiveNodetraits — backends no longer injected into graph nodes.
rill-io:
DirectView— interleaved/planar DMA access via raw pointers, implementsBufferView. Created per-callback by each backend.read_input()/write_output()operate directly on hardware DMA buffers — no copies between graph and backend.OutputWindow— adapter for backends that need partial buffer writes. WrapsIoPlayback+DirectView, handles multi-chunk DMA.ClockTick.is_final— flag for chunking backends, gatingsend_clock_tick(). Note: current chunking backends (PipeWire, JACK) leave ittrueon every chunk, so control modules receive oneClockTickperblock_sizeblock; sample-accurate placement is handled bySetParameter.sample_pos+ClockTick.io_quantum(see the entry at the top of this file), not by coalescing ticks per buffer.- PipeWire backend — major rewrite for chunk processing. DMA buffer
split into chunks of
block_size, per-chunk parameter updates viaParameterWrite. Zero-fill DMA remainder after chunk loop. (Buffer size is whatever PipeWire allocates — the backend does not yet negotiateSPA_PARAM_Buffers.) - JACK backend — chunk processing by
block_size, uses orchestratorrunningflag for shutdown.run()returns immediately (callback-driven),stop()coordinates with JACK thread. - PortAudio backend — unchanged structurally, gains
DirectViewOutputWindowfor output path.
- ALSA backend — unchanged structurally, poll-driven (
snd_pcm_wait), gains same view/window pattern.
rill-graph (backend_factory.rs):
BackendFactoryrefactored. Constructor signature changed:fn(params) -> Box<dyn IoBackend>→fn(params) -> (Arc<dyn IoDriver>, Option<Arc<dyn IoCapture>>, Option<Arc<dyn IoPlayback>>).- Bundle types:
DuplexBundle(driver + capture + playback),OutputBundle(driver + playback),InputBundle(driver + capture). create_any()— returns whatever capabilities the backend provides. Replacescreate() -> Box<dyn IoBackend>.- Caching — backends cached by name in factory, reused across racks.
Backend lifecycle (complete):
orchestrator:
1. factory.create_any(name, params) → (driver, capture, playback)
2. graph.into_processing_state() → ProcessingState
3. state.wire_backends(capture, playback)
4. driver.set_process_callback(|tick| { state.process_block(&tick); })
5. driver.run(running)
callback (RT thread):
state.process_block(&tick) → Source::generate → DSP → Sink::consume
state.send_clock_tick(&tick) [gated on tick.is_final]
Removed: LofiInput node (rill-lofi). Replaced by
LofiChipSource — a Source node wrapping any Algorithm<f32> +
ChipEmulator + ParameterWrite. IoControl trait provides
write_data() channel for chip register writes via the backend's
control interface.
🎹 MIDI Output (rill-io, rill-patchbay, rill-adrift)
MIDI output infrastructure — rill as MIDI master, sending Clock, Transport, and (future) Note messages to external devices.
rill-io — backend architecture:
MidiBackend→MidiInput(breaking rename) — trait now accurately reflects its input-only role (poll() -> Vec<MidiMessage>).MidiOutputtrait (new) —send(&mut self, &MidiMessage) -> IoResult<()>, symmetric toMidiInput. Together they mirror the audio-sideIoCapture/IoPlaybackseparation — input and output are distinct traits, each backend implements the direction(s) it supports.MidirBackend— struct refactored:_connfield changed fromMidiInputConnection<()>toMidirConnectionenum (Input/Outputvariants). New constructors:new_output(),new_output_by_name()usingmidir::MidiOutput::connect(). Backend can now be opened in either direction — reused across bothMidiInputandMidiOutputtrait impls.AlsaSeqBackend— struct unchanged (seq::Seqis inherently bidirectional). Newnew_output()constructor opens withDirection::Playback+PortCap::WRITE(vsCapture+READfor input). Newmidi_to_alsa_event()helper — reverse of existingalsa_event_to_midi()— convertsMidiMessageto ALSAEventforevent_output()+drain_output().JackMidiBackend— most significant struct change:rxsplit toOption<Receiver<MidiMessage>>, newtx: Option<SyncSender<MidiMessage>>.JackMidiHandler(process callback) becomes bidirectional:MidiInport → channel →MidiInput::poll(), and channel →MidiOutport →MidiOutput::send(). Both directions coexist in one JACK client —connect()opens input,connect_output()opens output. Same pattern for internal comms (input drainstx → rx, output feedstx → rxin reverse).
rill-patchbay:
MidiClockGenerator— output-side counterpart ofMidiClockTracker. Pure math: convertsClockTick→Vec<ControlEvent::MidiClock>using 24ppqn (24 pulses per quarter note). Derives tick spacing from absolute sample position — no cumulative drift. Transport state machine: Start resets phase, Stop/Continue follow standard MIDI transport semantics. 6 unit tests.spawn_midi_clock_output()— actor owningMidiClockGenerator+Box<dyn MidiOutput>. ReceivesClockTickvia Rack broadcast andMidiTransportcommands, serializes viaserialize_to_midi(), sends through backend.serialize_to_midi()— reverse ofparse_midi(). ConvertsControlEvent::MidiClock→0xF8,MidiTransport→0xFA/0xFB/0xFC,MidiNote→0x90/0x80. Round-trip tests:parse_midi(serialize_to_midi(e)) == e.ClockDef { backend, port_name, auto_start }— serializable MIDI clock output descriptor. Added toModuleDef::Clock(ClockDef)variant.- Re-exports:
MidiClockGenerator,spawn_midi_clock_output,serialize_to_midi,ClockDef.
rill-adrift:
ModuleDef::Clock(ClockDef)variant in adrift serialization layer, forModularSystemDefJSON documents.ClockConstructor— registered inModuleFactoryas"clock". CreatesMidiOutputbackend, callsspawn_midi_clock_output(), supportsauto_start.to_pb_module()+ rack dispatch —ClockDefconversion and module ID extraction for rack actor fan-out.
Design doc + plan: docs/superpowers/specs/2026-06-30-midi-output-design.md,
docs/superpowers/plans/2026-06-30-midi-output-plan.md.
⚡ Servo conflict resolution (rill-patchbay)
Servo::with_control()/Servo::with_conflict()— builder methods to configureControlStrategyandConflictStrategyon a Servo.with_control(Modulation { depth })— automaton output modulates aroundstate.base, combinable with HID input viaBasePlusModulation.with_conflict(TouchOverride)— HID input freezes automaton viastate.frozen, resumes onUiRelease.with_conflict(BasePlusModulation)— HID input updatesstate.base; automaton modulates around it on nextClockTick.ServoConstructornow passesServoDef.control_strategyandServoDef.conflict_strategythrough to Servo construction.Controlhandler fallback mapping arm now checksConflictStrategy: was ignoringstate.frozenandstate.base— now respects all three strategies.- Dead code removed:
UiCommandenum (strategy.rs) — never used. - Docs: all PortCombiner references replaced with Servo+strategy
architecture diagrams across
README.md,patchbay-rack.md,actor.md,two_thread_architecture.md.
[0.5.0-beta.5]
🕐 Unified RenderContext (Breaking)
rill-core (time/render.rs):
RenderContext— single stack-allocated context per processing block:sample_pos,samples_since_last,sample_rate,transport: TransportState,speed_ratio(hardware clock correction, default 1.0).TransportState—is_playing,bpm,frame_pos,time_sig_num/den,bar_start_frame. ReplacesClockTick::tempo: Option<f32>.- Musical methods moved from
ClockTicktoRenderContext:beat_position(),musical_position(),is_new_bar(),is_new_beat()— now use configurabletime_sig_num/den(no longer hardcoded 4/4). ProcessContextandActionContextremoved — replaced by&RenderContextthroughout the trait system.
Trait signatures (breaking):
Algorithm::process(input, output)—ctxparameter removed (97.4% of impls ignored it; 2 tape heads now useinit()for sample rate).Source::generate(&RenderContext, …),Processor::process(&RenderContext, …),Sink::consume(&RenderContext, …),Router::route(&RenderContext, …)— all use&RenderContextinstead of&ClockTick.Port::propagate()— context parameter removed; single&RenderContextflows through the DAG without re-wrapping.Port::run_action()— context parameter removed.Port::pre_process()—_tickparameter removed.
Graph:
Graph::run()I/O callback creates oneRenderContextper block and passes it to bothprocess_block()andpropagate()— no moreProcessContext+ActionContextduplication.Graph.system_clock: Option<Arc<SystemClock>>— when set, createsRenderContext::with_tempo()with BPM from the shared clock.
🎛️ MIDI Clock Sync
rill-patchbay (midi_clock.rs):
MidiClockTracker— counts 24ppqn clock pulses (0xF8), derives BPM via running average, writes atomically intoArc<SystemClock>.MidiClockStrategytrait with three built-in strategies:FreeRunning(BPM only),ResetOnStart(position reset on Start),SongPosition(position reset +is_playing()flag).is_playing: Arc<AtomicBool>— shared flag, set on MIDI Start/Continue, cleared on Stop. Sequencers and automations check this before producing output.- Integrated into
MidiHub— optional viaMidiHub::with_clock_tracker(). The tracker'sSystemClockfeeds BPM toGraph.system_clock.
🌐 OSC Sensor (rill-patchbay)
OscSensor(osc.rs) — OSC input sensor modelled afterMidiHub/spawn_midi_sensor. Binds a UDP socket in a dedicated OS thread, decodes incoming OSC packets viarill-osc, producesControlEvent::Osc { address, args }events. Bundles unwound recursively. ImplementsModule+Sensortraits.spawn_osc_sensor()— actor-model variant: spawns a control actor forSetEnabledcommands + UDP recv loop in OS thread. SendsCommandEnum::Control(event)to the servo for mapping.parse_osc()— convertsOscMessage→ControlEvent::Osc. Numeric args (Int,Float) collected; strings and blobs silently dropped.SensorDef::Osc { port, mappings }— serializable descriptor variant inmodule_def.rs.into_sensor()gated onany(feature = "midi", feature = "osc").OscConstructor— registered inModuleFactoryviarill-adrift: creates mapping-only servo +spawn_osc_sensor()pair. Activated byModuleDef::Sensor(SensorDef::Osc { ... }).- Feature gate:
osc = ["dep:rill-osc"]inrill-patchbay;rill-adrift/oscenablesrill-patchbay/oscpassthrough. - Existing
EventPattern::OscAddress/OscPatternmatching in servo works out-of-the-box — sensor producesControlEvent::Osc, servo matches viaEventPattern::matches().
🔌 JACK MIDI + Transport
rill-io:
JackMidiBackend— JACK MIDI input backend. Registers aMidiInport, bridges JACK process callback toMidiBackend::poll()via mpsc channel (same pattern asMidirBackend).JackBackend::set_system_clock()— JACK transport sync: reads BPM fromTransportBBTin process callback, writes atomically toSystemClock.
🔈 Lofi: DC Offset + Output Ceiling
rill-lofi (config.rs, lofi_processor.rs):
LofiConfig.dc_offset— subtracted from signal after dry/wet (before gain). Default 0.0. Use 0.5 for AY-3-8910 to centre [0, 1] around zero.LofiConfig.output_ceiling— hard clamp[-ceiling, +ceiling](default 1.0).- Formula order:
(dry_wet_mix - offset) * gain, clamp to ±ceiling. - New parameters exposed as
"dc_offset"and"output_ceiling"inLofiProcessormetadata → available throughSourceDef.parameters. - 3 new tests: offset removal, ceiling clamp, combined behaviour.
Registration (rill-adrift/src/registration.rs):
rill/lofi_inputconstructor now readsdc_offset,output_gain,output_ceilingfromParams.
🧱 Physical Modeling in rill-core-model
Four new resonant model modules (rill-core-model):
string— 1D digital waveguide with fractional-delay allpass interpolation, stiffness dispersion, and frequency-dependent damping. ImplementsAlgorithm<T>+ParameterizedAlgorithm<T, Params = StringParams<T>>.plate— 2D FDTD waveguide mesh on rectangular grid with clamped/free boundary conditions. Impulse excitation at configurable position.modal— parallel bank of 2-pole resonant filters for modal synthesis. Pre-built presets:bell_modes()(5 modes, inharmonic bell ratios) andmarimba_modes()(3 modes, harmonic bar ratios).cavity—HelmholtzCavity(single Helmholtz resonator with optional reed excitation for wind instrument modeling) andCavityArray(1D chain of coupled cavities for wave propagation experiments / acoustic metamaterials).
All four types implement Algorithm<T> + ParameterizedAlgorithm<T>.
24 new tests.
♻️ ParameterizedAlgorithm → rill-core
rill-core (traits/algorithm.rs):
ParameterizedAlgorithm<T>trait added — typed parameter access for anyAlgorithm(params(),set_params(),set_parameter()). Generic overtype Params: Clone + Send + Sync. Previously lived inrill-core-dsp.
rill-core-dsp:
rll-core-dsp/src/algorithm.rs— now re-exportsParameterizedAlgorithmfromrill-core; definition removed.Algorithm,AlgorithmCategory,AlgorithmMetadata,ActionContext,ProcessResultno longer re-exported fromrill-core-dsp— all consumers import directly fromrill_core::traits.- 7 filter
ParameterizedAlgorithmimpls unchanged.
📦 rill-core-wdf → rill-core-model
- Crate renamed:
rill-core-wdf→rill-core-model - Internal module
filters→wdf(path:rill_core_model::wdf::*) - All imports across workspace updated (4 crates, 14 docs, 3 scripts)
- Current module listing:
macros,analysis,constants,wdf,tape,string,plate,modal,cavity
📝 Terminology: «audio» → «signal» / «I/O»
Public API (breaking):
rill_oscillators::audio→rill_oscillators::signal— module renamePortType::is_audio_rate()→is_signal_rate()inrill-coreAudioTimer→SignalTimerinrill-coreAudioConfig→IoConfiginrill-coreRackCase::audio_thread→signal_threadinrill-adrift
Cargo.toml descriptions — «audio» → «signal» / «I/O» in 7 crates: rill-graph, rill-sampler, rill-telemetry, rill-router, rill-osc, rill-digital-effects, rill-adrift.
Documentation — «audio thread» → «signal thread», «audio data» → «signal data», «audio backends» → «I/O backends», «audio path» → «signal path», etc. (~120 occurrences across .rs doc comments, architecture docs, AGENTS.md, README.md).
IoBackend in rill-core formally positioned as a generic I/O archetype — applicable to any discrete data stream, not just audio.
Preserved: rill-io and rill-lofi keep «audio» terminology (genuinely audio-specific — hardware I/O, emulators).
🎛️ AY-3-8910 Emulator Fixes
rill-lofi:
- Mixer register R7 bit layout — bits 0–2 = tone A/B/C, 3–5 = noise A/B/C (fixed; was grouping bits 0-1,2-3,4-5 per channel)
- Envelope period divider —
f / (16 × EP)→f / (256 × EP)per AY-3-8910 datasheet - Noise LFSR output bit — save bit 0 before shift (was reading bit 16 after shift)
- Test
test_mixer_register_bit_mappingupdated for correct layout
🏭 Module Factory
rill-patchbay/src/module_factory.rs (new):
ModuleConstructortrait —construct(id, params, system, graph_ref) → BoxedModuleModuleFactory—register_fn(type_name, drain, closure),register_fn_send()Drainenum —OsThread { interval_ms },TokioTask { interval_ms }(for many actors without OS thread overhead)GenericModule— factory-providedModuleimpl, no manual struct needed
rill-patchbay/src/serialization/mod.rs:
ModuleDef::Custom { type_name, params }— dispatch throughModuleFactoryinbuild_servos()
rill-adrift/src/modular/mod.rs:
ModularSystem.module_factory: ModuleFactory—module_factory_mut()for pre-launch registration- Rack actor drain loop:
tokio::spawn→std::thread::spawn(avoidsSendrequirement on handler)
🎭 Actor Model Unification
rill-core-actor:
- Removed:
Actor<M>(oldSendvariant),LocalActor<M>,ActorCelltrait,MessageDispatcher,build_actor() - Added:
spawn_detached(name, make_handler, ms)— handler created inside spawned thread,ActorRefreturned immediately - Added:
spawn_detached_tokio(name, make_handler, ms)— same but on tokio task (handler:Send) spawn(name, handler)— remains for inline drain (Graph, Rack)- Actor design rule: handler is always created on the thread where it is drained; never crosses thread boundary;
Sendbound removed from handler closure
🔧 Sequencer & Servo Fixes
rill-patchbay/src/automaton/sequencer.rs:
- Removed dead
Step.valueandStep.curvefields (Stepnow only hasduration) - Fixed
step_duration()formula: removed× 4.0factor (now1.0= quarter note, not whole note)
rill-patchbay/src/engine.rs:
- Added
Servo::with_table()builder — propagatestablefromServoDeftoServo Servo::spawn()usesspawn_detached_tokio— handler created inside tokio task, no actor crossing thread boundary
rill-patchbay/src/serialization/mod.rs:
build_servos()now propagatesServoDef.table→Servo::with_table()
🎵 Chiptune Examples
rill-adrift/examples/chiptune.rs:
- 3-channel AY melody: Ch A (melody), Ch B (bass), Ch C (snare), 16 steps × 120ms, bass changes every 4 steps
- Fixed Output
channels=1(was defaulting to stereo, causing PipeWire panic) - Duration:
120ms → 0.24quarter-note beats (matching fixedstep_durationformula) - Removed unused
HashMapimport
rill-adrift/examples/chiptune_stc.rs:
- Rewritten to use
ModularSystemDef+ModuleFactory(register_fnwithDrain::OsThread) - STC player registered as
ModuleDef::Custom { type_name: "stc_player" } - Removed: manual
GraphBuilder,graph.run(),StcModulestruct,sys.spawn(),actor.drain(),thread::spawn
🔩 RackCase Fix
rill-adrift/src/modular/case.rs:
RackCase::stop()— addedhandle.thread().unpark()beforehandle.join()(was hanging on exit)taskstype:Vec<tokio::task::JoinHandle>→Vec<std::thread::JoinHandle>
📝 Documentation
docs/src/guides/chip-emulators.md:
- Rewritten: accurate register map, architecture diagram,
io_writecontrol chain, lofi processing chain - Known Limitations section — output sampling, anti-aliasing, register change timing, I/O ports, phase delay
- Timing accuracy section — tone/envelope/noise frequency formulas, accuracy bounds
docs/src/architecture/actor.md:
- Updated for current API:
Actor<M>, threespawnvariants, handler-creation design rule
🧹 Cleanup
- Removed: dead
Actor<M>(Send variant),ActorCell,build_actor(),Step.value/Step.curve rill-io/Cargo.toml— removed unusedbase64dependencyrill-core-actor/Cargo.toml— added optionaltokiodependency (feature-gatedspawn_detached_tokio)- PortAudio callback — removed debug
base64output
🏗️ Architecture: RackDef unification + CaseDef removal
rill-adrift/src/modular/serialization.rs:
- New
RackDefwithgraph: GraphDeffield — graph lives inside the rack, not in a separateCaseDef - New
ModuleDef::Graph { graph: GraphDef }variant — multiple graphs per rack build_servos()moved fromrill-patchbaytorill-adriftModularSystemDef.racks: Vec<RackDef>replacescases: Vec<CaseDef>CaseDefremoved entirely —patchbay: Option<RackDef>no longer needed
rill-adrift/src/modular/mod.rs:
launch()simplified: single loop overdef.racks, nohas_rackcheck- Rack actor drain:
tokio::spawn→std::thread::spawn(avoidsSendrequirement) - Graph construction stays in
launch()(not via factory)
rill-patchbay/src/serialization/mod.rs:
RackDef→PatchbayDef(backward-compatible rename, withoutgraphfield)ModuleDef(withoutGraphvariant) +build_servos()remain in rill-patchbay
rill-adrift/src/modular/config.rs:
LaunchConfig.rack_deftype:RackDef→PatchbayDef
🔌 CommandEnum::Stop + Drain::IoCallback
rill-core/src/queues/signal.rs:
CommandEnum::Stop+CommandType::Stop— shutdown command for I/O loops
rill-patchbay/src/module_factory.rs:
Drain::IoCallbackvariant — for graph modules with inline drain (not yet used via factory)
📝 Documentation
docs/src/architecture/actor.md:
- Updated for current API:
Actor<M>, threespawnvariants, handler-creation design rule
docs/src/guides/chip-emulators.md:
- Rewritten: accurate register map, known limitations, timing accuracy section
Previous (0.5.0-beta.4)
rill-core-actor:
Actor<M>— handler:Send, для многопоточных акторов (Patchbay через tokio)LocalActor<M>— handler:!Send, для однопоточных (Graph, RackCase)ActorSystem::spawn()/spawn_local()— создание акторов с handler-замыканиемActorRef<M>— lock-free handle для отправки сообщений, единственный внешний интерфейс- Удалены:
ActorCelltrait,Mbox,MessageDispatcher,ActorRef::new_pair(), genericActorSystem<M>
rill-graph:
GraphBuilder::build(&ActorSystem)— создаёт актор с handler'ом, захватывающим nodesGraph::run()— tick-замыкание владеет actor'ом напрямую (без*mut Graph)- Nodes хранятся в
Rc<UnsafeCell<Vec<NodeVariant>>>— interior mutability на одном потоке - Удалены:
*mut NodeVariant,*mut Graph,ActorCellimpl,mailboxполе - Сигнальные тесты:
test_graph_source_to_sink,test_graph_source_proc_sink
rill-patchbay:
Patchbaystruct удалён. Вместо него —Servo::spawn(self) → ActorRef<CommandEnum>- Создаёт актор с полным handler'ом (ClockTick → automaton.step → SetParameter)
- Запускает
std::threaddrain loop (1ms interval) - Внешний код получает только
ActorRef— никакого прямого доступа к состоянию
Servoбольше неModule— автономный актор, не type-erased boxPatchbayDef→RackDef—build_servos(&ActorSystem, &graph_ref) → HashMap<String, ActorRef>add_lfo,add_envelope,add_boxed_servoудалены — сборка вlaunch()напрямуюModuletrait — только для Sensor; убраныdrain(),update()- Channel-forwarding (mpsc) между actor'ами удалён — каждый актор самодрейнится
rill-adrift:
RackCase— минимальный хост:modules: HashMap<String, ActorRef>,tasks: Vec<JoinHandle>- Удалены:
patchbay,incoming,outgoing,ActorCellimpl, межкейсовый routing handle() → ActorRef— дляparent_refв Graphstop()— abort всех tasks, join audio thread
- Удалены:
launch():- Создаёт актор RackCase (с
Arc<Mutex<HashMap>>для модулей) - Запускает drain thread актора (пересылает ВСЕ сообщения всем модулям)
- Строит граф на audio thread
- Получает
graph_refчерез oneshot канал rack_def.build_servos()— создаёт Servo'ы с drain threads- Регистрирует servo ActorRef'ы в RackCase модулях
- Создаёт актор RackCase (с
- Удалены:
create_case(),load_patchbay(),load_graph(),create_patchbay(),tick(),start_osc(), OSC,control,control_shared,control_arc,AutomatonFactory
Архитектура ClockTick → Sequencer → Graph:
Graph.run() → tick: parent_ref.send(ClockTick)
→ RackCase actor (drain thread): for ref in modules: ref.send(msg)
→ Servo actor (drain thread): ClockTick → automaton.step() → graph_ref.send(SetParameter)
🔧 Сопутствующие исправления
- PortAudio: off-by-one в
write()—cap / nchтеперь используется как bound цикла (был крашindex out of bounds: 256) advanced_player: комментарий--features "cpal,…"→"portaudio,…"play_wav: пример ручной сборки графа переписан (был заглушкойlet _ = system)
SIMD acceleration (feature/simd)
-
Vector infrastructure:
SimdDetector— real CPU feature detection viastd::arch(SSE2/AVX/NEON/SIMD128)VectorMask<T, N>completed forF32x4,F32x8,F64x2,ScalarVector4VectorReduce,VectorScalarOpstraits with blanket implsScalar::from_usize()added to core math trait- Dead
exprmodule +vec_expr!/vec_eval!stubs removed
-
Algorithm SIMD (rill-core-dsp):
BasicOscillator— 6 waveforms viaScalarVector4block processing (4 samples/iter)- Saw BLEP —
VectorMask::selectreplaces per-lane scalar conditional (2.5× speedup) InterpolatedReader— 4-wide lerp math for linear/cubic interpolationCombFilter— batched 4-sample read/write whendelay_samples >= 4NoiseGenerator— White (batched xorshift), Brown (unrolled integrator), Blue/Violet (4-wide diff)Biquad— block state-space 4×4 feedforward matrix viaBiquadBlockprecomputationResampler<T>— sample-rate converter onInterpolatedReader(44.1k→48k etc.)
-
Node-level SIMD:
Distortion— HardClip/Tube 4-wide SIMD; zero-copy port outputDryWetMix— 4-wide multiply-add, stereo in one passWriteHead— batched 4-sample math per tape writepre_process()— feedback mix via 4-wide add (all feedback nodes accelerated)- 8 nodes: direct port buffer write eliminates 2
[T; BUF_SIZE]copies per block per node
-
WDF SIMD (rill-core-model):
process_incident_vectoronResistor,Capacitor,Inductor,DiodeviaScalarVector4- Diode Newton-Raphson vectorized with
VectorMask::all()early exit process_batch_simdfree function for batch processingsimd.rsdeleted (378 LOC) — no more parallel SIMD type hierarchy
-
I/O SIMD:
- Generic
f32_to_i16_chunk/i16_to_f32_chunkinrill-core::math::functions(reusable for ALSA, rill-lofi) - ALSA backend uses SIMD f32↔i16 conversion
- PipeWire byte→f32 batched 4-sample conversion
- Deinterleave/interleave SIMD in PipeWire backend
- Generic
-
Infrastructure:
FixedBuffernow#[repr(align(16))](hardware SIMD-ready)const { assert!(BUF_SIZE % 4 == 0) }inprocessable.rs(monomorphization-time check)- Criterion benchmarks: vector ops, 6 oscillators, 3 filters, 4 noise types, reader/resampler
- Benchmark results at
docs/superpowers/specs/2026-05-10-simd-benchmark-results.md - Key finding:
ScalarVector4+ LLVM auto-vectorization matches/exceeds explicitwidecrate on x86_64. Rill outperforms JUCE (C++) by 10-160× on key DSP primitives.
✨ Patchbay architecture refactor (feature/refactor/midi-hub, feature/refactor/sensor-midi)
-
Automaton trait redesigned:
(config, &mut internal, ¤t, time, action) → ParamValuetype Internal: Clone— mutable automaton-specific state (phase, RNG, step counter)initial_internal(),reset()with default impls- All state moved inside structs; old
State/Outputassociated types removed - All 6 automata (LFO, envelope, sequencer, function, random, cellular) updated
- LFO: now uses
self.waveform— all 8 waveform types functional (was hardcoded to Sine) - Random:
update_ratefield drives throttling vialast_update_timein Internal
-
Servo as actor:
Servo<A: Automaton>implementsActorCell<Msg = AutomatonMsg>AutomatonMsg { Tick(ClockTick), SetEnabled(bool), Reset }— unified queue for clock + commandsServo::update()drains mailbox before stepping (same pattern asGraph::run)Servo::handle()returnsActorRef<AutomatonMsg>for external controlServo::with_table(Vec<ParamValue>)— table-based step-to-value mapping for sequencersSequencerAutomatonreturnsParamValue::Int(step_index)→ Servo looks up in table
-
Sensor trait — unified external input bridge:
trait Sensor { attach(), start(), stop() }— MIDI, OSC, knobs, acoustic analysisMidiHubimplementsSensor— no moreArc<Mutex<Patchbay>>Patchbay::event_mailbox— singleMpscQueue<ControlEvent>for ALL sensorsevent_handle() → ActorRef<ControlEvent>,drain_events()called fromdrain_clock()- Multiple sensors can run independently, all events via one lock-free mailbox
-
Hearing module for future acoustic sensors:
PitchDetector,EnvelopeFollower,ZeroCrossing— audio analysis algorithms- Ready for wiring into graph telemetry (audio feedback → control signals)
🗑️ Removed
-
crossbeam-channel — removed from all crates (rill-core, rill-patchbay, rill-adrift)
CommandQueue(crossbeam-based) deleted;Commandtrait kept inrill-core::queuesTelemetryTx(crossbeam wrapper) deleted;Telemetrytypes kept for future useObservermoved torill-patchbay, now usesActorRef<Telemetry>SequencerHandle(crossbeam command channel) deletedattach_sequencer()(crossbeamReceiver<Telemetry>parameter) deleted
-
Manager (806 LOC) — deprecated sync rack, zero external callers
-
SnapshotSequencer + sequencer types (728 LOC in
sequencer/) -
SequencerDef serialization (170 LOC)
-
sensor/physical.rs (dead code referencing non-existent types)
-
automaton/mapping/ (156+183+155 LOC dead code)
-
MidiActorrenamed toMidiHub;midi_actor.rs→midi.rs -
Graph::receive()now drains viaActorCell(was manualset_parameterloop)
🔧 Fixes
- RT safety: MixerNode
vec![]→[f32; BUF_SIZE]stack allocation - RT safety: PortAudio
vec![]temp buffer →[f32; 8192]stack - RT safety: ParallelAdapter
Vec<T>→[T; 8]stack allocation Graph::receive()—debug_assert!for SetParameter misconfiguration- LFO: all 8 waveform types functional in
step()(was hardcoded to Sine) - Random:
update_ratefield drives throttling - Documentation synced with code (12 discrepancies fixed)
- Zero compiler warnings with
--all-features
✨ STC chiptune player (feature/feat/stc-player)
rill-adrift/examples/chiptune_stc.rs— full Sound Tracker Compiled (STC) player- Plays ZX Spectrum chiptune files through the
Ay38910BackendAY-3-8910 emulator - Loads the STC file (
Bonysoft - Popcorn (1993).stc) viainclude_bytes! - Implements the libayemu-compatible event-driven architecture:
- Per-channel byte-stream event reading with delay/interrupt timing
- Per-frame pitch computation:
ST_TABLE[note + ornament[pos] + transposition] ± sample_delta - 32-step sample (instrument) rendering with volume, tone/noise mixer masks, and pitch deltas
- Synchronized 32-step ornament (pitch modulation) and sample position advancement
- Sample repeat/loop logic, envelope triggering, position advancement on channel A end marker
- Timing at 48.828 Hz Pentagon INT rate via
step_ms()time accumulation from audio callbacks - Uses the same graph/clock architecture as
chiptune.rs— validates the engine timing
- Plays ZX Spectrum chiptune files through the
[0.5.0-beta.4] — 2026-05-08
✨ New
-
IoNode/ActiveNodetrait hierarchy inrill-core::traits::node:Node— base trait, no backend, no run methodIoNode: Node—resolve_backend(backend)for I/O-capable nodesActiveNode: IoNode—run(tick, running)for the single driver nodeas_io_node_mut()/as_active_node_mut()downcasting helpers onNodeInput,Output,LofiInputimplementIoNodeInput,OutputimplementActiveNodeGraphBuilder::build()uses downcasting instead of name-based matchingGraph::run()callsActiveNode::run()instead ofNode::run()GraphRunnertrait removed — replaced byBox<dyn FnMut(u64, f32)>- Inherent
resolve_backend()convenience methods onInput/Output
-
Chip emulator architecture — unified model for vintage sound chips:
Ay38910Chip+Ay38910Backend— AY-3-8910 / YM2149 (3 tone, noise, envelope)NesChip+NesBackend— NES 2A03 APU (2 pulse + sweep, triangle, noise, DPCM)IoControltrait inrill-core::io— uniform register write interfaceLofiInput<T, BUF_SIZE>—Sourcenode wrapping anyIoBackendwith lofi processing
-
WDF tape module in
rill-core-model:RecordHead<T>,PlaybackHead<T>— analog tape physics,Algorithm<T>OpAmp<T>— operational amplifier asWdfElement<T>CassetteDeckinrill-analog-effectsrefactored to use heads fromrill-core-model
-
Transcendentaltrait extended:tanh(),signum(),random()— enables stochastic modeling in generic WDF/dsp code -
NES 2A03 sweep unit — full hardware sweep emulation (divider, direction, shift, period underflow/overflow mute)
🔧 Fixes
rill-io:set_process_callbacksignature changed fromFn()toFn(f32)— each backend passes its actual negotiated sample rate to the process callback.ClockTick.sample_ratenow always reflects the true device rate.rill-io/jack: readsclient.sample_rate()after activation, passes to callback.rill-io/alsa: querieshw.get_rate()afterset_rate(Nearest), enforces exact period match (hw.get_period_size() == BUF_SIZE), rejects mismatches. Fixedwrite()— was hardcoded for stereo, now handles N channels with proper interleaving.rill-io/pipewire: output chunk no longer hardcoded to 512 samples — usesbuf_frames * out_channelsfor correct mono timing.write()fixed for N channels.rill-lofi/emulators: removedunsafe impl Send/Sync— backends run exclusively in the hard-RT audio thread.rill-core/io:IoBackendandIoControltraits no longer requireSend + Sync.rill-core-actor:ActorCellno longer requiresSend.rill-adrift/chiptune:step()usesf64timing (no millisecond quantization),Ay38910Backendlazily created with actual sample rate,lofi.init(sr)called for correct processor configuration.rill-adrift/record_mic: graph built inside audio thread spawn (noSendneeded).
✨ New
rill-io/portaudio— cross-platform PortAudio backend (portaudiofeature). Exact buffer size, noBufferSize::Defaultissues, simpler API. Default backend replacing CPAL.
🧹 Removed
rill-io/cpal— replaced byrill-io/portaudio(cross-platform, cleaner API)Ay38910Emulator,NesEmulator— replaced byChip+Backend+LofiInputrill-analog-effects::OperationalAmplifier— replaced byrill_core_model::OpAmp
📖 Documentation
- New guide: Chip Emulators (
docs/src/guides/chip-emulators.md) - Examples section added to root
README.md— all 5rill-adriftexamples described withcargo runcommands - Spec + plan for IoBackend-based emulator architecture in
docs/superpowers/
[0.5.0-beta.3] — 2026-05-07
✨ New
-
rill-core-actorcrate — actor model infrastructure:ActorRef<M>— thread-safe handle, strongArcreference,send()is lock-free and RT-safeActorCelltrait — for types that own a mailbox and process messagesMessageDispatcher<M>— dispatcher with dead letters supportActorSystem<M>— named mailbox registry,route(),broadcast(), dead letters
-
rill-adrift:serializationadded to default features —serde+tomlavailable out of the box -
rill-adrift:config.toml— new example config file withbackend_name,backend_params,sample_rate,block_size -
rill-adrift:RuntimeConfignow derivesserde::Deserialize(behindserializationfeature) -
Missing graph nodes registered:
rill/moog_ladder— digital Moog ladder filter (rill-digital-filters)rill/lofi— lo-fi processor (rill-lofi, gated behindlofi)rill/analog_moog_ladder— WDF Moog ladder filter (rill-analog-filters, gated behindanalog)rill/cassette_deck— cassette deck emulation (rill-analog-effects, gated behindanalog)rill/parametric_eq— parametric equalizer (rill-router)rill/graphic_eq— graphic equalizer (rill-router)- All router nodes (
dry_wet_mix,mixer, EQ) consolidated intoregister_router()
🧹 Removed
rill-core-dsp: removedunstablefeature (no code behind it, required nightly)rill-patchbay:PatchbayEngineremoved (folded intoEngine)rill-core:traits::actormodule removed (moved torill-core-actor)
🔧 Fixes
rill-io/pipewire: fixedAudioBackend::writestub returning0instead ofbuffer.len()rill-graph: removed redundantB as usizecast, pre-existing clippy warnings fixedrill-patchbay,rill-adrift: fixed redundant closures, unused imports, unused variablesrill-adrift:--no-default-featurescompilation fixed:register_all_nodesno longer gated behindio(oscillators, filters, effects available without I/O)register_backendscall inRuntime::new()gated behindiocfg_from_params()gated behindioPatchbayimport decoupled fromoscfeatureActorRefimport gated behindany(osc, serialization)- Dead
register_iostub removed
rill-adriftexamples:play_jsonrenamed toplayer— now readsconfig.tomlinstead of hardcoded paths- All examples have explicit
required-features(clear error with--no-default-features) play_wav: unusedregistrationimport removed
📝 Documentation
- Architecture article: actor model (
docs/src/architecture/actor.md) with RT boundary section AGENTS.md: quoting rules for commit messages with backticks- All docs updated to reflect
Engine,*Def,ActorRefnaming
[0.5.0-beta.1] — 2026-05-04
🎉 First beta release
All 17 crates published on crates.io at 0.5.0-beta.1.
✨ New
- WAV playback example (
rill-adrift/examples/play_wav.rs) — full pipeline from file to speaker: load WAV → SamplePlayer → BiquadFilter → AudioOutput - CLI backend selection —
cargo run --example play_wav -- [backend] [file] - 24-bit WAV support —
rill-samplernow handles 24-bit PCM in addition to 16-bit
🔧 Improvements
- All 4 audio backends produce clean audio: CPAL, ALSA, PipeWire, JACK
- OutputWindow pattern —
write_output()writes directly into DMA buffer, eliminating intermediate ring buffers and associated sizing issues (CPAL, PW, JACK) - Lock-free
IoRingBuffer— rewritten withUnsafeCellinterior mutability, all methods take&self, noMutex/RwLockin the RT path - No
thread::sleepin any backend — all backends are event-driven or callback-driven - WDF macros accept bare expressions —
$pr:exprreplaces$pr:tt, no more unnecessary braces
🧹 Dependencies removed
parking_lot— removed fromrill-iodependencies (all uses replaced withstd::sync::Mutex/AtomicU32or lock-free patterns)crossbeam-channel— removed fromrill-iodependencies (start/stop viaAtomicBool+thread::park/unpark, MIDI events viastd::sync::mpsc)
🏗️ Infrastructure
- CI — GitHub Actions with 4 jobs: lint, test, test-minimal, doc
- Pre-commit hook — rejects direct commits to
develop/main/master clippy.toml— workspace-level lint configuration (later removed,needless_range_loopallowed at workspace level)- 491 tests — all passing, 0 clippy warnings (excluding intentional
needless_range_loopin SIMD code)
📚 Documentation
- Root README: 1270 → 154 lines, English only, no duplication
- 6 new mdBook chapters:
core,graph,real-time-safety,world-of-automatons,git-flow, overhauledgetting-started - Doc comments on all public API items — 0 missing-docs warnings
- Doc link warnings: 48 → 0
- All 17 crate READMEs present and up to date
rill-sampler/README.mdwritten from scratchrill-patchbay/README.mdrewritten with green thread architecturerill-adrift/README.mdexpanded with feature flags tableCHANGELOG.md,MANIFESTO.mdmoved to repository root
🧪 Quality
cargo clippy --workspace: 0 warnings (down from 755)cargo doc --workspace --no-deps: 0 warnings (down from 48)cargo test --workspace: 491 passed, 0 failed
[0.4.0] — 2026-05-02
💥 Breaking changes
-
Audio→Signalrename across the entire API surface:AudioNode→SignalNodeAudioBuffer→SignalBufferAudioError/AudioResult→SignalError/SignalResultAudioGraph→SignalGraphAudioEngine→SignalEngine
All crates bumped to
0.4.0. Onlyrill-io::AudioBackendkeeps its name (genuinely audio-specific trait).
[0.4.1] — 2026-05-04
✨ Audio I/O backends — AudioIo trait
Реализован AudioIo для всех бэкендов:
| Бэкенд | Статус | Механизм вызова callback |
|---|---|---|
NullBackend | ✅ | Заглушка, callback не дёргается |
PipewireBackend | ✅ | RT callback (PW thread) |
JackBackend | ✅ | RT callback (JACK thread) |
AlsaBackend | ✅ | snd_pcm_wait() — event-driven, без thread::sleep |
CpalBackend | ✅ | Thread + thread::sleep(interval) — poll-driven |
AudioInput::init_backend(name, config)— узел сам создаёт бэкенд по имени (null,alsa,cpal,pipewire,jack), каждый под feature gateAudioOutput::set_active(source_idx)+start()— pull model (active Sink). Sink хранит ссылку на Source и дёргаетgenerate()+propagate()при каждом цикле обработки. Callback идентичен push-модели.AudioOutput::consume()— читает из собственных входных портов (self.inputs), а не из параметраsignal_inputs(пуст при вызове черезprocess_block→propagate)ParamValue::as_str()— доступ к строковому значениюString/Choice
🧹 Удалён глобальный реестр бэкендов
Из rill-adrift удалён BACKEND_PTR, set_audio_backend(),
clear_audio_backend(), get_audio_backend(). I/O узлы регистрируются
в фабрике без бэкенда — бэкенд создаётся внутри узла через
init_backend() при десериализации графа (параметр "backend").
⚡ ALSA: poll → event-driven
- Убран
thread::sleep(1000μs)изrun_alsa_thread(). Вместо этого используетсяpcm_playback.wait(None)(snd_pcm_wait()). Тред спит в ядре, просыпается только когда DMA готов. Никакого busy-wait.
🧪 Тесты
test_pull_model_sync_inject_and_verify— интеграционный тест pull model: граф SineOsc → AudioOutput черезGraphDocument,SyncBackendс ручным триггером, верификация данных в output ring.test_alsa_pull_model— ALSA loopback через snd-aloop, проверка xruns после работы pull model.
📝 Документация
- AGENTS.md: раздел Hard-RT safety переписан. Две модели бэкендов
(callback-driven / poll-driven),
thread::sleep()запрещён в RT path. Добавлен Known issues (ALSA/CPAL poll loop). Threading model исправлен — ALSA больше не указан как RT thread. - README.md: таблица версий обновлена (все 0.4.0).
- docs/architecture.md: версии крейтов обновлены (0.3.0 → 0.4.0).
- docs/src/index.md, docs/src/guides/getting-started.md: версии
зависимостей обновлены (
"0.3"→"0.4").
[0.3.2 / 0.3.1 / 0.3.1] — 2026-05-02
🆕 Новые крейты
| Крейт | Версия | Описание |
|---|---|---|
rill-sampler | 0.3.1 | Сэмплер + time-series reader (Source-узлы графа) |
✨ rill-core (0.3.2)
Interpolatetrait — дробно-индексное чтение&[T]с тремя стратегиями:interpolate_linear,interpolate_cubic(Hermite),interpolate_nearest. Blanket impl на[T]гдеT: Transcendental + Copy— работает дляVec<T>,Box<[T]>,[T; N]черезDeref.
✨ rill-core-dsp (0.3.1)
InterpolatedReader<T>— heap-буфер с дробной позицией, rate-ом и wrap-интерполяцией (clamp для семплов, periodic wrap для вейвтейблов). Основа для SamplePlayer и WavetableOscillator.WavetableOscillator<T, N>— переписан наInterpolatedReader. Добавленыset_cubic()/is_cubic(). МетодыGenerator<T>: frequency → rate, phase → normalized position, amplitude → gain.SamplePlayer<T>— воспроизведение буфера с loop-режимами.LoopMode(OneShot / Forward / PingPong), gate-управление, per-sample boundary check. МетодыGenerator<T>: частота отображается в rate, фаза — в normalized позицию.LoopMode— публичный enum для выбора стратегии зацикливания.
✨ rill-oscillators
WavetableOscNode<T, BUF_SIZE, WT_SIZE>— Source-узел графа, обёртка надWavetableOscillator. Параметры:"frequency","amplitude","phase","interpolation"(choice: linear / cubic).
✨ rill-sampler (0.3.1)
SamplePlayerNode<T, BUF_SIZE>— Source-узел для воспроизведения аудиосэмплов. Стерео (два output port — left/right). Параметры, automatable через patchbay:"gate","rate","loop_mode","start","end","amplitude","interpolation","position"(read-only).SampleBuffer<T>— контейнер для загруженных сэмплов с метаданными (sample_rate, channels, name). Mono / stereo deinterleaved.- WAV loading (feature
"wav") — 16-bit PCM, mono/stereo, черезhound. TimeSeriesReader<T>— читатель неравномерных временных рядов. Бинарный поиск поtimestamps→ отображение времени на дробный индекс →Interpolatetrait. Три стратегии: Nearest, Linear, Cubic.TimeSeriesNode<T, BUF_SIZE>— мультиканальный Source-узел (N output ports, по одному на канал). Параметры:"sample_rate"(виртуальная частота),"interpolation","play","speed","position". Заполняет блоки planar:[ch0_s0, ch0_s1, ..., chN_sBUF-1].from_csv()— загрузкаt,channel,value→TimeSeriesReader<f64>. Группировка по каналам, сортировка по времени, пропуск непарсируемых строк.
🏗️ Инфраструктура
rill-samplerдобавлен в workspace иrill-adrift(feature"sampler", включён в default). Обновлёнscripts/publish.sh.
📦 Публикации на crates.io
| Крейт | Версия |
|---|---|
rill-core | 0.3.2 |
rill-core-dsp | 0.3.1 |
rill-sampler | 0.3.1 |
📊 Статистика
| Метрика | Значение |
|---|---|
| Крейтов в workspace | 17 активных |
| Добавлено тестов | +46 |
[0.3.0] — 2026-04-27
🏗️ Фундаментальные изменения
Фреймворк переписан почти с нуля. Единый rill-core вместо россыпи мелких крейтов, новая система очередей и сигналов, модульная архитектура DSP.
Ядро
rill-core— единый крейт ядра: трейты (AudioNode,ParameterId,PortId,Clock), математика (AudioNum, вектора), буферы (кольцевые, FIFO), очереди (CommandQueue<T>,TelemetryQueue), время (ClockTick,SystemClock), макросы- Типобезопасные идентификаторы:
ParameterId(с валидацией),PortId(с типом порта: AudioIn, AudioOut, Control, CV) - Очереди как единый механизм коммуникации: неблокирующие MPMC очереди с политиками переполнения, телеметрия, наблюдатель микро-контроля
- Векторный eDSL — обобщённые математические абстракции над
AudioNumчерез трейтVector, подготовка к SIMD
DSP
rill-core-dsp— единое хранилище DSP-алгоритмов: трейтAlgorithm, фильтры (Biquad, SVF, Butterworth, Chebyshev, Comb, OnePole, MoogLadder), генераторы (Sine, Saw, Square, Triangle, Pulse, Noise, LFO, Envelope, FM), маппинг, сглаживание- Все алгоритмы работают через
process_blockсScalarVector - Векторные макросы (
simple_algorithm!,filter_algorithm!,effect_algorithm!,generator_algorithm!)
Аналоговое моделирование
rill-core-model— WDF-ядро: элементы (R, C, L, диод), адаптеры (последовательный, параллельный), анализ, MoogLadderrill-analog-filters— аналоговые фильтры на WDF (WdfMoogLadder, WdfRcPole)rill-analog-effects— аналоговые эффекты (операционный усилитель, кассетный декастер)
Граф и управление
rill-graph— аудиограф с топологической сортировкой, Source/Processor/Sinkrill-patchbay— мир автоматов: LFO, огибающие, случайные блуждания, сенсоры, серво, маппингrill-router— EQ (графический, параметрический) + микшер (каналы, посылы, мастер)
Обработка
rill-digital-filters— цифровые фильтры как Processor-узлыrill-digital-effects— Delay, Distortion, Limiterrill-oscillators— Sine, Noise, LFO, Envelope как Processor-узлыrill-lofi— lo-fi процессор (bitcrush, downsampling, noise, wow&flutter)
Ввод/вывод
rill-io— аудио-бекенды: NullBackend, CpalBackend, ALSA, PipeWire, JACKrill-telemetry— пробники и коллекторы телеметрииrill-server— OSC-сервер для удалённого управления (UDP, encode/decode, диспетчеризация по паттернам)
🆕 Новые крейты
| Крейт | Описание |
|---|---|
rill-core | Единое ядро (трейты, очереди, математика, макросы) |
rill-core-dsp | DSP-алгоритмы (фильтры, генераторы, векторные операции) |
rill-core-model | WDF-ядро (элементы, адаптеры, анализ) |
rill-patchbay | Автоматы, сенсоры, серво |
rill-router | EQ + микшер |
rill-telemetry | Пробники и коллекторы |
rill-analog-filters | Аналоговые фильтры на WDF |
rill-analog-effects | Аналоговые эффекты |
rill-server | OSC-сервер |
🗑️ Удалённые крейты
| Крейт | Замена |
|---|---|
rill-core-traits | rill-core |
rill-signal | rill-core::queues |
rill-buffers | rill-core::buffer + rill-core-dsp::buffer |
rill-automation | rill-patchbay |
rill-control | rill-patchbay |
rill-eq | rill-router::eq |
rill-mixer | rill-router::mixer |
rill-hp | rill-core-dsp (f64) |
📊 Статистика
| Метрика | Значение |
|---|---|
| Крейтов в workspace | 15 активных |
| Тестов | 300+ |
| Версия | 0.3.0 (единая для всех крейтов) |
[0.2.0] — 2026-02-23
Крупнейший рефакторинг: Единое ядро rill-core
- Создан
rill-core(объединениеrill-core-traits+rill-signal) - Все крейты обновлены до версии 0.2.0
ParameterId(экспериментальный),PortIdвыделен в отдельный модуль- Удалены старые крейты:
rill-core-traits,rill-signal