/
theosov
/
urandom.lain
Обзор
Документация
Войти
/
theosov
/
urandom.lain
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
src/params.rs
363 строки
12 KB
Theosov
v0.1.0
27 июл 2026, 17:39
27 июл 2026, 17:39
bc6cae4
Код
Авторство
О чём код?
//! Shared parameter types and lock-free state used by all engine modules. //! //! This module defines the canonical `GlobalParams`, `LayerParams`, and the //! `SharedState` / `ParamsSnapshot` interface between the main thread, the //! real-time audio callback, and the render thread. All numeric types are kept //! simple (`f32` / `u8`) so they can be copied into the audio callback without //! allocations. //! //! See `docs/specs/02-architecture.md` for the data flow. use std::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, Ordering}; use std::sync::Arc; /// Number of independent layers (audio + visual voices). pub const LAYER_COUNT: usize = 4; /// Maximum number of one-off scene events carried in a single `ParamsSnapshot`. /// Fixed capacity avoids allocations on every snapshot in the main thread. pub const MAX_SCENE_EVENTS: usize = 4; /// Lock-free atomic storage for an `f32` using bit-casting over `AtomicU32`. #[derive(Debug)] pub struct AtomicF32 { inner: AtomicU32, } impl AtomicF32 { /// Create a new `AtomicF32` initialized to `v`. pub fn new(v: f32) -> Self { Self { inner: AtomicU32::new(v.to_bits()), } } /// Store a value with the given memory ordering. pub fn store(&self, v: f32, order: Ordering) { self.inner.store(v.to_bits(), order); } /// Load a value with the given memory ordering. pub fn load(&self, order: Ordering) -> f32 { f32::from_bits(self.inner.load(order)) } } impl Default for AtomicF32 { fn default() -> Self { Self::new(0.0) } } // --------------------------------------------------------------------------- // Parameter structs // --------------------------------------------------------------------------- /// Global scene parameters, produced by the RNG mapper and interpolated by the /// scene engine. #[derive(Debug, Clone, Copy, PartialEq)] pub struct GlobalParams { /// Tempo in BPM. pub tempo: f32, /// Note/trigger density in the range 0..1. pub density: f32, /// Base MIDI note number. pub root: u8, /// Scale / scale set index. pub scale: u8, /// Mode / modality index within the scale. pub mode: u8, /// Scene duration in beats. pub scene_duration: f32, /// How strongly raw entropy influences the output. pub entropy_amount: f32, /// Depth of cross-layer modulation. pub cross_mod: f32, /// Master volume in the range 0..1. pub master_volume: f32, } impl Default for GlobalParams { fn default() -> Self { Self { tempo: 120.0, density: 0.5, root: 48, scale: 0, mode: 0, scene_duration: 8.0, entropy_amount: 0.5, cross_mod: 0.0, master_volume: 0.8, } } } /// Parameters for one layer (one audio + one visual voice). #[derive(Debug, Clone, Copy, Default, PartialEq)] pub struct LayerParams { /// Waveform index (0..15). See `docs/specs/03-rng-mapping.md`. pub waveform: u8, /// Pitch offset in semitones relative to `root`. pub pitch_offset: i8, /// Detune amount in cents. pub detune: f32, /// Filter cutoff frequency in Hz. pub filter_cutoff: f32, /// Filter resonance in the range 0..1. pub filter_resonance: f32, /// ADSR attack time in seconds. pub attack: f32, /// ADSR decay time in seconds. pub decay: f32, /// ADSR sustain level in the range 0..1. pub sustain: f32, /// ADSR release time in seconds. pub release: f32, /// Stereo pan in the range -1..1. pub pan: f32, /// Delay send amount in the range 0..1. pub delay_send: f32, /// Reverb send amount in the range 0..1. pub reverb_send: f32, /// Layer volume in the range 0..1. pub volume: f32, /// LFO rate in Hz. pub lfo_rate: f32, /// LFO depth in the range 0..1. pub lfo_depth: f32, /// Visual hue in the range 0..1. pub visual_hue: f32, /// Visual saturation in the range 0..1. pub visual_saturation: f32, /// Visual brightness/value in the range 0..1. pub visual_brightness: f32, /// Visual shape index (0..15). pub visual_shape: u8, /// Visual animation speed multiplier. pub visual_speed: f32, /// Visual zoom / scale. pub visual_zoom: f32, /// Visual distortion / glitch amount. pub visual_distortion: f32, /// Visual feedback / afterglow amount. pub visual_feedback: f32, } /// A one-off scene event such as a glitch, drop, burst, or stutter. /// /// Kept small and `Copy` so it can live in a fixed-size array inside /// `ParamsSnapshot` without heap allocations. #[derive(Debug, Clone, Copy, Default, PartialEq)] pub struct SceneEvent { /// Event type: 0=none, 1=glitch, 2=drop, 3=burst, 4=stutter. pub type_: u8, /// Target layer index, or 255 for all layers. pub target_layer: u8, /// Event strength in the range 0..1. pub strength: f32, } // --------------------------------------------------------------------------- // Shared state // --------------------------------------------------------------------------- /// Lock-free shared state accessible from the main thread, the real-time audio /// callback, and the render thread via `Arc<SharedState>`. /// /// All fields are atomic so they can be read and updated without mutexes. The /// audio callback is the authoritative source of `sample_clock`; the render /// thread reads it to derive `u_time`. The audio callback also writes the /// computed `audio_level` back so the visual engine can react to it. #[derive(Debug)] pub struct SharedState { /// Application is running. Set to `false` to request shutdown. pub running: AtomicBool, /// Total number of audio samples rendered since the stream started. pub sample_clock: AtomicU64, /// Sample rate of the active audio stream in Hz. Written once by the audio /// engine when the stream is built; the visual engine reads it to derive /// `u_time = sample_clock / sample_rate`. Defaults to 48000 until the /// stream starts. pub sample_rate: AtomicU32, // === Global scene params (atomic) === /// Tempo in BPM. pub tempo: AtomicF32, /// Note/trigger density in the range 0..1. pub density: AtomicF32, /// Base MIDI note number. pub root: AtomicU32, /// Scale / scale set index. pub scale: AtomicU32, /// Mode / modality index. pub mode: AtomicU32, /// Scene duration in beats. pub scene_duration: AtomicF32, /// Strength of raw entropy influence in the range 0..1. pub entropy_amount: AtomicF32, /// Depth of cross-layer modulation in the range 0..1. pub cross_mod: AtomicF32, /// Master volume in the range 0..1. pub master_volume: AtomicF32, // === Derived metrics === /// RMS level of the master bus in the range 0..1, updated by the audio callback. pub audio_level: AtomicF32, /// RMS level of each layer (0..1), updated by the audio callback before sends. pub layer_levels: [AtomicF32; LAYER_COUNT], } impl SharedState { /// Create a new `SharedState` wrapped in `Arc` with sensible defaults. pub fn new() -> Arc<Self> { Arc::new(Self { running: AtomicBool::new(true), sample_clock: AtomicU64::new(0), sample_rate: AtomicU32::new(48_000), tempo: AtomicF32::new(120.0), density: AtomicF32::new(0.5), root: AtomicU32::new(48), scale: AtomicU32::new(0), mode: AtomicU32::new(0), scene_duration: AtomicF32::new(8.0), entropy_amount: AtomicF32::new(0.5), cross_mod: AtomicF32::new(0.0), master_volume: AtomicF32::new(0.8), audio_level: AtomicF32::new(0.0), layer_levels: [ AtomicF32::new(0.0), AtomicF32::new(0.0), AtomicF32::new(0.0), AtomicF32::new(0.0), ], }) } } // --------------------------------------------------------------------------- // Snapshot // --------------------------------------------------------------------------- /// A complete copy of the scene parameters at a single moment in time. /// /// Produced by the scene engine on the main thread, sent to the audio callback /// through a lock-free SPSC ring buffer, and passed directly to the visual /// engine. Because it is owned and `Clone`, it can be consumed without /// blocking any other thread. /// /// Events are stored in a fixed-size array to avoid heap allocations. Only the /// first `event_count` entries are valid; remaining entries are zeroed. #[derive(Debug, Clone, Copy, Default, PartialEq)] pub struct ParamsSnapshot { pub global: GlobalParams, pub layers: [LayerParams; LAYER_COUNT], pub events: [SceneEvent; MAX_SCENE_EVENTS], pub event_count: u8, } impl ParamsSnapshot { /// Update the atomic fields of `SharedState` from this snapshot's global /// parameters. Called by the main thread each frame. pub fn write_to_shared_state(&self, shared: &SharedState) { shared.tempo.store(self.global.tempo, Ordering::Relaxed); shared.density.store(self.global.density, Ordering::Relaxed); shared .root .store(self.global.root as u32, Ordering::Relaxed); shared .scale .store(self.global.scale as u32, Ordering::Relaxed); shared .mode .store(self.global.mode as u32, Ordering::Relaxed); shared .scene_duration .store(self.global.scene_duration, Ordering::Relaxed); shared .entropy_amount .store(self.global.entropy_amount, Ordering::Relaxed); shared .cross_mod .store(self.global.cross_mod, Ordering::Relaxed); shared .master_volume .store(self.global.master_volume, Ordering::Release); } /// Return the active events as a slice. pub fn active_events(&self) -> &[SceneEvent] { let n = self.event_count.min(MAX_SCENE_EVENTS as u8) as usize; &self.events[..n] } } /// Convert a sample-clock position and tempo into elapsed beats. /// /// This helper is shared by the audio sequencer and the visual engine so both /// agree on the beat timeline. #[inline] pub fn beats_at(sample_clock: u64, tempo: f32, sample_rate: f32) -> f32 { (sample_clock as f32 / sample_rate.max(1.0)) * (tempo / 60.0) } #[cfg(test)] mod tests { use super::*; #[test] fn atomic_f32_round_trip() { let a = AtomicF32::new(1.5); assert_eq!(a.load(Ordering::Relaxed), 1.5); a.store(-0.25, Ordering::Relaxed); assert_eq!(a.load(Ordering::Relaxed), -0.25); } #[test] fn shared_state_defaults() { let s = SharedState::new(); assert!(s.running.load(Ordering::Relaxed)); assert_eq!(s.tempo.load(Ordering::Relaxed), 120.0); assert_eq!(s.root.load(Ordering::Relaxed), 48); } #[test] fn snapshot_writes_shared_state() { let shared = SharedState::new(); let mut snap = ParamsSnapshot::default(); snap.global.tempo = 144.0; snap.global.root = 60; snap.write_to_shared_state(&shared); assert_eq!(shared.tempo.load(Ordering::Relaxed), 144.0); assert_eq!(shared.root.load(Ordering::Relaxed), 60); } #[test] fn snapshot_active_events_respects_count() { let mut snap = ParamsSnapshot::default(); snap.events[0] = SceneEvent { type_: 1, target_layer: 0, strength: 0.5, }; snap.events[1] = SceneEvent { type_: 2, target_layer: 1, strength: 0.75, }; snap.event_count = 1; assert_eq!(snap.active_events().len(), 1); assert_eq!(snap.active_events()[0].type_, 1); } #[test] fn beats_at_matches_beat_timeline() { // 120 BPM at 48 kHz: 1 beat = 0.5 s = 24_000 samples. assert_eq!(beats_at(24_000, 120.0, 48_000.0), 1.0); // 60 BPM at 48 kHz: 1 beat = 1 s = 48_000 samples. assert_eq!(beats_at(48_000, 60.0, 48_000.0), 1.0); } }