use std::error::Error;
use std::f32::consts::TAU;
use std::net::SocketAddr;
use std::sync::Arc;
use std::sync::atomic::{AtomicU32, AtomicU64, Ordering};
use std::time::Instant;
use arc_swap::ArcSwap;
use rand::rngs::StdRng;
use rand::{Rng, SeedableRng};
#[cfg(test)]
use ratatui::Terminal;
use ratatui::{
Frame,
buffer::Buffer,
layout::{Alignment, Constraint, Direction, Layout, Rect},
style::{Color, Modifier, Style},
text::{Line, Span},
widgets::{Block, Borders, Padding, Paragraph, Widget},
};
use crate::audio::{self, StereoEngine};
use crate::fx::lfo::DriftingLfo;
use crate::fx::panner::StereoPanner;
use crate::midi::{MidiConfig, MidiInputManager, MidiOutputManager};
use crate::synth::envelope::Adsr;
use crate::synth::fm::{FmPair, FmStack, FmWave};
use crate::synth::noise::WhiteNoise;
use crate::synth::oscillator::SineOscillator;
use crate::update_check::{UpdateNotice, spawn_update_check};
mod auto;
mod automation;
mod capture;
mod controls;
mod coordinator;
mod edit;
mod effect;
mod engine;
mod gesture;
mod interaction;
mod mix_action;
mod module;
mod osc;
mod palette;
mod range_epoch;
mod recipe;
use capture::*;
mod registry;
#[cfg(test)]
mod replay;
mod runtime;
mod session;
mod song;
mod song_ids;
mod ui;
mod view;
mod visualizer;
mod voice;
mod widget;
#[cfg(test)]
mod tests;
#[cfg(test)]
mod gesture_audio_tests;
#[cfg(test)]
mod gesture_level_probe;
pub(crate) use auto::{
AutoControls, DEFAULT_AUTO_BARS, MorphPosition, MorphState, MorphWriter, decode_auto_states,
no_morph,
};
use automation::*;
use controls::*;
use coordinator::*;
use edit::*;
use effect::*;
use engine::*;
pub(crate) use engine::{TimingContext, Transport};
use gesture::*;
use module::*;
use palette::*;
use registry::*;
use session::*;
pub(crate) use song::{CODE_PREFIX, SongState, decode_song_code, encode_song_code};
use ui::*;
use view::*;
use visualizer::*;
pub(crate) use voice::pad_voicing;
use voice::*;
pub(crate) fn smoothstep(t: f32) -> f32 {
let t = t.clamp(0.0, 1.0);
t * t * (3.0 - 2.0 * t)
}
pub(crate) fn splitmix64_mix(mut z: u64) -> u64 {
z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
z ^ (z >> 31)
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum MusicalHit {
Beat,
Pad,
Perc,
Kick,
Tonal,
Clap,
Arp,
}
impl MusicalHit {
pub(crate) const ALL: [Self; 7] = [
Self::Beat,
Self::Pad,
Self::Perc,
Self::Kick,
Self::Tonal,
Self::Clap,
Self::Arp,
];
}
pub(crate) const MUSICAL_HIT_COUNT: usize = MusicalHit::ALL.len();
pub(crate) const NO_PITCH_CLASS: f32 = -1.0;
pub(crate) struct HitTelemetry {
pub(crate) pulse: AtomicU64,
pub(crate) level_bits: AtomicU32,
pub(crate) pitch_class_bits: AtomicU32,
}
impl Default for HitTelemetry {
fn default() -> Self {
Self {
pulse: AtomicU64::new(0),
level_bits: AtomicU32::new(0.0f32.to_bits()),
pitch_class_bits: AtomicU32::new(NO_PITCH_CLASS.to_bits()),
}
}
}
pub(crate) struct FluidTelemetry {
pub(crate) chord_slot: AtomicU64,
pub(crate) kick_pulse: AtomicU64,
pub(crate) beat_bits: AtomicU64,
pub(crate) kick_level_bits: AtomicU32,
pub(crate) hits: [HitTelemetry; MUSICAL_HIT_COUNT],
pub(crate) chord_attack_bits: AtomicU32,
pub(crate) chord_release_bits: AtomicU32,
pub(crate) chord_root_pitch_class_bits: AtomicU32,
pub(crate) level_bits: [AtomicU32; TAB_COUNT],
pub(crate) gesture_bits: [AtomicU32; GESTURE_COUNT],
}
impl Default for FluidTelemetry {
fn default() -> Self {
Self {
chord_slot: AtomicU64::new(0),
kick_pulse: AtomicU64::new(0),
beat_bits: AtomicU64::new(0.0f64.to_bits()),
kick_level_bits: AtomicU32::new(0.0f32.to_bits()),
hits: std::array::from_fn(|_| HitTelemetry::default()),
chord_attack_bits: AtomicU32::new(0.0f32.to_bits()),
chord_release_bits: AtomicU32::new(0.0f32.to_bits()),
chord_root_pitch_class_bits: AtomicU32::new(0.0f32.to_bits()),
level_bits: std::array::from_fn(|_| AtomicU32::new(0.0f32.to_bits())),
gesture_bits: std::array::from_fn(|_| AtomicU32::new(0.0f32.to_bits())),
}
}
}
pub(crate) const LEVEL_BLOCK: u64 = 256;
impl FluidTelemetry {
pub(crate) fn publish_beat(&self, beat: f64) {
self.beat_bits.store(beat.to_bits(), Ordering::Relaxed);
}
pub(crate) fn beat(&self) -> f64 {
f64::from_bits(self.beat_bits.load(Ordering::Relaxed))
}
pub(crate) fn publish_kick(&self, level: f32) {
self.kick_level_bits
.store(level.to_bits(), Ordering::Relaxed);
self.kick_pulse.fetch_add(1, Ordering::Release);
self.publish_hit(MusicalHit::Kick, level, NO_PITCH_CLASS);
}
pub(crate) fn publish_hit(&self, hit: MusicalHit, level: f32, pitch_class: f32) {
let telemetry = &self.hits[hit as usize];
telemetry
.level_bits
.store(level.to_bits(), Ordering::Relaxed);
telemetry
.pitch_class_bits
.store(pitch_class.to_bits(), Ordering::Relaxed);
telemetry.pulse.fetch_add(1, Ordering::Release);
}
pub(crate) fn publish_chord(
&self,
slot: u64,
root_pitch_class: f32,
attack_secs: f32,
release_secs: f32,
) {
self.chord_attack_bits
.store(attack_secs.to_bits(), Ordering::Relaxed);
self.chord_release_bits
.store(release_secs.to_bits(), Ordering::Relaxed);
self.chord_root_pitch_class_bits
.store(root_pitch_class.to_bits(), Ordering::Relaxed);
self.chord_slot.store(slot, Ordering::Release);
}
pub(crate) fn publish_level(&self, tab: Tab, rms: f32) {
self.level_bits[tab as usize].store(rms.to_bits(), Ordering::Relaxed);
}
pub(crate) fn publish_gesture(&self, kind: GestureKind, amount: f32) {
self.gesture_bits[kind as usize].store(amount.to_bits(), Ordering::Relaxed);
}
#[cfg(test)]
pub(crate) fn gesture(&self, kind: GestureKind) -> f32 {
f32::from_bits(self.gesture_bits[kind as usize].load(Ordering::Relaxed))
}
#[cfg(test)]
pub(crate) fn level(&self, tab: Tab) -> f32 {
f32::from_bits(self.level_bits[tab as usize].load(Ordering::Relaxed))
}
}
const APP_ID: &str = "nooise";
pub(crate) const DEFAULT_OSC_TARGET: &str = "127.0.0.1:9000";
pub(crate) fn run(osc: Option<SocketAddr>, midi: MidiConfig<'_>) -> Result<(), Box<dyn Error>> {
let mut rng = rand::thread_rng();
run_with_song_state(
randomized_start_song(&mut rng, midi.input.is_some() || midi.output.is_some()),
osc,
midi,
)
}
fn randomized_start_song(rng: &mut impl Rng, midi_connected: bool) -> SongState {
let mut controls = FluidControls::default();
controls.pad.progression = rng.gen_range(0..PROGRESSIONS.len()) as f32;
controls.tonal.phrase = rng.gen_range(0..TONAL_PHRASES.len()) as f32;
if midi_connected {
controls.pad.level = 0.0;
}
SongState::from_controls(controls)
}
fn apply_midi_start(song: &mut SongState, midi: MidiConfig<'_>) {
if midi.input.is_some() || midi.output.is_some() {
song.controls.pad.level = 0.0;
}
if midi.output.is_some() {
song.controls.pad.midi_in = 0.0;
song.controls.pad.midi_out = 1.0;
song.controls.midi_rows |= PAD_MIDI_OUT_ROW;
} else if midi.input.is_some() {
song.controls.pad.midi_in = 1.0;
song.controls.pad.midi_out = 0.0;
song.controls.midi_rows |= PAD_MIDI_IN_ROW;
}
}
fn apply_midi_start_to_states(states: &mut [SongState], midi: MidiConfig<'_>) {
for state in states {
apply_midi_start(state, midi);
}
}
pub(crate) fn run_with_song_state(
initial_song: SongState,
osc: Option<SocketAddr>,
midi: MidiConfig<'_>,
) -> Result<(), Box<dyn Error>> {
let auto_states = decode_auto_states();
run_interactive(
initial_song,
no_morph(),
auto_states,
DEFAULT_AUTO_BARS,
osc,
midi,
)
}
pub(crate) fn run_auto(
bars: u32,
osc: Option<SocketAddr>,
midi: MidiConfig<'_>,
) -> Result<(), Box<dyn Error>> {
let mut states = decode_auto_states();
apply_midi_start_to_states(&mut states, midi);
let initial_song = states[0].clone();
let morph = Arc::new(ArcSwap::from_pointee(Some(MorphState::new(
states.clone(),
bars,
))));
run_interactive(initial_song, morph, states, bars, osc, midi)
}
pub(crate) fn run_songs(
numbers: &[usize],
bars: u32,
osc: Option<SocketAddr>,
midi: MidiConfig<'_>,
) -> Result<(), Box<dyn Error>> {
let all = decode_auto_states();
if numbers.is_empty() {
return Err("expected at least one song".into());
}
let mut chosen = Vec::with_capacity(numbers.len());
for &number in numbers {
match all.get(number.wrapping_sub(1)) {
Some(song) if number >= 1 => chosen.push(song.clone()),
_ => {
return Err(format!(
"there is no song {number}; the built-ins are 1..={}",
all.len()
)
.into());
}
}
}
apply_midi_start_to_states(&mut chosen, midi);
let initial_song = chosen[0].clone();
let morph = Arc::new(ArcSwap::from_pointee(Some(MorphState::labelled(
chosen.clone(),
numbers.to_vec(),
bars,
))));
run_interactive(initial_song, morph, chosen, bars, osc, midi)
}
fn run_interactive(
mut initial_song: SongState,
morph: Arc<ArcSwap<Option<MorphState>>>,
mut auto_states: Vec<SongState>,
auto_bars: u32,
osc: Option<SocketAddr>,
midi: MidiConfig<'_>,
) -> Result<(), Box<dyn Error>> {
apply_midi_start(&mut initial_song, midi);
apply_midi_start_to_states(&mut auto_states, midi);
let session = LiveSession::new(LiveSessionSnapshot::from_song(&initial_song));
let session_for_engine = session.clone();
let morph_for_engine = Arc::clone(&morph);
let telemetry = Arc::new(FluidTelemetry::default());
let telemetry_for_engine = Arc::clone(&telemetry);
let updates = UpdateNotice::default();
spawn_update_check(updates.clone());
let _osc_emitter = osc
.map(|target| osc::OscEmitter::spawn(target, Arc::clone(&telemetry)))
.transpose()?;
let _midi_input = midi.input.map(MidiInputManager::open).transpose()?;
let midi_for_input = _midi_input.as_ref().map(MidiInputManager::source);
let _midi_output = midi.output.map(MidiOutputManager::open).transpose()?;
let midi_for_engine = _midi_output.as_ref().map(MidiOutputManager::sink);
let _audio_output = audio::start_stream(APP_ID, move |sr| {
let engine = FluidEngine::new_with_tonal_session_state(
sr,
session_for_engine.clone(),
Arc::clone(&morph_for_engine),
Arc::clone(&telemetry_for_engine),
true,
);
let engine = match &midi_for_engine {
Some(sink) => engine.with_midi(sink.clone()),
None => engine,
};
match &midi_for_input {
Some(source) => engine.with_midi_input(source.clone()),
None => engine,
}
})?;
let mut terminal = runtime::TerminalSession::enter()?;
let result = production_ui_loop(
&mut terminal,
UiSession {
live: session.clone(),
},
telemetry,
updates,
AutoControls::new(morph, auto_states, auto_bars),
);
let restore = terminal.restore();
result?;
restore?;
Ok(())
}
pub(crate) fn render_wav(
seconds: f32,
out: &std::path::Path,
seed: Option<u64>,
) -> Result<(), Box<dyn Error>> {
const RENDER_SAMPLE_RATE: u32 = 44_100;
let song = SongState::from_controls(FluidControls::default());
let session = LiveSession::new(LiveSessionSnapshot::from_song(&song));
let telemetry = Arc::new(FluidTelemetry::default());
let mut engine = FluidEngine::new(RENDER_SAMPLE_RATE as f32, session, no_morph(), telemetry);
if let Some(seed) = seed {
engine.reseed(seed);
}
let spec = hound::WavSpec {
channels: 2,
sample_rate: RENDER_SAMPLE_RATE,
bits_per_sample: 16,
sample_format: hound::SampleFormat::Int,
};
let mut writer = hound::WavWriter::create(out, spec)
.map_err(|e| format!("failed to create {}: {e}", out.display()))?;
let total_frames = (seconds * RENDER_SAMPLE_RATE as f32) as u64;
for _ in 0..total_frames {
let (left, right) = engine.next_stereo();
writer.write_sample((left.clamp(-1.0, 1.0) * i16::MAX as f32) as i16)?;
writer.write_sample((right.clamp(-1.0, 1.0) * i16::MAX as f32) as i16)?;
}
writer
.finalize()
.map_err(|e| format!("failed to finalize {}: {e}", out.display()))?;
println!(
"rendered {seconds} s ({total_frames} frames) at {RENDER_SAMPLE_RATE} Hz to {}",
out.display()
);
Ok(())
}