From 6a98e516916459b26e95ffd37a8d6ca8bc2e93b0 Mon Sep 17 00:00:00 2001 From: Mike Tsao Date: Sun, 24 Sep 2023 11:47:33 -0700 Subject: [PATCH] tests pass --- Cargo.toml | 2 + audio/src/lib.rs | 7 +- core/Cargo.toml | 2 +- core/src/control.rs | 4 +- core/src/generators.rs | 1947 ---------------------- core/src/lib.rs | 156 +- core/src/midi.rs | 56 +- core/src/time.rs | 38 +- core/src/voices.rs | 6 +- egui/src/lib.rs | 49 +- entities/Cargo.toml | 2 +- entities/src/controllers/arpeggiator.rs | 169 -- entities/src/controllers/calculator.rs | 6 +- entities/src/controllers/control_trip.rs | 2 +- entities/src/controllers/lfo.rs | 159 -- entities/src/controllers/mod.rs | 492 +----- entities/src/controllers/patterns.rs | 735 -------- entities/src/controllers/sequencers.rs | 719 -------- entities/src/effects/bitcrusher.rs | 2 +- entities/src/effects/chorus.rs | 83 - entities/src/effects/compressor.rs | 8 +- entities/src/effects/delay.rs | 355 ---- entities/src/effects/filter.rs | 1229 -------------- entities/src/effects/gain.rs | 75 - entities/src/effects/limiter.rs | 5 +- entities/src/effects/mixer.rs | 2 +- entities/src/effects/mod.rs | 18 - entities/src/effects/reverb.rs | 206 --- entities/src/instruments/drumkit.rs | 2 +- entities/src/instruments/fm.rs | 16 +- entities/src/instruments/sampler.rs | 2 +- entities/src/instruments/welsh.rs | 18 +- entities/src/lib.rs | 2 +- examples/explorer.rs | 37 +- examples/minicli.rs | 2 +- examples/minidaw.rs | 18 +- midi/src/lib.rs | 4 +- orchestration/Cargo.toml | 2 +- orchestration/src/entities.rs | 202 +-- orchestration/src/helpers.rs | 2 +- orchestration/src/lib.rs | 4 +- orchestration/src/messages.rs | 2 +- proc-macros/src/params.rs | 8 +- src/bin/groove-cli.rs | 2 +- src/bin/groove-egui.rs | 2 +- src/lib.rs | 68 +- src/mini/bus_station.rs | 5 +- src/mini/control_atlas.rs | 615 ------- src/mini/control_router.rs | 239 --- src/mini/drag_drop.rs | 197 --- src/mini/entities.rs | 148 -- src/mini/entity_factory.rs | 1 + src/mini/even_smaller_sequencer.rs | 314 ---- src/mini/humidifier.rs | 118 -- src/mini/midi_router.rs | 286 ---- src/mini/mod.rs | 67 - src/mini/orchestrator.rs | 1 + src/mini/piano_roll.rs | 1046 ------------ src/mini/selection_set.rs | 144 -- src/mini/sequencer.rs | 822 --------- src/mini/track.rs | 1007 ----------- src/mini/transport.rs | 4 +- src/mini/widgets/audio.rs | 227 --- src/mini/widgets/control.rs | 274 --- src/mini/widgets/controllers.rs | 117 -- src/mini/widgets/core.rs | 46 - src/mini/widgets/mod.rs | 38 - src/mini/widgets/pattern.rs | 279 ---- src/mini/widgets/placeholder.rs | 72 - src/mini/widgets/timeline.rs | 437 ----- src/mini/widgets/track.rs | 63 - src/panels/audio_panel.rs | 7 +- src/panels/control_panel.rs | 3 +- src/panels/legacy/audio_panel.rs | 2 +- src/panels/legacy/thing_browser.rs | 3 +- src/panels/midi_panel.rs | 2 +- src/panels/orchestrator_panel.rs | 21 +- src/panels/palette_panel.rs | 12 +- tests/automation.rs | 169 -- tests/aux_bus.rs | 100 -- tests/entity_validator.rs | 132 -- tests/integration.rs | 107 -- tests/sidechain.rs | 106 -- tests/song_editing.rs | 128 -- tests/song_programming.rs | 120 -- toys/Cargo.toml | 2 +- toys/src/controllers.rs | 62 - toys/src/effects.rs | 2 +- toys/src/instruments.rs | 522 +----- toys/src/lib.rs | 7 +- 90 files changed, 246 insertions(+), 14755 deletions(-) delete mode 100644 core/src/generators.rs delete mode 100644 entities/src/controllers/arpeggiator.rs delete mode 100644 entities/src/controllers/patterns.rs delete mode 100644 entities/src/controllers/sequencers.rs delete mode 100644 entities/src/effects/chorus.rs delete mode 100644 entities/src/effects/delay.rs delete mode 100644 entities/src/effects/filter.rs delete mode 100644 entities/src/effects/gain.rs delete mode 100644 entities/src/effects/reverb.rs delete mode 100644 src/mini/control_atlas.rs delete mode 100644 src/mini/control_router.rs delete mode 100644 src/mini/drag_drop.rs delete mode 100644 src/mini/entities.rs delete mode 100644 src/mini/even_smaller_sequencer.rs delete mode 100644 src/mini/humidifier.rs delete mode 100644 src/mini/midi_router.rs delete mode 100644 src/mini/piano_roll.rs delete mode 100644 src/mini/selection_set.rs delete mode 100644 src/mini/sequencer.rs delete mode 100644 src/mini/track.rs delete mode 100644 src/mini/widgets/audio.rs delete mode 100644 src/mini/widgets/control.rs delete mode 100644 src/mini/widgets/controllers.rs delete mode 100644 src/mini/widgets/core.rs delete mode 100644 src/mini/widgets/mod.rs delete mode 100644 src/mini/widgets/pattern.rs delete mode 100644 src/mini/widgets/placeholder.rs delete mode 100644 src/mini/widgets/timeline.rs delete mode 100644 src/mini/widgets/track.rs delete mode 100644 tests/automation.rs delete mode 100644 tests/aux_bus.rs delete mode 100644 tests/entity_validator.rs delete mode 100644 tests/integration.rs delete mode 100644 tests/sidechain.rs delete mode 100644 tests/song_editing.rs delete mode 100644 tests/song_programming.rs delete mode 100644 toys/src/controllers.rs diff --git a/Cargo.toml b/Cargo.toml index 9783e17f..7e95cbbf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,6 +25,8 @@ eframe = { version = "0.22" } egui-toast = { version = "0.8" } egui_extras = { version = "0.22" } ensnare = { path = "../ensnare" } +ensnare-core = { path = "../ensnare/core" } +ensnare-not-core = { path = "../ensnare/not-core" } ensnare-midi-interface = { path = "../ensnare/midi-interface" } ensnare-proc-macros = { path = "../ensnare/proc-macros" } enum-primitive-derive = "0.2" diff --git a/audio/src/lib.rs b/audio/src/lib.rs index b64e8d9b..5a1e1063 100644 --- a/audio/src/lib.rs +++ b/audio/src/lib.rs @@ -9,7 +9,7 @@ use cpal::{ }; use crossbeam::queue::ArrayQueue; use crossbeam_channel::{unbounded, Receiver, Sender}; -use ensnare::prelude::*; +use ensnare::{prelude::*, AudioQueue}; use std::{fmt::Debug, result::Result::Ok, sync::Arc, thread::JoinHandle, time::Instant}; pub enum AudioInterfaceInput { @@ -35,11 +35,6 @@ pub enum AudioInterfaceEvent { Quit, } -/// The producer-consumer queue of stereo samples that the audio stream consumes. -// -// TODO: why isn't this a ring buffer? -pub type AudioQueue = Arc>; - #[derive(Debug)] pub struct AudioStreamService { input_sender: Sender, diff --git a/core/Cargo.toml b/core/Cargo.toml index 25f3ea7b..5c99251e 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -11,7 +11,7 @@ anyhow = "1.0" bit-vec = "0.6" derive_more = "0.99" eframe = { version = "0.22" } -ensnare = { path = "../../ensnare" } +ensnare-core = { path = "../../ensnare/core" } enum-primitive-derive = "0.2" float-cmp = "0.9" ensnare-proc-macros = { path = "../../ensnare/proc-macros" } diff --git a/core/src/control.rs b/core/src/control.rs index 2209e834..61d210dc 100644 --- a/core/src/control.rs +++ b/core/src/control.rs @@ -1,8 +1,6 @@ // Copyright (c) 2023 Mike Tsao. All rights reserved. -use crate::{BipolarNormal, Normal}; -use ensnare::prelude::*; - +use ensnare_core::prelude::*; use serde::{Deserialize, Serialize}; #[derive(Debug, Serialize, Deserialize)] diff --git a/core/src/generators.rs b/core/src/generators.rs deleted file mode 100644 index c6286ca8..00000000 --- a/core/src/generators.rs +++ /dev/null @@ -1,1947 +0,0 @@ -// Copyright (c) 2023 Mike Tsao. All rights reserved. - -use crate::time::{ClockTimeUnit, Seconds}; -use eframe::{ - egui::{ComboBox, DragValue, Frame, Sense, Ui}, - emath, - epaint::{pos2, Color32, PathShape, Pos2, Rect, Shape, Stroke, Vec2}, -}; -use ensnare::{ - prelude::*, - traits::{prelude::*, GeneratesEnvelope}, -}; -use ensnare_proc_macros::{Control, Params}; -use kahan::KahanSum; -use more_asserts::{debug_assert_ge, debug_assert_le}; -use nalgebra::{Matrix3, Matrix3x1}; -use serde::{Deserialize, Serialize}; -use std::{f64::consts::PI, fmt::Debug, ops::Range}; -use strum::EnumCount; -use strum::IntoEnumIterator; -use strum_macros::{Display, EnumCount as EnumCountMacro, EnumIter, FromRepr, IntoStaticStr}; - -#[derive(Clone, Copy, Debug, Default)] -enum State { - #[default] - Idle, - Attack, - Decay, - Sustain, - Release, - Shutdown, -} - -impl EnvelopeParams { - pub fn new_with(attack: Normal, decay: Normal, sustain: Normal, release: Normal) -> Self { - Self { - attack, - decay, - sustain, - release, - } - } - - // The #[control] #[params] macro system doesn't currently let us override derived - // Default, and I wasn't sure whether it was right to default Normal to 1.0, - // so I'm creating a custom default method. I think that only test/toy code - // would rely on defaults for an envelope. - pub fn safe_default() -> Self { - Self::new_with(0.0.into(), 0.0.into(), 1.0.into(), 0.0.into()) - } -} - -#[derive(Debug, Default, Control, Params, Serialize, Deserialize)] -pub struct Envelope { - #[control] - #[params] - attack: Normal, - #[control] - #[params] - decay: Normal, - #[control] - #[params] - sustain: Normal, - #[control] - #[params] - release: Normal, - - #[serde(skip)] - sample_rate: SampleRate, - #[serde(skip)] - state: State, - #[serde(skip)] - was_reset: bool, - - #[serde(skip)] - ticks: usize, - #[serde(skip)] - time: Seconds, - - #[serde(skip)] - uncorrected_amplitude: KahanSum, - #[serde(skip)] - corrected_amplitude: f64, - #[serde(skip)] - delta: f64, - #[serde(skip)] - amplitude_target: f64, - #[serde(skip)] - time_target: Seconds, - - // Whether the amplitude was set to an explicit value during this frame, - // which means that the caller is expecting to get an amplitude of that - // exact value, which means that we should return the PRE-update value - // rather than the usual post-update value. - #[serde(skip)] - amplitude_was_set: bool, - - // Polynomial coefficients for convex - #[serde(skip)] - convex_a: f64, - #[serde(skip)] - convex_b: f64, - #[serde(skip)] - convex_c: f64, - - // Polynomial coefficients for concave - #[serde(skip)] - concave_a: f64, - #[serde(skip)] - concave_b: f64, - #[serde(skip)] - concave_c: f64, -} -impl GeneratesEnvelope for Envelope { - fn trigger_attack(&mut self) { - self.set_state(State::Attack); - } - fn trigger_release(&mut self) { - self.set_state(State::Release); - } - fn trigger_shutdown(&mut self) { - self.set_state(State::Shutdown); - } - fn is_idle(&self) -> bool { - matches!(self.state, State::Idle) - } -} -impl Generates for Envelope { - fn value(&self) -> Normal { - Normal::new(self.corrected_amplitude) - } - - fn generate_batch_values(&mut self, values: &mut [Normal]) { - // TODO: this is probably no more efficient than calling amplitude() - // individually, but for now we're just getting the interface right. - // Later we'll take advantage of it. - for v in values { - self.tick(1); - *v = self.value(); - } - } -} -impl Configurable for Envelope { - fn sample_rate(&self) -> SampleRate { - self.sample_rate - } - - fn update_sample_rate(&mut self, sample_rate: SampleRate) { - self.sample_rate = sample_rate; - self.was_reset = true; - } -} -impl Ticks for Envelope { - fn tick(&mut self, tick_count: usize) { - // TODO: same comment as above about not yet taking advantage of - // batching - for _ in 0..tick_count { - let pre_update_amplitude = self.uncorrected_amplitude.sum(); - if self.was_reset { - self.was_reset = false; - } else { - self.ticks += 1; - self.update_amplitude(); - } - self.time = Seconds(self.ticks as f64 / self.sample_rate.value() as f64); - - self.handle_state(); - - let linear_amplitude = if self.amplitude_was_set { - self.amplitude_was_set = false; - pre_update_amplitude - } else { - self.uncorrected_amplitude.sum() - }; - self.corrected_amplitude = match self.state { - State::Attack => self.transform_linear_to_convex(linear_amplitude), - State::Decay | State::Release => self.transform_linear_to_concave(linear_amplitude), - _ => linear_amplitude, - }; - } - } -} -impl Envelope { - pub const MIN_SECONDS: f64 = 0.0; - pub const MAX_SECONDS: f64 = 30.0; - - pub fn new_with(params: &EnvelopeParams) -> Self { - Self { - attack: params.attack(), - decay: params.decay(), - sustain: params.sustain(), - release: params.release(), - sample_rate: Default::default(), - state: State::Idle, - was_reset: true, - ticks: Default::default(), - time: Default::default(), - uncorrected_amplitude: Default::default(), - corrected_amplitude: 0.0, - delta: Default::default(), - amplitude_target: Default::default(), - time_target: Default::default(), - amplitude_was_set: Default::default(), - convex_a: Default::default(), - convex_b: Default::default(), - convex_c: Default::default(), - concave_a: Default::default(), - concave_b: Default::default(), - concave_c: Default::default(), - } - } - - pub fn from_seconds_to_normal(seconds: Seconds) -> Normal { - Normal::new(seconds.0 / Self::MAX_SECONDS) - } - - pub fn from_normal_to_seconds(normal: Normal) -> Seconds { - Seconds(normal.0 * Self::MAX_SECONDS) - } - - fn update_amplitude(&mut self) { - self.uncorrected_amplitude += self.delta; - } - - fn handle_state(&mut self) { - let (next_state, awaiting_target) = match self.state { - State::Idle => (State::Idle, false), - State::Attack => (State::Decay, true), - State::Decay => (State::Sustain, true), - State::Sustain => (State::Sustain, false), - State::Release => (State::Idle, true), - State::Shutdown => (State::Idle, true), - }; - if awaiting_target && self.has_reached_target() { - self.set_state(next_state); - } - } - - fn has_reached_target(&mut self) -> bool { - #[allow(clippy::if_same_then_else)] - let has_hit_target = if self.delta == 0.0 { - // This is probably a degenerate case, but we don't want to be stuck - // forever in the current state. - true - } else if self.time_target.0 != 0.0 && self.time >= self.time_target { - // If we have a time target and we've hit it, then we're done even - // if the amplitude isn't quite there yet. - true - } else { - // Is the difference between the current value and the target - // smaller than the delta? This is a fancy way of saying we're as - // close as we're going to get without overshooting the next time. - (self.uncorrected_amplitude.sum() - self.amplitude_target).abs() < self.delta.abs() - }; - - if has_hit_target { - // Set to the exact amplitude target in case of precision errors. We - // don't want to set self.amplitude_was_set here because this is - // happening after the update, so we'll already be returning the - // amplitude snapshotted at the right time. - self.uncorrected_amplitude = KahanSum::new_with_value(self.amplitude_target); - } - has_hit_target - } - - // For all the set_state_() methods, we assume that the prior state actually - // happened, and that the amplitude is set to a reasonable value. This - // matters, for example, if attack is zero and decay is non-zero. If we jump - // straight from idle to decay, then decay is decaying from the idle - // amplitude of zero, which is wrong. - fn set_state(&mut self, new_state: State) { - match new_state { - State::Idle => { - self.state = State::Idle; - self.uncorrected_amplitude = Default::default(); - self.delta = 0.0; - } - State::Attack => { - if self.attack == Normal::minimum() { - self.set_explicit_amplitude(Normal::maximum()); - self.set_state(State::Decay); - } else { - self.state = State::Attack; - let target_amplitude = Normal::maximum().value(); - self.set_target(Normal::maximum(), self.attack, false, false); - let current_amplitude = self.uncorrected_amplitude.sum(); - - (self.convex_a, self.convex_b, self.convex_c) = Self::calculate_coefficients( - current_amplitude, - current_amplitude, - (target_amplitude - current_amplitude) / 2.0 + current_amplitude, - (target_amplitude - current_amplitude) / 1.5 + current_amplitude, - target_amplitude, - target_amplitude, - ); - } - } - State::Decay => { - if self.decay == Normal::minimum() { - self.set_explicit_amplitude(self.sustain); - self.set_state(State::Sustain); - } else { - self.state = State::Decay; - let target_amplitude = self.sustain.value(); - self.set_target(self.sustain, self.decay, true, false); - let current_amplitude = self.uncorrected_amplitude.sum(); - (self.concave_a, self.concave_b, self.concave_c) = Self::calculate_coefficients( - current_amplitude, - current_amplitude, - (current_amplitude - target_amplitude) / 2.0 + target_amplitude, - (current_amplitude - target_amplitude) / 3.0 + target_amplitude, - target_amplitude, - target_amplitude, - ); - } - } - State::Sustain => { - self.state = State::Sustain; - self.set_target(self.sustain, Normal::maximum(), false, false); - } - State::Release => { - if self.release == Normal::minimum() { - self.set_explicit_amplitude(Normal::maximum()); - self.set_state(State::Idle); - } else { - self.state = State::Release; - let target_amplitude = 0.0; - self.set_target(Normal::minimum(), self.release, true, false); - let current_amplitude = self.uncorrected_amplitude.sum(); - (self.concave_a, self.concave_b, self.concave_c) = Self::calculate_coefficients( - current_amplitude, - current_amplitude, - (current_amplitude - target_amplitude) / 2.0 + target_amplitude, - (current_amplitude - target_amplitude) / 3.0 + target_amplitude, - target_amplitude, - target_amplitude, - ); - } - } - State::Shutdown => { - self.state = State::Shutdown; - self.set_target( - Normal::minimum(), - Envelope::from_seconds_to_normal(Seconds(1.0 / 1000.0)), - false, - true, - ); - } - } - } - - fn set_explicit_amplitude(&mut self, amplitude: Normal) { - self.uncorrected_amplitude = KahanSum::new_with_value(amplitude.value()); - self.amplitude_was_set = true; - } - - fn set_target( - &mut self, - target_amplitude: Normal, - duration: Normal, - calculate_for_full_amplitude_range: bool, - fast_reaction: bool, - ) { - self.amplitude_target = target_amplitude.into(); - if duration != Normal::maximum() { - let fast_reaction_extra_frame = if fast_reaction { 1.0 } else { 0.0 }; - let range = if calculate_for_full_amplitude_range { - -1.0 - } else { - self.amplitude_target - self.uncorrected_amplitude.sum() - }; - let duration_seconds = Self::from_normal_to_seconds(duration); - self.time_target = self.time + duration_seconds; - self.delta = if duration != Normal::minimum() { - range - / (duration_seconds.0 * self.sample_rate.value() as f64 - + fast_reaction_extra_frame) - } else { - 0.0 - }; - if fast_reaction { - self.uncorrected_amplitude += self.delta; - } - } else { - self.time_target = Seconds::infinite(); - self.delta = 0.0; - } - } - - fn calculate_coefficients( - x0: f64, - y0: f64, - x1: f64, - y1: f64, - x2: f64, - y2: f64, - ) -> (f64, f64, f64) { - if x0 == x1 && x1 == x2 && y0 == y1 && y1 == y2 { - // The curve we're asking about is actually just a point. Return an - // identity. - return (0.0, 1.0, 0.0); - } - let m = Matrix3::new( - 1.0, - x0, - x0.powi(2), - 1.0, - x1, - x1.powi(2), - 1.0, - x2, - x2.powi(2), - ); - let y = Matrix3x1::new(y0, y1, y2); - let r = m.try_inverse(); - if let Some(r) = r { - let abc = r * y; - (abc[0], abc[1], abc[2]) - } else { - (0.0, 0.0, 0.0) - } - } - - fn transform_linear_to_convex(&self, linear_value: f64) -> f64 { - self.convex_c * linear_value.powi(2) + self.convex_b * linear_value + self.convex_a - } - fn transform_linear_to_concave(&self, linear_value: f64) -> f64 { - self.concave_c * linear_value.powi(2) + self.concave_b * linear_value + self.concave_a - } - - pub fn attack(&self) -> Normal { - self.attack - } - - pub fn decay(&self) -> Normal { - self.decay - } - - pub fn sustain(&self) -> Normal { - self.sustain - } - - pub fn release(&self) -> Normal { - self.release - } - - pub fn set_attack(&mut self, attack: Normal) { - self.attack = attack; - } - - pub fn set_decay(&mut self, decay: Normal) { - self.decay = decay; - } - - pub fn set_sustain(&mut self, sustain: Normal) { - self.sustain = sustain; - } - - pub fn set_release(&mut self, release: Normal) { - self.release = release; - } - - // TODO: experimental, not sure if this is the right pattern. It is - // basically a from_params() that's meant to allow changes without - // disrupting everything, which probably means it won't be the kind of thing - // a macro can generate. - pub fn update_from_params(&mut self, params: &EnvelopeParams) { - self.set_attack(params.attack()); - self.set_decay(params.decay()); - self.set_sustain(params.sustain()); - self.set_release(params.release()); - } -} - -impl Waveform { - pub fn show(&mut self, ui: &mut Ui) -> eframe::egui::InnerResponse> { - let mut waveform = *self; - ComboBox::new(ui.next_auto_id(), "Waveform") - .selected_text(waveform.to_string()) - .show_ui(ui, |ui| { - for w in Waveform::iter() { - let s: &'static str = w.into(); - if ui.selectable_value(&mut waveform, w, s).clicked() { - *self = waveform; - return true; - } - } - return false; - }) - } -} - -impl Oscillator { - pub fn show(&mut self, ui: &mut Ui) -> eframe::egui::InnerResponse> { - self.waveform.show(ui) - } -} - -impl Envelope { - pub fn ui_content(&mut self, ui: &mut Ui) -> eframe::egui::Response { - let (mut response, painter) = - ui.allocate_painter(Vec2::new(ui.available_width(), 64.0), Sense::hover()); - - let to_screen = emath::RectTransform::from_to( - Rect::from_min_size(Pos2::ZERO, response.rect.size()), - response.rect, - ); - - let control_point_radius = 8.0; - - let x_max = response.rect.size().x; - let y_max = response.rect.size().y; - - let attack_x_scaled = self.attack.0 as f32 * x_max / 4.0; - let decay_x_scaled = self.decay.0 as f32 * x_max / 4.0; - let sustain_y_scaled = (1.0 - self.sustain.value() as f32) * y_max; - let release_x_scaled = self.release.0 as f32 * x_max / 4.0; - let mut control_points = vec![ - pos2(attack_x_scaled, 0.0), - pos2(attack_x_scaled + decay_x_scaled, sustain_y_scaled), - pos2( - attack_x_scaled - + decay_x_scaled - + (x_max - (attack_x_scaled + decay_x_scaled + release_x_scaled)) / 2.0, - sustain_y_scaled, - ), - pos2(x_max - release_x_scaled, sustain_y_scaled), - ]; - - let mut which_changed = usize::MAX; - let control_point_shapes: Vec = control_points - .iter_mut() - .enumerate() - .map(|(i, point)| { - let size = Vec2::splat(2.0 * control_point_radius); - - let point_in_screen = to_screen.transform_pos(*point); - let point_rect = Rect::from_center_size(point_in_screen, size); - let point_id = response.id.with(i); - let point_response = ui.interact(point_rect, point_id, Sense::drag()); - if point_response.drag_delta() != Vec2::ZERO { - which_changed = i; - } - - // Restrict change to only the dimension we care about, so - // it looks less janky. - let mut drag_delta = point_response.drag_delta(); - match which_changed { - 0 => drag_delta.y = 0.0, - 1 => drag_delta.y = 0.0, - 2 => drag_delta.x = 0.0, - 3 => drag_delta.y = 0.0, - usize::MAX => {} - _ => unreachable!(), - } - - *point += drag_delta; - *point = to_screen.from().clamp(*point); - - let point_in_screen = to_screen.transform_pos(*point); - let stroke = ui.style().interact(&point_response).fg_stroke; - - Shape::circle_stroke(point_in_screen, control_point_radius, stroke) - }) - .collect(); - - if which_changed != usize::MAX { - match which_changed { - 0 => { - self.set_attack((control_points[0].x / (x_max / 4.0)).into()); - } - 1 => { - self.set_decay( - ((control_points[1].x - control_points[0].x) / (x_max / 4.0)).into(), - ); - } - 2 => { - self.set_sustain((1.0 - control_points[2].y / y_max).into()); - } - 3 => { - self.set_release(((x_max - control_points[3].x) / (x_max / 4.0)).into()); - } - _ => unreachable!(), - } - } - - let control_points = vec![ - pos2(0.0, y_max), - control_points[0], - control_points[1], - control_points[2], - control_points[3], - pos2(x_max, y_max), - ]; - let points_in_screen: Vec = control_points.iter().map(|p| to_screen * *p).collect(); - - painter.add(PathShape::line( - points_in_screen, - Stroke { - width: 2.0, - color: Color32::YELLOW, - }, - )); - painter.extend(control_point_shapes); - - if which_changed != usize::MAX { - response.mark_changed(); - } - response - } -} -impl Displays for Envelope { - fn ui(&mut self, ui: &mut eframe::egui::Ui) -> eframe::egui::Response { - let mut attack = self.attack(); - let mut decay = self.decay(); - let mut sustain = self.sustain().to_percentage(); - let mut release = self.release(); - - let canvas_response = Frame::canvas(ui.style()) - .show(ui, |ui| self.ui_content(ui)) - .inner; - let attack_response = ui.add( - DragValue::new(&mut attack.0) - .speed(0.1) - .prefix("Attack: ") - .clamp_range(0.0..=100.0) - .suffix(" s"), - ); - if attack_response.changed() { - self.set_attack(attack); - } - ui.end_row(); - let decay_response = ui.add( - DragValue::new(&mut decay.0) - .speed(0.1) - .prefix("Decay: ") - .clamp_range(0.0..=100.0) - .suffix(" s"), - ); - if decay_response.changed() { - self.set_decay(decay); - } - ui.end_row(); - let sustain_response = ui.add( - DragValue::new(&mut sustain) - .speed(0.1) - .prefix("Sustain: ") - .clamp_range(0.0..=100.0) - .fixed_decimals(2) - .suffix("%"), - ); - if sustain_response.changed() { - self.set_sustain((sustain / 100.0).into()); - } - ui.end_row(); - let release_response = ui.add( - DragValue::new(&mut release.0) - .speed(0.1) - .prefix("Release: ") - .clamp_range(0.0..=100.0) - .suffix(" s"), - ); - if release_response.changed() { - self.set_release(release); - } - ui.end_row(); - canvas_response | attack_response | decay_response | sustain_response | release_response - } -} - -#[derive(Clone, Debug, Default, Serialize, Deserialize)] -pub enum SteppedEnvelopeFunction { - #[default] - Linear, - Logarithmic, - Exponential, -} - -#[derive(Clone, Debug, Default, Serialize, Deserialize)] -pub struct SteppedEnvelopeStep { - pub interval: Range, - pub start_value: SignalType, - pub end_value: SignalType, - pub step_function: SteppedEnvelopeFunction, -} - -#[derive(Clone, Debug, Default, Serialize, Deserialize)] -pub struct SteppedEnvelope { - time_unit: ClockTimeUnit, - steps: Vec, -} -impl SteppedEnvelope { - const EMPTY_STEP: SteppedEnvelopeStep = SteppedEnvelopeStep { - interval: Range { - start: 0.0, - end: 0.0, - }, - start_value: 0.0, - end_value: 0.0, - step_function: SteppedEnvelopeFunction::Linear, - }; - - pub fn new_with_time_unit(time_unit: ClockTimeUnit) -> Self { - Self { - time_unit, - ..Default::default() - } - } - - pub fn push_step(&mut self, step: SteppedEnvelopeStep) { - self.steps.push(step); - self.debug_validate_steps(); - } - - fn steps(&self) -> &[SteppedEnvelopeStep] { - &self.steps - } - - pub fn step_for_time(&self, time: f64) -> &SteppedEnvelopeStep { - let steps = self.steps(); - if steps.is_empty() { - return &Self::EMPTY_STEP; - } - - let mut candidate_step: &SteppedEnvelopeStep = steps.first().unwrap(); - for step in steps { - if candidate_step.interval.end == f64::MAX { - // Any step with max end_time is terminal. - break; - } - debug_assert!(step.interval.start >= candidate_step.interval.start); - debug_assert!(step.interval.end >= candidate_step.interval.start); - - if step.interval.start > time { - // This step starts in the future. If all steps' start times - // are in order, then we can't do better than what we have. - break; - } - if step.interval.end < time { - // This step already ended. It's invalid for this point in time. - continue; - } - candidate_step = step; - } - candidate_step - } - - pub fn value_for_step_at_time(&self, step: &SteppedEnvelopeStep, time: f64) -> SignalType { - if step.interval.start == step.interval.end || step.start_value == step.end_value { - return step.end_value; - } - let elapsed_time = time - step.interval.start; - let total_interval_time = step.interval.end - step.interval.start; - let percentage_complete = elapsed_time / total_interval_time; - let total_interval_value_delta = step.end_value - step.start_value; - - let multiplier = if percentage_complete == 0.0 { - 0.0 - } else { - match step.step_function { - SteppedEnvelopeFunction::Linear => percentage_complete, - SteppedEnvelopeFunction::Logarithmic => { - (percentage_complete.log(10000.0) * 2.0 + 1.0).clamp(0.0, 1.0) - } - SteppedEnvelopeFunction::Exponential => 100.0f64.powf(percentage_complete) / 100.0, - } - }; - let mut value = step.start_value + total_interval_value_delta * multiplier; - if (step.end_value > step.start_value && value > step.end_value) - || (step.end_value < step.start_value && value < step.end_value) - { - value = step.end_value; - } - value - } - - fn debug_validate_steps(&self) { - debug_assert!(!self.steps.is_empty()); - debug_assert_eq!(self.steps.first().unwrap().interval.start, 0.0); - // TODO: this should be optional depending on who's using it ..... debug_assert_eq!(self.steps.last().unwrap().interval.end, f32::MAX); - let mut start_time = 0.0; - let mut end_time = 0.0; - let steps = self.steps(); - for step in steps { - debug_assert_le!(step.interval.start, step.interval.end); // Next step has non-negative duration - debug_assert_ge!(step.interval.start, start_time); // We're not moving backward in time - debug_assert_le!(step.interval.start, end_time); // Next step leaves no gaps (overlaps OK) - start_time = step.interval.start; - end_time = step.interval.end; - - // We don't require subsequent steps to be valid, as long as - // an earlier step covered the rest of the time range. - if step.interval.end == f64::MAX { - break; - } - } - // TODO same debug_assert_eq!(end_time, f32::MAX); - } -} - -#[cfg(test)] -pub mod tests { - use super::*; - use crate::{util::tests::TestOnlyPaths, SAMPLE_BUFFER_SIZE}; - use ensnare::{midi::MidiNote, time::Transport, traits::Configurable, traits::Ticks}; - use float_cmp::approx_eq; - use more_asserts::{assert_gt, assert_lt}; - use std::path::PathBuf; - - pub trait DebugTicks: Ticks { - fn debug_tick_until(&mut self, tick_number: usize); - } - - impl DebugTicks for Oscillator { - fn debug_tick_until(&mut self, tick_number: usize) { - if self.ticks < tick_number { - self.tick(tick_number - self.ticks); - } - } - } - - fn create_oscillator(waveform: Waveform, tune: Ratio, note: MidiNote) -> Oscillator { - let mut oscillator = Oscillator::new_with(&OscillatorParams { - waveform, - frequency: FrequencyHz::from(note), - ..Default::default() - }); - oscillator.set_frequency_tune(tune); - oscillator - } - - #[test] - fn oscillator_pola() { - let mut oscillator = - Oscillator::new_with(&OscillatorParams::default_with_waveform(Waveform::Sine)); - - // we'll get a few samples in case the oscillator happens to start at - // zero - let mut values = [BipolarNormal::default(); 3]; - oscillator.generate_batch_values(&mut values); - assert_ne!( - 0.0, - values[1].value(), - "Default Oscillator should not be silent" - ); - } - - // Make sure we're dealing with at least a pulse-width wave of amplitude - // 1.0, which means that every value is either 1.0 or -1.0. - #[test] - fn square_wave_is_correct_amplitude() { - const SAMPLE_RATE: SampleRate = SampleRate::new(63949); // Prime number - const FREQUENCY: FrequencyHz = FrequencyHz(499.0); - let mut oscillator = Oscillator::new_with(&OscillatorParams { - waveform: Waveform::Square, - frequency: FREQUENCY, - ..Default::default() - }); - oscillator.update_sample_rate(SAMPLE_RATE); - - // Below Nyquist limit - assert_lt!(FREQUENCY, FrequencyHz((SAMPLE_RATE.value() / 2) as f64)); - - for _ in 0..SAMPLE_RATE.value() { - oscillator.tick(1); - let f = oscillator.value().value(); - assert_eq!(f, f.signum()); - } - } - - #[test] - fn square_wave_frequency_is_accurate() { - // For this test, we want the sample rate and frequency to be nice even - // numbers so that we don't have to deal with edge cases. - const SAMPLE_RATE: SampleRate = SampleRate::new(65536); - const FREQUENCY: FrequencyHz = FrequencyHz(128.0); - let mut oscillator = Oscillator::new_with(&OscillatorParams { - waveform: Waveform::Square, - frequency: FREQUENCY, - ..Default::default() - }); - oscillator.update_sample_rate(SAMPLE_RATE); - - let mut n_pos = 0; - let mut n_neg = 0; - let mut last_sample = 1.0; - let mut transitions = 0; - for _ in 0..SAMPLE_RATE.value() { - oscillator.tick(1); - let f = oscillator.value().value(); - if f == 1.0 { - n_pos += 1; - } else if f == -1.0 { - n_neg += 1; - } else { - panic!("square wave emitted strange amplitude: {f}"); - } - if f != last_sample { - transitions += 1; - last_sample = f; - } - } - assert_eq!(n_pos + n_neg, SAMPLE_RATE.value()); - assert_eq!(n_pos, n_neg); - - // The -1 is because we stop at the end of the cycle, and the transition - // back to 1.0 should be at the start of the next cycle. - assert_eq!(transitions, FREQUENCY.value() as i32 * 2 - 1); - } - - #[test] - fn square_wave_shape_is_accurate() { - const SAMPLE_RATE: SampleRate = SampleRate::new(65536); - const FREQUENCY: FrequencyHz = FrequencyHz(2.0); - let mut oscillator = Oscillator::new_with(&OscillatorParams { - waveform: Waveform::Square, - frequency: FREQUENCY, - ..Default::default() - }); - oscillator.update_sample_rate(SAMPLE_RATE); - - oscillator.tick(1); - assert_eq!( - oscillator.value().value(), - 1.0, - "the first sample of a square wave should be 1.0" - ); - - // Halfway between the first and second cycle, the wave should - // transition from 1.0 to -1.0. - // - // We're fast-forwarding two different ways in this test. The first is - // by just ticking the clock the desired number of times, so we're not - // really fast-forwarding; we're just playing normally and ignoring the - // results. The second is by testing that the oscillator responds - // reasonably to clock.set_samples(). I haven't decided whether entities - // need to pay close attention to clock.set_samples() other than not - // exploding, so I might end up deleting that part of the test. - oscillator.tick(SAMPLE_RATE.value() / 4 - 2); - assert_eq!(oscillator.value().value(), 1.0); - oscillator.tick(1); - assert_eq!(oscillator.value().value(), 1.0); - oscillator.tick(1); - assert_eq!(oscillator.value().value(), -1.0); - oscillator.tick(1); - assert_eq!(oscillator.value().value(), -1.0); - - // Then should transition back to 1.0 at the first sample of the second - // cycle. - // - // As noted above, we're using clock.set_samples() here. - oscillator.debug_tick_until(SAMPLE_RATE.value() / 2 - 2); - assert_eq!(oscillator.value().value(), -1.0); - oscillator.tick(1); - assert_eq!(oscillator.value().value(), -1.0); - oscillator.tick(1); - assert_eq!(oscillator.value().value(), 1.0); - oscillator.tick(1); - assert_eq!(oscillator.value().value(), 1.0); - } - - #[test] - fn sine_wave_is_balanced() { - const FREQUENCY: FrequencyHz = FrequencyHz(1.0); - let mut oscillator = Oscillator::new_with(&OscillatorParams { - waveform: Waveform::Sine, - frequency: FREQUENCY, - ..Default::default() - }); - oscillator.update_sample_rate(SampleRate::DEFAULT); - - let mut n_pos = 0; - let mut n_neg = 0; - let mut n_zero = 0; - for _ in 0..SampleRate::DEFAULT_SAMPLE_RATE { - oscillator.tick(1); - let f = oscillator.value().value(); - if f < -0.0000001 { - n_neg += 1; - } else if f > 0.0000001 { - n_pos += 1; - } else { - n_zero += 1; - } - } - assert_eq!(n_zero, 2); - assert_eq!(n_pos, n_neg); - assert_eq!(n_pos + n_neg + n_zero, SampleRate::DEFAULT_SAMPLE_RATE); - } - - // For now, only Oscillator implements source_signal(). We'll probably make - // it a trait later. - pub fn render_signal_as_audio_source( - source: &mut Oscillator, - run_length_in_seconds: usize, - ) -> Vec { - let mut samples = Vec::default(); - for _ in 0..SampleRate::DEFAULT_SAMPLE_RATE * run_length_in_seconds { - source.tick(1); - samples.push(Sample::from(source.value().value())); - } - samples - } - - fn read_samples_from_mono_wav_file(filename: &PathBuf) -> Vec { - let mut reader = hound::WavReader::open(filename).unwrap(); - let mut r = Vec::default(); - - for sample in reader.samples::() { - r.push(Sample::from( - sample.unwrap() as SampleType / i16::MAX as SampleType, - )); - } - r - } - - pub fn samples_match_known_good_wav_file( - samples: Vec, - filename: &PathBuf, - acceptable_deviation: SampleType, - ) -> bool { - let known_good_samples = read_samples_from_mono_wav_file(filename); - if known_good_samples.len() != samples.len() { - eprintln!("Provided samples of different length from known-good"); - return false; - } - for i in 0..samples.len() { - if (samples[i] - known_good_samples[i]).0.abs() >= acceptable_deviation { - eprintln!( - "Samples differed at position {i}: known-good {}, test {}", - known_good_samples[i].0, samples[i].0 - ); - return false; - } - } - true - } - - #[test] - fn square_matches_known_good() { - let test_cases = vec![ - (1.0, "1Hz"), - (100.0, "100Hz"), - (1000.0, "1000Hz"), - (10000.0, "10000Hz"), - (20000.0, "20000Hz"), - ]; - for test_case in test_cases { - let mut osc = Oscillator::new_with(&OscillatorParams { - waveform: Waveform::Square, - frequency: test_case.0.into(), - ..Default::default() - }); - let samples = render_signal_as_audio_source(&mut osc, 1); - let mut filename = TestOnlyPaths::data_path(); - filename.push("audacity"); - filename.push("44100Hz-mono"); - filename.push(format!("square-{}.wav", test_case.1)); - - assert!( - samples_match_known_good_wav_file(samples, &filename, 0.001), - "while testing square {}Hz", - test_case.0 - ); - } - } - - fn get_test_cases() -> Vec<(FrequencyHz, &'static str)> { - vec![ - (FrequencyHz(1.0), "1Hz"), - (FrequencyHz(100.0), "100Hz"), - (FrequencyHz(1000.0), "1000Hz"), - (FrequencyHz(10000.0), "10000Hz"), - (FrequencyHz(20000.0), "20000Hz"), - ] - } - - #[test] - fn sine_matches_known_good() { - for test_case in get_test_cases() { - let mut osc = Oscillator::new_with(&OscillatorParams { - waveform: Waveform::Sine, - frequency: test_case.0.into(), - ..Default::default() - }); - let samples = render_signal_as_audio_source(&mut osc, 1); - let mut filename = TestOnlyPaths::data_path(); - filename.push("audacity"); - filename.push("44100Hz-mono"); - filename.push(format!("sine-{}.wav", test_case.1)); - - assert!( - samples_match_known_good_wav_file(samples, &filename, 0.001), - "while testing sine {}Hz", - test_case.0 - ); - } - } - - #[test] - fn sawtooth_matches_known_good() { - for test_case in get_test_cases() { - let mut osc = Oscillator::new_with(&OscillatorParams { - waveform: Waveform::Sawtooth, - frequency: test_case.0.into(), - ..Default::default() - }); - let samples = render_signal_as_audio_source(&mut osc, 1); - let mut filename = TestOnlyPaths::data_path(); - filename.push("audacity"); - filename.push("44100Hz-mono"); - filename.push(format!("sawtooth-{}.wav", test_case.1)); - - assert!( - samples_match_known_good_wav_file(samples, &filename, 0.001), - "while testing sawtooth {}Hz", - test_case.0 - ); - } - } - - #[test] - fn triangle_matches_known_good() { - for test_case in get_test_cases() { - let mut osc = Oscillator::new_with(&OscillatorParams { - waveform: Waveform::Triangle, - frequency: test_case.0.into(), - ..Default::default() - }); - let samples = render_signal_as_audio_source(&mut osc, 1); - let mut filename = TestOnlyPaths::data_path(); - filename.push("audacity"); - filename.push("44100Hz-mono"); - filename.push(format!("triangle-{}.wav", test_case.1)); - - assert!( - samples_match_known_good_wav_file(samples, &filename, 0.01), - "while testing triangle {}Hz", - test_case.0 - ); - } - } - - #[test] - fn oscillator_modulated() { - let mut oscillator = create_oscillator(Waveform::Sine, Ratio::from(1.0), MidiNote::C4); - // Default - assert_eq!( - oscillator.adjusted_frequency(), - FrequencyHz::from(MidiNote::C4) - ); - - // Explicitly zero (none) - oscillator.set_frequency_modulation(BipolarNormal::from(0.0)); - assert_eq!( - oscillator.adjusted_frequency(), - FrequencyHz::from(MidiNote::C4) - ); - - // Max - oscillator.set_frequency_modulation(BipolarNormal::from(1.0)); - assert_eq!( - oscillator.adjusted_frequency(), - FrequencyHz::from(MidiNote::C5) - ); - - // Min - oscillator.set_frequency_modulation(BipolarNormal::from(-1.0)); - assert_eq!( - oscillator.adjusted_frequency(), - FrequencyHz::from(MidiNote::C3) - ); - - // Halfway between zero and max - oscillator.set_frequency_modulation(BipolarNormal::from(0.5)); - assert_eq!( - oscillator.adjusted_frequency(), - FrequencyHz::from(MidiNote::C4) * 2.0f64.sqrt() - ); - } - - #[test] - fn oscillator_cycle_restarts_on_time() { - let mut oscillator = - Oscillator::new_with(&OscillatorParams::default_with_waveform(Waveform::Sine)); - const FREQUENCY: FrequencyHz = FrequencyHz(2.0); - oscillator.set_frequency(FREQUENCY); - oscillator.update_sample_rate(SampleRate::DEFAULT); - - const TICKS_IN_CYCLE: usize = SampleRate::DEFAULT_SAMPLE_RATE / 2; // That 2 is FREQUENCY - assert_eq!(TICKS_IN_CYCLE, 44100 / 2); - - // We assume that synced oscillators can take care of their own init. - assert!( - !oscillator.should_sync(), - "On init, the oscillator should NOT flag that any init/reset work needs to happen." - ); - - // Now run through and see that we're flagging cycle start at the right - // time. Note the = in the for loop range; we're expecting a flag at the - // zeroth sample of each cycle. - for tick in 0..=TICKS_IN_CYCLE { - let expected = match tick { - 0 => true, // zeroth sample of first cycle - TICKS_IN_CYCLE => true, // zeroth sample of second cycle - _ => false, - }; - - oscillator.tick(1); - assert_eq!( - oscillator.should_sync(), - expected, - "expected {expected} at sample #{tick}" - ); - } - - // Let's try again after rewinding the clock. It should recognize - // something happened and restart the cycle. - oscillator.tick(1); - assert!( - !oscillator.should_sync(), - "Oscillator shouldn't sync midway through cycle." - ); - - // Then we actually change the clock. We'll pick something we know is - // off-cycle. We don't treat this as a should-sync event, because we - // assume that synced oscillators will also notice the clock change and - // do the right thing. At worst, we'll be off for a single main - // oscillator cycle. No normal audio performance will involve a clock - // shift, so it's OK to have the wrong timbre for a tiny fraction of a - // second. - oscillator.update_sample_rate(SampleRate::DEFAULT); - oscillator.tick(1); - assert!( - oscillator.should_sync(), - "After reset, oscillator should sync." - ); - oscillator.tick(1); - assert!( - !oscillator.should_sync(), - "Oscillator shouldn't sync twice when syncing after reset." - ); - - // Let's run through again, but this time go for a whole second, and - // count the number of flags. - oscillator.update_sample_rate(SampleRate::DEFAULT); - let mut cycles = 0; - for _ in 0..SampleRate::DEFAULT_SAMPLE_RATE { - oscillator.tick(1); - if oscillator.should_sync() { - cycles += 1; - } - } - assert_eq!(cycles, usize::from(FREQUENCY)); - } - - impl Envelope { - fn debug_state(&self) -> &State { - &self.state - } - - pub fn debug_is_shutting_down(&self) -> bool { - matches!(self.debug_state(), State::Shutdown) - } - - /// The current value of the envelope generator. Note that this value is - /// often not the one you want if you really care about getting the - /// amplitude at specific interesting time points in the envelope's - /// lifecycle. If you call it before the current time slice's tick(), then - /// you get the value before any pending events (which is probably bad), and - /// if you call it after the tick(), then you get the value for the *next* - /// time slice (which is probably bad). It's better to use the value - /// returned by tick(), which is in between pending events but after - /// updating for the time slice. - fn debug_amplitude(&self) -> Normal { - Normal::new(self.uncorrected_amplitude.sum()) - } - } - - // Where possible, we'll erase the envelope type and work only with the - // Envelope trait, so that we can confirm that the trait alone is useful. - fn get_ge_trait_stuff() -> (Transport, impl GeneratesEnvelope) { - let mut transport = Transport::default(); - transport.play(); - let envelope = Envelope::new_with(&EnvelopeParams::new_with( - (0.1).into(), - (0.2).into(), - Normal::new(0.8), - (0.3).into(), - )); - (transport, envelope) - } - - #[test] - fn generates_envelope_trait_idle() { - let (mut transport, mut e) = get_ge_trait_stuff(); - - assert!(e.is_idle(), "Envelope should be idle on creation."); - - e.tick(1); - transport.advance(1); - assert!(e.is_idle(), "Untriggered envelope should remain idle."); - assert_eq!( - e.value().value(), - 0.0, - "Untriggered envelope should remain amplitude zero." - ); - } - - fn run_until( - envelope: &mut impl GeneratesEnvelope, - transport: &mut Transport, - time_marker: MusicalTime, - mut test: F, - ) -> Normal - where - F: FnMut(Normal, &Transport), - { - let mut amplitude: Normal = Normal::new(0.0); - loop { - envelope.tick(1); - transport.advance(1); - let should_continue = transport.current_time() < time_marker; - if !should_continue { - break; - } - amplitude = envelope.value(); - test(amplitude, transport); - } - amplitude - } - - #[test] - fn generates_envelope_trait_instant_trigger_response() { - let (mut transport, mut e) = get_ge_trait_stuff(); - - transport.update_sample_rate(SampleRate::DEFAULT); - e.update_sample_rate(SampleRate::DEFAULT); - - e.trigger_attack(); - e.tick(1); - transport.advance(1); - assert!( - !e.is_idle(), - "Envelope should be active immediately upon trigger" - ); - - // We apply a small fudge factor to account for the fact that the MMA - // convex transform rounds to zero pretty aggressively, so attacks take - // a bit of time before they are apparent. I'm not sure whether this is - // a good thing; it objectively makes attack laggy (in this case 16 - // samples late!). - for _ in 0..17 { - e.tick(1); - transport.advance(1); - } - assert_gt!( - e.value().value(), - 0.0, - "Envelope amplitude should increase immediately upon trigger" - ); - } - - #[test] - fn generates_envelope_trait_attack_decay_duration() { - let mut transport = Transport::default(); - // This is an ugly way to get seconds and beats to match up. This - // happened because these tests were written for Clock, which worked in - // units of wall-clock time, and we migrated to MusicalTime, which is - // based on beats. - transport.set_tempo(Tempo(60.0)); - transport.play(); - - let attack: Normal = Envelope::from_seconds_to_normal(Seconds(0.1)); - let decay: Normal = Envelope::from_seconds_to_normal(Seconds(0.2)); - const SUSTAIN: Normal = Normal::new_const(0.8); - let release: Normal = Envelope::from_seconds_to_normal(Seconds(0.3)); - let mut envelope = - Envelope::new_with(&EnvelopeParams::new_with(attack, decay, SUSTAIN, release)); - - // An even sample rate means we can easily calculate how much time was spent in each state. - transport.update_sample_rate(SampleRate::from(100)); - envelope.update_sample_rate(SampleRate::from(100)); - - let mut time_marker = transport.current_time() - + MusicalTime::new_with_fractional_beats(Envelope::from_normal_to_seconds(attack).0); - envelope.trigger_attack(); - assert!( - matches!(envelope.debug_state(), State::Attack), - "Expected SimpleEnvelopeState::Attack after trigger, but got {:?} instead", - envelope.debug_state() - ); - let mut last_amplitude = envelope.value(); - - envelope.tick(1); - - let amplitude = run_until( - &mut envelope, - &mut transport, - time_marker, - |amplitude, transport| { - assert_lt!( - last_amplitude, - amplitude, - "Expected amplitude to rise through attack time ending at {time_marker}, but it didn't at time {}", transport.current_time().total_units() - ); - last_amplitude = amplitude; - }, - ); - assert!(matches!(envelope.debug_state(), State::Decay)); - assert!( - approx_eq!(f64, amplitude.value(), 1.0f64, epsilon = 0.0000000000001), - "Amplitude should reach maximum after attack (was {}, difference {}).", - amplitude.value(), - (1.0 - amplitude.value()).abs() - ); - - time_marker += - MusicalTime::new_with_fractional_beats(Envelope::from_normal_to_seconds(decay).0); - let amplitude = run_until( - &mut envelope, - &mut transport, - time_marker, - |_amplitude, _clock| {}, - ); - assert_eq!( - amplitude, SUSTAIN, - "Amplitude should reach sustain level after decay." - ); - assert!(matches!(envelope.debug_state(), State::Sustain)); - } - - // Decay and release rates should be determined as if the envelope stages - // were operating on a full 1.0..=0.0 amplitude range. Thus, the expected - // time for the stage is not necessarily the same as the parameter. - fn expected_decay_time(decay: Normal, sustain: Normal) -> Seconds { - Envelope::from_normal_to_seconds(decay * (1.0 - sustain.value())) - } - - fn expected_release_time(release: Normal, current_amplitude: Normal) -> Seconds { - Envelope::from_normal_to_seconds(release * current_amplitude) - } - - #[test] - fn generates_envelope_trait_sustain_duration_then_release() { - let mut transport = Transport::default(); - transport.set_tempo(Tempo(60.0)); - transport.play(); - - let attack: Normal = Envelope::from_seconds_to_normal(Seconds(0.1)); - let decay: Normal = Envelope::from_seconds_to_normal(Seconds(0.2)); - const SUSTAIN: Normal = Normal::new_const(0.8); - let release: Normal = Envelope::from_seconds_to_normal(Seconds(0.3)); - let mut envelope = - Envelope::new_with(&EnvelopeParams::new_with(attack, decay, SUSTAIN, release)); - - envelope.trigger_attack(); - envelope.tick(1); - let mut time_marker = transport.current_time() - + MusicalTime::new_with_fractional_beats( - Envelope::from_normal_to_seconds(attack).0 + expected_decay_time(decay, SUSTAIN).0, - ); - transport.advance(1); - - // Skip past attack/decay. - run_until( - &mut envelope, - &mut transport, - time_marker, - |_amplitude, _clock| {}, - ); - - time_marker += MusicalTime::new_with_fractional_beats(0.5); - let amplitude = run_until( - &mut envelope, - &mut transport, - time_marker, - |amplitude, _clock| { - assert_eq!( - amplitude, SUSTAIN, - "Amplitude should remain at sustain level while note is still triggered" - ); - }, - ) - .value(); - - envelope.trigger_release(); - time_marker += MusicalTime::new_with_fractional_beats( - expected_release_time(release, amplitude.into()).0, - ); - let mut last_amplitude = amplitude; - let amplitude = run_until( - &mut envelope, - &mut transport, - time_marker, - |inner_amplitude, _clock| { - assert_lt!( - inner_amplitude.value(), - last_amplitude, - "Amplitude should begin decreasing as soon as note off." - ); - last_amplitude = inner_amplitude.value(); - }, - ); - - // These assertions are checking the next frame's state, which is right - // because we want to test what happens after the release ends. - assert!( - envelope.is_idle(), - "Envelope should be idle when release ends, but it wasn't (amplitude is {})", - amplitude.value() - ); - assert_eq!( - envelope.debug_amplitude().value(), - 0.0, - "Amplitude should be zero when release ends" - ); - } - - #[test] - fn simple_envelope_interrupted_decay_with_second_attack() { - let mut transport = Transport::default(); - transport.set_tempo(Tempo(60.0)); - transport.play(); - - // These settings are copied from Welsh Piano's filter envelope, which - // is where I noticed some unwanted behavior. - let attack: Normal = Envelope::from_seconds_to_normal(Seconds(0.0)); - let decay: Normal = Envelope::from_seconds_to_normal(Seconds(5.22)); - const SUSTAIN: Normal = Normal::new_const(0.25); - let release: Normal = Envelope::from_seconds_to_normal(Seconds(0.5)); - let mut envelope = - Envelope::new_with(&EnvelopeParams::new_with(attack, decay, SUSTAIN, release)); - - transport.update_sample_rate(SampleRate::DEFAULT); - envelope.update_sample_rate(SampleRate::DEFAULT); - - envelope.tick(1); - transport.advance(1); - - assert_eq!( - envelope.value(), - Normal::minimum(), - "Amplitude should start at zero" - ); - - // See https://floating-point-gui.de/errors/comparison/ for standard - // warning about comparing floats and looking for epsilons. - envelope.trigger_attack(); - envelope.tick(1); - let mut time_marker = transport.current_time(); - transport.advance(1); - assert!( - approx_eq!( - f64, - envelope.value().value(), - Normal::maximum().value(), - ulps = 8 - ), - "Amplitude should reach peak upon trigger, but instead of {} we got {}", - Normal::maximum().value(), - envelope.value().value(), - ); - envelope.tick(1); - transport.advance(1); - assert_lt!( - envelope.value(), - Normal::maximum(), - "Zero-attack amplitude should begin decreasing immediately after peak" - ); - - // Jump to halfway through decay. - time_marker += MusicalTime::new_with_fractional_beats( - Envelope::from_normal_to_seconds(attack).0 - + Envelope::from_normal_to_seconds(decay).0 / 2.0, - ); - let amplitude = run_until( - &mut envelope, - &mut transport, - time_marker, - |_amplitude, _clock| {}, - ); - assert_lt!( - amplitude, - Normal::maximum(), - "Amplitude should have decayed halfway through decay" - ); - - // Release the trigger. - envelope.trigger_release(); - let _amplitude = envelope.tick(1); - transport.advance(1); - - // And hit it again. - envelope.trigger_attack(); - envelope.tick(1); - let mut time_marker = transport.current_time(); - transport.advance(1); - assert!( - approx_eq!( - f64, - envelope.value().value(), - Normal::maximum().value(), - ulps = 8 - ), - "Amplitude should reach peak upon second trigger" - ); - - // Then release again. - envelope.trigger_release(); - - // Check that we keep decreasing amplitude to zero, not to sustain. - time_marker += - MusicalTime::new_with_fractional_beats(Envelope::from_normal_to_seconds(release).0); - let mut last_amplitude = envelope.value().value(); - let _amplitude = run_until( - &mut envelope, - &mut transport, - time_marker, - |inner_amplitude, _clock| { - assert_lt!( - inner_amplitude.value(), - last_amplitude, - "Amplitude should continue decreasing after note off" - ); - last_amplitude = inner_amplitude.value(); - }, - ); - - // These assertions are checking the next frame's state, which is right - // because we want to test what happens after the release ends. - assert!( - envelope.is_idle(), - "Envelope should be idle when release ends" - ); - assert_eq!( - envelope.debug_amplitude().value(), - 0.0, - "Amplitude should be zero when release ends" - ); - } - - // Per Pirkle, DSSPC++, p.87-88, decay and release times determine the - // *slope* but not necessarily the *duration* of those phases of the - // envelope. The slope assumes the specified time across a full 1.0-to-0.0 - // range. This means that the actual decay and release times for a given - // envelope can be shorter than its parameters might suggest. - #[test] - fn generates_envelope_trait_decay_and_release_based_on_full_amplitude_range() { - let mut transport = Transport::default(); - transport.set_tempo(Tempo(60.0)); - transport.play(); - const ATTACK: Normal = Normal::minimum(); - let decay: Normal = Envelope::from_seconds_to_normal(Seconds(0.8)); - let sustain = Normal::new_const(0.5); - let release: Normal = Envelope::from_seconds_to_normal(Seconds(0.4)); - let mut envelope = - Envelope::new_with(&EnvelopeParams::new_with(ATTACK, decay, sustain, release)); - - transport.update_sample_rate(SampleRate::DEFAULT); - envelope.update_sample_rate(SampleRate::DEFAULT); - - // Decay after note-on should be shorter than the decay value. - envelope.trigger_attack(); - let mut time_marker = transport.current_time() - + MusicalTime::new_with_fractional_beats(expected_decay_time(decay, sustain).0); - let amplitude = run_until( - &mut envelope, - &mut transport, - time_marker, - |_amplitude, _clock| {}, - ) - .value(); - assert!(approx_eq!(f64, amplitude, sustain.value(), epsilon=0.0001), - "Expected to see sustain level {} instead of {} at time {} (which is {:.1}% of decay time {}, based on full 1.0..=0.0 amplitude range)", - sustain.value(), - amplitude, - time_marker, - decay, - 100.0 * (1.0 - sustain.value()) - ); - - // Release after note-off should also be shorter than the release value. - envelope.trigger_release(); - let expected_release_time = expected_release_time(release, envelope.value().into()); - time_marker += - MusicalTime::new_with_fractional_beats(expected_release_time.0 - 0.000000000000001); // I AM SICK OF FP PRECISION ERRORS - let amplitude = run_until( - &mut envelope, - &mut transport, - time_marker, - |inner_amplitude, transport| { - assert_gt!( - inner_amplitude.value(), - 0.0, - "We should not reach idle before time {}, but we did at time {}.", - &time_marker, - transport.current_time() - ) - }, - ); - let portion_of_full_amplitude_range = sustain.value(); - assert!( - envelope.is_idle(), - "Expected release to end after time {}, which is {:.1}% of release time {}. Amplitude is {}", - expected_release_time.0, - 100.0 * portion_of_full_amplitude_range, - release, - amplitude.value() - ); - } - - // https://docs.google.com/spreadsheets/d/1DSkut7rLG04Qx_zOy3cfI7PMRoGJVr9eaP5sDrFfppQ/edit#gid=0 - #[test] - fn coeff() { - let (a, b, c) = Envelope::calculate_coefficients(0.0, 1.0, 0.5, 0.25, 1.0, 0.0); - assert_eq!(a, 1.0); - assert_eq!(b, -2.0); - assert_eq!(c, 1.0); - } - - #[test] - fn envelope_amplitude_batching() { - let mut e = Envelope::new_with(&EnvelopeParams::new_with( - Envelope::from_seconds_to_normal(Seconds(0.1)), - Envelope::from_seconds_to_normal(Seconds(0.2)), - Normal::new(0.5), - Envelope::from_seconds_to_normal(Seconds(0.3)), - )); - - // Initialize the buffer with a nonsense value so we know it got - // overwritten by the method we're about to call. - // - // TODO: that buffer size should be pulled from somewhere centralized. - let mut amplitudes = [Normal::from(0.888); SAMPLE_BUFFER_SIZE]; - - // The envelope starts out in the idle state, and we haven't triggered - // it. - e.generate_batch_values(&mut amplitudes); - amplitudes.iter().for_each(|i| { - assert_eq!( - i.value(), - Normal::MIN, - "Each value in untriggered EG's buffer should be set to silence" - ); - }); - - // Now trigger the envelope and see what happened. - e.trigger_attack(); - e.generate_batch_values(&mut amplitudes); - assert!( - amplitudes.iter().any(|i| { i.value() != Normal::MIN }), - "Once triggered, the EG should generate non-silent values" - ); - } - - #[test] - fn envelope_shutdown_state() { - let mut e = Envelope::new_with(&EnvelopeParams::new_with( - Normal::minimum(), - Normal::minimum(), - Normal::maximum(), - Envelope::from_seconds_to_normal(Seconds(0.5)), - )); - e.update_sample_rate(SampleRate::from(2000)); - - // With sample rate 1000, each sample is 0.5 millisecond. - let mut amplitudes: [Normal; 10] = [Normal::default(); 10]; - - e.trigger_attack(); - e.generate_batch_values(&mut amplitudes); - assert!( - amplitudes.iter().all(|s| { s.value() == Normal::MAX }), - "After enqueueing attack, amplitude should be max" - ); - - e.trigger_shutdown(); - e.generate_batch_values(&mut amplitudes); - assert_lt!( - amplitudes[0].value(), - (Normal::MAX - Normal::MIN) / 2.0, - "At sample rate {}, shutdown state should take two samples to go from 1.0 to 0.0, but when we checked it's {}.", - e.sample_rate, amplitudes[0].value() - ); - assert_eq!( - amplitudes[1].value(), - Normal::MIN, - "At sample rate {}, shutdown state should reach 0.0 within two samples.", - e.sample_rate - ); - } - - // Bugfix: if sustain was 100%, attack was zero, and decay was nonzero, then - // the decay curve called for a change from amplitude 1.0 to amplitude 1.0, - // which meant we asked the matrix math to calculate coefficients for a - // singularity, which netted out to amplitude being zero while we waited for - // it to reach 1.0 (or for the decay timeout to fire, which was how we - // progressed at all to sustain). Solution: notice that start/end - // coordinates are identical, and return identity coefficients so that the - // conversion from linear to curved produced the target amplitude, causing - // the state to advance to sustain. Amazing that I didn't catch this right - // away. - #[test] - fn sustain_full() { - let mut e = Envelope::new_with(&EnvelopeParams::new_with( - Normal::minimum(), - Envelope::from_seconds_to_normal(Seconds(0.67)), - Normal::maximum(), - Envelope::from_seconds_to_normal(Seconds(0.5)), - )); - e.update_sample_rate(SampleRate::from(44100)); - assert_eq!(e.value().value(), 0.0); - e.tick(1); - assert_eq!(e.value().value(), 0.0); - - e.trigger_attack(); - e.tick(1); - assert_eq!(e.value(), Normal::maximum()); - } - - impl SteppedEnvelopeStep { - pub(crate) fn new_with_duration( - start_time: f64, - duration: f64, - start_value: SignalType, - end_value: SignalType, - step_function: SteppedEnvelopeFunction, - ) -> Self { - Self { - interval: Range { - start: start_time, - end: if duration == f64::MAX { - duration - } else { - start_time + duration - }, - }, - start_value, - end_value, - step_function, - } - } - } - - #[test] - fn envelope_step_functions() { - const START_TIME: f64 = 3.14159; - const DURATION: f64 = 2.71828; - const START_VALUE: SignalType = 1.0; - const END_VALUE: SignalType = 1.0 + 10.0; - - let mut envelope = SteppedEnvelope::default(); - // This envelope is here just to offset the one we're testing, - // to catch bugs where we assumed the start time was 0.0. - envelope.push_step(SteppedEnvelopeStep::new_with_duration( - 0.0, - START_TIME, - 0.0, - 0.0, - SteppedEnvelopeFunction::Linear, - )); - envelope.push_step(SteppedEnvelopeStep::new_with_duration( - START_TIME, - DURATION, - START_VALUE, - END_VALUE, - SteppedEnvelopeFunction::Linear, - )); - - // We're lazy and ask for the step only once because we know there's only one. - let step = envelope.step_for_time(START_TIME); - assert_eq!( - envelope.value_for_step_at_time(step, START_TIME), - START_VALUE - ); - assert_eq!( - envelope.value_for_step_at_time(step, START_TIME + DURATION / 2.0), - 1.0 + 10.0 / 2.0 - ); - assert_eq!( - envelope.value_for_step_at_time(step, START_TIME + DURATION), - END_VALUE - ); - - let mut envelope = SteppedEnvelope::default(); - envelope.push_step(SteppedEnvelopeStep::new_with_duration( - 0.0, - START_TIME, - 0.0, - 0.0, - SteppedEnvelopeFunction::Linear, - )); - envelope.push_step(SteppedEnvelopeStep::new_with_duration( - START_TIME, - DURATION, - START_VALUE, - END_VALUE, - SteppedEnvelopeFunction::Logarithmic, - )); - - let step = envelope.step_for_time(START_TIME); - assert_eq!( - envelope.value_for_step_at_time(step, START_TIME), - START_VALUE - ); // special case log(0) == 0.0 - assert!(approx_eq!( - f64, - envelope.value_for_step_at_time(step, START_TIME + DURATION / 2.0), - 1.0 + 8.49485, - epsilon = 0.001 - )); // log(0.5, 10000) corrected for (0.0..=1.0) - assert_eq!( - envelope.value_for_step_at_time(step, START_TIME + DURATION), - END_VALUE - ); - - let mut envelope = SteppedEnvelope::default(); - envelope.push_step(SteppedEnvelopeStep::new_with_duration( - 0.0, - START_TIME, - 0.0, - 0.0, - SteppedEnvelopeFunction::Linear, - )); - envelope.push_step(SteppedEnvelopeStep::new_with_duration( - START_TIME, - DURATION, - START_VALUE, - END_VALUE, - SteppedEnvelopeFunction::Exponential, - )); - - let step = envelope.step_for_time(START_TIME); - assert_eq!( - envelope.value_for_step_at_time(step, START_TIME), - START_VALUE - ); - assert_eq!( - envelope.value_for_step_at_time(step, START_TIME + DURATION / 2.0), - 1.0 + 10.0 * 0.1 - ); - assert_eq!( - envelope.value_for_step_at_time(step, START_TIME + DURATION), - END_VALUE - ); - } -} diff --git a/core/src/lib.rs b/core/src/lib.rs index 7ca81802..308f3c04 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -3,7 +3,7 @@ //! Fundamental structs and traits. use eframe::egui::Slider; -use ensnare::{prelude::*, traits::prelude::*}; +use ensnare_core::{prelude::*, traits::prelude::*}; use ensnare_proc_macros::{Control, Params}; use serde::{Deserialize, Serialize}; @@ -14,8 +14,6 @@ pub struct Groove; /// Handles automation, or real-time automatic control of one entity's /// parameters by another entity's output. pub mod control; -/// Contains things that generate signals, like oscillators and envelopes. -pub mod generators; /// Knows about [MIDI](https://en.wikipedia.org/wiki/MIDI). pub mod midi; /// Handles digital-audio, wall-clock, and musical time. @@ -28,155 +26,7 @@ pub const SAMPLE_BUFFER_SIZE: usize = 64; #[cfg(test)] mod tests { - use super::*; - - #[test] - fn mono_to_stereo() { - assert_eq!(StereoSample::from(Sample::MIN), StereoSample::MIN); - assert_eq!(StereoSample::from(Sample::SILENCE), StereoSample::SILENCE); - assert_eq!(StereoSample::from(Sample::MAX), StereoSample::MAX); - } - - #[test] - fn stereo_to_mono() { - assert_eq!(Sample::from(ensnare::core::StereoSample::MIN), Sample::MIN); - assert_eq!( - Sample::from(ensnare::core::StereoSample::SILENCE), - Sample::SILENCE - ); - assert_eq!(Sample::from(ensnare::core::StereoSample::MAX), Sample::MAX); - - assert_eq!( - Sample::from(ensnare::core::StereoSample::new(1.0.into(), 0.0.into())), - Sample::from(0.5) - ); - } - - #[test] - fn normal_mainline() { - let a = Normal::new(0.2); - let b = Normal::new(0.1); - - // Add(Normal) - assert_eq!(a + b, Normal::new(0.2 + 0.1), "Addition should work."); - - // Sub(Normal) - assert_eq!(a - b, Normal::new(0.1), "Subtraction should work."); - - // Add(f64) - assert_eq!(a + 0.2f64, Normal::new(0.4), "Addition of f64 should work."); - - // Sub(f64) - assert_eq!(a - 0.1, Normal::new(0.1), "Subtraction of f64 should work."); - } + use ensnare_core::modulators::{Dca, DcaParams}; - #[test] - fn normal_out_of_bounds() { - assert_eq!( - Normal::new(-1.0), - Normal::new(0.0), - "Normal below 0.0 should be clamped to 0.0" - ); - assert_eq!( - Normal::new(1.1), - Normal::new(1.0), - "Normal above 1.0 should be clamped to 1.0" - ); - } - - #[test] - fn convert_sample_to_normal() { - assert_eq!( - Normal::from(Sample(-0.5)), - Normal::new(0.25), - "Converting Sample -0.5 to Normal should yield 0.25" - ); - assert_eq!( - Normal::from(Sample(0.0)), - Normal::new(0.5), - "Converting Sample 0.0 to Normal should yield 0.5" - ); - } - - #[test] - fn convert_bipolar_normal_to_normal() { - assert_eq!( - Normal::from(BipolarNormal::from(-1.0)), - Normal::new(0.0), - "Bipolar -> Normal wrong" - ); - assert_eq!( - Normal::from(BipolarNormal::from(0.0)), - Normal::new(0.5), - "Bipolar -> Normal wrong" - ); - assert_eq!( - Normal::from(BipolarNormal::from(1.0)), - Normal::new(1.0), - "Bipolar -> Normal wrong" - ); - } - - #[test] - fn convert_normal_to_bipolar_normal() { - assert_eq!( - BipolarNormal::from(Normal::from(0.0)), - BipolarNormal::new(-1.0), - "Normal -> Bipolar wrong" - ); - assert_eq!( - BipolarNormal::from(Normal::from(0.5)), - BipolarNormal::new(0.0), - "Normal -> Bipolar wrong" - ); - assert_eq!( - BipolarNormal::from(Normal::from(1.0)), - BipolarNormal::new(1.0), - "Normal -> Bipolar wrong" - ); - } - - #[test] - fn dca_mainline() { - let mut dca = Dca::new_with(&DcaParams { - gain: 1.0.into(), - pan: BipolarNormal::zero(), - }); - const VALUE_IN: Sample = Sample(0.5); - const VALUE: Sample = Sample(0.5); - assert_eq!( - dca.transform_audio_to_stereo(VALUE_IN), - StereoSample::new(VALUE * 0.75, VALUE * 0.75), - "Pan center should give 75% equally to each channel" - ); - - dca.set_pan(BipolarNormal::new(-1.0)); - assert_eq!( - dca.transform_audio_to_stereo(VALUE_IN), - StereoSample::new(VALUE, 0.0.into()), - "Pan left should give 100% to left channel" - ); - - dca.set_pan(BipolarNormal::new(1.0)); - assert_eq!( - dca.transform_audio_to_stereo(VALUE_IN), - StereoSample::new(0.0.into(), VALUE), - "Pan right should give 100% to right channel" - ); - } - - #[test] - fn convert_sample_to_i16() { - assert_eq!(Sample::MAX.into_i16(), i16::MAX); - assert_eq!(Sample::MIN.into_i16(), i16::MIN); - assert_eq!(Sample::SILENCE.into_i16(), 0); - } - - #[test] - fn convert_stereo_sample_to_i16() { - let s = StereoSample(Sample::MIN, Sample::MAX); - let (l, r) = s.into_i16(); - assert_eq!(l, i16::MIN); - assert_eq!(r, i16::MAX); - } + use super::*; } diff --git a/core/src/midi.rs b/core/src/midi.rs index 60f6ccef..dec244d4 100644 --- a/core/src/midi.rs +++ b/core/src/midi.rs @@ -1,7 +1,7 @@ // Copyright (c) 2023 Mike Tsao. All rights reserved. use bit_vec::BitVec; -use ensnare::{midi::prelude::*, prelude::*}; +use ensnare_core::{midi::prelude::*, prelude::*}; use std::fmt::Debug; pub fn note_description_to_frequency(text: &str) -> Option { @@ -20,59 +20,13 @@ pub fn note_description_to_frequency(text: &str) -> Option { None } -/// [MidiNoteMinder] watches a MIDI message stream and remembers which notes are -/// currently active (we've gotten a note-on without a note-off). Then, when -/// asked, it produces a list of MIDI message that turn off all active notes. -/// -/// [MidiNoteMinder] doesn't know about [MidiChannel]s. It's up to the caller to -/// track channels, or else assume that if we got any message, it's for us, and -/// that the same is true for recipients of whatever we send. -#[derive(Debug)] -pub struct MidiNoteMinder { - active_notes: BitVec, -} -impl Default for MidiNoteMinder { - fn default() -> Self { - Self { - active_notes: BitVec::from_elem(128, false), - } - } -} -impl MidiNoteMinder { - pub fn watch_message(&mut self, message: &MidiMessage) { - #[allow(unused_variables)] - match message { - MidiMessage::NoteOff { key, vel } => { - self.active_notes.set(key.as_int() as usize, false); - } - MidiMessage::NoteOn { key, vel } => { - self.active_notes - .set(key.as_int() as usize, *vel != u7::from(0)); - } - _ => {} - } - } - - pub fn generate_off_messages(&self) -> Vec { - let mut v = Vec::default(); - for (i, active_note) in self.active_notes.iter().enumerate() { - if active_note { - v.push(MidiMessage::NoteOff { - key: u7::from_int_lossy(i as u8), - vel: u7::from(0), - }) - } - } - v - } -} - #[cfg(test)] mod tests { - use super::{note_description_to_frequency, MidiNoteMinder}; - use ensnare::{ + use super::note_description_to_frequency; + use ensnare_core::{ midi::{new_note_off, new_note_on, MidiNote}, - prelude::FrequencyHz, + prelude::*, + temp_impls::controllers::old_sequencer::MidiNoteMinder, }; use midly::{num::u7, MidiMessage}; diff --git a/core/src/time.rs b/core/src/time.rs index 553af335..5ea70fb7 100644 --- a/core/src/time.rs +++ b/core/src/time.rs @@ -306,42 +306,6 @@ impl Ord for PerfectTimeUnit { } impl Eq for PerfectTimeUnit {} -#[derive(Clone, Copy, Debug, Default, PartialEq, PartialOrd)] -pub struct Seconds(pub f64); -impl Seconds { - pub fn zero() -> Seconds { - Seconds(0.0) - } - - pub fn infinite() -> Seconds { - Seconds(-1.0) - } -} -impl From for Seconds { - fn from(value: f64) -> Self { - Self(value) - } -} -impl From for Seconds { - fn from(value: f32) -> Self { - Self(value as f64) - } -} -impl Add for Seconds { - type Output = Seconds; - - fn add(self, rhs: f64) -> Self::Output { - Seconds(self.0 + rhs) - } -} -impl Add for Seconds { - type Output = Seconds; - - fn add(self, rhs: Seconds) -> Self::Output { - Seconds(self.0 + rhs.0) - } -} - #[derive(Clone, Copy, Debug, Default, PartialEq)] pub struct MidiTicks(pub usize); @@ -393,7 +357,7 @@ impl Eq for MidiTicks {} #[cfg(test)] mod tests { - use ensnare::prelude::*; + use ensnare_core::prelude::*; #[cfg(obsolete)] mod obsolete { diff --git a/core/src/voices.rs b/core/src/voices.rs index 920265ed..def3c687 100644 --- a/core/src/voices.rs +++ b/core/src/voices.rs @@ -9,11 +9,7 @@ use derive_more::{Add, Display, From, Into}; use ensnare::prelude::*; use rustc_hash::FxHashMap; -#[derive(Clone, Copy, Debug, PartialEq, Eq, From, Into, Add, Display)] -#[cfg_attr( - feature = "serialization", - derive(serde::Serialize, serde::Deserialize) -)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, From, Into, Add, Display, Serialize, Deserialize)] pub struct VoiceCount(usize); impl Default for VoiceCount { fn default() -> Self { diff --git a/egui/src/lib.rs b/egui/src/lib.rs index 54970a1a..fb0c413c 100644 --- a/egui/src/lib.rs +++ b/egui/src/lib.rs @@ -5,52 +5,5 @@ use eframe::{ emath, epaint::{self, pos2, vec2, Color32, Pos2, Rect, Stroke}, }; -use ensnare::traits::Displays; +use ensnare::prelude::*; use serde::{Deserialize, Serialize}; - -#[derive(Debug, Default, Serialize, Deserialize)] -pub struct Waveform {} -impl Displays for Waveform { - fn ui(&mut self, ui: &mut egui::Ui) -> egui::Response { - let color = if ui.visuals().dark_mode { - Color32::from_additive_luminance(196) - } else { - Color32::from_black_alpha(240) - }; - - Frame::canvas(ui.style()).show(ui, |ui| { - ui.ctx().request_repaint(); - let time = ui.input(|i| i.time); - - let desired_size = ui.available_width() * vec2(1.0, 0.35); - let (_id, rect) = ui.allocate_space(desired_size); - - let to_screen = - emath::RectTransform::from_to(Rect::from_x_y_ranges(0.0..=1.0, -1.0..=1.0), rect); - - let mut shapes = vec![]; - - for &mode in &[2, 3, 5] { - let mode = mode as f64; - let n = 120; - let speed = 1.5; - - let points: Vec = (0..=n) - .map(|i| { - let t = i as f64 / (n as f64); - let amp = (time * speed * mode).sin() / mode; - let y = amp * (t * std::f64::consts::TAU / 2.0 * mode).sin(); - to_screen * pos2(t as f32, y as f32) - }) - .collect(); - - let thickness = 10.0 / mode as f32; - shapes.push(epaint::Shape::line(points, Stroke::new(thickness, color))); - } - - ui.painter().extend(shapes); - }); - ui.vertical_centered(|ui| ui.add(Label::new("hello!"))) - .inner - } -} diff --git a/entities/Cargo.toml b/entities/Cargo.toml index 9dc35be4..7b895818 100644 --- a/entities/Cargo.toml +++ b/entities/Cargo.toml @@ -14,7 +14,7 @@ egui_extras_xt = { git = "https://github.com/sowbug/egui_extras_xt", features = "knobs", "displays", ] } -ensnare = { path = "../../ensnare" } +ensnare-core = { path = "../../ensnare/core" } ensnare-proc-macros = { path = "../../ensnare/proc-macros" } float-cmp = "0.9" groove-core = { path = "../core" } diff --git a/entities/src/controllers/arpeggiator.rs b/entities/src/controllers/arpeggiator.rs deleted file mode 100644 index 3b07116c..00000000 --- a/entities/src/controllers/arpeggiator.rs +++ /dev/null @@ -1,169 +0,0 @@ -// Copyright (c) 2023 Mike Tsao. All rights reserved. - -use super::{sequencers::Sequencer, SequencerParams}; -use eframe::egui::{self, ComboBox, Ui}; -use ensnare::{ - midi::{new_note_off, new_note_on, MidiChannel, MidiMessage, MidiMessagesFn}, - prelude::*, - traits::prelude::*, -}; -use ensnare_proc_macros::{Control, IsController, Params, Uid}; -use serde::{Deserialize, Serialize}; -use std::{ops::Range, option::Option}; - -/// [Arpeggiator] creates [arpeggios](https://en.wikipedia.org/wiki/Arpeggio), -/// which "is a type of broken chord in which the notes that compose a chord are -/// individually and quickly sounded in a progressive rising or descending -/// order." You can also think of it as a hybrid MIDI instrument and MIDI -/// controller; you play it with MIDI, but instead of producing audio, it -/// produces more MIDI. -#[derive(Debug, Control, IsController, Params, Uid, Serialize, Deserialize)] -pub struct Arpeggiator { - uid: Uid, - midi_channel_out: MidiChannel, - sequencer: Sequencer, - - #[control] - #[params] - bpm: ParameterType, - - // A poor-man's semaphore that allows note-off events to overlap with the - // current note without causing it to shut off. Example is a legato - // playing-style of the MIDI instrument that controls the arpeggiator. If we - // turned on and off solely by the last note-on/off we received, then the - // arpeggiator would frequently get clipped. - note_semaphore: i16, -} -impl Serializable for Arpeggiator {} -impl Configurable for Arpeggiator { - fn sample_rate(&self) -> SampleRate { - self.sequencer.sample_rate() - } - - fn update_sample_rate(&mut self, sample_rate: SampleRate) { - self.sequencer.update_sample_rate(sample_rate); - } -} -impl Controls for Arpeggiator { - fn update_time(&mut self, range: &Range) { - self.sequencer.update_time(range); - } - - fn work(&mut self, control_events_fn: &mut ControlEventsFn) { - self.sequencer.work(control_events_fn) - } - - fn is_finished(&self) -> bool { - self.sequencer.is_finished() - } - - fn play(&mut self) { - self.sequencer.play(); - } - - fn stop(&mut self) { - self.sequencer.stop(); - } - - fn skip_to_start(&mut self) { - self.sequencer.skip_to_start(); - } - - fn is_performing(&self) -> bool { - self.sequencer.is_performing() - } -} -impl HandlesMidi for Arpeggiator { - fn handle_midi_message( - &mut self, - _channel: MidiChannel, - message: MidiMessage, - midi_messages_fn: &mut MidiMessagesFn, - ) { - match message { - MidiMessage::NoteOff { key: _, vel: _ } => { - self.note_semaphore -= 1; - if self.note_semaphore < 0 { - self.note_semaphore = 0; - } - self.sequencer.enable(self.note_semaphore > 0); - } - MidiMessage::NoteOn { key, vel } => { - self.note_semaphore += 1; - self.rebuild_sequence(key.as_int(), vel.as_int()); - self.sequencer.enable(true); - - // TODO: this scratches the itch of needing to respond - // to a note-down with a note *during this slice*, but - // it also has an edge condition where we need to cancel - // a different note that was might have been supposed to - // be sent instead during this slice, or at least - // immediately shut it off. This seems to require a - // two-phase Tick handler (one to decide what we're - // going to send, and another to send it), and an - // internal memory of which notes we've asked the - // downstream to play. TODO TODO TODO - self.sequencer - .generate_midi_messages_for_current_frame(midi_messages_fn); - } - MidiMessage::Aftertouch { key: _, vel: _ } => todo!(), - MidiMessage::Controller { - controller: _, - value: _, - } => todo!(), - MidiMessage::ProgramChange { program: _ } => todo!(), - MidiMessage::ChannelAftertouch { vel: _ } => todo!(), - MidiMessage::PitchBend { bend: _ } => todo!(), - } - } -} -impl Displays for Arpeggiator { - fn ui(&mut self, ui: &mut Ui) -> egui::Response { - let alternatives = ["major", "minor"]; - let mut selected = 1; - ComboBox::from_label("Scale") - .show_index(ui, &mut selected, alternatives.len(), |i| alternatives[i]) - } -} -impl Arpeggiator { - pub fn new_with(params: &ArpeggiatorParams, midi_channel_out: MidiChannel) -> Self { - Self { - uid: Default::default(), - midi_channel_out, - bpm: params.bpm, - sequencer: Sequencer::new_with(&SequencerParams { bpm: params.bpm() }), - note_semaphore: Default::default(), - } - } - - fn insert_one_note(&mut self, when: &MusicalTime, duration: &MusicalTime, key: u8, vel: u8) { - self.sequencer - .insert(when, self.midi_channel_out, new_note_on(key, vel)); - self.sequencer.insert( - &(*when + *duration), - self.midi_channel_out, - new_note_off(key, 0), - ); - } - - fn rebuild_sequence(&mut self, key: u8, vel: u8) { - self.sequencer.clear(); - - let start_beat = self.sequencer.cursor().clone(); - let duration = MusicalTime::new_with_parts(4); // TODO: we're ignoring time signature! - let scale_notes = [0, 2, 4, 5, 7, 9, 11]; - for (index, offset) in scale_notes.iter().enumerate() { - // TODO - more examples of needing wider range for smaller parts - let when = start_beat + MusicalTime::new_with_parts(4 * index); - self.insert_one_note(&when, &duration, key + offset, vel); - } - } - - pub fn bpm(&self) -> f64 { - self.bpm - } - - pub fn set_bpm(&mut self, bpm: ParameterType) { - self.bpm = bpm; - } -} diff --git a/entities/src/controllers/calculator.rs b/entities/src/controllers/calculator.rs index 27144134..003d977d 100644 --- a/entities/src/controllers/calculator.rs +++ b/entities/src/controllers/calculator.rs @@ -17,7 +17,7 @@ use eframe::{ epaint::{Color32, Stroke, Vec2}, }; use egui_extras_xt::displays::SegmentedDisplayWidget; -use ensnare::{ +use ensnare_core::{ instruments::Synthesizer, midi::prelude::*, prelude::*, traits::prelude::*, voices::VoicePerNoteStore, }; @@ -406,7 +406,7 @@ impl Configurable for Engine { fn update_sample_rate(&mut self, _sample_rate: SampleRate) {} - fn update_tempo(&mut self, _tempo: ensnare::time::Tempo) {} + fn update_tempo(&mut self, _tempo: Tempo) {} fn update_time_signature(&mut self, _time_signature: TimeSignature) {} } @@ -1385,7 +1385,7 @@ impl Displays for Calculator { mod tests { use super::{Engine, Pattern, Step}; use crate::controllers::calculator::{CalculatorTempo, Percentage, TempoValue}; - use ensnare::traits::prelude::*; + use ensnare_core::traits::prelude::*; impl Engine { fn chain_active_pattern(&mut self) { diff --git a/entities/src/controllers/control_trip.rs b/entities/src/controllers/control_trip.rs index 24beaddc..aa15510b 100644 --- a/entities/src/controllers/control_trip.rs +++ b/entities/src/controllers/control_trip.rs @@ -1,7 +1,7 @@ // Copyright (c) 2023 Mike Tsao. All rights reserved. use core::fmt::Debug; -use ensnare::prelude::*; +use ensnare_core::prelude::*; use std::option::Option; #[derive(Clone, Copy, Debug)] diff --git a/entities/src/controllers/lfo.rs b/entities/src/controllers/lfo.rs index c02c35d5..43057f57 100644 --- a/entities/src/controllers/lfo.rs +++ b/entities/src/controllers/lfo.rs @@ -1,160 +1 @@ // Copyright (c) 2023 Mike Tsao. All rights reserved. - -use core::fmt::Debug; -use eframe::egui::{Response, Ui}; -use ensnare::{prelude::*, traits::prelude::*}; -use ensnare_proc_macros::{Control, IsController, Params, Uid}; -use groove_core::generators::{Oscillator, OscillatorParams, Waveform}; -use serde::{Deserialize, Serialize}; -use std::{ - ops::{Range, RangeInclusive}, - option::Option, -}; - -/// Uses an internal LFO as a control source. -#[derive(Debug, Control, IsController, Params, Uid, Serialize, Deserialize)] -pub struct LfoController { - uid: Uid, - - #[control] - #[params] - waveform: Waveform, - #[control] - #[params] - frequency: FrequencyHz, - - oscillator: Oscillator, - - #[serde(skip)] - is_performing: bool, - - #[serde(skip)] - waveform_widget: groove_egui::Waveform, - - #[serde(skip)] - time_range: Range, - - #[serde(skip)] - last_frame: usize, -} -impl Serializable for LfoController {} -impl Configurable for LfoController { - fn sample_rate(&self) -> SampleRate { - self.oscillator.sample_rate() - } - fn update_sample_rate(&mut self, sample_rate: SampleRate) { - self.oscillator.update_sample_rate(sample_rate); - } -} -impl Controls for LfoController { - fn update_time(&mut self, range: &Range) { - self.time_range = range.clone(); - } - - fn work(&mut self, control_events_fn: &mut ControlEventsFn) { - let frames = self.time_range.start.as_frames( - Tempo::from(120), - SampleRate::from(self.oscillator.sample_rate()), - ); - - if frames != self.last_frame { - let tick_count = if frames >= self.last_frame { - // normal case; oscillator should advance the calculated number - // of frames - // - // TODO: this is unlikely to be frame-accurate, because - // Orchestrator is currently going from frames -> beats - // (inaccurate), and then we're going from beats -> frames. We - // could include frame count in update_time(), as discussed in - // #132, which would mean we don't have to be smart at all about - // it. - frames - self.last_frame - } else { - self.last_frame = frames; - 0 - }; - self.last_frame += tick_count; - self.oscillator.tick(tick_count); - } - control_events_fn( - self.uid, - EntityEvent::Control(self.oscillator.value().into()), - ); - } - - fn is_finished(&self) -> bool { - true - } - - fn play(&mut self) { - self.is_performing = true; - } - - fn stop(&mut self) { - self.is_performing = false; - } - - fn skip_to_start(&mut self) { - // TODO: think how important it is for LFO oscillator to start at zero - } - - fn is_performing(&self) -> bool { - self.is_performing - } -} -impl HandlesMidi for LfoController {} -impl LfoController { - pub fn new_with(params: &LfoControllerParams) -> Self { - Self { - uid: Default::default(), - oscillator: Oscillator::new_with(&OscillatorParams { - waveform: params.waveform, - frequency: params.frequency, - ..Default::default() - }), - waveform: params.waveform(), - frequency: params.frequency(), - is_performing: false, - - waveform_widget: Default::default(), - time_range: Default::default(), - last_frame: Default::default(), - } - } - - pub const fn frequency_range() -> RangeInclusive { - 0.0..=100.0 - } - - pub fn waveform(&self) -> Waveform { - self.waveform - } - - pub fn set_waveform(&mut self, waveform: Waveform) { - self.waveform = waveform; - self.oscillator.set_waveform(waveform); - } - - pub fn frequency(&self) -> FrequencyHz { - self.frequency - } - - pub fn set_frequency(&mut self, frequency: FrequencyHz) { - self.frequency = frequency; - self.oscillator.set_frequency(frequency); - } -} - -impl Displays for LfoController { - fn ui(&mut self, ui: &mut Ui) -> Response { - // TODO: come up with a better pattern for .changed() to happen at - // the same level as whoever called show(). - if self.frequency.show(ui, Self::frequency_range()) { - self.set_frequency(self.frequency); - } - if self.waveform.show(ui).inner.is_some() { - self.set_waveform(self.waveform); - } - self.waveform_widget.ui(ui) - } -} diff --git a/entities/src/controllers/mod.rs b/entities/src/controllers/mod.rs index 454c93d2..e6c5424c 100644 --- a/entities/src/controllers/mod.rs +++ b/entities/src/controllers/mod.rs @@ -1,512 +1,38 @@ // Copyright (c) 2023 Mike Tsao. All rights reserved. use eframe::egui::Ui; -use ensnare::{midi::prelude::*, prelude::*, traits::prelude::*}; -use ensnare_proc_macros::{Control, IsController, IsControllerEffect, Params, Uid}; -use midly::MidiMessage; - +use ensnare_core::{midi::prelude::*, prelude::*, temp_impls::prelude::*, traits::prelude::*}; +use ensnare_proc_macros::{Control, IsController, Uid}; use serde::{Deserialize, Serialize}; use std::ops::Range; -pub use arpeggiator::{Arpeggiator, ArpeggiatorParams}; pub use calculator::Calculator; pub use control_trip::{ControlPath, ControlStep}; -pub use lfo::{LfoController, LfoControllerParams}; -pub use patterns::{NewPattern, Note, Pattern, PatternManager, PatternProgrammer}; -pub use sequencers::{Sequencer, SequencerParams}; -mod arpeggiator; mod calculator; mod control_trip; mod lfo; -mod patterns; -mod sequencers; -#[derive(Clone, Copy, Debug)] -#[cfg_attr( - feature = "serialization", - derive(Serialize, Deserialize), - serde(rename = "midi", rename_all = "kebab-case") -)] +#[derive(Clone, Copy, Debug, Serialize, Deserialize)] +#[serde(rename = "midi", rename_all = "kebab-case")] pub struct MidiChannelParams { pub midi_in: MidiChannel, pub midi_out: MidiChannel, } -#[derive(Clone, Copy, Debug)] -#[cfg_attr( - feature = "serialization", - derive(Serialize, Deserialize), - serde(rename = "midi-in", rename_all = "kebab-case") -)] +#[derive(Clone, Copy, Debug, Serialize, Deserialize)] +#[serde(rename = "midi", rename_all = "kebab-case")] pub struct MidiChannelInputParams { pub midi_in: MidiChannel, } -#[derive(Clone, Copy, Debug)] -#[cfg_attr( - feature = "serialization", - derive(Serialize, Deserialize), - serde(rename = "midi-out", rename_all = "kebab-case") -)] +#[derive(Clone, Copy, Debug, Serialize, Deserialize)] +#[serde(rename = "midi", rename_all = "kebab-case")] pub struct MidiChannelOutputParams { pub midi_out: MidiChannel, } -/// [Timer] runs for a specified amount of time, then indicates that it's done. -/// It is useful when you need something to happen after a certain amount of -/// wall-clock time, rather than musical time. -#[derive(Debug, Control, IsController, Uid, Serialize, Deserialize)] -pub struct Timer { - uid: Uid, - - duration: MusicalTime, - - #[serde(skip)] - is_performing: bool, - - #[serde(skip)] - is_finished: bool, - - #[serde(skip)] - end_time: Option, -} -impl Serializable for Timer {} -impl Timer { - pub fn new_with(duration: MusicalTime) -> Self { - Self { - uid: Default::default(), - duration, - is_performing: false, - is_finished: false, - end_time: Default::default(), - } - } - - pub fn duration(&self) -> MusicalTime { - self.duration - } - - pub fn set_duration(&mut self, duration: MusicalTime) { - self.duration = duration; - } -} -impl HandlesMidi for Timer {} -impl Configurable for Timer { - fn update_sample_rate(&mut self, _sample_rate: SampleRate) {} -} -impl Controls for Timer { - fn update_time(&mut self, range: &Range) { - if self.is_performing { - if self.duration == MusicalTime::default() { - // Zero-length timers fire immediately. - self.is_finished = true; - } else { - if let Some(end_time) = self.end_time { - if range.contains(&end_time) { - self.is_finished = true; - } - } else { - // The first time we're called with an update_time() while - // performing, we take that as the start of the timer. - self.end_time = Some(range.start + self.duration); - } - } - } - } - - fn work(&mut self, _messages_fn: &mut ControlEventsFn) { - // All the state was computable during update_time(), so there's nothing to do here. - } - - fn is_finished(&self) -> bool { - self.is_finished - } - - fn play(&mut self) { - self.is_performing = true; - } - - fn stop(&mut self) { - self.is_performing = false; - } - - fn skip_to_start(&mut self) { - // TODO: think how important it is for LFO oscillator to start at zero - } - - fn is_performing(&self) -> bool { - self.is_performing - } -} - -// TODO: needs tests! -/// [Trigger] issues a control signal after a specified amount of time. -#[derive(Debug, Control, IsController, Uid, Serialize, Deserialize)] -pub struct Trigger { - uid: Uid, - - timer: Timer, - - value: ControlValue, - - has_triggered: bool, - is_performing: bool, -} -impl Serializable for Trigger {} -impl Controls for Trigger { - fn update_time(&mut self, range: &Range) { - self.timer.update_time(range) - } - - fn work(&mut self, control_events_fn: &mut ControlEventsFn) { - if self.timer.is_finished() && self.is_performing && !self.has_triggered { - self.has_triggered = true; - control_events_fn(self.uid, EntityEvent::Control(self.value)); - } - } - - fn is_finished(&self) -> bool { - self.timer.is_finished() - } - - fn play(&mut self) { - self.is_performing = true; - self.timer.play(); - } - - fn stop(&mut self) { - self.is_performing = false; - self.timer.stop(); - } - - fn skip_to_start(&mut self) { - self.has_triggered = false; - self.timer.skip_to_start(); - } - - fn is_performing(&self) -> bool { - self.is_performing - } -} -impl Configurable for Trigger { - fn sample_rate(&self) -> SampleRate { - self.timer.sample_rate() - } - fn update_sample_rate(&mut self, sample_rate: SampleRate) { - self.timer.update_sample_rate(sample_rate) - } -} -impl HandlesMidi for Trigger {} -impl Trigger { - pub fn new_with(timer: Timer, value: ControlValue) -> Self { - Self { - uid: Default::default(), - timer, - value, - has_triggered: false, - is_performing: false, - } - } - - pub fn value(&self) -> ControlValue { - self.value - } - - pub fn set_value(&mut self, value: ControlValue) { - self.value = value; - } -} - -#[derive(Debug, Default, Clone, Copy, Serialize, Deserialize)] -pub enum SignalPassthroughType { - #[default] - /// Maps -1.0..=1.0 to 0.0..=1.0. Min amplitude becomes 0.0, silence becomes - /// 0.5, and max amplitude becomes 1.0. - Compressed, - - /// Based on the absolute value of the sample. Silence is 0.0, and max - /// amplitude of either polarity is 1.0. - Amplitude, - - /// Based on the absolute value of the sample. Silence is 1.0, and max - /// amplitude of either polarity is 0.0. - AmplitudeInverted, -} - -/// Uses an input signal as a control source. Transformation depends on -/// configuration. Uses the standard Sample::from(StereoSample) methodology of -/// averaging the two channels to create a single signal. -#[derive(Control, Debug, Default, IsControllerEffect, Params, Uid, Serialize, Deserialize)] -pub struct SignalPassthroughController { - uid: Uid, - passthrough_type: SignalPassthroughType, - - #[serde(skip)] - control_value: ControlValue, - - // We don't issue consecutive identical events, so we need to remember - // whether we've sent the current value. - #[serde(skip)] - has_value_been_issued: bool, - - #[serde(skip)] - is_performing: bool, -} -impl Serializable for SignalPassthroughController {} -impl Configurable for SignalPassthroughController {} -impl Controls for SignalPassthroughController { - fn update_time(&mut self, _range: &Range) { - // We can ignore because we already have our own de-duplicating logic. - } - - fn work(&mut self, control_events_fn: &mut ControlEventsFn) { - if !self.is_performing { - return; - } - if !self.has_value_been_issued { - self.has_value_been_issued = true; - control_events_fn(self.uid, EntityEvent::Control(self.control_value)) - } - } - - fn is_finished(&self) -> bool { - true - } - - fn play(&mut self) { - self.is_performing = true; - } - - fn stop(&mut self) { - self.is_performing = false; - } - - fn skip_to_start(&mut self) {} - - fn is_performing(&self) -> bool { - self.is_performing - } -} -impl HandlesMidi for SignalPassthroughController {} -impl TransformsAudio for SignalPassthroughController { - fn transform_audio(&mut self, input_sample: StereoSample) -> StereoSample { - let sample: Sample = input_sample.into(); - let control_value = match self.passthrough_type { - SignalPassthroughType::Compressed => { - let as_bipolar_normal: BipolarNormal = sample.into(); - as_bipolar_normal.into() - } - SignalPassthroughType::Amplitude => ControlValue(sample.0.abs()), - SignalPassthroughType::AmplitudeInverted => ControlValue(1.0 - sample.0.abs()), - }; - if self.control_value != control_value { - self.has_value_been_issued = false; - self.control_value = control_value; - } - input_sample - } - - fn transform_channel(&mut self, _channel: usize, _input_sample: Sample) -> Sample { - // We've overridden transform_audio(), so nobody should be calling this - // method. - todo!(); - } -} -impl SignalPassthroughController { - pub fn new() -> Self { - Default::default() - } - - pub fn new_amplitude_passthrough_type() -> Self { - Self { - passthrough_type: SignalPassthroughType::Amplitude, - ..Default::default() - } - } - - pub fn new_amplitude_inverted_passthrough_type() -> Self { - Self { - passthrough_type: SignalPassthroughType::AmplitudeInverted, - ..Default::default() - } - } -} - -enum TestControllerAction { - Nothing, - NoteOn, - NoteOff, -} - -// -// use serde::{Deserialize, Serialize}; - -/// An [IsController](groove_core::traits::IsController) that emits a MIDI -/// note-on event on each beat, and a note-off event on each half-beat. -#[derive(Debug, Default, Control, IsController, Params, Uid, Serialize, Deserialize)] -pub struct ToyController { - uid: Uid, - - #[serde(skip)] - midi_channel_out: MidiChannel, - - #[serde(skip)] - is_enabled: bool, - #[serde(skip)] - is_playing: bool, - #[serde(skip)] - is_performing: bool, - - #[serde(skip)] - time_range: Range, - - #[serde(skip)] - last_time_handled: MusicalTime, -} -impl Serializable for ToyController {} -impl Controls for ToyController { - fn update_time(&mut self, range: &Range) { - self.time_range = range.clone(); - } - - fn work(&mut self, control_events_fn: &mut ControlEventsFn) { - match self.what_to_do() { - TestControllerAction::Nothing => {} - TestControllerAction::NoteOn => { - // This is elegant, I hope. If the arpeggiator is - // disabled during play, and we were playing a note, - // then we still send the off note, - if self.is_enabled && self.is_performing { - self.is_playing = true; - control_events_fn( - self.uid, - EntityEvent::Midi(self.midi_channel_out, new_note_on(60, 127)), - ); - } - } - TestControllerAction::NoteOff => { - if self.is_playing { - control_events_fn( - self.uid, - EntityEvent::Midi(self.midi_channel_out, new_note_off(60, 0)), - ); - } - } - } - } - - fn is_finished(&self) -> bool { - true - } - - fn play(&mut self) { - self.is_performing = true; - } - - fn stop(&mut self) { - self.is_performing = false; - } - - fn skip_to_start(&mut self) {} - - fn is_performing(&self) -> bool { - self.is_performing - } -} -impl Configurable for ToyController { - fn update_sample_rate(&mut self, _sample_rate: SampleRate) {} -} -impl HandlesMidi for ToyController { - fn handle_midi_message( - &mut self, - _channel: MidiChannel, - message: MidiMessage, - _: &mut MidiMessagesFn, - ) { - #[allow(unused_variables)] - match message { - MidiMessage::NoteOff { key, vel } => self.is_enabled = false, - MidiMessage::NoteOn { key, vel } => self.is_enabled = true, - _ => todo!(), - } - } -} -impl ToyController { - pub fn new_with(_params: &ToyControllerParams, midi_channel_out: MidiChannel) -> Self { - Self { - midi_channel_out, - ..Default::default() - } - } - - fn what_to_do(&mut self) -> TestControllerAction { - if !self.time_range.contains(&self.last_time_handled) { - self.last_time_handled = self.time_range.start; - if self.time_range.start.units() == 0 { - if self.time_range.start.parts() == 0 { - return TestControllerAction::NoteOn; - } - if self.time_range.start.parts() == 8 { - return TestControllerAction::NoteOff; - } - } - } - TestControllerAction::Nothing - } -} -// impl TestsValues for TestController { -// fn has_checkpoint_values(&self) -> bool { -// !self.checkpoint_values.is_empty() -// } - -// fn time_unit(&self) -> &ClockTimeUnit { -// &self.time_unit -// } - -// fn checkpoint_time(&self) -> f32 { -// self.checkpoint -// } - -// fn advance_checkpoint_time(&mut self) { -// self.checkpoint += self.checkpoint_delta; -// } - -// fn value_to_check(&self) -> f32 { -// self.tempo -// } - -// fn pop_checkpoint_value(&mut self) -> Option { -// self.checkpoint_values.pop_front() -// } -// } - -impl Displays for Timer { - fn ui(&mut self, ui: &mut Ui) -> eframe::egui::Response { - ui.label(self.name()) - } -} - -impl Displays for Trigger { - fn ui(&mut self, ui: &mut Ui) -> eframe::egui::Response { - ui.label(self.name()) - } -} - -impl Displays for SignalPassthroughController { - fn ui(&mut self, ui: &mut Ui) -> eframe::egui::Response { - ui.label(self.name()) - } -} - -impl Displays for ToyController { - fn ui(&mut self, ui: &mut Ui) -> eframe::egui::Response { - ui.label(self.name()) - } -} - #[cfg(test)] mod tests { - use crate::controllers::{Timer, Trigger}; - use ensnare::{prelude::*, traits::prelude::*}; + use ensnare_core::{prelude::*, temp_impls::prelude::*, traits::prelude::*}; use std::ops::Range; #[test] diff --git a/entities/src/controllers/patterns.rs b/entities/src/controllers/patterns.rs deleted file mode 100644 index 303a63fa..00000000 --- a/entities/src/controllers/patterns.rs +++ /dev/null @@ -1,735 +0,0 @@ -// Copyright (c) 2023 Mike Tsao. All rights reserved. - -use super::Sequencer; -use eframe::{ - egui::{Frame, Grid, ScrollArea, Sense, Ui}, - emath::{self, RectTransform}, - epaint::{Color32, Pos2, Rect, Rounding, Shape, Stroke, Vec2}, -}; -//use btreemultimap::BTreeMultiMap; -use ensnare::{ - midi::{MidiChannel, MidiMessage}, - prelude::*, - traits::prelude::*, -}; -use ensnare_proc_macros::{Control, IsController, Uid}; -use groove_core::time::PerfectTimeUnit; -use serde::{Deserialize, Serialize}; -use std::{cmp, fmt::Debug, ops::Range}; - -/// A [Note] represents a key-down and key-up event pair that lasts for a -/// specified duration. -#[derive(Clone, Debug, Default, Serialize, Deserialize)] -pub struct Note { - pub key: u8, - pub velocity: u8, - pub duration: PerfectTimeUnit, // expressed as multiple of the containing Pattern's note value. -} - -/// A [Pattern] is a series of [Note] rows that play simultaneously. -/// [PatternManager] uses [Patterns](Pattern) to program a [Sequencer]. -#[derive(Clone, Debug, Default, Serialize, Deserialize)] -pub struct Pattern { - pub note_value: Option, - pub notes: Vec>, -} - -impl Pattern { - #[allow(dead_code)] - pub(crate) fn new() -> Self { - Self { - ..Default::default() - } - } - - pub fn note_to_value(note: &str) -> u8 { - // TODO https://en.wikipedia.org/wiki/Scientific_pitch_notation labels, - // e.g., for General MIDI percussion - note.parse().unwrap_or_default() - } -} - -// There is so much paperwork for a vector because this will eventually become a -// substantial part of the GUI experience. -/// [PatternManager] stores all the [Patterns] that make up a song. -#[derive(Clone, Debug, Default, Control, IsController, Uid, Serialize, Deserialize)] -pub struct PatternManager { - uid: Uid, - patterns: Vec>, - selected_pattern: usize, -} -impl Serializable for PatternManager {} -impl HandlesMidi for PatternManager {} -impl Configurable for PatternManager {} -impl Controls for PatternManager { - fn update_time(&mut self, _range: &Range) {} - - fn work(&mut self, _: &mut ControlEventsFn) {} - - fn is_finished(&self) -> bool { - true - } - fn play(&mut self) {} - fn stop(&mut self) {} - fn skip_to_start(&mut self) {} - fn is_performing(&self) -> bool { - false - } -} -impl PatternManager { - pub fn new() -> Self { - Self::default() - } - - pub fn register(&mut self, pattern: Pattern) { - self.patterns.push(pattern); - } - - pub fn patterns(&self) -> &[Pattern] { - &self.patterns - } -} - -#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] -pub struct NewNote { - key: u8, - velocity: u8, - // duration: PerfectTimeUnit, - range: Range, - - #[serde(skip)] - ui_state: NewNoteUiState, -} - -//pub type NewPatternEventsMap = BTreeMultiMap; - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct NewPattern { - // notes: NewPatternEventsMap, - notes: Vec, - - #[serde(skip)] - dragged_note: Option, - - #[serde(skip)] - drag_from_start: bool, - - #[serde(skip)] - drag_from_end: bool, -} -impl NewPattern { - pub fn add(&mut self, note: NewNote, _when: PerfectTimeUnit) { - // self.notes.insert(when, note); - self.notes.push(note); - } - - pub fn remove(&mut self, note: NewNote, _when: PerfectTimeUnit) { - // if let Some(v) = self.notes.get_vec_mut(when) { - // if v.contains(¬e) { - // v.retain(|x| *x != note); - // } - // } - self.notes.retain(|v| *v != note); - } - - pub fn clear(&mut self) { - self.notes.clear(); - } -} -impl Default for NewPattern { - fn default() -> Self { - Self { - notes: vec![ - NewNote { - key: 1, - velocity: 126, - range: Range { - start: 0.0, - end: 1.0, - }, - - ui_state: Default::default(), - }, - NewNote { - key: 80, - velocity: 127, - range: Range { - start: 3.0, - end: 4.0, - }, - - ui_state: Default::default(), - }, - ], - - dragged_note: Default::default(), - - drag_from_start: Default::default(), - - drag_from_end: Default::default(), - } - } -} - -#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] -pub(crate) enum NewNoteUiState { - #[default] - Normal, - Hovered, - Selected, -} -impl Pattern { - pub const CELL_WIDTH: f32 = 32.0; - pub const CELL_HEIGHT: f32 = 24.0; -} -impl Displays for Pattern { - fn ui(&mut self, ui: &mut Ui) -> eframe::egui::Response { - if let Some(_v) = self.note_value.as_mut() { - // TODO - trait didn't match up during migration - // v.ui(ui); - } else { - // We want to inherit the beat value from orchestrator, but we - // don't have it! TODO - // - // TODO again: actually, what does it mean for a pattern to - // inherit a beat value? The pattern isn't going to change - // automatically if the time signature changes. I don't think - // this makes sense to be optional. - BeatValue::show_inherited(ui); - } - Grid::new(ui.next_auto_id()) - .show(ui, |ui| { - for notes in self.notes.iter_mut() { - for note in notes.iter_mut() { - Frame::none() - .stroke(Stroke::new(1.0, Color32::GRAY)) - .fill(Color32::DARK_GRAY) - .show(ui, |ui| { - let mut text = format!("{}", note.key); - if ui.text_edit_singleline(&mut text).changed() { - if let Ok(key) = text.parse() { - note.key = key; - } - }; - }); - } - } - }) - .response - } -} - -impl Displays for PatternManager { - fn ui(&mut self, ui: &mut Ui) -> eframe::egui::Response { - ui.set_min_width(16.0 * Pattern::CELL_WIDTH + 8.0); // 8 pixels margin - ScrollArea::vertical().show(ui, |ui| { - let mut is_first = true; - for pattern in self.patterns.iter_mut() { - if is_first { - is_first = false; - } else { - ui.separator(); - } - pattern.ui(ui); - } - }); - ui.label("TODO") - } -} - -impl Displays for NewPattern { - fn ui(&mut self, ui: &mut Ui) -> eframe::egui::Response { - Frame::canvas(ui.style()) - .show(ui, |ui| { - let notes_vert = 24.0; - let steps_horiz = 16.0; - - let desired_size = ui.available_size_before_wrap(); - let desired_size = Vec2::new(desired_size.x, 256.0); - let (mut response, painter) = - ui.allocate_painter(desired_size, Sense::click_and_drag()); - - let to_screen = emath::RectTransform::from_to( - Rect::from_min_size(Pos2::ZERO, Vec2::splat(1.0)), - response.rect, - ); - let from_screen = to_screen.inverse(); - - painter.rect_filled(response.rect, Rounding::default(), Color32::GRAY); - for i in 0..16 { - let x = i as f32 / steps_horiz; - let lines = [to_screen * Pos2::new(x, 0.0), to_screen * Pos2::new(x, 1.0)]; - painter.line_segment( - lines, - Stroke { - width: 1.0, - color: Color32::DARK_GRAY, - }, - ); - } - - // Are we over any existing note? - let mut hovered_note = None; - // if yes, are we hovering at a duration adjustment point? - let mut hovering_at_start = false; - let mut hovering_at_end = false; - if let Some(hover_pos) = response.hover_pos() { - for note in &self.notes { - let note_rect = to_screen.transform_rect(self.rect_for_note(note)); - if note_rect.contains(hover_pos) { - const SIDE_MARGIN: f32 = 6.0; - hovered_note = Some(note.clone()); - let smaller_rect = Rect::from_min_size( - note_rect.left_top(), - Vec2::new(SIDE_MARGIN, note_rect.height()), - ); - hovering_at_start = smaller_rect.contains(hover_pos); - let smaller_rect = smaller_rect - .translate(Vec2::new(note_rect.width() - SIDE_MARGIN, 0.0)); - hovering_at_end = smaller_rect.contains(hover_pos); - break; - } - } - } - - if response.clicked() { - if let Some(pointer_pos) = response.interact_pointer_pos() { - let note = self.note_for_position( - &from_screen, - steps_horiz, - notes_vert, - pointer_pos, - ); - - if let Some(hovered) = &hovered_note { - self.remove(hovered.clone(), PerfectTimeUnit::default()); - } else { - self.add(note, PerfectTimeUnit::default()); - } - response.mark_changed(); - } - } - - if response.drag_started() { - self.drag_from_start = false; - self.drag_from_end = false; - if hovered_note.is_some() { - if hovering_at_start { - self.drag_from_start = true; - } - if hovering_at_end { - self.drag_from_end = true; - } - self.dragged_note = hovered_note.take(); - if let Some(n) = &self.dragged_note { - self.remove(n.clone(), PerfectTimeUnit::default()); - } - } else { - self.dragged_note = None; - } - } - if response.dragged() { - if let Some(old_note) = &self.dragged_note { - if let Some(pointer_pos) = response.interact_pointer_pos() { - let new_note = if self.drag_from_start || self.drag_from_end { - let canvas_pos = from_screen * pointer_pos; - NewNote { - key: old_note.key, - velocity: old_note.velocity, - range: if self.drag_from_start { - Range { - start: (canvas_pos.x * steps_horiz * 8.0).floor() - / 32.0, - end: old_note.range.end, - } - } else { - Range { - start: old_note.range.start, - end: (canvas_pos.x * steps_horiz * 8.0).floor() / 32.0, - } - }, - ui_state: Default::default(), - } - } else { - self.note_for_position( - &from_screen, - steps_horiz, - notes_vert, - pointer_pos, - ) - }; - eprintln!("dragged note {:#?}", new_note); - painter.extend(self.make_note_shapes(&new_note, &to_screen, true)); - } - } - } - if response.drag_released() { - if let Some(old_note) = &self.dragged_note { - if let Some(pointer_pos) = response.interact_pointer_pos() { - let new_note = if self.drag_from_start || self.drag_from_end { - let canvas_pos = from_screen * pointer_pos; - NewNote { - key: old_note.key, - velocity: old_note.velocity, - range: if self.drag_from_start { - Range { - start: (canvas_pos.x * steps_horiz * 8.0).floor() - / 32.0, - end: old_note.range.end, - } - } else { - Range { - start: old_note.range.start, - end: (canvas_pos.x * steps_horiz * 8.0).floor() / 32.0, - } - }, - ui_state: Default::default(), - } - } else { - self.note_for_position( - &from_screen, - steps_horiz, - notes_vert, - pointer_pos, - ) - }; - self.add(new_note, PerfectTimeUnit::default()); - } - } - self.dragged_note = None; - } - - // if response.drag_started() { - // self.is_dragging = true; - - // if let Some(pointer_pos) = response.interact_pointer_pos() { - // self.drag_start_point = Some(pointer_pos); - // self.drag_end_point = Some(pointer_pos); - // } - // // let note = - // // self.note_for_position(&from_screen, steps_horiz, notes_vert, pointer_pos); - // // self.is_drag_deleting = self.notes.contains(¬e); - // } - // if response.drag_released() { - // self.is_dragging = false; - - // if let Some(pointer_pos) = response.interact_pointer_pos() { - // self.drag_end_point = Some(pointer_pos); - // let note = self.note_for_position( - // &from_screen, - // steps_horiz, - // notes_vert, - // self.drag_start_point, - // Some(pointer_pos), - // ); - // self.drag_start_point = None; - // } - // } - - let shapes = self.notes.iter().fold(Vec::default(), |mut v, note| { - let is_highlighted = if let Some(n) = &hovered_note { - n == note - } else { - false - }; - v.extend(self.make_note_shapes(note, &to_screen, is_highlighted)); - v - }); - - painter.extend(shapes); - - response - }) - .inner - } -} -impl NewPattern { - fn make_note_shapes( - &self, - note: &NewNote, - to_screen: &RectTransform, - is_highlighted: bool, - ) -> Vec { - let rect = to_screen - .transform_rect(self.rect_for_note(note)) - .shrink(1.0); - let color = if note.ui_state == NewNoteUiState::Selected { - Color32::LIGHT_GRAY - } else if is_highlighted { - Color32::WHITE - } else { - Color32::DARK_BLUE - }; - vec![ - Shape::rect_stroke(rect, Rounding::default(), Stroke { width: 2.0, color }), - Shape::rect_filled(rect.shrink(2.0), Rounding::default(), Color32::LIGHT_BLUE), - ] - } - - fn rect_for_note(&self, note: &NewNote) -> Rect { - let notes_vert = 24.0; - let ul = Pos2 { - x: note.range.start / 4.0, - y: (note.key as f32) / notes_vert, - }; - let br = Pos2 { - x: note.range.end / 4.0, - y: (1.0 + note.key as f32) / notes_vert, - }; - Rect::from_two_pos(ul, br) - } - - fn note_for_position( - &self, - from_screen: &RectTransform, - steps_horiz: f32, - notes_vert: f32, - pointer_pos: Pos2, - ) -> NewNote { - let canvas_pos = from_screen * pointer_pos; - let key = (canvas_pos.y * notes_vert) as u8; - let when = (canvas_pos.x * steps_horiz).floor() / 4.0; - - NewNote { - key, - velocity: 127, - range: Range { - start: when, - end: when + 0.25, - }, - ui_state: Default::default(), - } - } -} - -/// [PatternProgrammer] knows how to insert a given [Pattern] into a given -/// [Sequencer], respecting the [groove_core::time::TimeSignature] that it was -/// given at creation. -#[derive(Debug)] -pub struct PatternProgrammer { - time_signature: TimeSignature, - cursor: MusicalTime, -} -impl PatternProgrammer { - pub fn new_with(time_signature: TimeSignature) -> Self { - Self { - time_signature, - cursor: MusicalTime::default(), - } - } - - pub fn reset_cursor(&mut self) { - self.cursor = MusicalTime::default(); - } - - pub fn insert_pattern_at_cursor( - &mut self, - sequencer: &mut Sequencer, - channel: &MidiChannel, - pattern: &Pattern, - ) { - let pattern_note_value = if pattern.note_value.is_some() { - pattern.note_value.as_ref().unwrap().clone() - } else { - self.time_signature.beat_value() - }; - - // If the time signature is 4/4 and the pattern is also quarter-notes, - // then the multiplier is 1.0 because no correction is needed. - // - // If it's 4/4 and eighth notes, for example, the multiplier is 0.5, - // because each pattern note represents only a half-beat. - let pattern_multiplier = BeatValue::divisor(self.time_signature.beat_value()) - / BeatValue::divisor(pattern_note_value); - - let channel = *channel; - let mut max_track_len = 0; - for track in pattern.notes.iter() { - max_track_len = cmp::max(max_track_len, track.len()); - for (i, note) in track.iter().enumerate() { - if note.key == 0 { - // This is an empty slot in the pattern. Don't do anything. - continue; - } - let i = MusicalTime::new(&self.time_signature, 0, 0, i * 4, 0); - let note_start = self.cursor + i; - sequencer.insert( - ¬e_start, - channel, - MidiMessage::NoteOn { - key: note.key.into(), - vel: note.velocity.into(), - }, - ); - // This makes the dev-loop.json playback sound funny, since no - // note lasts longer than the pattern's note value. I'm going to - // leave it like this to force myself to implement duration - // expression correctly, rather than continuing to hardcode 0.49 - // as the duration. - sequencer.insert( - &(note_start + MusicalTime::new(&self.time_signature, 0, 0, 4, 0)), - channel, - MidiMessage::NoteOff { - key: note.key.into(), - vel: note.velocity.into(), - }, - ); - } - } - - // Round up to full measure, advance cursor, and make sure sequencer - // knows we have filled this space. - let top = self.time_signature.top as f64; - let rounded_max_pattern_len = - (max_track_len as f64 * pattern_multiplier / top).ceil() * top; - self.cursor = self.cursor + MusicalTime::new_with_beats(rounded_max_pattern_len as usize); - sequencer.set_min_end_time(&self.cursor); - } - - pub fn cursor(&self) -> MusicalTime { - self.cursor - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::controllers::SequencerParams; - - #[test] - fn pattern_mainline() { - let time_signature = TimeSignature::default(); - let mut sequencer = Sequencer::new_with(&SequencerParams { bpm: 128.0 }); - let mut programmer = PatternProgrammer::new_with(time_signature); - - // note that this is five notes, but the time signature is 4/4. This - // means that we should interpret this as TWO measures, the first having - // four notes, and the second having just one note and three rests. - let note_pattern = vec![ - Note { - key: 1, - velocity: 127, - duration: PerfectTimeUnit(1.0), - }, - Note { - key: 2, - velocity: 127, - duration: PerfectTimeUnit(1.0), - }, - Note { - key: 3, - velocity: 127, - duration: PerfectTimeUnit(1.0), - }, - Note { - key: 4, - velocity: 127, - duration: PerfectTimeUnit(1.0), - }, - Note { - key: 5, - velocity: 127, - duration: PerfectTimeUnit(1.0), - }, - ]; - let expected_note_count = note_pattern.len(); - let pattern = Pattern:: { - note_value: Some(BeatValue::Quarter), - notes: vec![note_pattern], - }; - assert_eq!(pattern.notes.len(), 1); - assert_eq!(pattern.notes[0].len(), expected_note_count); - - // We don't need to call reset_cursor(), but we do just once to make - // sure it's working. - assert_eq!(programmer.cursor(), MusicalTime::default()); - programmer.reset_cursor(); - assert_eq!(programmer.cursor(), MusicalTime::default()); - - programmer.insert_pattern_at_cursor(&mut sequencer, &MidiChannel::from(0), &pattern); - assert_eq!( - programmer.cursor(), - MusicalTime::new_with_bars(&TimeSignature::default(), 2) - ); - assert_eq!(sequencer.debug_events().len(), expected_note_count * 2); // one on, one off - } - - #[test] - fn multi_pattern_track() { - let time_signature = TimeSignature::new_with(7, 8).unwrap(); - let mut sequencer = Sequencer::new_with(&SequencerParams { bpm: 128.0 }); - let mut programmer = PatternProgrammer::new_with(time_signature); - - // since these patterns are denominated in a quarter notes, but the time - // signature calls for eighth notes, they last twice as long as they - // seem. - // - // four quarter-notes in 7/8 time = 8 beats = 2 measures (1 bar, 1 beat) - let mut note_pattern_1 = Vec::new(); - for i in 1..=4 { - note_pattern_1.push(Note { - key: i, - velocity: 127, - duration: PerfectTimeUnit(1.0), - }); - } - // eight quarter-notes in 7/8 time = 16 beats = 3 measures (2 bars, 2 beats) - let mut note_pattern_2 = Vec::new(); - for i in 11..=18 { - note_pattern_2.push(Note { - key: i, - velocity: 127, - duration: PerfectTimeUnit(1.0), - }); - } - let len_1 = note_pattern_1.len(); - let len_2 = note_pattern_2.len(); - let pattern = Pattern { - note_value: Some(BeatValue::Quarter), - notes: vec![note_pattern_1, note_pattern_2], - }; - - let expected_note_count = len_1 + len_2; - assert_eq!(pattern.notes.len(), 2); - assert_eq!(pattern.notes[0].len(), len_1); - assert_eq!(pattern.notes[1].len(), len_2); - - programmer.insert_pattern_at_cursor(&mut sequencer, &MidiChannel::from(0), &pattern); - - // expect max of (2, 3) measures - let expected = MusicalTime::new(&time_signature, 3, 0, 0, 0); - assert_eq!( - programmer.cursor(), - expected, - "got {}, but was expecting {}", - programmer.cursor(), - expected - ); - assert_eq!(sequencer.debug_events().len(), expected_note_count * 2); // one on, one off - } - - #[test] - fn pattern_default_note_value() { - let time_signature = TimeSignature::new_with(7, 4).unwrap(); - let mut sequencer = Sequencer::new_with(&SequencerParams { bpm: 128.0 }); - let mut programmer = PatternProgrammer::new_with(time_signature); - let pattern = Pattern { - note_value: None, - notes: vec![vec![Note { - key: 1, - velocity: 127, - duration: PerfectTimeUnit(1.0), - }]], - }; - programmer.insert_pattern_at_cursor(&mut sequencer, &MidiChannel::from(0), &pattern); - - let expected = MusicalTime::new(&time_signature, 1, 0, 0, 0); - assert_eq!( - programmer.cursor(), - expected, - "got {} but expected {}", - programmer.cursor(), - expected - ); - } -} diff --git a/entities/src/controllers/sequencers.rs b/entities/src/controllers/sequencers.rs deleted file mode 100644 index b4b1d262..00000000 --- a/entities/src/controllers/sequencers.rs +++ /dev/null @@ -1,719 +0,0 @@ -// Copyright (c) 2023 Mike Tsao. All rights reserved. - -use btreemultimap::BTreeMultiMap; -use eframe::egui::{RichText, Ui}; -use ensnare::{ - midi::{MidiChannel, MidiMessage, MidiMessagesFn}, - prelude::*, - traits::prelude::*, -}; -use ensnare_proc_macros::{Control, IsController, Params, Uid}; -use groove_core::{midi::MidiNoteMinder, time::PerfectTimeUnit}; -use serde::{Deserialize, Serialize}; -use std::{ - fmt::Debug, - ops::{ - Bound::{Excluded, Included}, - Range, - }, -}; - -pub(crate) type BeatEventsMap = BTreeMultiMap; - -/// [Sequencer] produces MIDI according to a programmed sequence. Its unit of -/// time is the beat. -#[derive(Debug, Control, IsController, Params, Uid, Serialize, Deserialize)] -pub struct Sequencer { - uid: Uid, - #[control] - #[params] - bpm: ParameterType, - #[serde(skip)] - next_instant: PerfectTimeUnit, - #[serde(skip)] - events: BeatEventsMap, - #[serde(skip)] - last_event_time: MusicalTime, - is_disabled: bool, - is_performing: bool, - - #[serde(skip)] - should_stop_pending_notes: bool, - #[serde(skip)] - active_notes: [MidiNoteMinder; 16], - - loop_range: Option>, - is_loop_enabled: bool, - - #[cfg(obsolete)] - temp_hack_clock: Clock, - - #[serde(skip)] - time_range: Range, - #[serde(skip)] - time_range_handled: bool, -} -impl Serializable for Sequencer {} -impl HandlesMidi for Sequencer {} -impl Sequencer { - pub fn new_with(params: &SequencerParams) -> Self { - Self { - uid: Default::default(), - bpm: params.bpm(), - next_instant: Default::default(), - events: Default::default(), - last_event_time: Default::default(), - is_disabled: Default::default(), - is_performing: Default::default(), - should_stop_pending_notes: Default::default(), - active_notes: Default::default(), - loop_range: Default::default(), - is_loop_enabled: Default::default(), - #[cfg(obsolete)] - temp_hack_clock: Clock::new_with( - params.bpm(), - 0, - TimeSignature::default(), // TODO - ), - time_range: Default::default(), - time_range_handled: Default::default(), - } - } - - pub(crate) fn clear(&mut self) { - self.events.clear(); - self.last_event_time = Default::default(); - self.skip_to_start(); - } - - pub(crate) fn cursor(&self) -> &MusicalTime { - &self.time_range.start - } - - pub fn insert(&mut self, when: &MusicalTime, channel: MidiChannel, message: MidiMessage) { - self.events.insert(*when, (channel, message)); - if *when > self.last_event_time { - self.last_event_time = *when; - } - } - - pub fn is_enabled(&self) -> bool { - !self.is_disabled - } - - pub fn enable(&mut self, is_enabled: bool) { - if !self.is_disabled && !is_enabled { - self.should_stop_pending_notes = true; - } - self.is_disabled = !is_enabled; - } - - // fn is_finished(&self) -> bool { - // (self.events.is_empty() && self.last_event_time == Default::default()) - // || self.next_instant > self.last_event_time - // } - - // In the case of a silent pattern, we don't ask the sequencer to insert any - // notes, yet we do want the sequencer to run until the end of the measure. - // So we provide a facility to advance the end-time marker (which might be a - // no-op if it's already later than requested). - pub fn set_min_end_time(&mut self, when: &MusicalTime) { - if &self.last_event_time < when { - self.last_event_time = when.clone(); - } - } - - pub fn next_instant(&self) -> PerfectTimeUnit { - self.next_instant - } - - fn stop_pending_notes(&mut self, control_events_fn: &mut ControlEventsFn) { - for channel in 0..MidiChannel::MAX { - let channel_msgs = self.active_notes[channel as usize].generate_off_messages(); - for msg in channel_msgs.into_iter() { - control_events_fn(self.uid, EntityEvent::Midi(channel.into(), msg)); - } - } - } - - fn generate_midi_messages_for_interval( - &mut self, - range: &Range, - midi_messages_fn: &mut MidiMessagesFn, - ) { - let range = (Included(range.start), Excluded(range.end)); - self.events.range(range).for_each(|(_when, event)| { - self.active_notes[event.0 .0 as usize].watch_message(&event.1); - midi_messages_fn(event.0, event.1); - }); - } - - pub fn generate_midi_messages_for_current_frame( - &mut self, - midi_messages_fn: &mut MidiMessagesFn, - ) { - let time_range = self.time_range.clone(); - self.generate_midi_messages_for_interval(&time_range, midi_messages_fn) - } - - pub fn debug_events(&self) -> &BeatEventsMap { - &self.events - } - - #[allow(dead_code)] - pub fn debug_dump_events(&self) { - println!("{:?}", self.events); - } - - pub fn bpm(&self) -> f64 { - self.bpm - } - - pub fn set_bpm(&mut self, bpm: ParameterType) { - self.bpm = bpm; - } -} -impl Configurable for Sequencer { - #[cfg(obsolete)] - fn update_sample_rate(&mut self, sample_rate: SampleRate) { - self.temp_hack_clock.update_sample_rate(sample_rate); - - // TODO: how can we make sure this stays in sync with the clock when the - // clock is changed? - self.next_instant = PerfectTimeUnit(self.temp_hack_clock.beats()); - } -} -impl Controls for Sequencer { - fn update_time(&mut self, range: &Range) { - if &self.time_range != range { - self.time_range = range.clone(); - self.time_range_handled = false; - } - } - - fn work(&mut self, control_events_fn: &mut ControlEventsFn) { - if !self.is_performing || self.is_finished() { - return; - } - if self.should_stop_pending_notes { - self.should_stop_pending_notes = false; - self.stop_pending_notes(control_events_fn); - } - - if self.is_enabled() { - if !self.time_range_handled { - self.time_range_handled = true; - let time_range = self.time_range.clone(); - let uid = self.uid; - self.generate_midi_messages_for_interval(&time_range, &mut |channel, message| { - control_events_fn(uid, EntityEvent::Midi(channel, message)) - }); - } - }; - } - - fn is_finished(&self) -> bool { - self.time_range.start >= self.last_event_time - } - - fn play(&mut self) { - self.is_performing = true; - } - - fn stop(&mut self) { - self.is_performing = false; - self.should_stop_pending_notes = true; - } - - fn skip_to_start(&mut self) { - #[cfg(obsolete)] - self.temp_hack_clock.seek(0); - self.next_instant = PerfectTimeUnit::default(); - self.should_stop_pending_notes = true; - } - - fn is_performing(&self) -> bool { - self.is_performing - } -} -impl Displays for Sequencer { - fn ui(&mut self, ui: &mut Ui) -> eframe::egui::Response { - for (when, (channel, message)) in &self.events { - let has_played = when < &self.time_range.start; - let mut text = RichText::new(format!("{}: {} -> {:?}", when, channel, message)); - if has_played { - text = text.italics(); - } - ui.label(text); - } - ui.label("TODO") - } -} - -#[cfg(tired)] -mod tired { - pub(crate) type MidiTickEventsMap = BTreeMultiMap; - - /// [MidiTickSequencer] is another kind of sequencer whose time unit is the MIDI - /// tick. It exists to make it easy for [MidiSmfReader] to turn MIDI files into - /// sequences. - #[derive(Debug, Control, IsController, Params, Uid, Serialize, Deserialize)] - pub struct MidiTickSequencer { - uid: Uid, - - #[control] - #[params] - midi_ticks_per_second: usize, - - #[serde(skip)] - next_instant: MidiTicks, - #[serde(skip)] - events: MidiTickEventsMap, - #[serde(skip)] - last_event_time: MidiTicks, - #[serde(skip)] - is_disabled: bool, - #[serde(skip)] - is_performing: bool, - #[serde(skip)] - active_notes: [MidiNoteMinder; 16], - - loop_range: Option>, - is_loop_enabled: bool, - - temp_hack_clock: Clock, - } - impl HandlesMidi for MidiTickSequencer {} - impl Performs for MidiTickSequencer { - fn play(&mut self) { - self.is_performing = true; - } - - fn stop(&mut self) { - self.is_performing = false; - } - - fn skip_to_start(&mut self) { - self.temp_hack_clock.seek(0); - self.next_instant = MidiTicks::MIN; - } - - fn set_loop(&mut self, range: &std::ops::Range) { - self.loop_range = Some(range.clone()); - } - - fn clear_loop(&mut self) { - self.loop_range = None; - } - - fn set_loop_enabled(&mut self, is_enabled: bool) { - self.is_loop_enabled = is_enabled; - } - - fn is_performing(&self) -> bool { - self.is_performing - } - } - - impl MidiTickSequencer { - pub fn new_with(params: &MidiTickSequencerParams) -> Self { - Self { - uid: Default::default(), - midi_ticks_per_second: params.midi_ticks_per_second(), - next_instant: Default::default(), - events: Default::default(), - last_event_time: Default::default(), - is_disabled: Default::default(), - is_performing: Default::default(), - active_notes: Default::default(), - loop_range: Default::default(), - is_loop_enabled: Default::default(), - temp_hack_clock: Clock::new_with(&ClockParams { - bpm: 0.0, - midi_ticks_per_second: params.midi_ticks_per_second(), - time_signature: TimeSignatureParams { top: 4, bottom: 4 }, // TODO - }), - } - } - - #[allow(dead_code)] - pub(crate) fn clear(&mut self) { - // TODO: should this also disconnect sinks? I don't think so - self.events.clear(); - self.last_event_time = MidiTicks::MIN; - self.skip_to_start(); - } - - pub fn insert(&mut self, when: MidiTicks, channel: MidiChannel, message: MidiMessage) { - self.events.insert(when, (channel, message)); - if when >= self.last_event_time { - self.last_event_time = when; - } - } - - pub fn is_enabled(&self) -> bool { - !self.is_disabled - } - - #[allow(dead_code)] - pub fn enable(&mut self, is_enabled: bool) { - self.is_disabled = !is_enabled; - } - - fn is_finished(&self) -> bool { - self.next_instant > self.last_event_time - } - - pub fn midi_ticks_per_second(&self) -> usize { - self.midi_ticks_per_second - } - - pub fn set_midi_ticks_per_second(&mut self, midi_ticks_per_second: usize) { - self.midi_ticks_per_second = midi_ticks_per_second; - } - } - impl Configurable for MidiTickSequencer { - fn reset(&mut self, sample_rate: SampleRate) { - self.temp_hack_clock.set_sample_rate(sample_rate); - self.temp_hack_clock.reset(sample_rate); - - // TODO: how can we make sure this stays in sync with the clock when the - // clock is changed? - self.next_instant = MidiTicks(0); - } - } - impl Controls for MidiTickSequencer { - fn work(&mut self, messages_fn: &mut dyn FnMut(Uid, Self::Message)) { - if self.is_finished() || !self.is_performing { - return (None, 0); - } - let mut v = Vec::default(); - let this_instant = MidiTicks(self.temp_hack_clock.midi_ticks()); - self.temp_hack_clock.tick_batch(tick_count); - self.next_instant = MidiTicks(self.temp_hack_clock.midi_ticks()); - - if self.is_enabled() { - // If the last instant marks a new interval, then we want to include - // any events scheduled at exactly that time. So the range is - // inclusive. - let range = (Included(this_instant), Excluded(self.next_instant)); - let events = self.events.range(range); - v.extend(events.into_iter().fold( - Vec::default(), - |mut vec, (_when, (channel, message))| { - self.active_notes[*channel as usize].watch_message(message); - vec.push(EntityEvent::Midi(*channel, *message)); - vec - }, - )); - } - if v.is_empty() { - (None, tick_count) - } else { - (Some(v), tick_count) - } - } - } - /// [MidiSmfReader] parses MIDI SMF files and programs [MidiTickSequencer] with - /// the data it finds. - pub struct MidiSmfReader {} - impl MidiSmfReader { - pub fn program_sequencer(sequencer: &mut MidiTickSequencer, data: &[u8]) { - let parse_result = midly::Smf::parse(data).unwrap(); - - struct MetaInfo { - // Pulses per quarter-note - ppq: u32, - - // Microseconds per quarter-note - tempo: u32, - - time_signature_numerator: u8, - time_signature_denominator_exp: u8, - } - let mut meta_info = MetaInfo { - ppq: match parse_result.header.timing { - midly::Timing::Metrical(ticks_per_beat) => ticks_per_beat.as_int() as u32, - _ => 0, - }, - tempo: 0, - - // https://en.wikipedia.org/wiki/Time_signature - time_signature_numerator: 0, - time_signature_denominator_exp: 0, - }; - for (track_number, track) in parse_result.tracks.iter().enumerate() { - println!("Processing track {track_number}"); - let mut track_time_ticks: usize = 0; // The relative time references start over at zero with each track. - - for t in track.iter() { - match t.kind { - TrackEventKind::Midi { channel, message } => { - let delta = t.delta.as_int() as usize; - track_time_ticks += delta; - sequencer.insert(MidiTicks(track_time_ticks), channel.into(), message); - // TODO: prior version of this code treated vel=0 as - // note-off. Do we need to handle that higher up? - } - - TrackEventKind::Meta(meta_message) => match meta_message { - midly::MetaMessage::TimeSignature( - numerator, - denominator_exp, - _cc, - _bb, - ) => { - meta_info.time_signature_numerator = numerator; - meta_info.time_signature_denominator_exp = denominator_exp; - //meta_info.ppq = cc; WHA??? - } - midly::MetaMessage::Tempo(tempo) => { - meta_info.tempo = tempo.as_int(); - } - midly::MetaMessage::TrackNumber(track_opt) => { - if track_opt.is_none() { - continue; - } - } - midly::MetaMessage::EndOfTrack => { - let _time_signature: (u32, u32) = ( - meta_info.time_signature_numerator.into(), - 2_u32.pow(meta_info.time_signature_denominator_exp.into()), - ); - let ticks_per_quarter_note: f32 = meta_info.ppq as f32; - let seconds_per_quarter_note: f32 = - meta_info.tempo as f32 / 1000000.0; - let _ticks_per_second = - ticks_per_quarter_note / seconds_per_quarter_note; - - let _bpm: f32 = (60.0 * 1000000.0) / (meta_info.tempo as f32); - - // sequencer.set_midi_ticks_per_second(ticks_per_second - // as usize); - } - _ => {} - }, - TrackEventKind::SysEx(_data) => { // TODO - } - TrackEventKind::Escape(_data) => { // TODO - } - } - } - } - println!("Done processing MIDI file"); - } - } -} - -#[cfg(test)] -mod tests { - #[cfg(tired)] - use super::{MidiTickEventsMap, MidiTickSequencer}; - use ensnare::midi::prelude::*; - #[cfg(tired)] - use groove_core::time::MidiTicks; - - #[cfg(tired)] - impl MidiTickSequencer { - #[allow(dead_code)] - pub(crate) fn debug_events(&self) -> &MidiTickEventsMap { - &self.events - } - } - - #[cfg(tired)] - impl MidiTickSequencer { - pub(crate) fn tick_for_beat(&self, clock: &Clock, beat: usize) -> MidiTicks { - // let tpb = self.midi_ticks_per_second.0 as f32 / - // (clock.bpm() / 60.0); - let tpb = 960.0 / (clock.bpm() / 60.0); // TODO: who should own the number of ticks/second? - MidiTicks::from(tpb * beat as f64) - } - } - - // fn advance_to_next_beat( - // clock: &mut Clock, - // sequencer: &mut dyn IsController, - // ) { - // let next_beat = clock.beats().floor() + 1.0; - // while clock.beats() < next_beat { - // // TODO: a previous version of this utility function had - // // clock.tick() first, meaning that the sequencer never got the 0th - // // (first) tick. No test ever cared, apparently. Fix this. - // let _ = sequencer.work(1); - // clock.tick(1); - // } - // } - - // // We're papering over the issue that MIDI events are firing a little late. - // // See Clock::next_slice_in_midi_ticks(). - // fn advance_one_midi_tick( - // clock: &mut Clock, - // sequencer: &mut dyn IsController, - // ) { - // let next_midi_tick = clock.midi_ticks() + 1; - // while clock.midi_ticks() < next_midi_tick { - // let _ = sequencer.work(1); - // clock.tick(1); - // } - // } - - #[allow(dead_code)] - #[allow(unused_variables)] - #[test] - fn sequencer_mainline() { - const DEVICE_MIDI_CHANNEL: MidiChannel = MidiChannel(7); - #[cfg(obsolete)] - let clock = Clock::new_with( - DEFAULT_BPM, - DEFAULT_MIDI_TICKS_PER_SECOND, - TimeSignature::default(), - ); - // let mut o = Orchestrator::new_with(DEFAULT_BPM); - // let mut sequencer = Box::new(MidiTickSequencer::new_with( - // DEFAULT_SAMPLE_RATE, - // DEFAULT_MIDI_TICKS_PER_SECOND, - // )); - // let instrument = Box::new(ToyInstrument::new_with(clock.sample_rate())); - // let device_uid = o.add(None, Entity::ToyInstrument(instrument)); - - // sequencer.insert( - // sequencer.tick_for_beat(&clock, 0), - // DEVICE_MIDI_CHANNEL, - // new_note_on(MidiNote::C4 as u8, 127), - // ); - // sequencer.insert( - // sequencer.tick_for_beat(&clock, 1), - // DEVICE_MIDI_CHANNEL, - // new_note_off(MidiNote::C4 as u8, 0), - // ); - // const SEQUENCER_ID: &str = "seq"; - // let _sequencer_uid = o.add(Some(SEQUENCER_ID), Entity::MidiTickSequencer(sequencer)); - // o.connect_midi_downstream(device_uid, DEVICE_MIDI_CHANNEL); - - // // TODO: figure out a reasonable way to test these things once they're - // // inside Store, and their type information has been erased. Maybe we - // // can send messages asking for state. Maybe we can send things that the - // // entities themselves assert. - // if let Some(entity) = o.get_mut(SEQUENCER_ID) { - // if let Some(sequencer) = entity.as_is_controller_mut() { - // advance_one_midi_tick(&mut clock, sequencer); - // { - // // assert!(instrument.is_playing); - // // assert_eq!(instrument.received_count, 1); - // // assert_eq!(instrument.handled_count, 1); - // } - // } - // } - - // if let Some(entity) = o.get_mut(SEQUENCER_ID) { - // if let Some(sequencer) = entity.as_is_controller_mut() { - // advance_to_next_beat(&mut clock, sequencer); - // { - // // assert!(!instrument.is_playing); - // // assert_eq!(instrument.received_count, 2); - // // assert_eq!(&instrument.handled_count, &2); - // } - // } - // } - } - - // TODO: re-enable later....................................................................... - // #[test] - // fn sequencer_multichannel() { - // let mut clock = Clock::default(); - // let mut sequencer = MidiTickSequencer::::default(); - - // let device_1 = rrc(TestMidiSink::default()); - // assert!(!device_1.borrow().is_playing); - // device_1.borrow_mut().set_midi_channel(0); - // sequencer.add_midi_sink(0, rrc_downgrade::>(&device_1)); - - // let device_2 = rrc(TestMidiSink::default()); - // assert!(!device_2.borrow().is_playing); - // device_2.borrow_mut().set_midi_channel(1); - // sequencer.add_midi_sink(1, rrc_downgrade::>(&device_2)); - - // sequencer.insert( - // sequencer.tick_for_beat(&clock, 0), - // 0, - // new_note_on(60, 0), - // ); - // sequencer.insert( - // sequencer.tick_for_beat(&clock, 1), - // 1, - // new_note_on(60, 0), - // ); - // sequencer.insert( - // sequencer.tick_for_beat(&clock, 2), - // 0, - // new_note_off(MidiNote::C4 as u8, 0), - // ); - // sequencer.insert( - // sequencer.tick_for_beat(&clock, 3), - // 1, - // new_note_off(MidiNote::C4 as u8, 0), - // ); - // assert_eq!(sequencer.debug_events().len(), 4); - - // // Let the tick #0 event(s) fire. - // assert_eq!(clock.samples(), 0); - // assert_eq!(clock.midi_ticks(), 0); - // advance_one_midi_tick(&mut clock, &mut sequencer); - // { - // let dp_1 = device_1.borrow(); - // assert!(dp_1.is_playing); - // assert_eq!(dp_1.received_count, 1); - // assert_eq!(dp_1.handled_count, 1); - - // let dp_2 = device_2.borrow(); - // assert!(!dp_2.is_playing); - // assert_eq!(dp_2.received_count, 0); - // assert_eq!(dp_2.handled_count, 0); - // } - - // advance_to_next_beat(&mut clock, &mut sequencer); - // assert_eq!(clock.beats().floor(), 1.0); // TODO: these floor() calls are a smell - // { - // let dp = device_1.borrow(); - // assert!(dp.is_playing); - // assert_eq!(dp.received_count, 1); - // assert_eq!(dp.handled_count, 1); - - // let dp_2 = device_2.borrow(); - // assert!(dp_2.is_playing); - // assert_eq!(dp_2.received_count, 1); - // assert_eq!(dp_2.handled_count, 1); - // } - - // advance_to_next_beat(&mut clock, &mut sequencer); - // assert_eq!(clock.beats().floor(), 2.0); - // // assert_eq!(sequencer.tick_sequencer.events.len(), 1); - // { - // let dp = device_1.borrow(); - // assert!(!dp.is_playing); - // assert_eq!(dp.received_count, 2); - // assert_eq!(dp.handled_count, 2); - - // let dp_2 = device_2.borrow(); - // assert!(dp_2.is_playing); - // assert_eq!(dp_2.received_count, 1); - // assert_eq!(dp_2.handled_count, 1); - // } - - // advance_to_next_beat(&mut clock, &mut sequencer); - // assert_eq!(clock.beats().floor(), 3.0); - // // assert_eq!(sequencer.tick_sequencer.events.len(), 0); - // { - // let dp = device_1.borrow(); - // assert!(!dp.is_playing); - // assert_eq!(dp.received_count, 2); - // assert_eq!(dp.handled_count, 2); - - // let dp_2 = device_2.borrow(); - // assert!(!dp_2.is_playing); - // assert_eq!(dp_2.received_count, 2); - // assert_eq!(dp_2.handled_count, 2); - // } - // } -} diff --git a/entities/src/effects/bitcrusher.rs b/entities/src/effects/bitcrusher.rs index 79f9f7b2..0080fc19 100644 --- a/entities/src/effects/bitcrusher.rs +++ b/entities/src/effects/bitcrusher.rs @@ -1,7 +1,7 @@ // Copyright (c) 2023 Mike Tsao. All rights reserved. use eframe::egui::{DragValue, Ui}; -use ensnare::{prelude::*, traits::prelude::*}; +use ensnare_core::{prelude::*, traits::prelude::*}; use ensnare_proc_macros::{Control, IsEffect, Params, Uid}; use serde::{Deserialize, Serialize}; diff --git a/entities/src/effects/chorus.rs b/entities/src/effects/chorus.rs deleted file mode 100644 index 7c360720..00000000 --- a/entities/src/effects/chorus.rs +++ /dev/null @@ -1,83 +0,0 @@ -// Copyright (c) 2023 Mike Tsao. All rights reserved. - -use super::delay::{DelayLine, Delays}; -use eframe::egui::Ui; -use ensnare::{prelude::*, traits::prelude::*}; -use ensnare_proc_macros::{Control, IsEffect, Params, Uid}; -use serde::{Deserialize, Serialize}; - -/// Schroeder reverb. Uses four parallel recirculating delay lines feeding into -/// a series of two all-pass delay lines. -#[derive(Debug, Default, Control, IsEffect, Params, Uid, Serialize, Deserialize)] -pub struct Chorus { - uid: Uid, - - #[control] - #[params] - voices: usize, - - #[control] - #[params] - delay_seconds: ParameterType, - - #[serde(skip)] - delay: DelayLine, -} -impl Serializable for Chorus {} -impl TransformsAudio for Chorus { - fn transform_channel(&mut self, _channel: usize, input_sample: Sample) -> Sample { - let index_offset = self.delay_seconds / self.voices as ParameterType; - let mut sum = self.delay.pop_output(input_sample); - for i in 1..self.voices as isize { - sum += self.delay.peek_indexed_output(i * index_offset as isize); - } - sum - } -} -impl Configurable for Chorus { - fn update_sample_rate(&mut self, sample_rate: SampleRate) { - self.delay.update_sample_rate(sample_rate); - } -} -impl Chorus { - #[allow(dead_code)] - fn new() -> Self { - Self::default() - } - - pub fn new_with(params: &ChorusParams) -> Self { - // TODO: the delay_seconds param feels like a hack - Self { - uid: Default::default(), - voices: params.voices(), - delay_seconds: params.delay_seconds(), - delay: DelayLine::new_with(params.delay_seconds(), 1.0), - } - } - - pub fn voices(&self) -> usize { - self.voices - } - - pub fn set_voices(&mut self, voices: usize) { - self.voices = voices; - } - - pub fn delay_seconds(&self) -> f64 { - self.delay_seconds - } - - pub fn set_delay_seconds(&mut self, delay_seconds: ParameterType) { - self.delay_seconds = delay_seconds; - } -} -impl Displays for Chorus { - fn ui(&mut self, ui: &mut Ui) -> eframe::egui::Response { - ui.label(self.name()) - } -} - -#[cfg(test)] -mod tests { - //TODO -} diff --git a/entities/src/effects/compressor.rs b/entities/src/effects/compressor.rs index 1439febf..b8ec90b0 100644 --- a/entities/src/effects/compressor.rs +++ b/entities/src/effects/compressor.rs @@ -1,7 +1,7 @@ // Copyright (c) 2023 Mike Tsao. All rights reserved. use eframe::egui::{DragValue, Ui}; -use ensnare::{prelude::*, traits::prelude::*}; +use ensnare_core::{prelude::*, traits::prelude::*}; use ensnare_proc_macros::{Control, IsEffect, Params, Uid}; use serde::{Deserialize, Serialize}; @@ -157,8 +157,10 @@ impl Displays for Compressor { #[cfg(test)] mod tests { use crate::effects::compressor::{Compressor, CompressorParams}; - use ensnare::core::{Normal, Sample, SampleType}; - use ensnare::traits::prelude::*; + use ensnare_core::{ + core::{Normal, Sample, SampleType}, + traits::prelude::*, + }; #[test] fn basic_compressor() { diff --git a/entities/src/effects/delay.rs b/entities/src/effects/delay.rs deleted file mode 100644 index 38753caa..00000000 --- a/entities/src/effects/delay.rs +++ /dev/null @@ -1,355 +0,0 @@ -// Copyright (c) 2023 Mike Tsao. All rights reserved. - -use eframe::egui::Ui; -use ensnare::prelude::*; -use ensnare::traits::prelude::*; -use ensnare_proc_macros::{Control, IsEffect, Params, Uid}; -use serde::{Deserialize, Serialize}; - -pub(super) trait Delays { - fn peek_output(&self, apply_decay: bool) -> Sample; - fn peek_indexed_output(&self, index: isize) -> Sample; - fn pop_output(&mut self, input: Sample) -> Sample; -} - -#[derive(Debug, Default)] -pub(crate) struct DelayLine { - sample_rate: SampleRate, - delay_seconds: ParameterType, - decay_factor: SignalType, - - buffer_size: usize, - buffer_pointer: usize, - buffer: Vec, -} -impl Configurable for DelayLine { - fn update_sample_rate(&mut self, sample_rate: SampleRate) { - self.sample_rate = sample_rate; - self.resize_buffer(); - } -} -impl DelayLine { - /// decay_factor: 1.0 = no decay - pub(super) fn new_with(delay_seconds: ParameterType, decay_factor: SignalType) -> Self { - Self { - sample_rate: Default::default(), - delay_seconds, - decay_factor, - - buffer_size: Default::default(), - buffer_pointer: 0, - buffer: Default::default(), - } - } - - pub(super) fn delay_seconds(&self) -> ParameterType { - self.delay_seconds - } - - pub(super) fn set_delay_seconds(&mut self, delay_seconds: ParameterType) { - if delay_seconds != self.delay_seconds { - self.delay_seconds = delay_seconds; - self.resize_buffer(); - } - } - - fn resize_buffer(&mut self) { - self.buffer_size = - (self.sample_rate.value() as ParameterType * self.delay_seconds) as usize; - self.buffer = Vec::with_capacity(self.buffer_size); - self.buffer.resize(self.buffer_size, Sample::SILENCE); - self.buffer_pointer = 0; - } - - pub(super) fn decay_factor(&self) -> SignalType { - self.decay_factor - } -} -impl Delays for DelayLine { - fn peek_output(&self, apply_decay: bool) -> Sample { - if self.buffer_size == 0 { - Sample::SILENCE - } else if apply_decay { - self.buffer[self.buffer_pointer] * self.decay_factor() - } else { - self.buffer[self.buffer_pointer] - } - } - - fn peek_indexed_output(&self, index: isize) -> Sample { - if self.buffer_size == 0 { - Sample::SILENCE - } else { - let mut index = -index; - while index < 0 { - index += self.buffer_size as isize; - } - self.buffer[self.buffer_pointer] - } - } - - fn pop_output(&mut self, input: Sample) -> Sample { - if self.buffer_size == 0 { - input - } else { - let out = self.peek_output(true); - self.buffer[self.buffer_pointer] = input; - self.buffer_pointer += 1; - if self.buffer_pointer >= self.buffer_size { - self.buffer_pointer = 0; - } - out - } - } -} - -#[derive(Debug, Default)] -pub struct RecirculatingDelayLine { - delay: DelayLine, -} -impl RecirculatingDelayLine { - pub(super) fn new_with( - delay_seconds: ParameterType, - decay_seconds: ParameterType, - final_amplitude: Normal, - peak_amplitude: Normal, - ) -> Self { - Self { - delay: DelayLine::new_with( - delay_seconds, - (peak_amplitude.value() * final_amplitude.value()) - .powf(delay_seconds / decay_seconds) as SignalType, - ), - } - } - - pub(super) fn decay_factor(&self) -> SignalType { - self.delay.decay_factor() - } -} -impl Delays for RecirculatingDelayLine { - fn peek_output(&self, apply_decay: bool) -> Sample { - self.delay.peek_output(apply_decay) - } - - fn peek_indexed_output(&self, index: isize) -> Sample { - self.delay.peek_indexed_output(index) - } - - fn pop_output(&mut self, input: Sample) -> Sample { - let output = self.peek_output(true); - self.delay.pop_output(input + output); - output - } -} -impl Configurable for RecirculatingDelayLine { - fn update_sample_rate(&mut self, sample_rate: SampleRate) { - self.delay.update_sample_rate(sample_rate); - } -} - -#[derive(Debug, Default)] -pub(super) struct AllPassDelayLine { - delay: RecirculatingDelayLine, -} -impl AllPassDelayLine { - pub(super) fn new_with( - delay_seconds: ParameterType, - decay_seconds: ParameterType, - final_amplitude: Normal, - peak_amplitude: Normal, - ) -> Self { - Self { - delay: RecirculatingDelayLine::new_with( - delay_seconds, - decay_seconds, - final_amplitude, - peak_amplitude, - ), - } - } -} -impl Delays for AllPassDelayLine { - fn peek_output(&self, _apply_decay: bool) -> Sample { - panic!("AllPassDelay doesn't allow peeking") - } - - fn peek_indexed_output(&self, _: isize) -> Sample { - panic!("AllPassDelay doesn't allow peeking") - } - - fn pop_output(&mut self, input: Sample) -> Sample { - let decay_factor = self.delay.decay_factor(); - let vm = self.delay.peek_output(false); - let vn = input - (vm * decay_factor); - self.delay.pop_output(vn); - vm + vn * decay_factor - } -} -impl Configurable for AllPassDelayLine { - fn update_sample_rate(&mut self, sample_rate: SampleRate) { - self.delay.update_sample_rate(sample_rate) - } -} - -#[derive(Debug, Default, Control, IsEffect, Params, Uid, Serialize, Deserialize)] -pub struct Delay { - uid: Uid, - - #[control] - #[params] - seconds: ParameterType, - - #[serde(skip)] - delay: DelayLine, -} -impl Serializable for Delay {} -impl Configurable for Delay { - fn update_sample_rate(&mut self, sample_rate: SampleRate) { - self.delay.update_sample_rate(sample_rate); - } -} -impl TransformsAudio for Delay { - fn transform_channel(&mut self, _channel: usize, input_sample: Sample) -> Sample { - self.delay.pop_output(input_sample) - } -} -impl Delay { - #[allow(dead_code)] - fn new() -> Self { - Self::default() - } - - pub fn new_with(params: &DelayParams) -> Self { - Self { - seconds: params.seconds(), - delay: DelayLine::new_with(params.seconds(), 1.0), - ..Default::default() - } - } - - pub fn seconds(&self) -> ParameterType { - self.delay.delay_seconds() - } - - pub fn set_seconds(&mut self, seconds: ParameterType) { - self.delay.set_delay_seconds(seconds); - } -} -impl Displays for Delay { - fn ui(&mut self, ui: &mut Ui) -> eframe::egui::Response { - ui.label(self.name()) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use float_cmp::approx_eq; - use more_asserts::{assert_gt, assert_lt}; - - // This small rate allows us to observe expected behavior after a small - // number of iterations. - const CURIOUSLY_SMALL_SAMPLE_RATE: SampleRate = SampleRate::new(3); - - #[test] - fn basic_delay() { - let mut fx = Delay::new_with(&DelayParams { seconds: 1.0 }); - fx.update_sample_rate(SampleRate::DEFAULT); - - // Add a unique first sample. - assert_eq!(fx.transform_channel(0, Sample::from(0.5)), Sample::SILENCE); - - // Push a whole bunch more. - for i in 0..SampleRate::DEFAULT_SAMPLE_RATE - 1 { - assert_eq!( - fx.transform_channel(0, Sample::MAX), - Sample::SILENCE, - "unexpected value at sample {}", - i - ); - } - - // We should get back our first sentinel sample. - assert_eq!(fx.transform_channel(0, Sample::SILENCE), Sample::from(0.5)); - - // And the next should be one of the bunch. - assert_eq!(fx.transform_channel(0, Sample::SILENCE), Sample::MAX); - } - - #[test] - fn delay_zero() { - let mut fx = Delay::new_with(&DelayParams { seconds: 0.0 }); - fx.update_sample_rate(SampleRate::DEFAULT); - - // We should keep getting back what we put in. - let mut rng = oorandom::Rand32::new(0); - for i in 0..SampleRate::DEFAULT_SAMPLE_RATE { - let random_bipolar_normal = rng.rand_float() * 2.0 - 1.0; - let sample = Sample::from(random_bipolar_normal); - assert_eq!( - fx.transform_channel(0, sample), - sample, - "unexpected value at sample {}", - i - ); - } - } - - #[test] - fn delay_line() { - // It's very simple: it should return an input sample, attenuated, after - // the specified delay. - let mut delay = DelayLine::new_with(1.0, 0.3); - delay.update_sample_rate(CURIOUSLY_SMALL_SAMPLE_RATE); - - assert_eq!(delay.pop_output(Sample::from(0.5)), Sample::SILENCE); - assert_eq!(delay.pop_output(Sample::from(0.4)), Sample::SILENCE); - assert_eq!(delay.pop_output(Sample::from(0.3)), Sample::SILENCE); - assert_eq!(delay.pop_output(Sample::from(0.2)), Sample::from(0.5 * 0.3)); - } - - #[test] - fn recirculating_delay_line() { - // Recirculating means that the input value is added to the value at the - // back of the buffer, rather than replacing that value. So if we put in - // a single value, we should expect to get it back, usually quieter, - // each time it cycles through the buffer. - let mut delay = - RecirculatingDelayLine::new_with(1.0, 1.5, Normal::from(0.001), Normal::from(1.0)); - delay.update_sample_rate(CURIOUSLY_SMALL_SAMPLE_RATE); - - assert_eq!(delay.pop_output(Sample::from(0.5)), Sample::SILENCE); - assert_eq!(delay.pop_output(Sample::from(0.0)), Sample::SILENCE); - assert_eq!(delay.pop_output(Sample::from(0.0)), Sample::SILENCE); - assert!(approx_eq!( - SampleType, - delay.pop_output(Sample::from(0.0)).0, - Sample::from(0.5 * 0.01).0, - epsilon = 0.001 - )); - assert_eq!(delay.pop_output(Sample::from(0.0)), Sample::SILENCE); - assert_eq!(delay.pop_output(Sample::from(0.0)), Sample::SILENCE); - assert!(approx_eq!( - SampleType, - delay.pop_output(Sample::from(0.0)).0, - Sample::from(0.5 * 0.01 * 0.01).0, - epsilon = 0.001 - )); - assert_eq!(delay.pop_output(Sample::from(0.0)), Sample::SILENCE); - assert_eq!(delay.pop_output(Sample::from(0.0)), Sample::SILENCE); - } - - #[test] - fn allpass_delay_line() { - // TODO: I'm not sure what this delay line is supposed to do. - let mut delay = - AllPassDelayLine::new_with(1.0, 1.5, Normal::from(0.001), Normal::from(1.0)); - delay.update_sample_rate(CURIOUSLY_SMALL_SAMPLE_RATE); - - assert_lt!(delay.pop_output(Sample::from(0.5)), Sample::from(0.5)); - assert_eq!(delay.pop_output(Sample::from(0.0)), Sample::SILENCE); - assert_eq!(delay.pop_output(Sample::from(0.0)), Sample::SILENCE); - assert_gt!(delay.pop_output(Sample::from(0.0)), Sample::SILENCE); // Note! > not = - } -} diff --git a/entities/src/effects/filter.rs b/entities/src/effects/filter.rs deleted file mode 100644 index da137d5e..00000000 --- a/entities/src/effects/filter.rs +++ /dev/null @@ -1,1229 +0,0 @@ -// Copyright (c) 2023 Mike Tsao. All rights reserved. - -use eframe::egui::{Slider, Ui}; -use ensnare::{prelude::*, traits::prelude::*}; -use ensnare_proc_macros::{Control, IsEffect, Params, Uid}; -use serde::{Deserialize, Serialize}; -use std::f64::consts::PI; - -#[derive(Debug, Control, IsEffect, Params, Uid, Serialize, Deserialize)] -pub struct BiQuadFilterLowPass24db { - #[control] - #[params] - cutoff: FrequencyHz, - #[control] - #[params] - passband_ripple: ParameterType, - - uid: Uid, - #[serde(skip)] - sample_rate: SampleRate, - #[serde(skip)] - channels: [BiQuadFilterLowPass24dbChannel; 2], -} -impl Default for BiQuadFilterLowPass24db { - fn default() -> Self { - Self { - cutoff: FrequencyHz::from(1000.0), - passband_ripple: 1.0, - uid: Default::default(), - sample_rate: Default::default(), - channels: Default::default(), - } - } -} -impl Serializable for BiQuadFilterLowPass24db {} -impl Configurable for BiQuadFilterLowPass24db { - fn update_sample_rate(&mut self, sample_rate: SampleRate) { - self.sample_rate = sample_rate; - self.update_coefficients(); - } - - fn sample_rate(&self) -> SampleRate { - self.sample_rate - } -} -impl TransformsAudio for BiQuadFilterLowPass24db { - fn transform_channel(&mut self, channel: usize, input_sample: Sample) -> Sample { - match channel { - 0 | 1 => self.channels[channel].transform_channel(channel, input_sample), - _ => panic!(), - } - } -} -impl BiQuadFilterLowPass24db { - pub fn new_with(params: &BiQuadFilterLowPass24dbParams) -> Self { - let mut r = Self { - cutoff: params.cutoff(), - passband_ripple: params.passband_ripple(), - uid: Default::default(), - sample_rate: Default::default(), - channels: [ - BiQuadFilterLowPass24dbChannel::default(), - BiQuadFilterLowPass24dbChannel::default(), - ], - }; - r.update_coefficients(); - r - } - - fn update_coefficients(&mut self) { - self.channels[0].update_coefficients(self.sample_rate, self.cutoff, self.passband_ripple); - self.channels[1].update_coefficients(self.sample_rate, self.cutoff, self.passband_ripple); - } - - pub fn cutoff(&self) -> FrequencyHz { - self.cutoff - } - pub fn set_cutoff(&mut self, cutoff: FrequencyHz) { - if self.cutoff != cutoff { - self.cutoff = cutoff; - self.update_coefficients(); - } - } - pub fn passband_ripple(&self) -> ParameterType { - self.passband_ripple - } - pub fn set_passband_ripple(&mut self, passband_ripple: ParameterType) { - if self.passband_ripple != passband_ripple { - self.passband_ripple = passband_ripple; - self.update_coefficients(); - } - } - - // TODO: (see Envelope's method and comments) -- this looks wasteful because - // it could compute coefficients twice in a single transaction, but the use - // case (egui change notifications) calls for only one thing changing at a - // time. - pub fn update_from_params(&mut self, params: &BiQuadFilterLowPass24dbParams) { - self.set_cutoff(params.cutoff()); - self.set_passband_ripple(params.passband_ripple()); - } -} - -#[derive(Debug, Default)] -struct BiQuadFilterLowPass24dbChannel { - inner: BiQuadFilter, - coefficients2: CoefficientSet2, -} -impl TransformsAudio for BiQuadFilterLowPass24dbChannel { - fn transform_channel(&mut self, _: usize, input_sample: Sample) -> Sample { - // Thanks - // https://www.musicdsp.org/en/latest/Filters/229-lpf-24db-oct.html - let input = input_sample.0; - let stage_1 = self.inner.coefficients.b0 * input + self.inner.state_0; - self.inner.state_0 = self.inner.coefficients.b1 * input - + self.inner.coefficients.a1 * stage_1 - + self.inner.state_1; - self.inner.state_1 = - self.inner.coefficients.b2 * input + self.inner.coefficients.a2 * stage_1; - let output = self.coefficients2.b3 * stage_1 + self.inner.state_2; - self.inner.state_2 = - self.coefficients2.b4 * stage_1 + self.coefficients2.a4 * output + self.inner.state_3; - self.inner.state_3 = self.coefficients2.b5 * stage_1 + self.coefficients2.a5 * output; - Sample::from(output) - } -} -impl BiQuadFilterLowPass24dbChannel { - fn update_coefficients( - &mut self, - sample_rate: SampleRate, - cutoff: FrequencyHz, - passband_ripple: ParameterType, - ) { - let k = (PI * cutoff.value() / sample_rate.value() as f64).tan(); - let sg = passband_ripple.sinh(); - let cg = passband_ripple.cosh() * passband_ripple.cosh(); - - let c0 = 1.0 / (cg - 0.853_553_390_593_273_7); - let c1 = k * c0 * sg * 1.847_759_065_022_573_5; - let c2 = 1.0 / (cg - 0.146_446_609_406_726_24); - let c3 = k * c2 * sg * 0.765_366_864_730_179_6; - let k = k * k; - - let a0 = 1.0 / (c1 + k + c0); - let a1 = 2.0 * (c0 - k) * a0; - let a2 = (c1 - k - c0) * a0; - let b0 = a0 * k; - let b1 = 2.0 * b0; - let b2 = b0; - self.inner.set_coefficients(CoefficientSet { - a0, - a1, - a2, - b0, - b1, - b2, - }); - - let a3 = 1.0 / (c3 + k + c2); - let a4 = 2.0 * (c2 - k) * a3; - let a5 = (c3 - k - c2) * a3; - let b3 = a3 * k; - let b4 = 2.0 * b3; - let b5 = b3; - self.coefficients2 = CoefficientSet2 { a4, a5, b3, b4, b5 }; - } -} - -#[derive(Debug, Control, IsEffect, Params, Uid, Serialize, Deserialize)] -pub struct BiQuadFilterLowPass12db { - #[control] - #[params] - cutoff: FrequencyHz, - #[control] - #[params] - q: ParameterType, - - uid: Uid, - #[serde(skip)] - sample_rate: SampleRate, - #[serde(skip)] - channels: [BiQuadFilterLowPass12dbChannel; 2], -} -impl Serializable for BiQuadFilterLowPass12db {} -impl Configurable for BiQuadFilterLowPass12db { - fn update_sample_rate(&mut self, sample_rate: SampleRate) { - self.sample_rate = sample_rate; - self.update_coefficients(); - } -} -impl TransformsAudio for BiQuadFilterLowPass12db { - fn transform_channel(&mut self, channel: usize, input_sample: Sample) -> Sample { - match channel { - 0 | 1 => self.channels[channel].transform_channel(channel, input_sample), - _ => panic!(), - } - } -} -impl BiQuadFilterLowPass12db { - pub fn new_with(params: &BiQuadFilterLowPass12dbParams) -> Self { - Self { - cutoff: params.cutoff(), - q: params.q(), - uid: Default::default(), - sample_rate: Default::default(), - channels: [ - BiQuadFilterLowPass12dbChannel::default(), - BiQuadFilterLowPass12dbChannel::default(), - ], - } - } - - fn update_coefficients(&mut self) { - self.channels[0].update_coefficients(self.sample_rate, self.cutoff, self.q); - self.channels[1].update_coefficients(self.sample_rate, self.cutoff, self.q); - } - - pub fn cutoff(&self) -> FrequencyHz { - self.cutoff - } - pub fn set_cutoff(&mut self, cutoff: FrequencyHz) { - if self.cutoff != cutoff { - self.cutoff = cutoff; - self.update_coefficients(); - } - } - pub fn q(&self) -> ParameterType { - self.q - } - pub fn set_q(&mut self, q: ParameterType) { - if self.q != q { - self.q = q; - self.update_coefficients(); - } - } -} - -#[derive(Debug, Default)] -struct BiQuadFilterLowPass12dbChannel { - inner: BiQuadFilter, -} -impl TransformsAudio for BiQuadFilterLowPass12dbChannel { - fn transform_channel(&mut self, channel: usize, input_sample: Sample) -> Sample { - self.inner.transform_channel(channel, input_sample) - } -} -impl BiQuadFilterLowPass12dbChannel { - fn update_coefficients( - &mut self, - sample_rate: SampleRate, - cutoff: FrequencyHz, - q: ParameterType, - ) { - let (_w0, w0cos, _w0sin, alpha) = - BiQuadFilter::rbj_intermediates_q(sample_rate, cutoff.value(), q); - - self.inner.coefficients = CoefficientSet { - a0: 1.0 + alpha, - a1: -2.0f64 * w0cos, - a2: 1.0 - alpha, - b0: (1.0 - w0cos) / 2.0f64, - b1: (1.0 - w0cos), - b2: (1.0 - w0cos) / 2.0f64, - } - } -} - -#[derive(Debug, Control, IsEffect, Params, Uid, Serialize, Deserialize)] -pub struct BiQuadFilterHighPass { - #[control] - #[params] - cutoff: FrequencyHz, - #[control] - #[params] - q: ParameterType, - - uid: Uid, - #[serde(skip)] - sample_rate: SampleRate, - #[serde(skip)] - channels: [BiQuadFilterHighPassChannel; 2], -} -impl Serializable for BiQuadFilterHighPass {} -impl Configurable for BiQuadFilterHighPass { - fn update_sample_rate(&mut self, sample_rate: SampleRate) { - self.sample_rate = sample_rate; - self.update_coefficients(); - } -} -impl TransformsAudio for BiQuadFilterHighPass { - fn transform_channel(&mut self, channel: usize, input_sample: Sample) -> Sample { - match channel { - 0 | 1 => self.channels[channel].transform_channel(channel, input_sample), - _ => panic!(), - } - } -} -impl BiQuadFilterHighPass { - pub fn new_with(params: &BiQuadFilterHighPassParams) -> Self { - let mut r = Self { - cutoff: params.cutoff(), - q: params.q(), - uid: Default::default(), - sample_rate: Default::default(), - channels: [ - BiQuadFilterHighPassChannel::default(), - BiQuadFilterHighPassChannel::default(), - ], - }; - r.update_coefficients(); - r - } - - fn update_coefficients(&mut self) { - self.channels[0].update_coefficients(self.sample_rate, self.cutoff, self.q); - self.channels[1].update_coefficients(self.sample_rate, self.cutoff, self.q); - } - - pub fn cutoff(&self) -> FrequencyHz { - self.cutoff - } - pub fn set_cutoff(&mut self, cutoff: FrequencyHz) { - if self.cutoff != cutoff { - self.cutoff = cutoff; - self.update_coefficients(); - } - } - pub fn q(&self) -> ParameterType { - self.q - } - pub fn set_q(&mut self, q: ParameterType) { - if self.q != q { - self.q = q; - self.update_coefficients(); - } - } -} - -#[derive(Debug, Default)] -struct BiQuadFilterHighPassChannel { - inner: BiQuadFilter, -} -impl TransformsAudio for BiQuadFilterHighPassChannel { - fn transform_channel(&mut self, channel: usize, input_sample: Sample) -> Sample { - self.inner.transform_channel(channel, input_sample) - } -} -impl BiQuadFilterHighPassChannel { - fn update_coefficients( - &mut self, - sample_rate: SampleRate, - cutoff: FrequencyHz, - q: ParameterType, - ) { - let (_w0, w0cos, _w0sin, alpha) = - BiQuadFilter::rbj_intermediates_q(sample_rate, cutoff.value(), q); - - self.inner.coefficients = CoefficientSet { - a0: 1.0 + alpha, - a1: -2.0f64 * w0cos, - a2: 1.0 - alpha, - b0: (1.0 + w0cos) / 2.0f64, - b1: -(1.0 + w0cos), - b2: (1.0 + w0cos) / 2.0f64, - } - } -} - -#[derive(Debug, Control, IsEffect, Params, Uid, Serialize, Deserialize)] -pub struct BiQuadFilterAllPass { - #[control] - #[params] - cutoff: FrequencyHz, - #[control] - #[params] - q: ParameterType, - - uid: Uid, - #[serde(skip)] - sample_rate: SampleRate, - #[serde(skip)] - channels: [BiQuadFilterAllPassChannel; 2], -} -impl Serializable for BiQuadFilterAllPass {} -impl Configurable for BiQuadFilterAllPass { - fn update_sample_rate(&mut self, sample_rate: SampleRate) { - self.sample_rate = sample_rate; - self.update_coefficients(); - } -} -impl TransformsAudio for BiQuadFilterAllPass { - fn transform_channel(&mut self, channel: usize, input_sample: Sample) -> Sample { - match channel { - 0 | 1 => self.channels[channel].transform_channel(channel, input_sample), - _ => panic!(), - } - } -} -impl BiQuadFilterAllPass { - pub fn new_with(params: &BiQuadFilterAllPassParams) -> Self { - Self { - cutoff: params.cutoff(), - q: params.q(), - uid: Default::default(), - sample_rate: Default::default(), - channels: [ - BiQuadFilterAllPassChannel::default(), - BiQuadFilterAllPassChannel::default(), - ], - } - } - - fn update_coefficients(&mut self) { - self.channels[0].update_coefficients(self.sample_rate, self.cutoff, self.q); - self.channels[1].update_coefficients(self.sample_rate, self.cutoff, self.q); - } - - pub fn cutoff(&self) -> FrequencyHz { - self.cutoff - } - pub fn set_cutoff(&mut self, cutoff: FrequencyHz) { - if self.cutoff != cutoff { - self.cutoff = cutoff; - self.update_coefficients(); - } - } - pub fn q(&self) -> ParameterType { - self.q - } - pub fn set_q(&mut self, q: ParameterType) { - if self.q != q { - self.q = q; - self.update_coefficients(); - } - } -} - -#[derive(Debug, Default)] -struct BiQuadFilterAllPassChannel { - inner: BiQuadFilter, -} -impl TransformsAudio for BiQuadFilterAllPassChannel { - fn transform_channel(&mut self, channel: usize, input_sample: Sample) -> Sample { - self.inner.transform_channel(channel, input_sample) - } -} -impl BiQuadFilterAllPassChannel { - fn update_coefficients( - &mut self, - sample_rate: SampleRate, - cutoff: FrequencyHz, - q: ParameterType, - ) { - let (_w0, w0cos, _w0sin, alpha) = - BiQuadFilter::rbj_intermediates_q(sample_rate, cutoff.value(), q); - self.inner.coefficients = CoefficientSet { - a0: 1.0 + alpha, - a1: -2.0f64 * w0cos, - a2: 1.0 - alpha, - b0: 1.0 - alpha, - b1: -2.0f64 * w0cos, - b2: 1.0 + alpha, - } - } -} - -#[derive(Debug, Control, IsEffect, Params, Uid, Serialize, Deserialize)] -pub struct BiQuadFilterBandPass { - #[control] - #[params] - cutoff: FrequencyHz, - #[control] - #[params] - bandwidth: ParameterType, // TODO: maybe this should be FrequencyHz - - uid: Uid, - #[serde(skip)] - sample_rate: SampleRate, - #[serde(skip)] - channels: [BiQuadFilterBandPassChannel; 2], -} -impl Serializable for BiQuadFilterBandPass {} -impl Configurable for BiQuadFilterBandPass { - fn update_sample_rate(&mut self, sample_rate: SampleRate) { - self.sample_rate = sample_rate; - self.update_coefficients(); - } -} -impl TransformsAudio for BiQuadFilterBandPass { - fn transform_channel(&mut self, channel: usize, input_sample: Sample) -> Sample { - match channel { - 0 | 1 => self.channels[channel].transform_channel(channel, input_sample), - _ => panic!(), - } - } -} -impl BiQuadFilterBandPass { - pub fn new_with(params: &BiQuadFilterBandPassParams) -> Self { - Self { - cutoff: params.cutoff(), - bandwidth: params.bandwidth(), - uid: Default::default(), - sample_rate: Default::default(), - channels: [ - BiQuadFilterBandPassChannel::default(), - BiQuadFilterBandPassChannel::default(), - ], - } - } - - fn update_coefficients(&mut self) { - self.channels[0].update_coefficients(self.sample_rate, self.cutoff, self.bandwidth); - self.channels[1].update_coefficients(self.sample_rate, self.cutoff, self.bandwidth); - } - - pub fn cutoff(&self) -> FrequencyHz { - self.cutoff - } - pub fn set_cutoff(&mut self, cutoff: FrequencyHz) { - if self.cutoff != cutoff { - self.cutoff = cutoff; - self.update_coefficients(); - } - } - pub fn bandwidth(&self) -> ParameterType { - self.bandwidth - } - pub fn set_bandwidth(&mut self, bandwidth: ParameterType) { - if self.bandwidth != bandwidth { - self.bandwidth = bandwidth; - self.update_coefficients(); - } - } -} - -#[derive(Debug, Default)] -struct BiQuadFilterBandPassChannel { - inner: BiQuadFilter, -} -impl TransformsAudio for BiQuadFilterBandPassChannel { - fn transform_channel(&mut self, channel: usize, input_sample: Sample) -> Sample { - self.inner.transform_channel(channel, input_sample) - } -} -impl BiQuadFilterBandPassChannel { - fn update_coefficients( - &mut self, - sample_rate: SampleRate, - cutoff: FrequencyHz, - bandwidth: ParameterType, - ) { - let (_w0, w0cos, _w0sin, alpha) = - BiQuadFilter::rbj_intermediates_bandwidth(sample_rate, cutoff.value(), bandwidth); - self.inner.coefficients = CoefficientSet { - a0: 1.0 + alpha, - a1: -2.0f64 * w0cos, - a2: 1.0 - alpha, - b0: alpha, - b1: 0.0, - b2: -alpha, - }; - } -} - -#[derive(Debug, Control, IsEffect, Params, Uid, Serialize, Deserialize)] -pub struct BiQuadFilterBandStop { - #[control] - #[params] - cutoff: FrequencyHz, - #[control] - #[params] - bandwidth: ParameterType, // TODO: maybe this should be FrequencyHz - - uid: Uid, - #[serde(skip)] - sample_rate: SampleRate, - - #[serde(skip)] - channels: [BiQuadFilterBandStopChannel; 2], -} -impl Serializable for BiQuadFilterBandStop {} -impl Configurable for BiQuadFilterBandStop { - fn update_sample_rate(&mut self, sample_rate: SampleRate) { - self.sample_rate = sample_rate; - self.update_coefficients(); - } -} -impl TransformsAudio for BiQuadFilterBandStop { - fn transform_channel(&mut self, channel: usize, input_sample: Sample) -> Sample { - match channel { - 0 | 1 => self.channels[channel].transform_channel(channel, input_sample), - _ => panic!(), - } - } -} -impl BiQuadFilterBandStop { - pub fn new_with(params: &BiQuadFilterBandStopParams) -> Self { - Self { - cutoff: params.cutoff(), - bandwidth: params.bandwidth(), - uid: Default::default(), - sample_rate: Default::default(), - channels: [ - BiQuadFilterBandStopChannel::default(), - BiQuadFilterBandStopChannel::default(), - ], - } - } - - fn update_coefficients(&mut self) { - self.channels[0].update_coefficients(self.sample_rate, self.cutoff, self.bandwidth); - self.channels[1].update_coefficients(self.sample_rate, self.cutoff, self.bandwidth); - } - - pub fn cutoff(&self) -> FrequencyHz { - self.cutoff - } - pub fn set_cutoff(&mut self, cutoff: FrequencyHz) { - if self.cutoff != cutoff { - self.cutoff = cutoff; - self.update_coefficients(); - } - } - pub fn bandwidth(&self) -> ParameterType { - self.bandwidth - } - pub fn set_bandwidth(&mut self, bandwidth: ParameterType) { - if self.bandwidth != bandwidth { - self.bandwidth = bandwidth; - self.update_coefficients(); - } - } -} - -#[derive(Debug, Default)] -struct BiQuadFilterBandStopChannel { - inner: BiQuadFilter, -} -impl TransformsAudio for BiQuadFilterBandStopChannel { - fn transform_channel(&mut self, channel: usize, input_sample: Sample) -> Sample { - self.inner.transform_channel(channel, input_sample) - } -} -impl BiQuadFilterBandStopChannel { - fn update_coefficients( - &mut self, - sample_rate: SampleRate, - cutoff: FrequencyHz, - bandwidth: ParameterType, - ) { - let (_w0, w0cos, _w0sin, alpha) = - BiQuadFilter::rbj_intermediates_bandwidth(sample_rate, cutoff.value(), bandwidth); - - self.inner.coefficients = CoefficientSet { - a0: 1.0 + alpha, - a1: -2.0f64 * w0cos, - a2: 1.0 - alpha, - b0: 1.0, - b1: -2.0f64 * w0cos, - b2: 1.0, - } - } -} - -#[derive(Debug, Control, IsEffect, Params, Uid, Serialize, Deserialize)] -pub struct BiQuadFilterPeakingEq { - #[control] - #[params] - cutoff: FrequencyHz, - - // I didn't know what to call this. RBJ says "...except for peakingEQ in - // which A*Q is the classic EE Q." I think Q is close enough to get the gist. - #[control] - #[params] - q: ParameterType, - - uid: Uid, - #[serde(skip)] - sample_rate: SampleRate, - #[serde(skip)] - channels: [BiQuadFilterPeakingEqChannel; 2], -} -impl Serializable for BiQuadFilterPeakingEq {} -impl Configurable for BiQuadFilterPeakingEq { - fn update_sample_rate(&mut self, sample_rate: SampleRate) { - self.sample_rate = sample_rate; - self.update_coefficients(); - } -} -impl TransformsAudio for BiQuadFilterPeakingEq { - fn transform_channel(&mut self, channel: usize, input_sample: Sample) -> Sample { - match channel { - 0 | 1 => self.channels[channel].transform_channel(channel, input_sample), - _ => panic!(), - } - } -} -impl BiQuadFilterPeakingEq { - pub fn new_with(params: &BiQuadFilterPeakingEqParams) -> Self { - let mut r = Self { - cutoff: params.cutoff(), - q: params.q(), - uid: Default::default(), - sample_rate: Default::default(), - channels: [ - BiQuadFilterPeakingEqChannel::default(), - BiQuadFilterPeakingEqChannel::default(), - ], - }; - r.update_coefficients(); - r - } - - fn update_coefficients(&mut self) { - self.channels[0].update_coefficients(self.sample_rate, self.cutoff, self.q); - self.channels[1].update_coefficients(self.sample_rate, self.cutoff, self.q); - } - - pub fn cutoff(&self) -> FrequencyHz { - self.cutoff - } - pub fn set_cutoff(&mut self, cutoff: FrequencyHz) { - if self.cutoff != cutoff { - self.cutoff = cutoff; - self.update_coefficients(); - } - } - pub fn q(&self) -> ParameterType { - self.q - } - pub fn set_q(&mut self, q: ParameterType) { - if self.q != q { - self.q = q; - self.update_coefficients(); - } - } -} - -#[derive(Debug, Default)] -struct BiQuadFilterPeakingEqChannel { - inner: BiQuadFilter, -} -impl TransformsAudio for BiQuadFilterPeakingEqChannel { - fn transform_channel(&mut self, channel: usize, input_sample: Sample) -> Sample { - self.inner.transform_channel(channel, input_sample) - } -} -impl BiQuadFilterPeakingEqChannel { - fn update_coefficients( - &mut self, - sample_rate: SampleRate, - cutoff: FrequencyHz, - q: ParameterType, - ) { - let (_w0, w0cos, _w0sin, alpha) = BiQuadFilter::rbj_intermediates_q( - sample_rate, - cutoff.value(), - std::f64::consts::FRAC_1_SQRT_2, - ); - let a = 10f64.powf(q / 10.0f64).sqrt(); - - self.inner.coefficients = CoefficientSet { - a0: 1.0 + alpha / a, - a1: -2.0f64 * w0cos, - a2: 1.0 - alpha / a, - b0: 1.0 + alpha * a, - b1: -2.0f64 * w0cos, - b2: 1.0 - alpha * a, - } - } -} - -#[derive(Debug, Control, IsEffect, Params, Uid, Serialize, Deserialize)] -pub struct BiQuadFilterLowShelf { - #[control] - #[params] - cutoff: FrequencyHz, - #[control] - #[params] - db_gain: ParameterType, - - uid: Uid, - #[serde(skip)] - sample_rate: SampleRate, - #[serde(skip)] - channels: [BiQuadFilterLowShelfChannel; 2], -} -impl Serializable for BiQuadFilterLowShelf {} -impl Configurable for BiQuadFilterLowShelf { - fn update_sample_rate(&mut self, sample_rate: SampleRate) { - self.sample_rate = sample_rate; - self.update_coefficients(); - } -} -impl TransformsAudio for BiQuadFilterLowShelf { - fn transform_channel(&mut self, channel: usize, input_sample: Sample) -> Sample { - match channel { - 0 | 1 => self.channels[channel].transform_channel(channel, input_sample), - _ => panic!(), - } - } -} -impl BiQuadFilterLowShelf { - pub fn new_with(params: &BiQuadFilterLowShelfParams) -> Self { - Self { - cutoff: params.cutoff(), - db_gain: params.db_gain(), - uid: Default::default(), - sample_rate: Default::default(), - channels: [ - BiQuadFilterLowShelfChannel::default(), - BiQuadFilterLowShelfChannel::default(), - ], - } - } - - fn update_coefficients(&mut self) { - self.channels[0].update_coefficients(self.sample_rate, self.cutoff, self.db_gain); - self.channels[1].update_coefficients(self.sample_rate, self.cutoff, self.db_gain); - } - - pub fn cutoff(&self) -> FrequencyHz { - self.cutoff - } - pub fn set_cutoff(&mut self, cutoff: FrequencyHz) { - if self.cutoff != cutoff { - self.cutoff = cutoff; - self.update_coefficients(); - } - } - pub fn db_gain(&self) -> ParameterType { - self.db_gain - } - pub fn set_db_gain(&mut self, db_gain: ParameterType) { - if self.db_gain != db_gain { - self.db_gain = db_gain; - self.update_coefficients(); - } - } -} - -#[derive(Debug, Default)] -struct BiQuadFilterLowShelfChannel { - inner: BiQuadFilter, -} -impl TransformsAudio for BiQuadFilterLowShelfChannel { - fn transform_channel(&mut self, channel: usize, input_sample: Sample) -> Sample { - self.inner.transform_channel(channel, input_sample) - } -} -impl BiQuadFilterLowShelfChannel { - fn update_coefficients( - &mut self, - sample_rate: SampleRate, - cutoff: FrequencyHz, - db_gain: ParameterType, - ) { - let a = 10f64.powf(db_gain / 10.0f64).sqrt(); - let (_w0, w0cos, _w0sin, alpha) = - BiQuadFilter::rbj_intermediates_shelving(sample_rate, cutoff.value(), a, 1.0); - - self.inner.coefficients = CoefficientSet { - a0: (a + 1.0) + (a - 1.0) * w0cos + 2.0 * a.sqrt() * alpha, - a1: -2.0 * ((a - 1.0) + (a + 1.0) * w0cos), - a2: (a + 1.0) + (a - 1.0) * w0cos - 2.0 * a.sqrt() * alpha, - b0: a * ((a + 1.0) - (a - 1.0) * w0cos + 2.0 * a.sqrt() * alpha), - b1: 2.0 * a * ((a - 1.0) - (a + 1.0) * w0cos), - b2: a * ((a + 1.0) - (a - 1.0) * w0cos - 2.0 * a.sqrt() * alpha), - }; - } -} - -#[derive(Debug, Control, IsEffect, Params, Uid, Serialize, Deserialize)] -pub struct BiQuadFilterHighShelf { - #[control] - #[params] - cutoff: FrequencyHz, - #[control] - #[params] - db_gain: ParameterType, - - uid: Uid, - #[serde(skip)] - sample_rate: SampleRate, - #[serde(skip)] - channels: [BiQuadFilterHighShelfChannel; 2], -} -impl Serializable for BiQuadFilterHighShelf {} -impl Configurable for BiQuadFilterHighShelf { - fn update_sample_rate(&mut self, sample_rate: SampleRate) { - self.sample_rate = sample_rate; - self.update_coefficients(); - } -} -impl TransformsAudio for BiQuadFilterHighShelf { - fn transform_channel(&mut self, channel: usize, input_sample: Sample) -> Sample { - match channel { - 0 | 1 => self.channels[channel].transform_channel(channel, input_sample), - _ => panic!(), - } - } -} -impl BiQuadFilterHighShelf { - pub fn new_with(params: &BiQuadFilterHighShelfParams) -> Self { - Self { - cutoff: params.cutoff(), - db_gain: params.db_gain(), - uid: Default::default(), - sample_rate: Default::default(), - channels: [ - BiQuadFilterHighShelfChannel::default(), - BiQuadFilterHighShelfChannel::default(), - ], - } - } - - fn update_coefficients(&mut self) { - self.channels[0].update_coefficients(self.sample_rate, self.cutoff, self.db_gain); - self.channels[1].update_coefficients(self.sample_rate, self.cutoff, self.db_gain); - } - - pub fn cutoff(&self) -> FrequencyHz { - self.cutoff - } - pub fn set_cutoff(&mut self, cutoff: FrequencyHz) { - if self.cutoff != cutoff { - self.cutoff = cutoff; - self.update_coefficients(); - } - } - pub fn db_gain(&self) -> ParameterType { - self.db_gain - } - pub fn set_db_gain(&mut self, db_gain: ParameterType) { - if self.db_gain != db_gain { - self.db_gain = db_gain; - self.update_coefficients(); - } - } -} - -#[derive(Debug, Default)] -struct BiQuadFilterHighShelfChannel { - inner: BiQuadFilter, -} -impl TransformsAudio for BiQuadFilterHighShelfChannel { - fn transform_channel(&mut self, channel: usize, input_sample: Sample) -> Sample { - self.inner.transform_channel(channel, input_sample) - } -} -impl BiQuadFilterHighShelfChannel { - fn update_coefficients( - &mut self, - sample_rate: SampleRate, - cutoff: FrequencyHz, - db_gain: ParameterType, - ) { - let a = 10f64.powf(db_gain / 10.0f64).sqrt(); - let (_w0, w0cos, _w0sin, alpha) = - BiQuadFilter::rbj_intermediates_shelving(sample_rate, cutoff.value(), a, 1.0); - - self.inner.coefficients = CoefficientSet { - a0: (a + 1.0) - (a - 1.0) * w0cos + 2.0 * a.sqrt() * alpha, - a1: 2.0 * ((a - 1.0) - (a + 1.0) * w0cos), - a2: (a + 1.0) - (a - 1.0) * w0cos - 2.0 * a.sqrt() * alpha, - b0: a * ((a + 1.0) + (a - 1.0) * w0cos + 2.0 * a.sqrt() * alpha), - b1: -2.0 * a * ((a - 1.0) + (a + 1.0) * w0cos), - b2: a * ((a + 1.0) + (a - 1.0) * w0cos - 2.0 * a.sqrt() * alpha), - } - } -} - -/// This filter does nothing, expensively. It exists for debugging. I might -/// delete it later. -#[derive(Debug, Control, IsEffect, Params, Uid, Serialize, Deserialize)] -pub struct BiQuadFilterNone { - uid: Uid, - #[serde(skip)] - sample_rate: SampleRate, - #[serde(skip)] - channels: [BiQuadFilter; 2], -} -impl Serializable for BiQuadFilterNone {} -impl Configurable for BiQuadFilterNone { - fn update_sample_rate(&mut self, sample_rate: SampleRate) { - self.sample_rate = sample_rate; - } -} -impl TransformsAudio for BiQuadFilterNone { - fn transform_channel(&mut self, channel: usize, input_sample: Sample) -> Sample { - match channel { - 0 | 1 => self.channels[channel].transform_channel(channel, input_sample), - _ => panic!(), - } - } -} -impl BiQuadFilterNone { - pub fn new_with(_: BiQuadFilterNoneParams) -> Self { - Self { - uid: Default::default(), - sample_rate: Default::default(), - channels: [BiQuadFilter::default(), BiQuadFilter::default()], - } - } -} - -#[derive(Clone, Debug)] -struct CoefficientSet { - a0: f64, - a1: f64, - a2: f64, - b0: f64, - b1: f64, - b2: f64, -} -impl Default for CoefficientSet { - // This is an identity set. - fn default() -> Self { - Self { - a0: 1.0, - a1: 0.0, - a2: 0.0, - b0: 0.0, - b1: 0.0, - b2: 0.0, - } - } -} - -#[derive(Clone, Debug, Default)] -struct CoefficientSet2 { - // a3 isn't needed right now - a4: f64, - a5: f64, - b3: f64, - b4: f64, - b5: f64, -} - -/// -#[derive(Clone, Debug, Default)] -pub struct BiQuadFilter { - coefficients: CoefficientSet, - - // Working variables - sample_m1: f64, // "sample minus two" or x(n-2) - sample_m2: f64, - output_m1: f64, - output_m2: f64, - - state_0: f64, - state_1: f64, - state_2: f64, - state_3: f64, -} -impl TransformsAudio for BiQuadFilter { - // Everyone but LowPassFilter24db uses this implementation - fn transform_channel(&mut self, _channel: usize, input_sample: Sample) -> Sample { - let s64 = input_sample.0; - let r = (self.coefficients.b0 / self.coefficients.a0) * s64 - + (self.coefficients.b1 / self.coefficients.a0) * self.sample_m1 - + (self.coefficients.b2 / self.coefficients.a0) * self.sample_m2 - - (self.coefficients.a1 / self.coefficients.a0) * self.output_m1 - - (self.coefficients.a2 / self.coefficients.a0) * self.output_m2; - - // Scroll everything forward in time. - self.sample_m2 = self.sample_m1; - self.sample_m1 = s64; - - self.output_m2 = self.output_m1; - self.output_m1 = r; - Sample::from(r) - } -} -impl BiQuadFilter { - // A placeholder for an intelligent mapping of 0.0..=1.0 to a reasonable Q - // range - pub fn denormalize_q(value: Normal) -> ParameterType { - value.value() * value.value() * 10.0 + 0.707 - } - - // A placeholder for an intelligent mapping of 0.0..=1.0 to a reasonable - // 24db passband parameter range - pub fn convert_passband(value: f32) -> f32 { - value * 100.0 + 0.1 - } - - // Excerpted from Robert Bristow-Johnson's audio cookbook to explain various - // parameters - // - // Fs (the sampling frequency) - // - // f0 ("wherever it's happenin', man." Center Frequency or Corner - // Frequency, or shelf midpoint frequency, depending on which filter - // type. The "significant frequency".) - // - // dBgain (used only for peaking and shelving filters) - // - // Q (the EE kind of definition, except for peakingEQ in which A*Q is the - // classic EE Q. That adjustment in definition was made so that a boost of - // N dB followed by a cut of N dB for identical Q and f0/Fs results in a - // precisely flat unity gain filter or "wire".) - // - // - _or_ BW, the bandwidth in octaves (between -3 dB frequencies for BPF - // and notch or between midpoint (dBgain/2) gain frequencies for peaking - // EQ) - // - // - _or_ S, a "shelf slope" parameter (for shelving EQ only). When S = 1, - // the shelf slope is as steep as it can be and remain monotonically - // increasing or decreasing gain with frequency. The shelf slope, in - // dB/octave, remains proportional to S for all other values for a fixed - // f0/Fs and dBgain. - - fn rbj_intermediates_q( - sample_rate: SampleRate, - cutoff: ParameterType, - q: ParameterType, - ) -> (f64, f64, f64, f64) { - let w0 = 2.0f64 * PI * cutoff / sample_rate.value() as f64; - let w0cos = w0.cos(); - let w0sin = w0.sin(); - let alpha = w0sin / (2.0f64 * q); - (w0, w0cos, w0sin, alpha) - } - - fn rbj_intermediates_bandwidth( - sample_rate: SampleRate, - cutoff: ParameterType, - bandwidth: ParameterType, - ) -> (f64, f64, f64, f64) { - let w0 = 2.0f64 * PI * cutoff / sample_rate.value() as f64; - let w0cos = w0.cos(); - let w0sin = w0.sin(); - let alpha = w0sin * (2.0f64.ln() / 2.0 * bandwidth as f64 * w0 / w0.sin()).sinh(); - (w0, w0cos, w0sin, alpha) - } - - fn rbj_intermediates_shelving( - sample_rate: SampleRate, - cutoff: ParameterType, - db_gain: ParameterType, - s: f64, - ) -> (f64, f64, f64, f64) { - let w0 = 2.0f64 * PI * cutoff as f64 / sample_rate.value() as f64; - let w0cos = w0.cos(); - let w0sin = w0.sin(); - let alpha = w0sin / 2.0 * ((db_gain + 1.0 / db_gain) * (1.0 / s - 1.0) + 2.0).sqrt(); - (w0, w0cos, w0sin, alpha) - } - - fn set_coefficients(&mut self, coefficient_set: CoefficientSet) { - self.coefficients = coefficient_set; - } -} - -impl Displays for BiQuadFilterAllPass { - fn ui(&mut self, ui: &mut Ui) -> eframe::egui::Response { - ui.label(self.name()) - } -} - -impl Displays for BiQuadFilterLowPass12db { - fn ui(&mut self, ui: &mut Ui) -> eframe::egui::Response { - ui.label(self.name()) - } -} - -impl Displays for BiQuadFilterHighPass { - fn ui(&mut self, ui: &mut Ui) -> eframe::egui::Response { - ui.label(self.name()) - } -} - -impl Displays for BiQuadFilterHighShelf { - fn ui(&mut self, ui: &mut Ui) -> eframe::egui::Response { - ui.label(self.name()) - } -} - -impl Displays for BiQuadFilterPeakingEq { - fn ui(&mut self, ui: &mut Ui) -> eframe::egui::Response { - ui.label(self.name()) - } -} - -impl Displays for BiQuadFilterBandPass { - fn ui(&mut self, ui: &mut Ui) -> eframe::egui::Response { - ui.label(self.name()) - } -} - -impl Displays for BiQuadFilterBandStop { - fn ui(&mut self, ui: &mut Ui) -> eframe::egui::Response { - ui.label(self.name()) - } -} - -impl Displays for BiQuadFilterLowShelf { - fn ui(&mut self, ui: &mut Ui) -> eframe::egui::Response { - ui.label(self.name()) - } -} - -impl Displays for BiQuadFilterNone { - fn ui(&mut self, ui: &mut Ui) -> eframe::egui::Response { - ui.label(self.name()) - } -} - -impl Displays for BiQuadFilterLowPass24db { - fn ui(&mut self, ui: &mut Ui) -> eframe::egui::Response { - let mut cutoff = self.cutoff().value(); - let mut pbr = self.passband_ripple(); - let cutoff_response = ui.add(Slider::new(&mut cutoff, FrequencyHz::range()).text("Cutoff")); - if cutoff_response.changed() { - self.set_cutoff(cutoff.into()); - }; - let passband_response = ui.add(Slider::new(&mut pbr, 0.0..=10.0).text("Passband")); - if passband_response.changed() { - self.set_passband_ripple(pbr); - }; - cutoff_response | passband_response - } -} - -#[cfg(test)] -mod tests { - // TODO: get FFT working, and then write tests. -} diff --git a/entities/src/effects/gain.rs b/entities/src/effects/gain.rs deleted file mode 100644 index e3a9130a..00000000 --- a/entities/src/effects/gain.rs +++ /dev/null @@ -1,75 +0,0 @@ -// Copyright (c) 2023 Mike Tsao. All rights reserved. - -use eframe::egui::{DragValue, Ui}; -use ensnare::{prelude::*, traits::prelude::*}; -use ensnare_proc_macros::{Control, IsEffect, Params, Uid}; -use serde::{Deserialize, Serialize}; - -#[derive(Debug, Default, Control, IsEffect, Params, Uid, Serialize, Deserialize)] -pub struct Gain { - uid: Uid, - - #[control] - #[params] - ceiling: Normal, -} -impl Serializable for Gain {} -impl Configurable for Gain {} -impl TransformsAudio for Gain { - fn transform_channel(&mut self, _channel: usize, input_sample: Sample) -> Sample { - Sample(input_sample.0 * self.ceiling.value()) - } -} -impl Gain { - pub fn new_with(params: &GainParams) -> Self { - Self { - uid: Default::default(), - ceiling: params.ceiling, - } - } - - pub fn ceiling(&self) -> Normal { - self.ceiling - } - - pub fn set_ceiling(&mut self, ceiling: Normal) { - self.ceiling = ceiling; - } -} -impl Displays for Gain { - fn ui(&mut self, ui: &mut Ui) -> eframe::egui::Response { - let mut ceiling = self.ceiling().to_percentage(); - let response = ui.add( - DragValue::new(&mut ceiling) - .clamp_range(0.0..=100.0) - .fixed_decimals(2) - .suffix(" %"), - ); - if response.changed() { - self.set_ceiling(Normal::from_percentage(ceiling)); - }; - response - } -} - -#[cfg(test)] -mod tests { - use super::*; - use groove_toys::{ToyAudioSource, ToyAudioSourceParams}; - - #[test] - fn gain_mainline() { - let mut gain = Gain::new_with(&GainParams { - ceiling: Normal::new(0.5), - }); - assert_eq!( - gain.transform_audio( - ToyAudioSource::new_with(&ToyAudioSourceParams { - level: ToyAudioSource::LOUD - }) - .value() - ), - StereoSample::from(0.5) - ); - } -} diff --git a/entities/src/effects/limiter.rs b/entities/src/effects/limiter.rs index 35684ae4..e31308ee 100644 --- a/entities/src/effects/limiter.rs +++ b/entities/src/effects/limiter.rs @@ -1,7 +1,7 @@ // Copyright (c) 2023 Mike Tsao. All rights reserved. use eframe::egui::{Slider, Ui}; -use ensnare::{prelude::*, traits::prelude::*}; +use ensnare_core::{prelude::*, traits::prelude::*}; use ensnare_proc_macros::{Control, IsEffect, Params, Uid}; use serde::{Deserialize, Serialize}; @@ -90,12 +90,13 @@ impl Displays for Limiter { } } +/// re-enable when moved into new crate #[cfg(test)] mod tests { use super::*; - use groove_toys::{ToyAudioSource, ToyAudioSourceParams}; use more_asserts::{assert_gt, assert_lt}; + #[cfg(test_I_AM_DISABLED)] #[test] fn limiter_mainline() { // audio sources are at or past boundaries diff --git a/entities/src/effects/mixer.rs b/entities/src/effects/mixer.rs index f5d6be98..6485acb7 100644 --- a/entities/src/effects/mixer.rs +++ b/entities/src/effects/mixer.rs @@ -1,7 +1,7 @@ // Copyright (c) 2023 Mike Tsao. All rights reserved. use eframe::egui::Ui; -use ensnare::{prelude::*, traits::prelude::*}; +use ensnare_core::{prelude::*, traits::prelude::*}; use ensnare_proc_macros::{Control, IsEffect, Params, Uid}; use serde::{Deserialize, Serialize}; diff --git a/entities/src/effects/mod.rs b/entities/src/effects/mod.rs index a75394b6..6b05cd23 100644 --- a/entities/src/effects/mod.rs +++ b/entities/src/effects/mod.rs @@ -1,29 +1,11 @@ // Copyright (c) 2023 Mike Tsao. All rights reserved. pub use bitcrusher::{Bitcrusher, BitcrusherParams}; -pub use chorus::{Chorus, ChorusParams}; pub use compressor::{Compressor, CompressorParams}; -pub use delay::{Delay, DelayParams}; -pub use filter::{ - BiQuadFilter, BiQuadFilterAllPass, BiQuadFilterAllPassParams, BiQuadFilterBandPass, - BiQuadFilterBandPassParams, BiQuadFilterBandStop, BiQuadFilterBandStopParams, - BiQuadFilterHighPass, BiQuadFilterHighPassParams, BiQuadFilterHighShelf, - BiQuadFilterHighShelfParams, BiQuadFilterLowPass12db, BiQuadFilterLowPass12dbParams, - BiQuadFilterLowPass24db, BiQuadFilterLowPass24dbParams, BiQuadFilterLowShelf, - BiQuadFilterLowShelfParams, BiQuadFilterNone, BiQuadFilterNoneParams, BiQuadFilterPeakingEq, - BiQuadFilterPeakingEqParams, -}; -pub use gain::{Gain, GainParams}; pub use limiter::{Limiter, LimiterParams}; pub use mixer::{Mixer, MixerParams}; -pub use reverb::{Reverb, ReverbParams}; pub(crate) mod bitcrusher; -pub(crate) mod chorus; pub(crate) mod compressor; -pub(crate) mod delay; -pub(crate) mod filter; -pub(crate) mod gain; pub(crate) mod limiter; pub(crate) mod mixer; -pub(crate) mod reverb; diff --git a/entities/src/effects/reverb.rs b/entities/src/effects/reverb.rs deleted file mode 100644 index 6e21635d..00000000 --- a/entities/src/effects/reverb.rs +++ /dev/null @@ -1,206 +0,0 @@ -// Copyright (c) 2023 Mike Tsao. All rights reserved. - -use super::delay::{AllPassDelayLine, Delays, RecirculatingDelayLine}; -use eframe::egui::Ui; -use ensnare::{prelude::*, traits::prelude::*}; -use ensnare_proc_macros::{Control, IsEffect, Params, Uid}; -use serde::{Deserialize, Serialize}; - -/// Schroeder reverb. Uses four parallel recirculating delay lines feeding into -/// a series of two all-pass delay lines. -#[derive(Debug, Control, IsEffect, Params, Uid, Serialize, Deserialize)] -pub struct Reverb { - uid: Uid, - - #[serde(skip)] - sample_rate: SampleRate, - - /// How much the effect should attenuate the input. - #[control] - #[params] - attenuation: Normal, - - #[control] - #[params] - seconds: ParameterType, - - #[serde(skip)] - channels: [ReverbChannel; 2], -} -impl Serializable for Reverb {} -impl Configurable for Reverb { - fn sample_rate(&self) -> SampleRate { - self.sample_rate - } - - fn update_sample_rate(&mut self, sample_rate: SampleRate) { - self.sample_rate = sample_rate; - self.channels[0].update_sample_rate(sample_rate); - self.channels[1].update_sample_rate(sample_rate); - } -} -impl TransformsAudio for Reverb { - fn transform_channel(&mut self, channel: usize, input_sample: Sample) -> Sample { - self.channels[channel].transform_channel(channel, input_sample) - } -} -impl Reverb { - pub fn new_with(params: &ReverbParams) -> Self { - // Thanks to https://basicsynth.com/ (page 133 of paperback) for - // constants. - Self { - uid: Default::default(), - sample_rate: Default::default(), - attenuation: params.attenuation(), - seconds: params.seconds(), - channels: [ - ReverbChannel::new_with(params), - ReverbChannel::new_with(params), - ], - } - } - - pub fn attenuation(&self) -> Normal { - self.attenuation - } - - pub fn set_attenuation(&mut self, attenuation: Normal) { - self.attenuation = attenuation; - self.channels - .iter_mut() - .for_each(|c| c.set_attenuation(attenuation)); - } - - pub fn seconds(&self) -> f64 { - self.seconds - } - - pub fn set_seconds(&mut self, seconds: ParameterType) { - self.seconds = seconds; - self.channels - .iter_mut() - .for_each(|c| c.set_seconds(seconds)); - } -} - -#[derive(Debug, Default)] -struct ReverbChannel { - attenuation: Normal, - - recirc_delay_lines: Vec, - allpass_delay_lines: Vec, -} -impl TransformsAudio for ReverbChannel { - fn transform_channel(&mut self, _channel: usize, input_sample: Sample) -> Sample { - let input_attenuated = input_sample * self.attenuation.value(); - let recirc_output = self.recirc_delay_lines[0].pop_output(input_attenuated) - + self.recirc_delay_lines[1].pop_output(input_attenuated) - + self.recirc_delay_lines[2].pop_output(input_attenuated) - + self.recirc_delay_lines[3].pop_output(input_attenuated); - let adl_0_out = self.allpass_delay_lines[0].pop_output(recirc_output); - self.allpass_delay_lines[1].pop_output(adl_0_out) - } -} -impl Serializable for ReverbChannel {} -impl Configurable for ReverbChannel { - fn update_sample_rate(&mut self, sample_rate: SampleRate) { - self.recirc_delay_lines - .iter_mut() - .for_each(|r| r.update_sample_rate(sample_rate)); - self.allpass_delay_lines - .iter_mut() - .for_each(|r| r.update_sample_rate(sample_rate)); - } -} -impl ReverbChannel { - pub fn new_with(params: &ReverbParams) -> Self { - // Thanks to https://basicsynth.com/ (page 133 of paperback) for - // constants. - Self { - attenuation: params.attenuation(), - recirc_delay_lines: Self::instantiate_recirc_delay_lines(params.seconds()), - allpass_delay_lines: Self::instantiate_allpass_delay_lines(), - } - } - - fn set_attenuation(&mut self, attenuation: Normal) { - self.attenuation = attenuation; - } - - fn set_seconds(&mut self, seconds: ParameterType) { - self.recirc_delay_lines = Self::instantiate_recirc_delay_lines(seconds); - } - - fn instantiate_recirc_delay_lines(seconds: ParameterType) -> Vec { - vec![ - RecirculatingDelayLine::new_with( - 0.0297, - seconds, - Normal::from(0.001), - Normal::from(1.0), - ), - RecirculatingDelayLine::new_with( - 0.0371, - seconds, - Normal::from(0.001), - Normal::from(1.0), - ), - RecirculatingDelayLine::new_with( - 0.0411, - seconds, - Normal::from(0.001), - Normal::from(1.0), - ), - RecirculatingDelayLine::new_with( - 0.0437, - seconds, - Normal::from(0.001), - Normal::from(1.0), - ), - ] - } - - fn instantiate_allpass_delay_lines() -> Vec { - vec![ - AllPassDelayLine::new_with(0.09683, 0.0050, Normal::from(0.001), Normal::from(1.0)), - AllPassDelayLine::new_with(0.03292, 0.0017, Normal::from(0.001), Normal::from(1.0)), - ] - } -} -impl Displays for Reverb { - fn ui(&mut self, ui: &mut Ui) -> eframe::egui::Response { - ui.label(self.name()) - } -} - -#[cfg(test)] -mod tests { - use super::Reverb; - use crate::{effects::ReverbParams, tests::DEFAULT_SAMPLE_RATE}; - use ensnare::{prelude::*, traits::prelude::*}; - - #[test] - fn reverb_does_anything_at_all() { - // This test is lame, because I can't think of a programmatic way to - // test that reverb works. I observed that with the Schroeder reverb set - // to 0.5 seconds, we start getting back nonzero samples (first - // 0.47767496) at samples: 29079, seconds: 0.65938777. This doesn't look - // wrong, but I couldn't have predicted that exact number. - let mut fx = Reverb::new_with(&ReverbParams { - attenuation: Normal::from(0.9), - seconds: 0.5, - }); - fx.update_sample_rate(SampleRate::DEFAULT); - assert_eq!(fx.transform_channel(0, Sample::from(0.8)), Sample::SILENCE); - let mut s = Sample::default(); - for _ in 0..DEFAULT_SAMPLE_RATE { - s += fx.transform_channel(0, Sample::SILENCE); - } - assert!(s != Sample::SILENCE); - - // TODO: this test might not do anything. I refactored it in a hurry and - // took something that looked critical (skipping the clock to 0.5 - // seconds) out of it, but it still passed. I might not actually be - // testing anything useful. - } -} diff --git a/entities/src/instruments/drumkit.rs b/entities/src/instruments/drumkit.rs index e74de0d8..95fad68d 100644 --- a/entities/src/instruments/drumkit.rs +++ b/entities/src/instruments/drumkit.rs @@ -3,7 +3,7 @@ use super::{sampler::SamplerVoice, Sampler}; use anyhow::anyhow; use eframe::egui::Ui; -use ensnare::{ +use ensnare_core::{ instruments::Synthesizer, midi::{prelude::*, GeneralMidiPercussionProgram}, prelude::*, diff --git a/entities/src/instruments/fm.rs b/entities/src/instruments/fm.rs index 7c245fb4..ae759e95 100644 --- a/entities/src/instruments/fm.rs +++ b/entities/src/instruments/fm.rs @@ -1,15 +1,17 @@ // Copyright (c) 2023 Mike Tsao. All rights reserved. use eframe::egui::Ui; -use ensnare::{ - instruments::Synthesizer, midi::prelude::*, prelude::*, traits::prelude::*, - traits::GeneratesEnvelope, voices::StealingVoiceStore, -}; -use ensnare_proc_macros::{Control, IsInstrument, Params, Uid}; -use groove_core::{ +use ensnare_core::{ generators::{Envelope, EnvelopeParams, Oscillator, OscillatorParams, Waveform}, - Dca, DcaParams, + instruments::Synthesizer, + midi::prelude::*, + modulators::{Dca, DcaParams}, + prelude::*, + traits::prelude::*, + traits::GeneratesEnvelope, + voices::StealingVoiceStore, }; +use ensnare_proc_macros::{Control, IsInstrument, Params, Uid}; use serde::{Deserialize, Serialize}; #[derive(Debug, Default)] diff --git a/entities/src/instruments/sampler.rs b/entities/src/instruments/sampler.rs index 66b195ef..dd91291f 100644 --- a/entities/src/instruments/sampler.rs +++ b/entities/src/instruments/sampler.rs @@ -2,7 +2,7 @@ use anyhow::{anyhow, Result}; use eframe::egui::Ui; -use ensnare::{ +use ensnare_core::{ instruments::Synthesizer, midi::prelude::*, prelude::*, diff --git a/entities/src/instruments/welsh.rs b/entities/src/instruments/welsh.rs index 3f257991..48c7d01a 100644 --- a/entities/src/instruments/welsh.rs +++ b/entities/src/instruments/welsh.rs @@ -1,20 +1,22 @@ // Copyright (c) 2023 Mike Tsao. All rights reserved. -use crate::effects::{BiQuadFilterLowPass24db, BiQuadFilterLowPass24dbParams}; use core::fmt::Debug; use eframe::{ egui::{CollapsingHeader, Response, Sense, Ui}, epaint::{Color32, Stroke, Vec2}, }; -use ensnare::{ - instruments::Synthesizer, midi::prelude::*, prelude::*, traits::prelude::*, - traits::GeneratesEnvelope, voices::StealingVoiceStore, -}; -use ensnare_proc_macros::{Control, IsInstrument, Params, Uid}; -use groove_core::{ +use ensnare_core::{ generators::{Envelope, EnvelopeParams, Oscillator, OscillatorParams}, - Dca, DcaParams, + instruments::Synthesizer, + midi::prelude::*, + modulators::{Dca, DcaParams}, + prelude::*, + temp_impls::effects::filter::{BiQuadFilterLowPass24db, BiQuadFilterLowPass24dbParams}, + traits::prelude::*, + traits::GeneratesEnvelope, + voices::StealingVoiceStore, }; +use ensnare_proc_macros::{Control, IsInstrument, Params, Uid}; use serde::{Deserialize, Serialize}; use strum_macros::{EnumCount as EnumCountMacro, FromRepr}; diff --git a/entities/src/lib.rs b/entities/src/lib.rs index bf5963e9..60b91ee0 100644 --- a/entities/src/lib.rs +++ b/entities/src/lib.rs @@ -30,7 +30,7 @@ pub mod instruments; #[cfg(test)] mod tests { - use ensnare::core::ParameterType; + use ensnare_core::core::ParameterType; pub(crate) const DEFAULT_SAMPLE_RATE: usize = 44100; pub(crate) const DEFAULT_BPM: ParameterType = 128.0; diff --git a/examples/explorer.rs b/examples/explorer.rs index dca5f8ef..902b6c50 100644 --- a/examples/explorer.rs +++ b/examples/explorer.rs @@ -13,23 +13,23 @@ use eframe::{ epaint::vec2, CreationContext, }; -use ensnare::{midi::prelude::*, prelude::*, traits::prelude::*}; -use groove::{ - app_version, - mini::{ - register_factory_entities, - widgets::{ - audio::{self, CircularSampleBuffer}, - control, - controllers::es_sequencer, - pattern, placeholder, timeline, track, - }, - ControlAtlas, ControlRouter, DragDropEvent, DragDropManager, DragDropSource, ESSequencer, - ESSequencerBuilder, EntityStore, Note, PatternUid, PianoRoll, Sequencer, TrackTitle, - TrackUid, +use ensnare::prelude::*; +use ensnare_core::{ + control::{ControlAtlas, ControlRouter}, + drag_drop::{DragDropEvent, DragDropManager, DragDropSource}, + entities::EntityStore, + midi::prelude::*, + piano_roll::{Note, PatternUid, PianoRoll}, + prelude::*, + temp_impls::controllers::{ + even_smaller_sequencer::{ESSequencer, ESSequencerBuilder}, + mini_sequencer::Sequencer, }, - EntityFactory, + track::{TrackTitle, TrackUid}, + traits::prelude::*, + widgets::{audio::CircularSampleBuffer, prelude::*}, }; +use groove::app_version; use std::ops::Range; #[derive(Debug)] @@ -215,7 +215,7 @@ pub fn device_chain<'a>( #[derive(Debug)] pub enum DeviceChainAction { - NewDevice(groove::mini::Key), + NewDevice(EntityKey), } #[derive(Debug)] @@ -531,7 +531,10 @@ impl ESSequencerSettings { fn show(&mut self, ui: &mut Ui) { if !self.hide { - ui.add(es_sequencer(&mut self.sequencer, self.view_range.clone())); + ui.add(controllers::es_sequencer( + &mut self.sequencer, + self.view_range.clone(), + )); } } } diff --git a/examples/minicli.rs b/examples/minicli.rs index f020558d..5f22f95e 100644 --- a/examples/minicli.rs +++ b/examples/minicli.rs @@ -4,7 +4,7 @@ //! [Orchestrator]. use clap::Parser; -use groove::mini::Orchestrator; +use ensnare_core::prelude::Orchestrator; use regex::Regex; use std::{fs::File, io::BufReader, path::PathBuf}; diff --git a/examples/minidaw.rs b/examples/minidaw.rs index 0733f48c..d1878a64 100644 --- a/examples/minidaw.rs +++ b/examples/minidaw.rs @@ -18,17 +18,15 @@ use eframe::{ CreationContext, }; use egui_toast::{Toast, ToastOptions, Toasts}; -use ensnare::traits::prelude::*; +use ensnare::prelude::*; +use ensnare_core::{drag_drop::DragDropManager, prelude::*, traits::prelude::*}; use ensnare_midi_interface::{MidiInterfaceInput, MidiPortDescriptor}; -use groove::{ - app_version, - mini::{register_factory_entities, DragDropManager, EntityFactory, Key, Orchestrator}, - panels::{ - audio_settings, midi_settings, AudioPanel, AudioPanelEvent, AudioSettings, ControlPanel, - ControlPanelAction, MidiPanel, MidiPanelEvent, MidiSettings, NeedsAudioFn, - OrchestratorEvent, OrchestratorInput, OrchestratorPanel, PaletteAction, PalettePanel, - }, +use ensnare_not_core::panels::{ + audio_settings, midi_settings, AudioPanel, AudioPanelEvent, AudioSettings, ControlPanel, + ControlPanelAction, MidiPanel, MidiPanelEvent, MidiSettings, NeedsAudioFn, OrchestratorEvent, + OrchestratorInput, OrchestratorPanel, PaletteAction, PalettePanel, }; +use groove::app_version; use serde::{Deserialize, Serialize}; use std::{ io::{Read, Write}, @@ -222,7 +220,7 @@ enum MenuBarAction { TrackDuplicate, TrackDelete, TrackRemoveSelectedPatterns, - TrackAddEntity(Key), + TrackAddEntity(EntityKey), ComingSoon, } diff --git a/midi/src/lib.rs b/midi/src/lib.rs index 559c9578..3639bb3a 100644 --- a/midi/src/lib.rs +++ b/midi/src/lib.rs @@ -5,9 +5,9 @@ //! to exchange [MidiHandlerInput] and [MidiHandlerEvent] messages. use crossbeam_channel::{unbounded, Receiver, Sender}; -use ensnare::midi::{u4, LiveEvent, MidiChannel, MidiMessage}; -use ensnare_midi_interface::MidiPortDescriptor; +use ensnare::prelude::*; use midir::{MidiInput, MidiInputConnection, MidiOutput, MidiOutputConnection, SendError}; +use midly::live::LiveEvent; use std::{fmt::Debug, thread::JoinHandle}; /// The client sends requests to the MIDI interface through [MidiInterfaceInput] messages. diff --git a/orchestration/Cargo.toml b/orchestration/Cargo.toml index f81e7a30..922b9178 100644 --- a/orchestration/Cargo.toml +++ b/orchestration/Cargo.toml @@ -11,7 +11,7 @@ crossbeam = "0.8" dipstick = { version = "0.9", optional = true } eframe = { version = "0.22" } egui_extras = { version = "0.22" } -ensnare = { path = "../../ensnare" } +ensnare-core = { path = "../../ensnare/core" } ensnare-proc-macros = { path = "../../ensnare/proc-macros" } groove-core = { path = "../core" } groove-entities = { path = "../entities" } diff --git a/orchestration/src/entities.rs b/orchestration/src/entities.rs index 275750d8..4c7fd287 100644 --- a/orchestration/src/entities.rs +++ b/orchestration/src/entities.rs @@ -1,140 +1,140 @@ -// Copyright (c) 2023 Mike Tsao. All rights reserved. +// // Copyright (c) 2023 Mike Tsao. All rights reserved. -//! An [EntityObsolete] wraps a musical device, giving it the ability to be managed by -//! [crate::Orchestrator] and automated by other devices in the system. +// //! An [EntityObsolete] wraps a musical device, giving it the ability to be managed by +// //! [crate::Orchestrator] and automated by other devices in the system. -use groove_entities::{ - controllers::{ - Arpeggiator, Calculator, LfoController, PatternManager, Sequencer, - SignalPassthroughController, Timer, ToyController, Trigger, - }, - effects::{ - BiQuadFilterAllPass, BiQuadFilterBandPass, BiQuadFilterBandStop, BiQuadFilterHighPass, - BiQuadFilterHighShelf, BiQuadFilterLowPass12db, BiQuadFilterLowPass24db, - BiQuadFilterLowShelf, BiQuadFilterNone, BiQuadFilterPeakingEq, Bitcrusher, Chorus, - Compressor, Delay, Gain, Limiter, Mixer, Reverb, - }, - instruments::{Drumkit, FmSynth, Sampler, WelshSynth}, -}; -//use groove_proc_macros::Everything; -use groove_toys::{DebugSynth, ToyAudioSource, ToyEffect, ToyInstrument, ToySynth}; +// use groove_entities::{ +// controllers::{ +// Arpeggiator, Calculator, LfoController, PatternManager, Sequencer, +// SignalPassthroughController, Trigger, +// }, +// effects::{ +// BiQuadFilterAllPass, BiQuadFilterBandPass, BiQuadFilterBandStop, BiQuadFilterHighPass, +// BiQuadFilterHighShelf, BiQuadFilterLowPass12db, BiQuadFilterLowPass24db, +// BiQuadFilterLowShelf, BiQuadFilterNone, BiQuadFilterPeakingEq, Bitcrusher, Chorus, +// Compressor, Delay, Gain, Limiter, Mixer, Reverb, +// }, +// instruments::{Drumkit, FmSynth, Sampler, WelshSynth}, +// }; +// //use groove_proc_macros::Everything; +// use groove_toys::{DebugSynth, ToyEffect, ToySynth}; -// PRO TIP: use `cargo expand --lib entities` to see what's being generated +// // PRO TIP: use `cargo expand --lib entities` to see what's being generated -/// The #[derive] macro uses [Everything] to generate a lot of boilerplate code. -/// The enum itself is otherwise unused. -#[allow(dead_code)] -//#[derive(Everything)] -enum Everything { - //#[everything(controller, midi, controllable)] - Arpeggiator(Arpeggiator), +// /// The #[derive] macro uses [Everything] to generate a lot of boilerplate code. +// /// The enum itself is otherwise unused. +// #[allow(dead_code)] +// //#[derive(Everything)] +// enum Everything { +// //#[everything(controller, midi, controllable)] +// Arpeggiator(Arpeggiator), - //#[everything(effect, controllable)] - BiQuadFilterAllPass(BiQuadFilterAllPass), +// //#[everything(effect, controllable)] +// BiQuadFilterAllPass(BiQuadFilterAllPass), - //#[everything(effect, controllable)] - BiQuadFilterBandPass(BiQuadFilterBandPass), +// //#[everything(effect, controllable)] +// BiQuadFilterBandPass(BiQuadFilterBandPass), - //#[everything(effect, controllable)] - BiQuadFilterBandStop(BiQuadFilterBandStop), +// //#[everything(effect, controllable)] +// BiQuadFilterBandStop(BiQuadFilterBandStop), - //#[everything(effect, controllable)] - BiQuadFilterHighPass(BiQuadFilterHighPass), +// //#[everything(effect, controllable)] +// BiQuadFilterHighPass(BiQuadFilterHighPass), - //#[everything(effect, controllable)] - BiQuadFilterHighShelf(BiQuadFilterHighShelf), +// //#[everything(effect, controllable)] +// BiQuadFilterHighShelf(BiQuadFilterHighShelf), - //#[everything(effect, controllable)] - BiQuadFilterLowPass12db(BiQuadFilterLowPass12db), +// //#[everything(effect, controllable)] +// BiQuadFilterLowPass12db(BiQuadFilterLowPass12db), - //#[everything(effect, controllable)] - BiQuadFilterLowPass24db(BiQuadFilterLowPass24db), +// //#[everything(effect, controllable)] +// BiQuadFilterLowPass24db(BiQuadFilterLowPass24db), - //#[everything(effect, controllable)] - BiQuadFilterLowShelf(BiQuadFilterLowShelf), +// //#[everything(effect, controllable)] +// BiQuadFilterLowShelf(BiQuadFilterLowShelf), - //#[everything(effect, controllable)] - BiQuadFilterNone(BiQuadFilterNone), +// //#[everything(effect, controllable)] +// BiQuadFilterNone(BiQuadFilterNone), - //#[everything(effect, controllable)] - BiQuadFilterPeakingEq(BiQuadFilterPeakingEq), +// //#[everything(effect, controllable)] +// BiQuadFilterPeakingEq(BiQuadFilterPeakingEq), - //#[everything(effect, controllable)] - Bitcrusher(Bitcrusher), +// //#[everything(effect, controllable)] +// Bitcrusher(Bitcrusher), - //#[everything(effect, controllable)] - Chorus(Chorus), +// //#[everything(effect, controllable)] +// Chorus(Chorus), - //#[everything(effect, controllable)] - Compressor(Compressor), +// //#[everything(effect, controllable)] +// Compressor(Compressor), - // //#[everything(controller, midi)] - // ControlTrip(ControlTrip), - //#[everything(instrument, midi, controllable)] - DebugSynth(DebugSynth), +// // //#[everything(controller, midi)] +// // ControlTrip(ControlTrip), +// //#[everything(instrument, midi, controllable)] +// DebugSynth(DebugSynth), - //#[everything(effect, controllable)] - Delay(Delay), +// //#[everything(effect, controllable)] +// Delay(Delay), - //#[everything(instrument, midi)] - Drumkit(Drumkit), +// //#[everything(instrument, midi)] +// Drumkit(Drumkit), - //#[everything(instrument, midi, controllable)] - FmSynth(FmSynth), +// //#[everything(instrument, midi, controllable)] +// FmSynth(FmSynth), - //#[everything(effect, controllable)] - Gain(Gain), +// //#[everything(effect, controllable)] +// Gain(Gain), - //#[everything(instrument, controller)] - Integrated(Calculator), +// //#[everything(instrument, controller)] +// Integrated(Calculator), - //#[everything(controller, midi)] - LfoController(LfoController), +// //#[everything(controller, midi)] +// LfoController(LfoController), - //#[everything(effect, controllable)] - Limiter(Limiter), +// //#[everything(effect, controllable)] +// Limiter(Limiter), - // //#[everything(controllable, instrument, midi)] - // Metronome(Metronome), - //#[everything(effect)] - Mixer(Mixer), +// // //#[everything(controllable, instrument, midi)] +// // Metronome(Metronome), +// //#[everything(effect)] +// Mixer(Mixer), - //#[everything(controller, midi)] - PatternManager(PatternManager), +// //#[everything(controller, midi)] +// PatternManager(PatternManager), - //#[everything(effect, controllable)] - Reverb(Reverb), +// //#[everything(effect, controllable)] +// Reverb(Reverb), - //#[everything(instrument, midi)] - Sampler(Sampler), +// //#[everything(instrument, midi)] +// Sampler(Sampler), - //#[everything(controller, midi)] - Sequencer(Sequencer), +// //#[everything(controller, midi)] +// Sequencer(Sequencer), - //#[everything(controller, effect, midi)] - SignalPassthroughController(SignalPassthroughController), +// //#[everything(controller, effect, midi)] +// SignalPassthroughController(SignalPassthroughController), - //#[everything(controller, midi)] - Timer(Timer), +// //#[everything(controller, midi)] +// // Timer(Timer), - //#[everything(instrument, midi)] - ToyAudioSource(ToyAudioSource), +// //#[everything(instrument, midi)] +// // ToyAudioSource(ToyAudioSource), - //#[everything(controller, midi)] - ToyController(ToyController), +// //#[everything(controller, midi)] +// // ToyController(ToyController), - //#[everything(effect, controllable)] - ToyEffect(ToyEffect), +// //#[everything(effect, controllable)] +// ToyEffect(ToyEffect), - //#[everything(instrument, midi, controllable)] - ToyInstrument(ToyInstrument), +// // #[everything(instrument, midi, controllable)] +// // ToyInstrument(ToyInstrument), - //#[everything(instrument, midi, controllable)] - ToySynth(ToySynth), +// //#[everything(instrument, midi, controllable)] +// ToySynth(ToySynth), - //#[everything(controller)] - Trigger(Trigger), +// //#[everything(controller)] +// Trigger(Trigger), - //#[everything(instrument, midi, controllable)] - WelshSynth(WelshSynth), -} +// //#[everything(instrument, midi, controllable)] +// WelshSynth(WelshSynth), +// } diff --git a/orchestration/src/helpers.rs b/orchestration/src/helpers.rs index 3a130b21..6ab32f3b 100644 --- a/orchestration/src/helpers.rs +++ b/orchestration/src/helpers.rs @@ -9,7 +9,7 @@ use cpal::{ traits::{DeviceTrait, HostTrait}, SupportedStreamConfig, }; -use ensnare::prelude::*; +use ensnare_core::prelude::*; use std::path::PathBuf; pub struct IOHelper {} diff --git a/orchestration/src/lib.rs b/orchestration/src/lib.rs index 1debea47..63ae03ec 100644 --- a/orchestration/src/lib.rs +++ b/orchestration/src/lib.rs @@ -21,11 +21,11 @@ mod metrics; #[cfg(test)] mod tests { mod params { - use ensnare::{prelude::*, traits::prelude::*}; + use ensnare_core::{prelude::*, traits::prelude::*}; use ensnare_proc_macros::{Control, Params, Uid}; + use serde::{Deserialize, Serialize}; use strum::EnumCount; use strum_macros::{EnumCount as EnumCountMacro, FromRepr}; - use serde::{Deserialize, Serialize}; #[derive( Clone, Copy, Debug, Default, EnumCountMacro, FromRepr, PartialEq, Serialize, Deserialize, diff --git a/orchestration/src/messages.rs b/orchestration/src/messages.rs index 7e5dbd6e..9999716f 100644 --- a/orchestration/src/messages.rs +++ b/orchestration/src/messages.rs @@ -2,7 +2,7 @@ //! The [messages](crate::messages) module defines the app's messages. -use ensnare::{ +use ensnare_core::{ midi::prelude::*, prelude::*, traits::{prelude::*, MessageBounds}, diff --git a/proc-macros/src/params.rs b/proc-macros/src/params.rs index 9ac39551..275ac865 100644 --- a/proc-macros/src/params.rs +++ b/proc-macros/src/params.rs @@ -108,12 +108,8 @@ pub(crate) fn impl_params_derive(input: TokenStream, primitives: &HashSet } let params_struct_block = quote! { - #[derive(Debug, Default, PartialEq)] - #[cfg_attr( - feature = "serialization", - derive(Serialize, Deserialize), - serde(rename = #struct_snake_case_name, rename_all = "kebab-case") - )] + #[derive(Debug, Default, PartialEq,Serialize, Deserialize)] + #[serde(rename = #struct_snake_case_name, rename_all = "kebab-case")] #[allow(missing_docs)] pub struct #params_name { #( pub #field_names: #field_types ),* diff --git a/src/bin/groove-cli.rs b/src/bin/groove-cli.rs index 295a47d6..38c6f59d 100644 --- a/src/bin/groove-cli.rs +++ b/src/bin/groove-cli.rs @@ -6,7 +6,7 @@ mod obsolete { use anyhow::Ok; use clap::Parser; - use ensnare::prelude::*; + use ensnare_core::prelude::*; use groove::{app_version, DEFAULT_BPM}; use groove_core::SAMPLE_BUFFER_SIZE; use groove_orchestration::{helpers::IOHelper, Orchestrator}; diff --git a/src/bin/groove-egui.rs b/src/bin/groove-egui.rs index 61bc3664..1e1d4c13 100644 --- a/src/bin/groove-egui.rs +++ b/src/bin/groove-egui.rs @@ -17,7 +17,7 @@ mod obsolete { CreationContext, }; use egui_toast::{Toast, ToastOptions, Toasts}; - use ensnare::{midi::prelude::*, prelude::*, traits::prelude::*}; + use ensnare_core::{midi::prelude::*, prelude::*, traits::prelude::*}; use groove::{ app_version, panels::{ diff --git a/src/lib.rs b/src/lib.rs index faa560a5..43066922 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,72 +5,11 @@ #![warn(rustdoc::missing_doc_code_examples)] //! An audio engine designed to support a DAW (digital audio workstation). -//! -//! ``` -//! use ensnare::{midi::prelude::*, prelude::*, traits::prelude::*}; -//! use groove::mini::{Note, Orchestrator}; -//! use groove_entities::{effects::Compressor, effects::CompressorParams}; -//! use groove_toys::{ToySynth, ToySynthParams}; -//! use std::path::PathBuf; -//! -//! const BPM: f64 = 128.0; -//! const MIDI_0: MidiChannel = MidiChannel(0); -//! -//! // The system needs a working buffer for audio. -//! let mut buffer = [StereoSample::SILENCE; 64]; -//! -//! // ToySynth is a MIDI instrument that makes simple sounds. -//! let mut synth = ToySynth::new_with(&ToySynthParams::default()); -//! synth.set_uid(Uid(2001)); -//! -//! // An effect takes the edge off the synth. -//! let mut compressor = Compressor::new_with(&CompressorParams { -//! threshold: Normal::from(0.8), -//! ratio: 0.5, -//! attack: 0.05, -//! release: 0.1, -//! }); -//! compressor.set_uid(Uid(2002)); -//! -//! // Orchestrator understands the relationships among the instruments, -//! // controllers, and effects, and uses them to produce a song. -//! let mut orchestrator = Orchestrator::default(); -//! -//! // Orchestrator owns the sample rate and propagates it to the devices -//! // that it controls. -//! orchestrator.update_sample_rate(SampleRate::DEFAULT); -//! -//! // An Orchestrator manages a set of Tracks, which are what actually contains -//! // musical devices. -//! let track_uid = orchestrator.new_midi_track().unwrap(); -//! let track = orchestrator.get_track_mut(&track_uid).unwrap(); -//! -//! // The sequencer sends MIDI commands to the synth. Each MIDI track -//! // automatically includes one. There are lots of different ways to populate -//! // the sequencer with notes. -//! let mut sequencer = track.sequencer_mut(); -//! -//! // TODO - not working yet! -//! // sequencer.append_note(&Note::new_with_midi_note( -//! // MidiNote::A4, -//! // MusicalTime::START, -//! // MusicalTime::DURATION_QUARTER, -//! // )); -//! -//! // Adding an entity to a track forms a chain that sends MIDI, control, and -//! // audio data appropriately. -//! let synth_id = track.append_entity(Box::new(synth)).unwrap(); -//! let compressor_id = track.append_entity(Box::new(compressor)).unwrap(); -//! -//! // Once everything is set up, the orchestrator renders an audio stream. -//! let _ = orchestrator.write_to_file(&PathBuf::from("output.wav")); -//! ``` // #[deprecated] // pub use groove_orchestration::EntityObsolete; -pub use mini::EntityFactory; -pub use mini::Orchestrator; +use ensnare_core::prelude::ParameterType; /// Widgets for egui pub mod panels; @@ -80,8 +19,7 @@ pub mod mini; /// Recommended imports for first-time users. pub mod prelude { - pub use super::mini::Orchestrator; - pub use ensnare::core::StereoSample; + pub use ensnare_core::core::StereoSample; } // TODO: these should be #[cfg(test)] because nobody should be assuming these @@ -92,7 +30,7 @@ pub mod prelude { pub const DEFAULT_SAMPLE_RATE: usize = 44100; #[doc(hidden)] /// A typical BPM (beats per minute) for EDM. -pub const DEFAULT_BPM: ensnare::core::ParameterType = 128.0; +pub const DEFAULT_BPM: ParameterType = 128.0; #[doc(hidden)] /// The most common time signature pub const DEFAULT_TIME_SIGNATURE: (usize, usize) = (4, 4); diff --git a/src/mini/bus_station.rs b/src/mini/bus_station.rs index fd2e4d75..5aadde6e 100644 --- a/src/mini/bus_station.rs +++ b/src/mini/bus_station.rs @@ -1,7 +1,6 @@ // Copyright (c) 2023 Mike Tsao. All rights reserved. -use super::TrackUid; -use ensnare::core::Normal; +use ensnare_core::{core::Normal, track::TrackUid}; use serde::{Deserialize, Serialize}; use std::collections::HashMap; @@ -55,7 +54,7 @@ impl BusStation { #[cfg(test)] mod tests { - use ensnare::prelude::*; + use ensnare_core::prelude::*; use super::*; diff --git a/src/mini/control_atlas.rs b/src/mini/control_atlas.rs deleted file mode 100644 index b52966be..00000000 --- a/src/mini/control_atlas.rs +++ /dev/null @@ -1,615 +0,0 @@ -// Copyright (c) 2023 Mike Tsao. All rights reserved. - -use super::rng::Rng; -use derive_builder::Builder; -use eframe::egui::Ui; -use ensnare::traits::{ - Configurable, ControlEventsFn, Controls, Displays, DisplaysInTimeline, EntityEvent, - HandlesMidi, Serializable, -}; -use ensnare::{prelude::*, uid::Uid}; -use ensnare_proc_macros::{IsController, Uid}; -use serde::{Deserialize, Serialize}; -use std::{ - ops::{Range, RangeInclusive}, - vec::Vec, -}; - -impl ControlTripBuilder { - /// Generates a random [ControlTrip]. For development/prototyping only. - pub fn random(&mut self, start: MusicalTime) -> &mut Self { - let mut rng = Rng::default(); - - let mut pos = start; - for _ in 0..rng.0.rand_range(5..8) { - self.step( - ControlStepBuilder::default() - .time(pos) - .path(ControlTripPath::Flat) - .value(ControlValue(rng.0.rand_float())) - .build() - .unwrap(), - ); - pos += MusicalTime::new_with_beats(rng.0.rand_range(4..12) as usize); - } - self - } -} - -#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize)] -/// Specifies what a [ControlStep]'s path should look like. -pub enum ControlTripPath { - /// No path. This step's value should be ignored. - #[default] - None, - /// Stairstep. The path should be level at the [ControlStep]'s value. - Flat, - /// Linear. Straight line from this [ControlStep]'s value to the next one. - Linear, - /// Curved. Starts out changing quickly and ends up changing slowly. - Logarithmic, - /// Curved. Starts out changing slowly and ends up changing quickly. - Exponential, -} -impl ControlTripPath { - /// Returns the next enum, wrapping to zero if needed. - pub fn next(&self) -> Self { - match self { - ControlTripPath::None => ControlTripPath::None, - ControlTripPath::Flat => ControlTripPath::Linear, - ControlTripPath::Linear => ControlTripPath::Flat, - ControlTripPath::Logarithmic => ControlTripPath::Logarithmic, - ControlTripPath::Exponential => ControlTripPath::Exponential, - } - } -} - -#[derive(Debug)] -pub struct ControlTripEphemerals { - /// The time range for this work slice. This is a copy of the value passed - /// in Controls::update_time(). - range: Range, - - /// The GUI view's time range. - view_range: Range, - - /// Which step we're currently processing. - current_step: usize, - /// The type of path we should be following. - current_path: ControlTripPath, - /// The range of values for the current step. - value_range: RangeInclusive, - /// The timespan of the current step. - time_range: Range, - - /// The value that we last issued as a Control event. We keep track of this - /// to avoid issuing consecutive identical events. - last_published_value: f64, - - /// Whether the current_step working variables are an unknown state -- - /// either just-initialized, or the work cursor is jumping to an earlier - /// position. - is_current_step_clean: bool, -} -impl Default for ControlTripEphemerals { - fn default() -> Self { - Self { - range: Default::default(), - view_range: Default::default(), - current_step: Default::default(), - current_path: Default::default(), - value_range: ControlValue::default()..=ControlValue::default(), - time_range: MusicalTime::empty_range(), - last_published_value: Default::default(), - is_current_step_clean: Default::default(), - } - } -} -impl ControlTripEphemerals { - fn reset_current_path_if_needed(&mut self) { - if !self.is_current_step_clean { - self.is_current_step_clean = true; - self.current_step = Default::default(); - self.current_path = Default::default(); - self.value_range = ControlValue::default()..=ControlValue::default(); - self.time_range = MusicalTime::empty_range(); - } - } -} - -/// A [ControlTrip] is a single track of automation. It can run as long as the -/// whole song. -/// -/// A trip consists of [ControlStep]s ordered by time. Each step specifies a -/// point in time, a [ControlValue], and a [ControlPath] that indicates how to -/// progress from the current [ControlStep] to the next one. -#[derive(Serialize, Deserialize, Debug, Default, IsController, Uid, Builder)] -#[builder(setter(skip), default)] -pub struct ControlTrip { - uid: Uid, - - /// The [ControlStep]s that make up this trip. They must be in ascending - /// time order. TODO: enforce that. - #[builder(default, setter(each(name = "step", into)))] - steps: Vec, - - #[serde(skip)] - e: ControlTripEphemerals, -} -impl ControlTrip { - fn update_interval(&mut self) { - self.e.reset_current_path_if_needed(); - - // Are we in the middle of handling a step? - if self.e.time_range.contains(&self.e.range.start) { - // Yes; all the work is configured. Let's return so we can do it. - return; - } - - // The current step does not contain the current work slice. Find one that does. - match self.steps.len() { - 0 => { - // Empty trip. Mark that we don't have a path. This is a - // terminal state. - self.e.current_path = ControlTripPath::None; - } - 1 => { - // This trip has only one step, indicating that we should stay - // level at its value. - let step = &self.steps[0]; - self.e.current_path = ControlTripPath::Flat; - self.e.value_range = step.value..=step.value; - - // Mark the time range to include all time so that we'll - // early-exit this method in future calls. - self.e.time_range = MusicalTime::START..MusicalTime::TIME_MAX; - } - _ => { - // We have multiple steps. Find the one that corresponds to the - // current work slice. Start with the current step, build a - // range from it, and see whether it fits. - - let (mut end_time, mut end_value) = if self.e.current_step == 0 { - (MusicalTime::START, self.steps[0].value) - } else { - ( - self.steps[self.e.current_step - 1].time, - self.steps[self.e.current_step - 1].value, - ) - }; - loop { - let is_last = self.e.current_step == self.steps.len() - 1; - let step = &self.steps[self.e.current_step]; - let next_step = if !is_last { - self.steps[self.e.current_step + 1].clone() - } else { - ControlStep { - value: step.value, - time: MusicalTime::TIME_MAX, - path: ControlTripPath::Flat, - } - }; - let start_time = end_time; - let start_value = end_value; - (end_time, end_value) = (next_step.time, next_step.value); - - // Build the range. Is it the right one? - let step_time_range = start_time..end_time; - if step_time_range.contains(&self.e.range.start) { - // Yes, this range contains the current work slice. Set - // it up, and get out of here. - self.e.current_path = step.path; - self.e.time_range = step_time_range; - self.e.value_range = match step.path { - ControlTripPath::None => todo!(), - ControlTripPath::Flat => start_value..=start_value, - ControlTripPath::Linear => start_value..=end_value, - ControlTripPath::Logarithmic => todo!(), - ControlTripPath::Exponential => todo!(), - }; - break; - } else { - // No. Continue searching. - debug_assert!( - !is_last, - "Something is wrong. The last step's time range should be endless." - ); - self.e.current_step += 1; - } - } - } - } - } - - pub(crate) fn steps(&mut self) -> &[ControlStep] { - &self.steps - } - - pub(crate) fn steps_mut(&mut self) -> &mut Vec { - &mut self.steps - } -} -impl DisplaysInTimeline for ControlTrip { - fn set_view_range(&mut self, view_range: &std::ops::Range) { - self.e.view_range = view_range.clone(); - } -} -impl Displays for ControlTrip { - fn ui(&mut self, _ui: &mut eframe::egui::Ui) -> eframe::egui::Response { - unimplemented!("Use the trip widget rather than calling this directly") - } -} -impl HandlesMidi for ControlTrip {} -impl Controls for ControlTrip { - fn update_time(&mut self, range: &Range) { - if range.start < self.e.range.start { - // The cursor is jumping around. Mark things dirty. - self.e.is_current_step_clean = false; - } - self.e.range = range.clone(); - self.update_interval(); - } - - fn work(&mut self, control_events_fn: &mut ControlEventsFn) { - // If we have no current path, then we're all done. - if matches!(self.e.current_path, ControlTripPath::None) { - return; - } - if self.e.range.start >= self.e.time_range.end - || self.e.range.end <= self.e.time_range.start - { - self.update_interval(); - } - let current_point = self.e.range.start.total_units() as f64; - let start = self.e.time_range.start.total_units() as f64; - let end = self.e.time_range.end.total_units() as f64; - let duration = end - start; - let current_point = current_point - start; - let percentage = if duration > 0.0 { - current_point / duration - } else { - 0.0 - }; - let current_value = self.e.value_range.start().0 - + percentage * (self.e.value_range.end().0 - self.e.value_range.start().0); - if current_value != self.e.last_published_value { - self.e.last_published_value = current_value; - control_events_fn( - self.uid, - EntityEvent::Control(ControlValue::from(current_value)), - ); - } - } - - fn is_finished(&self) -> bool { - matches!(self.e.current_path, ControlTripPath::None) - || self.e.current_step + 1 == self.steps.len() - } - - fn play(&mut self) { - todo!() - } - - fn stop(&mut self) { - todo!() - } - - fn skip_to_start(&mut self) { - todo!() - } - - fn is_performing(&self) -> bool { - false - } -} -impl Configurable for ControlTrip {} -impl Serializable for ControlTrip {} - -/// Describes a step of a [ControlTrip]. A [ControlStep] has a starting value as -/// of the specified time, and a [ControlPath] that specifies how to get from -/// the current value to the next [ControlStep]'s value. -/// -/// If the first [ControlStep] in a [ControlTrip] does not start at -/// MusicalTime::START, then we synthesize a flat path, at this step's value, -/// from time zero to this step's time. Likewise, the last [ControlStep] in a -/// [ControlTrip] is always flat until MusicalTime::MAX. -#[derive(Serialize, Deserialize, Debug, Default, Builder, Clone)] -pub struct ControlStep { - /// The initial value of this step. - pub value: ControlValue, - /// When this step begins. - pub time: MusicalTime, - /// How the step should progress to the next step. If this step is the last - /// in a trip, then it's ControlPath::Flat. - pub path: ControlTripPath, -} - -/// A [ControlAtlas] manages a group of [ControlTrip]s. (An atlas is a book of -/// maps.) -#[derive(Serialize, Deserialize, IsController, Debug, Uid)] -pub struct ControlAtlas { - uid: Uid, - trips: Vec, -} -impl Default for ControlAtlas { - fn default() -> Self { - let mut r = Self { - uid: Default::default(), - trips: Default::default(), - }; - r.add_trip( - ControlTripBuilder::default() - .random(MusicalTime::START) - .build() - .unwrap(), - ); - r - } -} -impl Displays for ControlAtlas { - fn ui(&mut self, _ui: &mut Ui) -> eframe::egui::Response { - unimplemented!("Use the atlas widget rather than calling this directly") - } -} -impl HandlesMidi for ControlAtlas {} -impl Controls for ControlAtlas { - fn update_time(&mut self, range: &Range) { - self.trips.iter_mut().for_each(|t| t.update_time(range)); - } - - fn work(&mut self, control_events_fn: &mut ControlEventsFn) { - self.trips - .iter_mut() - .for_each(|t| t.work(control_events_fn)); - } - - fn is_finished(&self) -> bool { - self.trips.iter().all(|ct| ct.is_finished()) - } - - fn play(&mut self) { - todo!() - } - - fn stop(&mut self) { - todo!() - } - - fn skip_to_start(&mut self) { - todo!() - } - - fn is_performing(&self) -> bool { - false - } -} -impl Serializable for ControlAtlas {} -impl Configurable for ControlAtlas { - fn update_sample_rate(&mut self, sample_rate: SampleRate) { - self.trips - .iter_mut() - .for_each(|t| t.update_sample_rate(sample_rate)); - } - - fn update_tempo(&mut self, tempo: Tempo) { - self.trips.iter_mut().for_each(|t| t.update_tempo(tempo)); - } - - fn update_time_signature(&mut self, time_signature: TimeSignature) { - self.trips - .iter_mut() - .for_each(|t| t.update_time_signature(time_signature)); - } -} -impl ControlAtlas { - /// Adds the given [ControlTrip] to this atlas. TODO: specify any ordering constraints - pub fn add_trip(&mut self, trip: ControlTrip) { - self.trips.push(trip); - } - - /// Removes the given [ControlTrip] from this atlas. - pub fn remove_trip(&mut self, uid: Uid) { - self.trips.retain(|t| t.uid != uid); - } - - #[allow(missing_docs)] - pub fn trips_mut(&mut self) -> &mut Vec { - &mut self.trips - } -} - -#[cfg(test)] -mod tests { - use super::*; - - impl ControlTrip { - // Causes the next work() to emit a Control event, even if the value - // matches the last event's value. - fn debug_reset_last_value(&mut self) { - self.e.last_published_value = f64::MAX; - } - } - - #[test] - fn control_step_basics() { - let step = ControlStepBuilder::default() - .value(ControlValue(0.5)) - .time(MusicalTime::START + MusicalTime::DURATION_WHOLE) - .path(ControlTripPath::Flat) - .build(); - assert!(step.is_ok()); - } - - #[test] - fn control_trip_one_step() { - let mut ct = ControlTripBuilder::default() - .step(ControlStep { - value: ControlValue(0.5), - time: MusicalTime::START + MusicalTime::DURATION_WHOLE, - path: ControlTripPath::Flat, - }) - .build() - .unwrap(); - - let range = MusicalTime::START..MusicalTime::DURATION_QUARTER; - ct.update_time(&range); - const MESSAGE: &str = "If there is only one control step, then the trip should remain at that step's level at all times."; - let mut received_event = None; - ct.work(&mut |_uid, event| { - assert!(received_event.is_none()); - received_event = Some(event); - }); - match received_event.unwrap() { - EntityEvent::Control(value) => assert_eq!(value.0, 0.5, "{}", MESSAGE), - _ => panic!(), - } - assert!( - ct.is_finished(), - "A one-step ControlTrip is always finished" - ); - } - - #[test] - fn control_trip_two_flat_steps() { - let mut ct = ControlTripBuilder::default() - .step(ControlStep { - value: ControlValue(0.5), - time: MusicalTime::START, - path: ControlTripPath::Flat, - }) - .step(ControlStep { - value: ControlValue(0.75), - time: MusicalTime::START + MusicalTime::DURATION_WHOLE, - path: ControlTripPath::Flat, - }) - .build() - .unwrap(); - - let range = MusicalTime::START..MusicalTime::DURATION_QUARTER; - ct.update_time(&range); - let mut received_event = None; - ct.work(&mut |_uid, event| { - assert!(received_event.is_none()); - received_event = Some(event); - }); - match received_event.unwrap() { - EntityEvent::Control(value) => assert_eq!(value.0, 0.5, "{}", "Flat step should work"), - _ => panic!(), - } - assert!(!ct.is_finished()); - let range = MusicalTime::START + MusicalTime::DURATION_WHOLE - ..MusicalTime::DURATION_WHOLE + MusicalTime::new_with_units(1); - ct.update_time(&range); - let mut received_event = None; - ct.work(&mut |_uid, event| { - assert!(received_event.is_none()); - received_event = Some(event); - }); - match received_event.unwrap() { - EntityEvent::Control(value) => assert_eq!(value.0, 0.75, "{}", "Flat step should work"), - _ => panic!(), - } - assert!(ct.is_finished()); - } - - #[test] - fn control_trip_linear_step() { - let mut ct = ControlTripBuilder::default() - .step(ControlStep { - value: ControlValue(0.0), - time: MusicalTime::START, - path: ControlTripPath::Linear, - }) - .step(ControlStep { - value: ControlValue(1.0), - time: MusicalTime::new_with_beats(2), - path: ControlTripPath::Flat, - }) - .build() - .unwrap(); - - let range = MusicalTime::new_with_beats(1) - ..MusicalTime::new_with_beats(1) + MusicalTime::new_with_units(1); - ct.update_time(&range); - let mut received_event = None; - ct.work(&mut |_uid, event| { - assert!(received_event.is_none()); - received_event = Some(event); - }); - match received_event.unwrap() { - EntityEvent::Control(value) => assert_eq!( - value.0, 0.5, - "{}", - "Halfway through linear 0.0..=1.0 should be 0.5" - ), - _ => panic!(), - } - assert!(!ct.is_finished()); - } - - #[test] - fn control_trip_many_steps() { - for i in 0..2 { - let mut ct = ControlTripBuilder::default() - .step(ControlStep { - value: ControlValue(0.1), - time: MusicalTime::new_with_units(10), - path: ControlTripPath::Flat, - }) - .step(ControlStep { - value: ControlValue(0.2), - time: MusicalTime::new_with_units(20), - path: ControlTripPath::Flat, - }) - .step(ControlStep { - value: ControlValue(0.3), - time: MusicalTime::new_with_units(30), - path: ControlTripPath::Flat, - }) - .build() - .unwrap(); - - let mut test_values = vec![ - (0, 0.1, false), - (5, 0.1, false), - (10, 0.1, false), - (11, 0.1, false), - (20, 0.2, false), - (21, 0.2, false), - (30, 0.3, true), - (31, 0.3, true), - (9999999999, 0.3, true), - ]; - if i == 1 { - test_values.reverse(); - } - - for (unit, ev, finished) in test_values { - let time = MusicalTime::new_with_units(unit); - ct.update_time(&(time..(time + MusicalTime::new_with_units(1)))); - let mut received_event = None; - ct.work(&mut |_uid, event| { - assert!(received_event.is_none()); - received_event = Some(event); - }); - assert!(received_event.is_some()); - match received_event.unwrap() { - EntityEvent::Control(value) => { - assert_eq!( - value.0, ev, - "{i}: Expected {ev} at {time} but got {}", - value.0 - ) - } - _ => panic!(), - } - assert_eq!( - ct.is_finished(), - finished, - "At time {time} expected is_finished({finished})" - ); - ct.debug_reset_last_value(); - } - } - } -} diff --git a/src/mini/control_router.rs b/src/mini/control_router.rs deleted file mode 100644 index 5bbaf7b4..00000000 --- a/src/mini/control_router.rs +++ /dev/null @@ -1,239 +0,0 @@ -// Copyright (c) 2023 Mike Tsao. All rights reserved. - -use ensnare::{prelude::*, uid::Uid}; -use serde::{Deserialize, Serialize}; -use std::collections::HashMap; - -/// Routes automation control events. -/// -/// An [Entity] that produces control events can be linked to one or more -/// surfaces of other entities. An example of an event producer is an LFO that -/// generates an audio signal, and an example of an event consumer is a synth -/// that exposes its low-pass filter cutoff as a controllable parameter. Linking -/// them means that the cutoff should follow the LFO. When the LFO's value -/// changes, the synth receives a notification of the new [ControlValue] and -/// responds by updating its cutoff. -#[derive(Serialize, Deserialize, Debug, Default)] -pub struct ControlRouter { - uid_to_control: HashMap>, -} -impl ControlRouter { - /// Registers a link between a source [Entity] and a controllable surface on - /// a target [Entity]. - pub fn link_control(&mut self, source_uid: Uid, target_uid: Uid, control_index: ControlIndex) { - self.uid_to_control - .entry(source_uid) - .or_default() - .push((target_uid, control_index)); - } - - /// Removes a control link matching the source/target [Uid] and - /// [ControlIndex]. - pub fn unlink_control( - &mut self, - source_uid: Uid, - target_uid: Uid, - control_index: ControlIndex, - ) { - self.uid_to_control - .entry(source_uid) - .or_default() - .retain(|(uid, index)| !(*uid == target_uid && *index == control_index)); - } - - /// Returns all the control links for a given [Entity]. - pub fn control_links(&self, source_uid: Uid) -> Option<&Vec<(Uid, ControlIndex)>> { - self.uid_to_control.get(&source_uid) - } - - /// Given a control event consisting of a source [Entity] and a - /// [ControlValue], routes that event to the control surfaces linked to it. - pub fn route( - &mut self, - entity_store_fn: &mut dyn FnMut(&Uid, ControlIndex, ControlValue), - source_uid: Uid, - value: ControlValue, - ) -> anyhow::Result<()> { - if let Some(control_links) = self.control_links(source_uid) { - control_links.iter().for_each(|(target_uid, index)| { - entity_store_fn(target_uid, *index, value); - }); - } - Ok(()) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::mini::entity_factory::EntityStore; - use ensnare::traits::{ - Configurable, Controllable, Displays, Generates, HandlesMidi, Serializable, Ticks, - }; - use ensnare_proc_macros::{IsInstrument, Uid}; - use std::sync::{Arc, RwLock}; - - #[derive(Debug, Default, IsInstrument, Uid, Serialize, Deserialize)] - struct TestControllable { - uid: Uid, - - #[serde(skip)] - tracker: Arc>>, - } - impl TestControllable { - fn new_with( - uid: Uid, - tracker: Arc>>, - ) -> Self { - Self { uid, tracker } - } - } - impl Controllable for TestControllable { - fn control_set_param_by_index(&mut self, index: ControlIndex, value: ControlValue) { - if let Ok(mut tracker) = self.tracker.write() { - tracker.push((self.uid, index, value)); - } - } - } - impl HandlesMidi for TestControllable {} - impl Generates for TestControllable { - fn value(&self) -> StereoSample { - StereoSample::SILENCE - } - - fn generate_batch_values(&mut self, _values: &mut [StereoSample]) { - todo!() - } - } - impl Ticks for TestControllable { - fn tick(&mut self, _tick_count: usize) { - todo!() - } - } - impl Serializable for TestControllable {} - impl Configurable for TestControllable {} - impl Displays for TestControllable {} - - #[test] - fn crud_works() { - let mut cr = ControlRouter::default(); - assert!( - cr.uid_to_control.is_empty(), - "new ControlRouter should be empty" - ); - - let source_uid = Uid(1); - let source_2_uid = Uid(2); - let target_uid = Uid(3); - let target_2_uid = Uid(4); - - cr.link_control(source_uid, target_uid, ControlIndex(0)); - assert_eq!( - cr.uid_to_control.len(), - 1, - "there should be one vec after inserting one link" - ); - cr.link_control(source_uid, target_2_uid, ControlIndex(1)); - assert_eq!( - cr.uid_to_control.len(), - 1, - "there should still be one vec after inserting a second link for same source_uid" - ); - cr.link_control(source_2_uid, target_uid, ControlIndex(0)); - assert_eq!( - cr.uid_to_control.len(), - 2, - "there should be two vecs after inserting one link for a second Uid" - ); - - assert_eq!( - cr.control_links(source_uid).unwrap().len(), - 2, - "the first source's vec should have two entries" - ); - assert_eq!( - cr.control_links(source_2_uid).unwrap().len(), - 1, - "the second source's vec should have one entry" - ); - - let mut es = EntityStore::default(); - let tracker = Arc::new(RwLock::new(Vec::default())); - let controllable = TestControllable::new_with(target_uid, Arc::clone(&tracker)); - let _ = es.add(Box::new(controllable)); - let controllable = TestControllable::new_with(target_2_uid, Arc::clone(&tracker)); - let _ = es.add(Box::new(controllable)); - - let _ = cr.route( - &mut |target_uid, index, value| { - if let Some(e) = es.get_mut(target_uid) { - if let Some(e) = e.as_controllable_mut() { - e.control_set_param_by_index(index, value); - } - } - }, - source_uid, - ControlValue(0.5), - ); - if let Ok(t) = tracker.read() { - assert_eq!( - t.len(), - 2, - "there should be expected number of control events after the route {:#?}", - t - ); - assert_eq!(t[0], (target_uid, ControlIndex(0), ControlValue(0.5))); - assert_eq!(t[1], (target_2_uid, ControlIndex(1), ControlValue(0.5))); - }; - - // Try removing links. Start with nonexistent link - if let Ok(mut t) = tracker.write() { - t.clear(); - } - cr.unlink_control(source_uid, target_uid, ControlIndex(99)); - let _ = cr.route( - &mut |target_uid, index, value| { - if let Some(e) = es.get_mut(target_uid) { - if let Some(e) = e.as_controllable_mut() { - e.control_set_param_by_index(index, value); - } - } - }, - source_uid, - ControlValue(0.5), - ); - if let Ok(t) = tracker.read() { - assert_eq!( - t.len(), - 2, - "route results shouldn't change when removing nonexistent link {:#?}", - t - ); - }; - - if let Ok(mut t) = tracker.write() { - t.clear(); - } - cr.unlink_control(source_uid, target_uid, ControlIndex(0)); - let _ = cr.route( - &mut |target_uid, index, value| { - if let Some(e) = es.get_mut(target_uid) { - if let Some(e) = e.as_controllable_mut() { - e.control_set_param_by_index(index, value); - } - } - }, - source_uid, - ControlValue(0.5), - ); - if let Ok(t) = tracker.read() { - assert_eq!( - t.len(), - 1, - "removing a link should continue routing to remaining ones {:#?}", - t - ); - assert_eq!(t[0], (target_2_uid, ControlIndex(1), ControlValue(0.5))); - }; - } -} diff --git a/src/mini/drag_drop.rs b/src/mini/drag_drop.rs deleted file mode 100644 index 39dd330d..00000000 --- a/src/mini/drag_drop.rs +++ /dev/null @@ -1,197 +0,0 @@ -// Copyright (c) 2023 Mike Tsao. All rights reserved. - -use super::{entity_factory::Key, piano_roll::PatternUid, TrackUid}; -use eframe::{ - egui::{CursorIcon, Id as EguiId, InnerResponse, LayerId, Order, Sense, Ui}, - epaint::{self, Color32, Rect, Shape, Stroke, Vec2}, -}; -use ensnare::prelude::*; -use once_cell::sync::OnceCell; -use std::sync::Mutex; -use strum_macros::Display; - -/// The one and only DragDropManager. Access it with `DragDropManager::global()`. -static DD_MANAGER: OnceCell> = OnceCell::new(); - -#[allow(missing_docs)] -#[derive(Clone, Debug, Display, PartialEq, Eq)] -pub enum DragDropSource { - NewDevice(Key), - Pattern(PatternUid), - ControlTrip(Uid), -} - -#[allow(missing_docs)] -#[derive(Clone, Debug, Display)] -pub enum DragDropEvent { - AddDeviceToTrack(Key, TrackUid), - AddPatternToTrack(PatternUid, TrackUid, MusicalTime), -} - -// TODO: a way to express rules about what can and can't be dropped -#[allow(missing_docs)] -#[derive(Debug, Default)] -pub struct DragDropManager { - source: Option, - events: Vec, -} -#[allow(missing_docs)] -impl DragDropManager { - /// Provides the one and only [DragDropManager]. - pub fn global() -> &'static Mutex { - DD_MANAGER - .get() - .expect("DragDropManager has not been initialized") - } - - pub fn reset() { - Self::global().lock().unwrap().source = None; - } - - pub fn enqueue_event(event: DragDropEvent) { - Self::global().lock().unwrap().events.push(event); - } - - pub fn take_and_clear_events() -> Vec { - let mut drag_drop_manager = Self::global().lock().unwrap(); - let events = drag_drop_manager.events.clone(); - drag_drop_manager.events.clear(); - events.into_iter().rev().collect() - } - - // These two functions are based on egui_demo_lib/src/demo/drag_and_drop.rs - pub fn drag_source( - ui: &mut Ui, - id: EguiId, - source: DragDropSource, - body: impl FnOnce(&mut Ui), - ) { - // This allows the app to avoid having to call reset() on every event - // loop iteration, and fixes the bug that a drop target could see only - // the drag sources that were instantiated earlier in the main event - // loop. - if !Self::is_anything_being_dragged(ui) { - Self::reset(); - } - - if ui.memory(|mem| mem.is_being_dragged(id)) { - // It is. So let's mark that it's the one. - Self::global().lock().unwrap().source = Some(source); - - // Indicate in UI that we're dragging. - ui.ctx().set_cursor_icon(CursorIcon::Grabbing); - - // Plan to draw above everything else except debug. - let layer_id = LayerId::new(Order::Tooltip, id); - - // Draw the body and grab the response. - let response = ui.with_layer_id(layer_id, body).response; - - // Shift the entire tooltip layer to keep up with mouse movement. - if let Some(pointer_pos) = ui.ctx().pointer_interact_pos() { - let delta = pointer_pos - response.rect.center(); - ui.ctx().translate_layer(layer_id, delta); - } - } else { - // Let the body draw itself, but scope to undo any style changes. - let response = ui.scope(body).response; - - // If the mouse is still over the item, change cursor to indicate - // that user could drag. - let response = ui.interact(response.rect, id, Sense::drag()); - if response.hovered() { - ui.ctx().set_cursor_icon(CursorIcon::Grab); - } - } - } - - pub fn drop_target( - ui: &mut Ui, - can_accept_what_is_being_dragged: bool, - body: impl FnOnce(&mut Ui) -> R, - ) -> InnerResponse { - // Is there any drag source at all? - let is_anything_dragged = Self::is_anything_being_dragged(ui); - - // Carve out a UI-sized area but leave a bit of margin to draw DnD - // highlight. - let margin = Vec2::splat(2.0); - let outer_rect_bounds = ui.available_rect_before_wrap(); - let inner_rect = outer_rect_bounds.shrink2(margin); - - // We want this to draw behind the body, but we're not sure what it is - // yet. - let where_to_put_background = ui.painter().add(Shape::Noop); - - // Draw the potential target. - let mut content_ui = ui.child_ui(inner_rect, *ui.layout()); - let ret = body(&mut content_ui); - - // I think but am not sure that this calculates the actual boundaries of - // what the body drew. - let outer_rect = - Rect::from_min_max(outer_rect_bounds.min, content_ui.min_rect().max + margin); - - // Figure out what's going on in that rect. - let (rect, response) = ui.allocate_at_least(outer_rect.size(), Sense::hover()); - - // Adjust styling depending on whether this is still a potential target. - let style = if is_anything_dragged && can_accept_what_is_being_dragged && response.hovered() - { - ui.visuals().widgets.active - } else { - ui.visuals().widgets.inactive - }; - let mut fill = style.bg_fill; - let mut stroke = style.bg_stroke; - if is_anything_dragged { - if !can_accept_what_is_being_dragged { - fill = ui.visuals().gray_out(fill); - stroke.color = ui.visuals().gray_out(stroke.color); - } - } else { - fill = Color32::TRANSPARENT; - stroke = Stroke::NONE; - }; - - // Update the background border based on target state. - ui.painter().set( - where_to_put_background, - epaint::RectShape { - rounding: style.rounding, - fill, - stroke, - rect, - }, - ); - - if is_anything_dragged && !can_accept_what_is_being_dragged { - ui.ctx().set_cursor_icon(CursorIcon::NotAllowed); - } - - InnerResponse::new(ret, response) - } - - fn is_anything_being_dragged(ui: &mut Ui) -> bool { - ui.memory(|mem| mem.is_anything_being_dragged()) - } - - fn is_source_set() -> bool { - Self::global().lock().unwrap().source.is_some() - } - - pub fn is_dropped(ui: &mut Ui, response: &eframe::egui::Response) -> bool { - Self::is_anything_being_dragged(ui) - && response.hovered() - && ui.input(|i| i.pointer.any_released()) - && Self::is_source_set() - } - - pub fn source() -> Option { - Self::global().lock().unwrap().source.clone() - } - - pub fn initialize(drag_drop_manager: Self) -> Result<(), Mutex> { - DD_MANAGER.set(Mutex::new(drag_drop_manager)) - } -} diff --git a/src/mini/entities.rs b/src/mini/entities.rs deleted file mode 100644 index d62b66a4..00000000 --- a/src/mini/entities.rs +++ /dev/null @@ -1,148 +0,0 @@ -// Copyright (c) 2023 Mike Tsao. All rights reserved. - -use super::{sequencer::SequencerBuilder, ControlTrip, EntityFactory, Key}; -use ensnare::{midi::MidiChannel, prelude::*}; -use groove_core::generators::Waveform; -use groove_entities::{ - controllers::{ - Arpeggiator, ArpeggiatorParams, LfoController, LfoControllerParams, - SignalPassthroughController, Timer, - }, - effects::{ - BiQuadFilterLowPass24db, BiQuadFilterLowPass24dbParams, Gain, GainParams, Reverb, - ReverbParams, - }, - instruments::{Drumkit, DrumkitParams, WelshSynth, WelshSynthParams}, -}; -use groove_toys::{ - ToyControllerAlwaysSendsMidiMessage, ToyInstrument, ToyInstrumentParams, ToySynth, - ToySynthParams, -}; -use groove_utils::Paths; - -/// Registers all [EntityFactory]'s entities. Note that the function returns a -/// EntityFactory, rather than operating on an &mut. This encourages -/// one-and-done creation, after which the factory is immutable: -/// -/// ```ignore -/// let factory = register_factory_entities(EntityFactory::default()); -/// ``` -#[must_use] -pub fn register_factory_entities(mut factory: EntityFactory) -> EntityFactory { - // TODO: might be nice to move HasUid::name() to be a function. - - factory.register_entity(Key::from("arpeggiator"), || { - Box::new(Arpeggiator::new_with( - &ArpeggiatorParams::default(), - MidiChannel(0), - )) - }); - factory.register_entity(Key::from("sequencer"), || { - Box::new( - SequencerBuilder::default() - .midi_channel_out(MidiChannel(0)) - .build() - .unwrap(), - ) - }); - factory.register_entity(Key::from("reverb"), || { - Box::new(Reverb::new_with(&ReverbParams { - attenuation: Normal::from(0.8), - seconds: 1.0, - })) - }); - factory.register_entity(Key::from("gain"), || { - Box::new(Gain::new_with(&GainParams { - ceiling: Normal::from(0.5), - })) - }); - // TODO: this is lazy. It's too hard right now to adjust parameters within - // code, so I'm creating a special instrument with the parameters I want. - factory.register_entity(Key::from("mute"), || { - Box::new(Gain::new_with(&GainParams { - ceiling: Normal::minimum(), - })) - }); - factory.register_entity(Key::from("filter-low-pass-24db"), || { - Box::new(BiQuadFilterLowPass24db::new_with( - &BiQuadFilterLowPass24dbParams::default(), - )) - }); - factory.register_entity(Key::from("timer"), || { - Box::new(Timer::new_with(MusicalTime::DURATION_QUARTER)) - }); - factory.register_entity(Key::from("toy-synth"), || { - Box::new(ToySynth::new_with(&ToySynthParams::default())) - }); - factory.register_entity(Key::from("toy-instrument"), || { - Box::new(ToyInstrument::new_with(&ToyInstrumentParams::default())) - }); - factory.register_entity(Key::from("toy-controller-noisy"), || { - Box::new(ToyControllerAlwaysSendsMidiMessage::default()) - }); - factory.register_entity(Key::from("welsh-synth"), || { - Box::new(WelshSynth::new_with(&WelshSynthParams::default())) - }); - factory.register_entity(Key::from("drumkit"), || { - Box::new(Drumkit::new_with( - &DrumkitParams::default(), - &Paths::default(), - )) - }); - factory.register_entity(Key::from("lfo"), || { - Box::new(LfoController::new_with(&LfoControllerParams { - frequency: FrequencyHz::from(0.2), - waveform: Waveform::Sawtooth, - })) - }); - factory.register_entity(Key::from("control-trip"), || { - Box::new(ControlTrip::default()) - }); - factory.register_entity(Key::from("signal-passthrough"), || { - Box::new(SignalPassthroughController::default()) - }); - factory.register_entity(Key::from("signal-amplitude-passthrough"), || { - Box::new(SignalPassthroughController::new_amplitude_passthrough_type()) - }); - factory.register_entity(Key::from("signal-amplitude-inverted-passthrough"), || { - Box::new(SignalPassthroughController::new_amplitude_inverted_passthrough_type()) - }); - - factory.complete_registration(); - - factory -} - -#[cfg(test)] -use { - groove_entities::controllers::{ToyController, ToyControllerParams}, - groove_toys::ToyEffect, -}; - -/// Registers all [EntityFactory]'s entities. Note that the function returns an -/// &EntityFactory. This encourages usage like this: -/// -/// ``` -/// let mut factory = EntityFactory::default(); -/// let factory = register_test_factory_entities(&mut factory); -/// ``` -/// -/// This makes the factory immutable once it's set up. -#[cfg(test)] -#[must_use] -pub fn register_test_factory_entities(mut factory: EntityFactory) -> EntityFactory { - factory.register_entity(Key::from("instrument"), || { - Box::new(ToyInstrument::new_with(&ToyInstrumentParams::default())) - }); - factory.register_entity(Key::from("controller"), || { - Box::new(ToyController::new_with( - &ToyControllerParams::default(), - MidiChannel::from(0), - )) - }); - factory.register_entity(Key::from("effect"), || Box::new(ToyEffect::default())); - - factory.complete_registration(); - - factory -} diff --git a/src/mini/entity_factory.rs b/src/mini/entity_factory.rs index e69de29b..8b137891 100644 --- a/src/mini/entity_factory.rs +++ b/src/mini/entity_factory.rs @@ -0,0 +1 @@ + diff --git a/src/mini/even_smaller_sequencer.rs b/src/mini/even_smaller_sequencer.rs deleted file mode 100644 index 6f982a1e..00000000 --- a/src/mini/even_smaller_sequencer.rs +++ /dev/null @@ -1,314 +0,0 @@ -// Copyright (c) 2023 Mike Tsao. All rights reserved. - -use super::{piano_roll::Pattern, rng::Rng, Note}; -use btreemultimap::BTreeMultiMap; -use derive_builder::Builder; -use eframe::egui::Ui; -use ensnare::midi::{MidiChannel, MidiMessage}; -use ensnare::prelude::*; -use ensnare::traits::{ - Configurable, ControlEventsFn, Controls, Displays, EntityEvent, HandlesMidi, Serializable, -}; - -use ensnare_proc_macros::{Control, IsController, Uid}; -use serde::{Deserialize, Serialize}; -use std::ops::Range; - -impl ESSequencerBuilder { - /// Builds the [ESSequencer]. - pub fn build(&self) -> Result { - match self.build_from_builder() { - Ok(mut s) => { - s.after_deser(); - Ok(s) - } - Err(e) => Err(e), - } - } - - /// Produces a random sequence of quarter-note notes. For debugging. - pub fn random(&mut self, range: Range) -> &mut Self { - let mut rng = Rng::default(); - - for _ in 0..32 { - let beat_range = range.start.total_beats() as u64..range.end.total_beats() as u64; - let note_start = MusicalTime::new_with_beats(rng.0.rand_range(beat_range) as usize); - self.note(Note { - key: rng.0.rand_range(16..100) as u8, - range: note_start..note_start + MusicalTime::DURATION_QUARTER, - }); - } - self - } -} - -#[derive(Debug, Default)] -pub struct ESSequencerEphemerals { - // The sequencer should be performing work for this time slice. - range: Range, - // The actual events that the sequencer emits. - events: BTreeMultiMap, - // The latest end time (exclusive) of all the events. - final_event_time: MusicalTime, - // The next place to insert a note. - cursor: MusicalTime, - // Whether we're performing, in the [Performs] sense. - is_performing: bool, -} - -/// [ESSequencer] replays [MidiMessage]s according to [MusicalTime]. -#[derive(Debug, Default, Control, IsController, Uid, Serialize, Deserialize, Builder)] -#[builder(build_fn(private, name = "build_from_builder"))] -pub struct ESSequencer { - #[allow(missing_docs)] - #[builder(default)] - uid: Uid, - #[allow(missing_docs)] - #[builder(default)] - midi_channel_out: MidiChannel, - - /// The [Note]s to be sequenced. - #[builder(default, setter(each(name = "note", into)))] - notes: Vec, - - /// The [Pattern]s to be sequenced. - #[builder(default, setter(each(name = "pattern", into)))] - patterns: Vec<(MusicalTime, Pattern)>, - - /// The default time signature. - #[builder(default)] - time_signature: TimeSignature, - - #[serde(skip)] - #[builder(setter(skip))] - e: ESSequencerEphemerals, -} -impl ESSequencer { - #[allow(dead_code)] - fn cursor(&self) -> MusicalTime { - self.e.cursor - } - - /// Adds the [Pattern] at the specified location. Returns the duration of - /// the inserted pattern. - pub fn insert_pattern( - &mut self, - pattern: &Pattern, - position: MusicalTime, - ) -> anyhow::Result { - self.patterns.push((position, pattern.clone())); - self.calculate_events(); - Ok(pattern.duration()) - } - - /// Adds the [Pattern] at the sequencer cursor, and advances the cursor. - pub fn append_pattern(&mut self, pattern: &Pattern) -> anyhow::Result<()> { - let position = self.e.cursor; - let duration = self.insert_pattern(pattern, position)?; - self.e.cursor += duration; - Ok(()) - } - - /// Adds the [Note] at the specified location. - pub fn insert_note(&mut self, note: &Note, position: MusicalTime) -> anyhow::Result<()> { - self.notes.push(Note { - key: note.key, - range: (note.range.start + position)..(note.range.end + position), - }); - self.calculate_events(); - Ok(()) - } - - /// Adds the [Note] at the sequencer cursor, and advances the cursor. - pub fn append_note(&mut self, note: &Note) -> anyhow::Result<()> { - let position = self.e.cursor; - self.insert_note(note, position)?; - self.e.cursor += MusicalTime::new_with_beats(1); - Ok(()) - } - - fn insert_note_as_event(&mut self, note: &Note) { - self.e.events.insert( - note.range.start, - MidiMessage::NoteOn { - key: note.key.into(), - vel: 127.into(), - }, - ); - self.e.events.insert( - note.range.end, - MidiMessage::NoteOff { - key: note.key.into(), - vel: 0.into(), - }, - ); - if note.range.end > self.e.final_event_time { - self.e.final_event_time = note.range.end; - } - } - - // TODO: can we reduce visibility? - pub(crate) fn calculate_events(&mut self) { - self.e.events.clear(); - self.e.final_event_time = MusicalTime::START; - - self.notes.clone().iter().for_each(|note| { - self.insert_note_as_event(note); - }); - self.patterns - .clone() - .iter() - .for_each(|(position, pattern)| { - pattern.notes().iter().for_each(|note| { - self.insert_note_as_event(&Note { - key: note.key, - range: (note.range.start + *position)..(note.range.end + *position), - }); - }); - }); - } - - // This method is private because callers need to remember to call - // calculate_events() when they're done. - pub(crate) fn toggle_note(&mut self, note: Note) { - if self.notes.contains(¬e) { - self.notes.retain(|n| n != ¬e); - } else { - self.notes.push(note); - } - } - - #[allow(missing_docs)] - pub fn notes(&self) -> &[Note] { - self.notes.as_ref() - } - - #[allow(missing_docs)] - pub fn patterns(&self) -> &[(MusicalTime, Pattern)] { - self.patterns.as_ref() - } -} -impl Displays for ESSequencer { - fn ui(&mut self, _ui: &mut Ui) -> eframe::egui::Response { - unimplemented!("use es_sequencer widget instead") - } -} -impl HandlesMidi for ESSequencer {} -impl Controls for ESSequencer { - fn update_time(&mut self, range: &std::ops::Range) { - self.e.range = range.clone(); - } - - fn work(&mut self, control_events_fn: &mut ControlEventsFn) { - let events = self.e.events.range(self.e.range.start..self.e.range.end); - for event in events { - control_events_fn(self.uid, EntityEvent::Midi(MidiChannel(0), *event.1)); - } - } - - fn is_finished(&self) -> bool { - // both these are exclusive range bounds - self.e.range.end >= self.e.final_event_time - } - - fn play(&mut self) { - self.e.is_performing = true; - } - - fn stop(&mut self) { - self.e.is_performing = false; - } - - fn skip_to_start(&mut self) {} - - fn is_performing(&self) -> bool { - self.e.is_performing - } -} -impl Configurable for ESSequencer {} -impl Serializable for ESSequencer { - fn after_deser(&mut self) { - self.calculate_events(); - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::mini::PatternBuilder; - - #[test] - fn basic() { - let s = ESSequencer::default(); - assert!(s.notes.is_empty(), "default sequencer has no notes"); - assert!(s.e.events.is_empty(), "default sequencer has no events"); - } - - #[test] - fn adding_notes_translates_to_events() { - let mut s = ESSequencerBuilder::default() - .note(Note { - key: 69, - range: MusicalTime::DURATION_WHOLE - ..MusicalTime::DURATION_WHOLE + MusicalTime::DURATION_QUARTER, - }) - .build() - .unwrap(); - s.after_deser(); - assert_eq!( - s.e.events.len(), - 2, - "Adding one note should create two events" - ); - - let _ = s.append_note(&Note { - key: 70, - range: MusicalTime::DURATION_WHOLE - ..MusicalTime::DURATION_WHOLE + MusicalTime::DURATION_QUARTER, - }); - assert_eq!( - s.e.events.len(), - 4, - "Adding a second note should create two more events" - ); - } - - #[test] - fn adding_patterns_translates_to_events() { - let mut s = ESSequencerBuilder::default() - .pattern(( - MusicalTime::DURATION_QUARTER, - PatternBuilder::default() - .note(Note { - key: 1, - range: MusicalTime::START - ..MusicalTime::START + MusicalTime::DURATION_QUARTER, - }) - .build() - .unwrap(), - )) - .build() - .unwrap(); - s.after_deser(); - assert_eq!( - s.e.events.len(), - 2, - "Adding a pattern with one note should create two events" - ); - - let _ = s.append_pattern( - &PatternBuilder::default() - .note(Note { - key: 1, - range: MusicalTime::START..MusicalTime::START + MusicalTime::DURATION_QUARTER, - }) - .build() - .unwrap(), - ); - assert_eq!( - s.e.events.len(), - 4, - "Appending another pattern with one note should create two more events" - ); - } -} diff --git a/src/mini/humidifier.rs b/src/mini/humidifier.rs deleted file mode 100644 index 76dae11e..00000000 --- a/src/mini/humidifier.rs +++ /dev/null @@ -1,118 +0,0 @@ -// Copyright (c) 2023 Mike Tsao. All rights reserved. - -use ensnare::prelude::*; -use serde::{Deserialize, Serialize}; -use std::collections::HashMap; - -/// Controls the wet/dry mix of arranged effects. -#[derive(Serialize, Deserialize, Debug, Default)] -pub struct Humidifier { - uid_to_humidity: HashMap, -} -impl Humidifier { - pub fn get_humidity_by_uid(&self, uid: &Uid) -> Normal { - if let Some(humidity) = self.uid_to_humidity.get(uid) { - *humidity - } else { - Normal::default() - } - } - - pub fn set_humidity_by_uid(&mut self, uid: Uid, humidity: Normal) { - self.uid_to_humidity.insert(uid, humidity); - } - - pub fn transform_audio( - &mut self, - humidity: Normal, - pre_effect: StereoSample, - post_effect: StereoSample, - ) -> StereoSample { - StereoSample( - self.transform_channel(humidity, 0, pre_effect.0, post_effect.0), - self.transform_channel(humidity, 1, pre_effect.1, post_effect.1), - ) - } - - fn transform_channel( - &mut self, - humidity: Normal, - _: usize, - pre_effect: Sample, - post_effect: Sample, - ) -> Sample { - let humidity = humidity.value(); - let aridity = 1.0 - humidity; - post_effect * humidity + pre_effect * aridity - } -} - -#[cfg(test)] -mod tests { - use crate::mini::humidifier::Humidifier; - use ensnare::prelude::*; - use ensnare::traits::prelude::*; - use groove_toys::ToyEffect; - - #[test] - fn lookups_work() { - let mut wd = Humidifier::default(); - assert_eq!( - wd.get_humidity_by_uid(&Uid(1)), - Normal::maximum(), - "a missing Uid should return default humidity 1.0" - ); - - let uid = Uid(1); - wd.set_humidity_by_uid(uid, Normal::from(0.5)); - assert_eq!( - wd.get_humidity_by_uid(&Uid(1)), - Normal::from(0.5), - "a non-missing Uid should return the humidity that we set" - ); - } - - #[test] - fn processing_works() { - let mut humidifier = Humidifier::default(); - - let mut effect = ToyEffect::default(); - assert_eq!( - effect.transform_channel(0, Sample::MAX), - Sample::MIN, - "we expected ToyEffect to negate the input" - ); - - let pre_effect = Sample::MAX; - assert_eq!( - humidifier.transform_channel( - Normal::maximum(), - 0, - pre_effect, - effect.transform_channel(0, pre_effect), - ), - Sample::MIN, - "Wetness 1.0 means full effect, zero pre-effect" - ); - assert_eq!( - humidifier.transform_channel( - Normal::from_percentage(50.0), - 0, - pre_effect, - effect.transform_channel(0, pre_effect), - ), - Sample::from(0.0), - "Wetness 0.5 means even parts effect and pre-effect" - ); - assert_eq!( - humidifier.transform_channel( - Normal::zero(), - 0, - pre_effect, - effect.transform_channel(0, pre_effect), - ), - pre_effect, - "Wetness 0.0 means no change from pre-effect to post" - ); - } -} diff --git a/src/mini/midi_router.rs b/src/mini/midi_router.rs deleted file mode 100644 index cc13f07b..00000000 --- a/src/mini/midi_router.rs +++ /dev/null @@ -1,286 +0,0 @@ -// Copyright (c) 2023 Mike Tsao. All rights reserved. - -use super::entity_factory::EntityStore; -use anyhow::anyhow; -use ensnare::{ - midi::{MidiChannel, MidiMessage}, - prelude::*, -}; -use serde::{Deserialize, Serialize}; -use std::collections::HashMap; - -#[derive(Serialize, Deserialize, Debug, Default)] -pub struct MidiRouter { - /// MIDI connections - midi_channel_to_receiver_uid: HashMap>, -} -impl MidiRouter { - /// The entities receiving on the given MIDI channel. - pub fn receivers(&mut self, channel: &MidiChannel) -> &Vec { - self.midi_channel_to_receiver_uid - .entry(*channel) - .or_default() - } - - /// Connect an entity to the given MIDI channel. - pub fn connect(&mut self, receiver_uid: Uid, channel: MidiChannel) { - self.midi_channel_to_receiver_uid - .entry(channel) - .or_default() - .push(receiver_uid); - } - - /// Disconnect an entity from the given MIDI channel. - #[allow(dead_code)] - pub fn disconnect(&mut self, receiver_uid: Uid, channel: MidiChannel) { - self.midi_channel_to_receiver_uid - .entry(channel) - .or_default() - .retain(|&uid| uid != receiver_uid); - } - - /// Route the [MidiMessage] to everyone listening on the [MidiChannel], - /// using the provided closure to map [Uid] to [HandlesMidi]. Also routes - /// all the [MidiMessage]s that are produced in response. - // - // TODO: I think this is incomplete. If an external sequencer drives an - // internal arpeggiator that drives an external instrument, then I don't - // think the arp's MIDI gets back to the outside world. - pub fn route( - &mut self, - entity_store: &mut EntityStore, - channel: MidiChannel, - message: MidiMessage, - ) -> anyhow::Result<()> { - let mut loop_detected = false; - let mut v = Vec::default(); - v.push((channel, message)); - while let Some((channel, message)) = v.pop() { - let receiver_uids = self.receivers(&channel); - receiver_uids.iter().for_each(|uid| { - if let Some(e) = entity_store.get_mut(uid) { - if let Some(e) = e.as_handles_midi_mut() { - e.handle_midi_message(channel, message, &mut | response_channel, response_message| { - if channel != response_channel { - v.push((response_channel, response_message)); - } else if !loop_detected { - loop_detected = true; - eprintln!("Warning: loop detected; while sending to channel {channel}, received request to send {:#?} to same channel", &response_message); - } - }); - } else { - eprintln!("Warning: somehow device id {uid} that doesn't handle MIDI got on a receiver list"); - } - } else { - eprintln!("Warning: a receiver list refers to nonexistent entity id {uid}"); - } - }); - } - if loop_detected { - Err(anyhow!("Device attempted to send MIDI message to itself")) - } else { - Ok(()) - } - } -} - -#[cfg(test)] -mod tests { - use super::MidiRouter; - use crate::mini::entity_factory::EntityStore; - use ensnare::{midi::prelude::*, prelude::*, traits::prelude::*}; - use ensnare_proc_macros::{Control, IsInstrument, Uid}; - use serde::{Deserialize, Serialize}; - use std::sync::{Arc, RwLock}; - - #[derive(Debug, Control, Default, IsInstrument, Uid, Serialize, Deserialize)] - struct TestHandlesMidi { - uid: Uid, - - rebroadcast_to: Option, - - #[serde(skip)] - tracker: Arc>>, - } - impl TestHandlesMidi { - fn new_with( - uid: Uid, - rebroadcast_to: Option, - tracker: Arc>>, - ) -> Self { - Self { - uid, - rebroadcast_to, - tracker, - } - } - } - impl HandlesMidi for TestHandlesMidi { - fn handle_midi_message( - &mut self, - channel: MidiChannel, - message: MidiMessage, - midi_messages_fn: &mut MidiMessagesFn, - ) { - if let Ok(mut tracker) = self.tracker.write() { - tracker.push((self.uid, channel, message)) - }; - if let Some(rebroadcast_channel) = self.rebroadcast_to { - midi_messages_fn(rebroadcast_channel, message); - } - } - } - impl Serializable for TestHandlesMidi {} - impl Configurable for TestHandlesMidi {} - impl Displays for TestHandlesMidi {} - impl Generates for TestHandlesMidi { - fn value(&self) -> StereoSample { - todo!() - } - - #[allow(unused_variables)] - fn generate_batch_values(&mut self, values: &mut [StereoSample]) { - todo!() - } - } - impl Ticks for TestHandlesMidi { - #[allow(unused_variables)] - fn tick(&mut self, tick_count: usize) { - todo!() - } - } - - #[test] - fn routes_to_correct_channels() { - let tracker = Arc::new(RwLock::new(Vec::default())); - let mut es = EntityStore::default(); - let entity = Box::new(TestHandlesMidi::new_with( - Uid(1), - None, - Arc::clone(&tracker), - )); - let _ = es.add(entity); - let entity = Box::new(TestHandlesMidi::new_with( - Uid(2), - None, - Arc::clone(&tracker), - )); - let _ = es.add(entity); - - let mut r = MidiRouter::default(); - r.connect(Uid(1), MidiChannel(1)); - r.connect(Uid(2), MidiChannel(2)); - - let m = new_note_on(1, 1); - - assert!(r.route(&mut es, MidiChannel(99), m).is_ok()); - if let Ok(t) = tracker.read() { - assert!( - t.is_empty(), - "no messages received after routing to nonexistent MIDI channel" - ); - } - assert!(r.route(&mut es, MidiChannel(1), m).is_ok()); - if let Ok(t) = tracker.read() { - assert_eq!( - t.len(), - 1, - "after routing to channel 1, only one listener should receive" - ); - assert_eq!( - t[0], - (Uid(1), MidiChannel(1), m), - "after routing to channel 1, only channel 1 listener should receive" - ); - }; - assert!(r.route(&mut es, MidiChannel(2), m).is_ok()); - if let Ok(t) = tracker.read() { - assert_eq!( - t.len(), - 2, - "after routing to channel 2, only one listener should receive" - ); - assert_eq!( - t[1], - (Uid(2), MidiChannel(2), m), - "after routing to channel 2, only channel 2 listener should receive" - ); - }; - } - - #[test] - fn also_routes_produced_messages() { - let tracker = Arc::new(RwLock::new(Vec::default())); - let mut es = EntityStore::default(); - let entity = Box::new(TestHandlesMidi::new_with( - Uid(1), - Some(MidiChannel(2)), - Arc::clone(&tracker), - )); - let _ = es.add(entity); - let entity = Box::new(TestHandlesMidi::new_with( - Uid(2), - None, - Arc::clone(&tracker), - )); - let _ = es.add(entity); - - let mut r = MidiRouter::default(); - r.connect(Uid(1), MidiChannel(1)); - r.connect(Uid(2), MidiChannel(2)); - - let m = new_note_on(1, 1); - - assert!(r.route(&mut es, MidiChannel(1), m).is_ok()); - if let Ok(t) = tracker.read() { - assert_eq!( - t.len(), - 2, - "routing to a producing receiver should produce and route a second message" - ); - assert_eq!( - t[0], - (Uid(1), MidiChannel(1), m), - "original message should be received" - ); - assert_eq!( - t[1], - (Uid(2), MidiChannel(2), m), - "produced message should be received" - ); - }; - let m = new_note_on(2, 3); - assert!(r.route(&mut es, MidiChannel(2), m).is_ok()); - if let Ok(t) = tracker.read() { - assert_eq!( - t.len(), - 3, - "routing to a non-producing receiver shouldn't produce anything" - ); - assert_eq!( - t[2], - (Uid(2), MidiChannel(2), m), - "after routing to channel 2, only channel 2 listener should receive" - ); - }; - } - - #[test] - fn detects_loops() { - let tracker = Arc::new(RwLock::new(Vec::default())); - let mut es = EntityStore::default(); - let entity = Box::new(TestHandlesMidi::new_with( - Uid(1), - Some(MidiChannel(1)), - Arc::clone(&tracker), - )); - let _ = es.add(entity); - - let mut r = MidiRouter::default(); - r.connect(Uid(1), MidiChannel(1)); - - let m = new_note_on(1, 1); - - assert!(r.route(&mut es, MidiChannel(1), m).is_err()); - } -} diff --git a/src/mini/mod.rs b/src/mini/mod.rs index 0fc450a9..1728bdf3 100644 --- a/src/mini/mod.rs +++ b/src/mini/mod.rs @@ -1,76 +1,9 @@ // Copyright (c) 2023 Mike Tsao. All rights reserved. -pub use control_atlas::{ - ControlAtlas, ControlStep, ControlStepBuilder, ControlTrip, ControlTripBuilder, ControlTripPath, -}; -pub use control_router::ControlRouter; -pub use drag_drop::{DragDropEvent, DragDropManager, DragDropSource}; -pub use entities::register_factory_entities; -pub use entity_factory::{EntityFactory, EntityStore, Key}; -pub use even_smaller_sequencer::{ESSequencer, ESSequencerBuilder}; -pub use orchestrator::{Orchestrator, OrchestratorAction, OrchestratorBuilder}; -pub use piano_roll::{Note, Pattern, PatternBuilder, PatternUid, PianoRoll}; -pub use selection_set::SelectionSet; -pub use sequencer::{ArrangedPattern, ArrangedPatternBuilder, Sequencer}; -pub use track::{Track, TrackAction, TrackTitle, TrackUid}; pub use transport::Transport; -#[cfg(test)] -pub use entities::register_test_factory_entities; - -use crossbeam_channel::{Receiver, Sender}; -use ensnare::uid::IsUid; -use serde::{Deserialize, Serialize}; - mod bus_station; -mod control_atlas; -mod control_router; -mod drag_drop; -mod entities; mod entity_factory; -mod even_smaller_sequencer; -mod humidifier; -mod midi_router; mod orchestrator; -mod piano_roll; mod rng; -mod selection_set; -mod sequencer; -mod track; mod transport; - -/// egui widgets -pub mod widgets; - -/// Generates unique [Uid]s. This factory is not threadsafe. -#[derive(Debug, Default, Serialize, Deserialize)] -pub struct UidFactory { - previous_uid: U, -} -impl UidFactory { - /// Generates the next unique [Uid]. - pub fn mint_next(&mut self) -> U { - *self.previous_uid.increment() - } -} - -/// A convenience struct to bundle both halves of a [crossbeam_channel] -/// together. -/// -/// This is actually for more than just convenience: because Serde needs to be -/// able to assign defaults to individual fields on a struct by calling -/// stateless functions, we have to create both sender and receiver at once in a -/// single field. -#[derive(Debug)] -pub struct ChannelPair { - #[allow(missing_docs)] - pub sender: Sender, - #[allow(missing_docs)] - pub receiver: Receiver, -} -impl Default for ChannelPair { - fn default() -> Self { - let (sender, receiver) = crossbeam_channel::unbounded(); - Self { sender, receiver } - } -} diff --git a/src/mini/orchestrator.rs b/src/mini/orchestrator.rs index e69de29b..8b137891 100644 --- a/src/mini/orchestrator.rs +++ b/src/mini/orchestrator.rs @@ -0,0 +1 @@ + diff --git a/src/mini/piano_roll.rs b/src/mini/piano_roll.rs deleted file mode 100644 index acf38f40..00000000 --- a/src/mini/piano_roll.rs +++ /dev/null @@ -1,1046 +0,0 @@ -// Copyright (c) 2023 Mike Tsao. All rights reserved. - -use super::{ - rng::Rng, - widgets::pattern::{self, grid}, - SelectionSet, UidFactory, -}; -use anyhow::anyhow; -use derive_builder::Builder; -use eframe::{ - egui::{Response, Sense, Ui}, - emath::RectTransform, - epaint::{pos2, Color32, Pos2, Rect, RectShape, Rounding, Shape, Stroke}, -}; -use ensnare::{midi::MidiNote, prelude::*, traits::prelude::*, uid::IsUid}; -use serde::{Deserialize, Serialize}; -use std::{collections::HashMap, fmt::Display, ops::Range}; - -/// Identifies a [Pattern]. -#[derive( - Copy, Clone, Debug, Serialize, Deserialize, Default, Eq, PartialEq, Ord, PartialOrd, Hash, -)] -pub struct PatternUid(pub usize); -impl IsUid for PatternUid { - fn increment(&mut self) -> &Self { - self.0 += 1; - self - } -} -impl Display for PatternUid { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_fmt(format_args!("{}", self.0)) - } -} - -/// A [Note] is a single played note. It knows which key it's playing (which -/// is more or less assumed to be a MIDI key value), and when (start/end) it's -/// supposed to play, relative to time zero. -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] -pub struct Note { - /// The MIDI key code for the note. 69 is (usually) A4. - pub key: u8, - /// The range of time when this note should play. - pub range: Range, -} -impl Note { - /// Creates a [Note] from a u8. - pub fn new_with(key: u8, start: MusicalTime, duration: MusicalTime) -> Self { - Self { - key, - range: start..(start + duration), - } - } - - /// Creates a [Note] from a [MidiNote]. - pub fn new_with_midi_note(key: MidiNote, start: MusicalTime, duration: MusicalTime) -> Self { - Self { - key: key as u8, - range: start..(start + duration), - } - } -} - -/// A [Pattern] contains a musical sequence that is suitable for -/// pattern-based composition. It is a series of [Note]s and a -/// [TimeSignature]. All the notes should fit into the pattern's duration, and -/// the duration should be a round multiple of the length implied by the time -/// signature. -#[derive(Clone, Debug, Serialize, Deserialize, Builder)] -#[builder(build_fn(private, name = "build_from_builder"))] -pub struct Pattern { - /// The pattern's [TimeSignature]. - #[builder(default)] - time_signature: TimeSignature, - - /// The duration is the amount of time from the start of the pattern to the - /// point when the next pattern should start. This does not necessarily mean - /// the time between the first note-on and the first note-off! For example, - /// an empty 4/4 pattern lasts for 4 beats. - #[builder(setter(skip))] - duration: MusicalTime, - - /// The notes that make up this pattern. When it is in a [Pattern], a - /// [Note]'s range is relative to the start of the [Pattern]. For example, a - /// note that plays immediately would have a range start of zero. TODO: - /// specify any ordering restrictions. - #[builder(default, setter(each(name = "note", into)))] - notes: Vec, - // TODO: Nobody is writing to this. I haven't implemented selection - // operations on notes yet. - // #[serde(skip)] - // #[builder(setter(skip))] - // note_selection_set: HashSet, -} -impl PatternBuilder { - /// The length of a note generated by the random() method - pub const DURATION: MusicalTime = MusicalTime::DURATION_QUARTER; - - /// Builds the [Pattern]. - pub fn build(&self) -> Result { - match self.build_from_builder() { - Ok(mut s) => { - s.after_deser(); - Ok(s) - } - Err(e) => Err(e), - } - } - - fn random(&mut self) -> &mut Self { - let mut rng = Rng::default(); - - for _ in 0..rng.0.rand_range(8..16) { - let start = MusicalTime::new_with_parts(rng.0.rand_range(0..64) as usize); - let duration = Self::DURATION; - self.note(Note { - key: rng.0.rand_range(32..96) as u8, - range: start..start + duration, - }); - } - self - } - - /// Given a sequence of MIDI note numbers and an optional grid value that - /// overrides the one implied by the time signature, adds [Note]s one after - /// another into the pattern. The value 255 is reserved for rest (no note, - /// or silence). - /// - /// The optional grid_value is similar to the time signature's bottom value - /// (1 is a whole note, 2 is a half note, etc.). For example, for a 4/4 - /// pattern, None means each note number produces a quarter note, and we - /// would provide sixteen note numbers to fill the pattern with 4 beats of - /// four quarter-notes each. For a 4/4 pattern, Some(8) means each note - /// number should produce an eighth note., and 4 x 8 = 32 note numbers would - /// fill the pattern. - /// - /// If midi_note_numbers contains fewer than the maximum number of note - /// numbers for the grid value, then the rest of the pattern is silent. - pub fn note_sequence( - &mut self, - midi_note_numbers: Vec, - grid_value: Option, - ) -> &mut Self { - let grid_value = grid_value.unwrap_or(self.time_signature.unwrap_or_default().bottom); - let mut position = MusicalTime::START; - let position_delta = MusicalTime::new_with_fractional_beats(1.0 / grid_value as f64); - for note in midi_note_numbers { - if note != 255 { - self.note(Note { - key: note, - range: position..position + position_delta, - }); - } - position += position_delta; - } - self - } -} -impl Default for Pattern { - fn default() -> Self { - let mut r = Self { - time_signature: TimeSignature::default(), - duration: Default::default(), - notes: Default::default(), - // note_selection_set: Default::default(), - }; - r.after_deser(); - r - } -} -impl Displays for Pattern { - fn ui(&mut self, ui: &mut Ui) -> eframe::egui::Response { - let (response, painter) = - ui.allocate_painter(ui.available_size_before_wrap(), Sense::click()); - let to_screen = RectTransform::from_to( - eframe::epaint::Rect::from_x_y_ranges( - MusicalTime::START.total_parts() as f32..=self.duration.total_parts() as f32, - 128.0..=0.0, - ), - response.rect, - ); - let from_screen = to_screen.inverse(); - - // Identify the local x and y values of the cursor. - let mut key = 255; - let mut position = MusicalTime::TIME_MAX; - if let Some(screen_pos) = ui.ctx().pointer_interact_pos() { - let local_pos = from_screen * screen_pos; - key = local_pos.y as u8; - position = MusicalTime::new_with_parts(local_pos.x as usize); - } - - // Add or remove a note. - if response.clicked() { - let new_note = Note { - key, - range: position..position + PatternBuilder::DURATION, - }; - if self.notes.contains(&new_note) { - self.notes.retain(|n| &new_note != n); - } else { - self.notes.push(new_note); - } - } - - let fill = ui.ctx().style().visuals.widgets.active.bg_fill; - let shapes: Vec = self - .notes - .iter() - .map(|note| { - let rect = Rect::from_two_pos( - to_screen * pos2(note.range.start.total_parts() as f32, note.key as f32), - to_screen * pos2(note.range.end.total_parts() as f32, note.key as f32 + 1.0), - ); - let hovered = note.key == key && note.range.contains(&position); - let stroke = if hovered { - ui.ctx().style().visuals.widgets.active.fg_stroke - } else { - ui.ctx().style().visuals.widgets.active.bg_stroke - }; - - Shape::Rect(RectShape { - rect, - rounding: Rounding::default(), - fill, - stroke, - }) - }) - .collect(); - - painter.extend(shapes); - - response - } -} -impl Serializable for Pattern { - fn after_deser(&mut self) { - self.refresh_internals(); - } -} -impl Pattern { - /// Returns the number of notes in the pattern. - pub fn note_count(&self) -> usize { - self.notes.len() - } - - /// Returns the pattern grid's number of subdivisions, which is calculated - /// from the time signature. The number is simply the time signature's top x - /// bottom. For example, a 3/4 pattern will have 12 subdivisions (three - /// beats per measure, each beat divided into four quarter notes). - /// - /// This is just a UI default and doesn't affect the actual granularity of a - /// note position. - pub fn default_grid_value(&self) -> usize { - self.time_signature.top * self.time_signature.bottom - } - - fn refresh_internals(&mut self) { - let final_event_time = self - .notes - .iter() - .map(|n| n.range.end) - .max() - .unwrap_or_default(); - - // This is how we deal with Range<> being inclusive start, exclusive - // end. It matters because we want the calculated duration to be rounded - // up to the next measure, but we don't want a note-off event right on - // the edge to extend that calculation to include another bar. - let final_event_time = if final_event_time == MusicalTime::START { - final_event_time - } else { - final_event_time - MusicalTime::new_with_units(1) - }; - let beats = final_event_time.total_beats(); - let top = self.time_signature.top; - let rounded_up_bars = (beats + top) / top; - self.duration = MusicalTime::new_with_bars(&self.time_signature, rounded_up_bars); - } - - /// Adds a note to this pattern. Does not check for duplicates. It's OK to - /// add notes in any time order. - pub fn add_note(&mut self, note: Note) { - self.notes.push(note); - self.refresh_internals(); - } - - /// Removes all notes matching this one in this pattern. - pub fn remove_note(&mut self, note: &Note) { - self.notes.retain(|v| v != note); - self.refresh_internals(); - } - - /// Removes all notes in this pattern. - pub fn clear(&mut self) { - self.notes.clear(); - self.refresh_internals(); - } - - pub(crate) fn make_note_shapes( - &self, - note: &Note, - to_screen: &RectTransform, - is_selected: bool, - is_highlighted: bool, - ) -> Vec { - let rect = to_screen - .transform_rect(self.rect_for_note(note)) - .shrink(1.0); - let color = if is_selected { - Color32::LIGHT_GRAY - } else if is_highlighted { - Color32::WHITE - } else { - Color32::DARK_BLUE - }; - let rect = if (rect.right() - rect.left()).abs() < 1.0 { - Rect::from_two_pos(rect.left_top(), pos2(rect.left() + 1.0, rect.bottom())) - } else { - rect - }; - let rect = if (rect.bottom() - rect.top()).abs() < 1.0 { - Rect::from_two_pos(rect.left_top(), pos2(rect.right(), rect.top() + 1.0)) - } else { - rect - }; - debug_assert!(rect.area() != 0.0); - vec![Shape::Rect(RectShape { - rect, - rounding: Rounding::default(), - fill: Color32::LIGHT_BLUE, - stroke: Stroke { width: 2.0, color }, - })] - } - - fn rect_for_note(&self, note: &Note) -> Rect { - let notes_vert = 24.0; - const FIGURE_THIS_OUT: f32 = 16.0; - let ul = Pos2 { - x: note.range.start.total_parts() as f32 / FIGURE_THIS_OUT, - y: (note.key as f32) / notes_vert, - }; - let br = Pos2 { - x: note.range.end.total_parts() as f32 / FIGURE_THIS_OUT, - y: (1.0 + note.key as f32) / notes_vert, - }; - Rect::from_two_pos(ul, br) - } - - /// This pattern's duration in [MusicalTime]. - pub fn duration(&self) -> MusicalTime { - self.duration - } - - /// Sets a new start time for all notes in the Pattern matching the given - /// [Note]. If any are found, returns the new version. - pub fn move_note(&mut self, note: &Note, new_start: MusicalTime) -> anyhow::Result { - let mut new_note = note.clone(); - let new_note_length = new_note.range.end - new_note.range.start; - new_note.range = new_start..new_start + new_note_length; - self.replace_note(note, new_note) - } - - /// Sets a new start time and duration for all notes in the Pattern matching - /// the given [Note]. If any are found, returns the new version. - pub fn move_and_resize_note( - &mut self, - note: &Note, - new_start: MusicalTime, - duration: MusicalTime, - ) -> anyhow::Result { - let mut new_note = note.clone(); - new_note.range = new_start..new_start + duration; - self.replace_note(note, new_note) - } - - /// Sets a new key for all notes in the Pattern matching the given [Note]. - /// If any are found, returns the new version. - pub fn change_note_key(&mut self, note: &Note, new_key: u8) -> anyhow::Result { - let mut new_note = note.clone(); - new_note.key = new_key; - self.replace_note(note, new_note) - } - - /// Replaces all notes in the Pattern matching the given [Note] with a new - /// [Note]. If any are found, returns the new version. - pub fn replace_note(&mut self, note: &Note, new_note: Note) -> anyhow::Result { - let mut found = false; - - self.notes.iter_mut().filter(|n| n == ¬e).for_each(|n| { - *n = new_note.clone(); - found = true; - }); - if found { - self.refresh_internals(); - Ok(new_note) - } else { - Err(anyhow!("replace_note: couldn't find note {:?}", note)) - } - } - - #[allow(missing_docs)] - pub fn time_signature(&self) -> TimeSignature { - self.time_signature - } - - /// Returns a read-only slice of all the [Note]s in this pattern. No order - /// is currently defined. - pub fn notes(&self) -> &[Note] { - self.notes.as_ref() - } -} - -/// [PianoRoll] manages all [Pattern]s. -#[derive(Debug, Deserialize, Serialize)] -pub struct PianoRoll { - uid_factory: UidFactory, - uids_to_patterns: HashMap, - ordered_pattern_uids: Vec, - pattern_selection_set: SelectionSet, -} -impl Default for PianoRoll { - fn default() -> Self { - let mut r = Self { - uid_factory: Default::default(), - uids_to_patterns: Default::default(), - ordered_pattern_uids: Default::default(), - pattern_selection_set: Default::default(), - }; - for _ in 0..16 { - let _ = r.insert(PatternBuilder::default().random().build().unwrap()); - } - r - } -} -impl PianoRoll { - /// Adds a [Pattern]. Returns its [PatternUid]. - pub fn insert(&mut self, pattern: Pattern) -> PatternUid { - let uid = self.uid_factory.mint_next(); - self.uids_to_patterns.insert(uid, pattern); - self.ordered_pattern_uids.push(uid); - uid - } - - /// Removes the [Pattern] having the given [PatternUid], if any. - pub fn remove(&mut self, pattern_uid: &PatternUid) { - self.uids_to_patterns.remove(pattern_uid); - self.ordered_pattern_uids.retain(|uid| uid != pattern_uid); - } - - /// Returns a reference to the specified [Pattern]. - pub fn get_pattern(&self, pattern_uid: &PatternUid) -> Option<&Pattern> { - self.uids_to_patterns.get(pattern_uid) - } - - /// Returns a mutable reference to the specified [Pattern]. - pub fn get_pattern_mut(&mut self, pattern_uid: &PatternUid) -> Option<&mut Pattern> { - self.uids_to_patterns.get_mut(pattern_uid) - } - - fn ui_pattern_edit(&mut self, ui: &mut Ui) -> Response { - if let Some(pattern_uid) = self.pattern_selection_set.single_selection() { - if let Some(pattern) = self.uids_to_patterns.get_mut(pattern_uid) { - let (_id, rect) = ui.allocate_space(ui.available_size_before_wrap()); - ui.add_enabled_ui(false, |ui| { - ui.allocate_ui_at_rect(rect, |ui| ui.add(grid(pattern.duration))) - .inner - }); - return ui.allocate_ui_at_rect(rect, |ui| pattern.ui(ui)).inner; - } - } - ui.label("nothing selected") - } -} -impl Displays for PianoRoll { - fn ui(&mut self, ui: &mut Ui) -> Response { - ui.vertical(|ui| { - ui.add(pattern::carousel( - &self.ordered_pattern_uids, - &self.uids_to_patterns, - &mut self.pattern_selection_set, - )); - self.ui_pattern_edit(ui); - }) - .response - } -} - -#[cfg(test)] -mod tests { - use super::*; - - impl Note { - /// half-note - const TEST_C4: Note = Note { - key: MidiNote::C4 as u8, - range: MusicalTime::START..MusicalTime::DURATION_HALF, - }; - /// whole note - const TEST_D4: Note = Note { - key: MidiNote::D4 as u8, - range: MusicalTime::START..MusicalTime::DURATION_WHOLE, - }; - /// two whole notes - const TEST_E4: Note = Note { - key: MidiNote::E4 as u8, - range: MusicalTime::START..MusicalTime::DURATION_BREVE, - }; - } - - impl PianoRoll { - /// For testing only; adds simple patterns. - pub fn populate_pattern( - &mut self, - pattern_number: usize, - ) -> (PatternUid, usize, MusicalTime) { - let pattern = match pattern_number { - 0 => PatternBuilder::default() - .notes(vec![ - Note::new_with_midi_note( - MidiNote::C4, - MusicalTime::TIME_ZERO, - MusicalTime::DURATION_WHOLE, - ), - Note::new_with_midi_note( - MidiNote::D4, - MusicalTime::TIME_END_OF_FIRST_BEAT, - MusicalTime::DURATION_WHOLE, - ), - Note::new_with_midi_note( - MidiNote::E4, - MusicalTime::TIME_END_OF_FIRST_BEAT * 2, - MusicalTime::DURATION_WHOLE, - ), - ]) - .build(), - 1 => PatternBuilder::default() - .notes(vec![ - Note::new_with_midi_note( - MidiNote::C5, - MusicalTime::TIME_ZERO, - MusicalTime::DURATION_WHOLE, - ), - Note::new_with_midi_note( - MidiNote::D5, - MusicalTime::TIME_END_OF_FIRST_BEAT, - MusicalTime::DURATION_WHOLE, - ), - Note::new_with_midi_note( - MidiNote::E5, - MusicalTime::TIME_END_OF_FIRST_BEAT * 2, - MusicalTime::DURATION_WHOLE, - ), - ]) - .build(), - _ => panic!(), - } - .unwrap(); - - // Optimize this. I dare you. - let len = pattern.notes().len(); - let duration = pattern.duration(); - (self.insert(pattern), len, duration) - } - } - - #[test] - fn pattern_defaults() { - let p = Pattern::default(); - assert_eq!(p.note_count(), 0, "Default pattern should have zero notes"); - - let p = PatternBuilder::default().build().unwrap(); - assert_eq!( - p.note_count(), - 0, - "Default built pattern should have zero notes" - ); - - assert_eq!( - p.time_signature(), - TimeSignature::COMMON_TIME, - "Default built pattern should have 4/4 time signature" - ); - - assert_eq!( - p.duration(), - MusicalTime::new_with_bars(&TimeSignature::COMMON_TIME, 1), - "Default built pattern's duration should be one measure" - ); - } - - #[test] - fn pattern_one_half_note_is_one_bar() { - let mut p = PatternBuilder::default().build().unwrap(); - p.add_note(Note::TEST_C4); - assert_eq!( - p.duration().total_bars(&p.time_signature()), - 1, - "Pattern with one half-note should be 1 bar" - ); - } - - #[test] - fn pattern_one_breve_is_one_bar() { - let mut p = PatternBuilder::default().build().unwrap(); - p.add_note(Note::TEST_E4); - assert_eq!( - p.duration().total_bars(&p.time_signature()), - 1, - "Pattern with one note of length breve should be 1 bar" - ); - } - - #[test] - fn pattern_one_long_note_is_one_bar() { - let p = PatternBuilder::default() - .note(Note::new_with_midi_note( - MidiNote::C0, - MusicalTime::new_with_beats(0), - MusicalTime::new_with_beats(4), - )) - .build() - .unwrap(); - assert_eq!( - p.duration().total_bars(&p.time_signature()), - 1, - "Pattern with a single bar-long note is one bar" - ); - } - - #[test] - fn pattern_one_beat_with_1_4_time_signature_is_one_bar() { - let p = PatternBuilder::default() - .time_signature(TimeSignature::new_with(1, 4).unwrap()) - .note(Note::new_with_midi_note( - MidiNote::C0, - MusicalTime::new_with_beats(0), - MusicalTime::new_with_beats(1), - )) - .build() - .unwrap(); - assert_eq!( - p.duration().total_bars(&p.time_signature()), - 1, - "Pattern with a single whole note in 1/4 time is one bar" - ); - } - - #[test] - fn pattern_three_half_notes_is_one_bar() { - let p = PatternBuilder::default() - .note(Note::new_with_midi_note( - MidiNote::C0, - MusicalTime::new_with_beats(0), - MusicalTime::DURATION_HALF, - )) - .note(Note::new_with_midi_note( - MidiNote::C0, - MusicalTime::new_with_beats(1), - MusicalTime::DURATION_HALF, - )) - .note(Note::new_with_midi_note( - MidiNote::C0, - MusicalTime::new_with_beats(2), - MusicalTime::DURATION_HALF, - )) - .build() - .unwrap(); - assert_eq!( - p.duration().total_bars(&p.time_signature()), - 1, - "Pattern with three half-notes on beat should be 1 bar" - ); - } - - #[test] - fn pattern_four_whole_notes_is_one_bar() { - let p = PatternBuilder::default() - .note(Note::new_with_midi_note( - MidiNote::C0, - MusicalTime::new_with_beats(0), - MusicalTime::DURATION_WHOLE, - )) - .note(Note::new_with_midi_note( - MidiNote::C0, - MusicalTime::new_with_beats(1), - MusicalTime::DURATION_WHOLE, - )) - .note(Note::new_with_midi_note( - MidiNote::C0, - MusicalTime::new_with_beats(2), - MusicalTime::DURATION_WHOLE, - )) - .note(Note::new_with_midi_note( - MidiNote::C0, - MusicalTime::new_with_beats(3), - MusicalTime::DURATION_WHOLE, - )) - .build() - .unwrap(); - assert_eq!( - p.duration().total_bars(&p.time_signature()), - 1, - "Pattern with four whole notes on beat should be 1 bar" - ); - } - - #[test] - fn pattern_five_notes_is_two_bars() { - let p = PatternBuilder::default() - .note(Note::new_with_midi_note( - MidiNote::C0, - MusicalTime::new_with_beats(0), - MusicalTime::DURATION_WHOLE, - )) - .note(Note::new_with_midi_note( - MidiNote::C0, - MusicalTime::new_with_beats(1), - MusicalTime::DURATION_WHOLE, - )) - .note(Note::new_with_midi_note( - MidiNote::C0, - MusicalTime::new_with_beats(2), - MusicalTime::DURATION_WHOLE, - )) - .note(Note::new_with_midi_note( - MidiNote::C0, - MusicalTime::new_with_beats(3), - MusicalTime::DURATION_WHOLE, - )) - .note(Note::new_with_midi_note( - MidiNote::C0, - MusicalTime::new_with_beats(4), - MusicalTime::DURATION_SIXTEENTH, - )) - .build() - .unwrap(); - assert_eq!( - p.duration().total_bars(&p.time_signature()), - 2, - "Pattern with four whole notes and then a sixteenth should be 2 bars" - ); - } - - #[test] - fn default_pattern_builder() { - let p = PatternBuilder::default().build().unwrap(); - assert_eq!( - p.notes.len(), - 0, - "Default PatternBuilder yields pattern with zero notes" - ); - assert_eq!( - p.duration, - MusicalTime::new_with_bars(&p.time_signature, 1), - "Default PatternBuilder yields one-measure pattern" - ); - } - - #[test] - fn pattern_api_is_ergonomic() { - let mut p = PatternBuilder::default() - .note(Note::TEST_C4.clone()) - .note(Note::TEST_D4.clone()) - .build() - .unwrap(); - assert_eq!(p.notes.len(), 2, "PatternBuilder can add multiple notes"); - - p.add_note(Note::TEST_C4.clone()); - assert_eq!( - p.notes.len(), - 3, - "Pattern can add duplicate notes. This is probably not desirable to allow." - ); - - assert!(p - .move_note(&Note::TEST_C4, MusicalTime::new_with_beats(4)) - .is_ok()); - assert_eq!(p.notes.len(), 3, "Moving a note doesn't copy or destroy"); - p.remove_note(&Note::TEST_D4); - assert_eq!(p.notes.len(), 2, "remove_note() removes notes"); - p.remove_note(&Note::TEST_C4); - assert_eq!( - p.notes.len(), - 2, - "remove_note() must specify the note correctly." - ); - p.remove_note(&Note::new_with_midi_note( - MidiNote::C4, - MusicalTime::new_with_beats(4), - MusicalTime::DURATION_HALF, - )); - assert!(p.notes.is_empty(), "remove_note() removes duplicate notes."); - } - - #[test] - fn move_note_inside_pattern() { - let mut p = PatternBuilder::default().build().unwrap(); - - p.add_note(Note::TEST_C4.clone()); - assert!(p - .move_note( - &Note::TEST_C4, - MusicalTime::START + MusicalTime::DURATION_SIXTEENTH, - ) - .is_ok()); - assert_eq!( - p.notes[0].range.start, - MusicalTime::START + MusicalTime::DURATION_SIXTEENTH, - "moving a note works" - ); - assert_eq!( - p.duration, - MusicalTime::new_with_beats(4), - "Moving a note in pattern doesn't change duration" - ); - - assert!( - p.move_note(&Note::TEST_E4, MusicalTime::default()).is_err(), - "moving nonexistent note should fail" - ); - } - - #[test] - fn move_note_outside_pattern() { - let mut p = PatternBuilder::default().build().unwrap(); - - p.add_note(Note::TEST_C4.clone()); - assert!(p - .move_note(&Note::TEST_C4, MusicalTime::new_with_beats(4)) - .is_ok()); - assert_eq!( - p.duration, - MusicalTime::new_with_beats(4 * 2), - "Moving a note out of pattern increases duration" - ); - } - - #[test] - fn move_and_resize_note() { - let mut p = PatternBuilder::default().build().unwrap(); - - p.add_note(Note::TEST_C4.clone()); - - assert!(p - .move_and_resize_note( - &Note::TEST_C4, - MusicalTime::START + MusicalTime::DURATION_EIGHTH, - MusicalTime::DURATION_WHOLE, - ) - .is_ok()); - let expected_range = (MusicalTime::START + MusicalTime::DURATION_EIGHTH) - ..(MusicalTime::START + MusicalTime::DURATION_EIGHTH + MusicalTime::DURATION_WHOLE); - assert_eq!( - p.notes[0].range, expected_range, - "moving/resizing a note works" - ); - assert_eq!( - p.duration, - MusicalTime::new_with_beats(4), - "moving/resizing within pattern doesn't change duration" - ); - - assert!(p - .move_and_resize_note( - &Note::new_with_midi_note( - MidiNote::C4, - expected_range.start, - expected_range.end - expected_range.start, - ), - MusicalTime::new_with_beats(4), - MusicalTime::DURATION_WHOLE, - ) - .is_ok()); - assert_eq!( - p.duration, - MusicalTime::new_with_beats(8), - "moving/resizing outside current pattern makes the pattern longer" - ); - - assert!( - p.move_and_resize_note( - &Note::TEST_E4, - MusicalTime::default(), - MusicalTime::default() - ) - .is_err(), - "moving/resizing nonexistent note should fail" - ); - } - - #[test] - fn change_note_key() { - let mut p = PatternBuilder::default().build().unwrap(); - - p.add_note(Note::TEST_C4.clone()); - assert_eq!(p.notes[0].key, MidiNote::C4 as u8); - assert!(p - .change_note_key(&Note::TEST_C4, MidiNote::C5 as u8) - .is_ok()); - assert_eq!(p.notes[0].key, MidiNote::C5 as u8); - - assert!( - p.change_note_key(&Note::TEST_C4, 254).is_err(), - "changing key of nonexistent note should fail" - ); - } - - #[test] - fn pattern_dimensions_are_valid() { - let p = Pattern::default(); - assert_eq!( - p.time_signature, - TimeSignature::COMMON_TIME, - "default pattern should have sensible time signature" - ); - - for ts in [ - TimeSignature::COMMON_TIME, - TimeSignature::CUT_TIME, - TimeSignature::new_with(7, 64).unwrap(), - ] { - let p = PatternBuilder::default() - .time_signature(ts) - .build() - .unwrap(); - assert_eq!( - p.duration, - MusicalTime::new_with_beats(ts.top), - "Pattern's beat count matches its time signature" - ); - - // A typical 4/4 pattern has 16 subdivisions, which is a common - // pattern resolution in other pattern-based sequencers and piano - // rolls. - assert_eq!(p.default_grid_value(), ts.bottom * ts.top, - "Pattern's default grid value should be the time signature's beat count times its note value"); - } - } - - #[test] - fn pattern_note_insertion_is_easy() { - let sixteen_notes = vec![ - 60, 61, 62, 63, 64, 65, 66, 67, 60, 61, 62, 63, 64, 65, 66, 67, - ]; - let len_16 = sixteen_notes.len(); - let p = PatternBuilder::default() - .note_sequence(sixteen_notes, None) - .build() - .unwrap(); - assert_eq!(p.note_count(), len_16, "sixteen quarter notes"); - assert_eq!(p.notes[15].key, 67); - assert_eq!( - p.notes[15].range, - MusicalTime::DURATION_QUARTER * 15..MusicalTime::DURATION_WHOLE * p.time_signature.top - ); - assert_eq!( - p.duration, - MusicalTime::DURATION_WHOLE * p.time_signature.top - ); - - let seventeen_notes = vec![ - 60, 61, 62, 63, 64, 65, 66, 67, 60, 61, 62, 63, 64, 65, 66, 67, 68, - ]; - let p = PatternBuilder::default() - .note_sequence(seventeen_notes, None) - .build() - .unwrap(); - assert_eq!( - p.duration, - MusicalTime::DURATION_WHOLE * p.time_signature.top * 2, - "17 notes in 4/4 pattern produces two bars" - ); - - let four_notes = vec![60, 61, 62, 63]; - let len_4 = four_notes.len(); - let p = PatternBuilder::default() - .note_sequence(four_notes, Some(4)) - .build() - .unwrap(); - assert_eq!(p.note_count(), len_4, "four quarter notes"); - assert_eq!( - p.duration, - MusicalTime::DURATION_WHOLE * p.time_signature.top - ); - - let three_notes_and_silence = vec![60, 0, 62, 63]; - let len_3_1 = three_notes_and_silence.len(); - let p = PatternBuilder::default() - .note_sequence(three_notes_and_silence, Some(4)) - .build() - .unwrap(); - assert_eq!(p.note_count(), len_3_1, "three quarter notes with one rest"); - assert_eq!( - p.duration, - MusicalTime::DURATION_WHOLE * p.time_signature.top - ); - - let eight_notes = vec![60, 61, 62, 63, 64, 65, 66, 67]; - let len_8 = eight_notes.len(); - let p = PatternBuilder::default() - .time_signature(TimeSignature::CUT_TIME) - .note_sequence(eight_notes, None) - .build() - .unwrap(); - assert_eq!( - p.note_count(), - len_8, - "eight eighth notes in 2/2 time is two bars long" - ); - assert_eq!( - p.duration, - MusicalTime::DURATION_WHOLE * p.time_signature.top * 2 - ); - - let one_note = vec![60]; - let len_1 = one_note.len(); - let p = PatternBuilder::default() - .note_sequence(one_note, None) - .build() - .unwrap(); - assert_eq!( - p.note_count(), - len_1, - "one quarter note, and the rest is silence" - ); - assert_eq!(p.notes[0].key, 60); - assert_eq!( - p.notes[0].range, - MusicalTime::START..MusicalTime::DURATION_QUARTER - ); - assert_eq!( - p.duration, - MusicalTime::DURATION_WHOLE * p.time_signature.top - ); - } - - #[test] - fn cut_time_duration() { - let p = PatternBuilder::default() - .time_signature(TimeSignature::CUT_TIME) - .build() - .unwrap(); - assert_eq!(p.duration, MusicalTime::new_with_beats(2)); - } -} diff --git a/src/mini/selection_set.rs b/src/mini/selection_set.rs deleted file mode 100644 index 504d911f..00000000 --- a/src/mini/selection_set.rs +++ /dev/null @@ -1,144 +0,0 @@ -// Copyright (c) 2023 Mike Tsao. All rights reserved. - -use ensnare::uid::IsUid; -use serde::{Deserialize, Serialize}; -use std::collections::{hash_set::Iter, HashSet}; - -/// A utility class to help manage selection sets of things that implement the -/// [IsUid] trait. -#[derive(Clone, Debug, Default, Serialize, Deserialize)] -pub struct SelectionSet { - selected_uids: HashSet, -} - -impl SelectionSet { - /// Adds an item to the selection set. - pub fn insert(&mut self, uid: T) { - self.selected_uids.insert(uid); - } - - /// Removes an item from the selection set. - pub fn remove(&mut self, uid: &T) { - self.selected_uids.remove(uid); - } - - /// Changes the selection state of an item. - pub fn set_selected(&mut self, uid: T, selected: bool) { - if selected { - self.insert(uid); - } else { - self.remove(&uid); - } - } - - /// Indicates whether the given item is selected. - pub fn contains(&self, uid: &T) -> bool { - self.selected_uids.contains(uid) - } - - /// Select none. - pub fn clear(&mut self) { - self.selected_uids.clear(); - } - - /// Returns an iterator of all selected items. - pub fn iter(&self) -> Iter<'_, T> { - self.selected_uids.iter() - } - - /// Returns the number of selected items. - pub fn len(&self) -> usize { - self.selected_uids.len() - } - - #[allow(missing_docs)] - pub fn is_empty(&self) -> bool { - self.selected_uids.is_empty() - } - - /// Convenience method to handle a click on an item that's meant as a - /// selection action. `modify_selection_set` is typically set when the user - /// is holding down the control or Command key while clicking. - /// - /// TODO: this struct isn't smart enough to handle the shift modifier key. - /// It doesn't know about any item in the set that isn't selected, nor does - /// it know the topology of the set, so it doesn't have any way of - /// determining how to select all the items between two items. If this is - /// interesting in the future, then add it. - pub fn click(&mut self, uid: &T, modify_selection_set: bool) { - let is_selected = self.contains(uid); - if modify_selection_set { - // The user is holding down the control key. This means that the - // indicated item's selection state should be toggled, but the rest - // of the items in the set shouldn't change. - if is_selected { - self.remove(uid); - } else { - self.insert(*uid); - } - } else { - // A plain click with no modifier keys. Just toggle this item's - // selection state. - self.clear(); - if !is_selected { - self.insert(*uid); - } - } - } - - /// If a single item is selected, returns it. Otherwise returns None. - pub fn single_selection(&self) -> Option<&T> { - if self.selected_uids.len() == 1 { - self.selected_uids.iter().next() - } else { - None - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use ensnare::uid::Uid; - - #[test] - fn select_mainline() { - let mut st = SelectionSet::default(); - - assert!(st.is_empty()); - assert_eq!(st.len(), 0); - - let uid2048 = Uid(2048); - let uid2049 = Uid(2049); - assert!(!st.contains(&uid2048)); - - st.insert(uid2048); - assert!(st.contains(&uid2048)); - - st.clear(); - assert!(st.is_empty()); - - st.click(&uid2048, false); - assert_eq!(st.len(), 1); - assert!(st.contains(&uid2048)); - assert!(!st.contains(&uid2049)); - - st.click(&uid2049, true); - assert_eq!(st.len(), 2); - assert!(st.contains(&uid2048)); - assert!(st.contains(&uid2049)); - - st.click(&uid2049, true); - assert!(st.contains(&uid2048)); - assert!(!st.contains(&uid2049)); - - st.click(&uid2048, true); - assert!(st.is_empty()); - - assert!(st.single_selection().is_none()); - st.set_selected(uid2048, true); - assert_eq!(st.single_selection().unwrap(), &uid2048); - st.set_selected(uid2049, true); - assert!(st.single_selection().is_none()); - } -} diff --git a/src/mini/sequencer.rs b/src/mini/sequencer.rs deleted file mode 100644 index e6ef1b00..00000000 --- a/src/mini/sequencer.rs +++ /dev/null @@ -1,822 +0,0 @@ -// Copyright (c) 2023 Mike Tsao. All rights reserved. - -use super::{ - piano_roll::{Pattern, PatternUid, PianoRoll}, - selection_set::SelectionSet, - widgets::timeline, - DragDropManager, TrackUid, UidFactory, -}; -use anyhow::anyhow; -use btreemultimap::BTreeMultiMap; -use derive_builder::Builder; -use eframe::{ - egui::{Response, Sense, Ui, Widget, WidgetInfo, WidgetType}, - emath::{self, lerp}, - epaint::{pos2, vec2, Color32, Pos2, Rect, Rounding, Stroke, Vec2}, -}; -use ensnare::{ - midi::{new_note_off, new_note_on, MidiChannel, MidiMessage}, - prelude::*, - traits::prelude::*, - uid::IsUid, -}; -use ensnare_proc_macros::{Control, IsController, Uid}; -use serde::{Deserialize, Serialize}; -use std::{ - collections::HashMap, - fmt::Display, - ops::Range, - sync::{Arc, RwLock}, -}; - -/// Identifies an arrangement of a [Pattern]. -#[derive( - Copy, Clone, Debug, Serialize, Deserialize, Default, Eq, PartialEq, Ord, PartialOrd, Hash, -)] -pub struct ArrangedPatternUid(pub usize); -impl IsUid for ArrangedPatternUid { - fn increment(&mut self) -> &Self { - self.0 += 1; - self - } -} -impl Display for ArrangedPatternUid { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_fmt(format_args!("{}", self.0)) - } -} - -/// A placement of a [Pattern] within an arrangement. -#[derive(Debug, Serialize, Deserialize, Builder)] -pub struct ArrangedPattern { - /// The identifier of the underlying pattern being arranged. - pattern_uid: PatternUid, - /// Where to place the pattern. - position: MusicalTime, -} -impl ArrangedPattern { - fn ui_content(&self, ui: &mut Ui, pattern: &Pattern, is_selected: bool) -> Response { - let steps_horiz = pattern.time_signature().bottom * 4; - - let desired_size = vec2((pattern.duration().total_beats() * 16) as f32, 64.0); - let (response, painter) = ui.allocate_painter(desired_size, Sense::click()); - - let to_screen = emath::RectTransform::from_to( - Rect::from_min_size(Pos2::ZERO, Vec2::splat(1.0)), - response.rect, - ); - - painter.rect_filled(response.rect, Rounding::default(), Color32::DARK_GRAY); - painter.rect_stroke( - response.rect, - Rounding::none(), - Stroke::new(if is_selected { 2.0 } else { 0.0 }, Color32::WHITE), - ); - let steps_horiz_f32 = steps_horiz as f32; - for i in 0..steps_horiz { - let x = i as f32 / steps_horiz_f32; - let lines = [to_screen * Pos2::new(x, 0.0), to_screen * Pos2::new(x, 1.0)]; - painter.line_segment( - lines, - Stroke { - width: 1.0, - color: Color32::DARK_GRAY, - }, - ); - } - - let shapes = pattern.notes().iter().fold(Vec::default(), |mut v, note| { - v.extend(pattern.make_note_shapes(note, &to_screen, false, false)); - v - }); - - painter.extend(shapes); - - response - } - - /// The placement of the [ArrangedPattern]. - pub fn position(&self) -> MusicalTime { - self.position - } -} - -#[derive(Debug)] -pub enum SequencerAction { - ArrangePatternAppend(PatternUid), - ToggleArrangedPatternSelection(ArrangedPatternUid), -} - -#[derive(Debug, Default)] -pub struct SequencerEphemerals { - // The sequencer should be performing work for this time slice. - range: Range, - // The actual events that the sequencer emits. These are composed of arranged patterns. - events: BTreeMultiMap, - // The latest end time (exclusive) of all the events. - final_event_time: MusicalTime, - // The next place to insert a pattern. - arrangement_cursor: MusicalTime, - // Whether we're performing, in the [Performs] sense. - is_performing: bool, - // The source of [Pattern]s. - piano_roll: Arc>, - - view_range: Range, -} - -/// [Sequencer] converts a chain of [Pattern]s into MIDI notes according to a -/// given [Tempo] and [TimeSignature]. It is read-only with respect to -/// [Pattern]s; the smallest unit of music it works with is a [Pattern]. -#[derive(Debug, Default, Control, IsController, Uid, Serialize, Deserialize, Builder)] -pub struct Sequencer { - #[builder(default)] - uid: Uid, - #[builder(default)] - midi_channel_out: MidiChannel, - - #[builder(default)] - time_signature: TimeSignature, - - #[builder(setter(skip))] - arranged_pattern_uid_factory: UidFactory, - - #[builder(setter(skip))] - arranged_patterns: HashMap, - - #[builder(setter(skip))] - arranged_pattern_selection_set: SelectionSet, - - #[serde(skip)] - #[builder(setter(skip))] - e: SequencerEphemerals, -} -impl Sequencer { - #[allow(missing_docs)] - pub fn set_piano_roll(&mut self, piano_roll: Arc>) { - self.e.piano_roll = piano_roll; - } - - fn next_arrangement_position(&self) -> MusicalTime { - self.e.arrangement_cursor - } - - #[allow(dead_code)] - fn arranged_pattern_by_uid(&self, uid: &ArrangedPatternUid) -> Option<&ArrangedPattern> { - self.arranged_patterns.get(uid) - } - - #[allow(dead_code)] - fn shift_arranged_pattern_left(&mut self, uid: &ArrangedPatternUid) -> anyhow::Result<()> { - if let Some(ap) = self.arranged_patterns.get_mut(uid) { - if ap.position >= MusicalTime::DURATION_WHOLE { - ap.position -= MusicalTime::DURATION_WHOLE; - } - Ok(()) - } else { - Err(anyhow!("Couldn't find pattern {uid}")) - } - } - - #[allow(dead_code)] - fn shift_arranged_pattern_right(&mut self, uid: &ArrangedPatternUid) -> anyhow::Result<()> { - if let Some(ap) = self.arranged_patterns.get_mut(uid) { - ap.position += MusicalTime::DURATION_WHOLE; - Ok(()) - } else { - Err(anyhow!("Couldn't find pattern {uid}")) - } - } - - fn arrange_pattern_append(&mut self, uid: &PatternUid) -> anyhow::Result { - if let Ok(apuid) = self.arrange_pattern( - uid, - self.next_arrangement_position().bars(&self.time_signature), - ) { - if let Some(pattern) = self.e.piano_roll.read().unwrap().get_pattern(uid) { - self.e.arrangement_cursor += pattern.duration(); - } - Ok(apuid) - } else { - Err(anyhow!("something went wrong")) - } - } - - /// Arranges the given [Pattern] at the specified position, in bars. - pub fn arrange_pattern( - &mut self, - uid: &PatternUid, - position_in_bars: usize, - ) -> anyhow::Result { - let position = MusicalTime::new_with_bars(&self.time_signature, position_in_bars); - if self.e.piano_roll.read().unwrap().get_pattern(uid).is_some() { - let arranged_pattern_uid = self.arranged_pattern_uid_factory.mint_next(); - self.arranged_patterns.insert( - arranged_pattern_uid, - ArrangedPattern { - pattern_uid: *uid, - position, - }, - ); - if let Err(r) = self.calculate_events() { - Err(r) - } else { - Ok(arranged_pattern_uid) - } - } else { - Err(anyhow!("Pattern {uid} not found during arrangement")) - } - } - - #[allow(dead_code)] - fn move_pattern( - &mut self, - uid: &ArrangedPatternUid, - position_in_bars: usize, - ) -> anyhow::Result<()> { - let position = MusicalTime::new_with_bars(&self.time_signature, position_in_bars); - if let Some(pattern) = self.arranged_patterns.get_mut(uid) { - pattern.position = position; - self.calculate_events() - } else { - Err(anyhow!("Couldn't find arranged pattern {}", uid.0)) - } - } - - /// Renders the owning track's arrangement view. - pub fn ui_arrangement( - &mut self, - ui: &mut Ui, - _track_uid: TrackUid, - ) -> (Response, Option) { - ( - ui.horizontal_top(|ui| { - let mut time_pointer = self.e.view_range.start; - while time_pointer < self.e.view_range.end { - let range_size = self.e.view_range.end - - self.e.view_range.start - - MusicalTime::new_with_units(1); - let half_range_size = MusicalTime::new_with_units(range_size.total_units() / 2); - let section_end = (time_pointer + half_range_size).min(self.e.view_range.end); - - ui.add(timeline::empty_space( - time_pointer..section_end, - self.e.view_range.clone(), - )); - time_pointer = section_end; - } - }) - .response, - None, - ) - } - - /// Renders the owning track's arrangement view. - #[must_use] - pub fn ui_arrangement_old( - &mut self, - ui: &mut Ui, - _track_uid: TrackUid, - view_range: &Range, - ) -> (Response, Option) { - let desired_size = ui.available_size(); - let (id, rect) = ui.allocate_space(desired_size); - let painter = ui.painter_at(rect); - - let response = ui.interact(rect, id, Sense::click_and_drag()); - - let start_beat = view_range.start.total_beats(); - let end_beat = view_range.end.total_beats(); - let to_screen = emath::RectTransform::from_to( - Rect::from_x_y_ranges(start_beat as f32..=end_beat as f32, 0.0..=1.0), - rect, - ); - - painter.rect_filled(rect, Rounding::default(), Color32::GRAY); - - // This is a near copy of the label code in - // Orchestrator::ui_arrangement_labels(). TODO refactor - let start_beat = view_range.start.total_beats(); - let end_beat = view_range.end.total_beats(); - let beat_count = end_beat - start_beat; - let to_screen_beats = emath::RectTransform::from_to( - Rect::from_x_y_ranges( - view_range.start.total_beats() as f32..=view_range.end.total_beats() as f32, - 0.0..=1.0, - ), - rect, - ); - - let skip = self.time_signature.top; - let shapes = Vec::default(); - let mut last_segment = [ - to_screen_beats * pos2(start_beat as f32, 0.0), - to_screen_beats * pos2(start_beat as f32, 1.0), - ]; - ui.horizontal(|ui| { - for (i, beat) in (start_beat..end_beat).enumerate() { - if i != 0 && i != beat_count - 1 && i % skip != 0 { - continue; - } - let this_segment = [ - to_screen_beats * pos2(beat as f32, 0.0), - to_screen_beats * pos2(beat as f32, 1.0), - ]; - let _hover_rect = Rect::from_two_pos(last_segment[0], this_segment[1]); - // let can_accept = if let Some(source) = dd.source() { - // match source { - // super::DragDropSource::NewDevice(_) => false, - // super::DragDropSource::Pattern(_) => true, - // } - // } else { - // false - // }; - let can_accept = false; // TODO: commented out the block above here - let r = DragDropManager::drop_target(ui, can_accept, |ui| { - ui.add(space()) - // shapes.push(Shape::LineSegment { - // points: this_segment, - // stroke: if ui.interact(hover_rect, id, Sense::hover()).hovered() { - // ui.style().visuals.widgets.active.bg_stroke - // } else { - // ui.style().visuals.widgets.inactive.bg_stroke - // }, - // }); - - // if let Some(source) = source { - // match source { - // DragDropSource::NewDevice(key) => { - // eprintln!("nope - I'm a pattern target {:?}", source) - // } - // DragDropSource::Pattern(_) => { - // eprintln!("sure - I'm a pattern target {:?}", source) - // } - // } - // } - - // if ui.interact(hover_rect, id, Sense::hover()).hovered() { - // shapes.push(Shape::Rect(RectShape { - // rect: hover_rect, - // rounding: Rounding::none(), - // fill: Color32::DARK_GRAY, - // stroke: Stroke { - // width: 2.0, - // color: Color32::YELLOW, - // }, - // })); - // } - - // super::drag_drop::DragDropTarget::TrackLocation( - // track_uid, - // MusicalTime::new_with_beats(beat), - // ) - - // if ui.interact(hover_rect, id, Sense::hover()).hovered() { - // if let Some(source) = source { - // eprintln!("track beat {beat} - {:?}", source); - // } - // }; - }); - - if DragDropManager::is_dropped(ui, &r.response) { - eprintln!("something happened"); - // eprintln!("dropped at track beat {beat}: {:#?}", dd.source()); - DragDropManager::reset(); - } - last_segment = this_segment; - } - }); - - painter.extend(shapes); - - for (_arranged_pattern_uid, arranged_pattern) in self.arranged_patterns.iter() { - if let Some(pattern) = self - .e - .piano_roll - .read() - .unwrap() - .get_pattern(&arranged_pattern.pattern_uid) - { - let start = arranged_pattern.position; - let end = start + pattern.duration(); - let start_beats = start.total_beats(); - let end_beats = end.total_beats(); - - let ap_rect = Rect::from_two_pos( - to_screen * pos2(start_beats as f32, 0.0), - to_screen * pos2(end_beats as f32, 1.0), - ); - let to_screen_ap = emath::RectTransform::from_to( - Rect::from_x_y_ranges(0.0..=1.0, 0.0..=1.0), - ap_rect, - ); - painter.rect_filled(ap_rect, Rounding::default(), Color32::LIGHT_BLUE); - - let shapes = pattern.notes().iter().fold(Vec::default(), |mut v, note| { - v.extend(pattern.make_note_shapes(note, &to_screen_ap, false, false)); - v - }); - - painter.extend(shapes); - - // if arranged_pattern - // .ui_content( - // ui, - // pattern, - // self.arranged_pattern_selection_set - // .contains(arranged_pattern_uid), - // ) - // .clicked() - // { - // // TODO: handle shift/control - // uid_to_toggle = Some(*arranged_pattern_uid); - // } - } - } - - (response, None) - } - - /// Renders the arrangement view. - #[must_use] - pub fn show_arrangement(&mut self, ui: &mut Ui) -> (Response, Option) { - let action = None; - let desired_size = vec2(ui.available_width(), 64.0); - let (_id, rect) = ui.allocate_space(desired_size); - let painter = ui.painter_at(rect); - - let to_screen = - emath::RectTransform::from_to(Rect::from_min_size(Pos2::ZERO, Vec2::splat(1.0)), rect); - - painter.rect_filled(rect, Rounding::default(), Color32::GRAY); - for i in 0..16 { - let x = i as f32 / 16.0; - let lines = [to_screen * Pos2::new(x, 0.0), to_screen * Pos2::new(x, 1.0)]; - painter.line_segment( - lines, - Stroke { - width: 1.0, - color: Color32::DARK_GRAY, - }, - ); - } - - ( - ui.allocate_ui_at_rect(rect, |ui| { - ui.style_mut().spacing.item_spacing = Vec2::ZERO; - ui.horizontal_top(|ui| { - let mut uid_to_toggle = None; - for (arranged_pattern_uid, arranged_pattern) in self.arranged_patterns.iter() { - if let Some(pattern) = self - .e - .piano_roll - .read() - .unwrap() - .get_pattern(&arranged_pattern.pattern_uid) - { - if arranged_pattern - .ui_content( - ui, - pattern, - self.arranged_pattern_selection_set - .contains(arranged_pattern_uid), - ) - .clicked() - { - // TODO: handle shift/control - uid_to_toggle = Some(*arranged_pattern_uid); - } - } - } - if let Some(uid) = uid_to_toggle { - self.toggle_arranged_pattern_selection(&uid); - } - }) - .response - }) - .inner, - action, - ) - } - - /// Removes all selected arranged patterns. - pub fn remove_selected_arranged_patterns(&mut self) { - self.arranged_patterns - .retain(|uid, _ap| !self.arranged_pattern_selection_set.contains(uid)); - self.arranged_pattern_selection_set.clear(); - } - - fn calculate_events(&mut self) -> anyhow::Result<()> { - self.e.events.clear(); - self.e.final_event_time = MusicalTime::default(); - for ap in self.arranged_patterns.values() { - let uid = ap.pattern_uid; - if let Some(pattern) = self.e.piano_roll.read().unwrap().get_pattern(&uid) { - for note in pattern.notes() { - self.e - .events - .insert(ap.position + note.range.start, new_note_on(note.key, 127)); - let end_time = ap.position + note.range.end; - if end_time > self.e.final_event_time { - self.e.final_event_time = end_time; - } - self.e.events.insert(end_time, new_note_off(note.key, 0)); - } - } else { - return Err(anyhow!( - "Pattern {uid} not found during event recalculation" - )); - } - } - Ok(()) - } - - fn toggle_arranged_pattern_selection(&mut self, uid: &ArrangedPatternUid) { - if self.arranged_pattern_selection_set.contains(uid) { - self.arranged_pattern_selection_set.remove(uid); - } else { - self.arranged_pattern_selection_set.insert(*uid); - } - } - - #[allow(dead_code)] - fn remove_arranged_pattern(&mut self, uid: &ArrangedPatternUid) { - self.arranged_patterns.remove(uid); - } -} -impl Displays for Sequencer { - fn ui(&mut self, ui: &mut Ui) -> eframe::egui::Response { - ui.allocate_ui(vec2(ui.available_width(), 64.0), |ui| { - self.arranged_patterns.values().for_each(|ap| { - if let Some(pattern) = self - .e - .piano_roll - .read() - .unwrap() - .get_pattern(&ap.pattern_uid) - { - ap.ui_content(ui, pattern, false); - } - }) - }) - .response - } -} -impl DisplaysInTimeline for Sequencer { - fn set_view_range(&mut self, view_range: &std::ops::Range) { - self.e.view_range = view_range.clone(); - } -} -impl HandlesMidi for Sequencer {} -impl Controls for Sequencer { - fn update_time(&mut self, range: &std::ops::Range) { - self.e.range = range.clone(); - } - - fn work(&mut self, control_events_fn: &mut ControlEventsFn) { - let events = self.e.events.range(self.e.range.start..self.e.range.end); - for event in events { - control_events_fn(self.uid, EntityEvent::Midi(MidiChannel(0), *event.1)); - } - } - - fn is_finished(&self) -> bool { - // both these are exclusive range bounds - self.e.range.end >= self.e.final_event_time - } - - fn play(&mut self) { - self.e.is_performing = true; - } - - fn stop(&mut self) { - self.e.is_performing = false; - } - - fn skip_to_start(&mut self) {} - - fn is_performing(&self) -> bool { - self.e.is_performing - } -} -impl Configurable for Sequencer {} -impl Serializable for Sequencer { - fn after_deser(&mut self) { - let _ = self.calculate_events(); - } -} - -fn space_ui(ui: &mut Ui) -> Response { - let mut on_it = true; - let on = &mut on_it; - let desired_size = ui.spacing().interact_size.y * vec2(2.0, 1.0); - let (rect, mut response) = ui.allocate_exact_size(desired_size, Sense::click()); - if response.clicked() { - *on = !*on; - response.mark_changed(); - } - response.widget_info(|| WidgetInfo::selected(WidgetType::Checkbox, *on, "")); - - if ui.is_rect_visible(rect) { - let how_on = ui.ctx().animate_bool(response.id, *on); - let visuals = ui.style().interact_selectable(&response, *on); - let rect = rect.expand(visuals.expansion); - let radius = 0.5 * rect.height(); - ui.painter() - .rect(rect, radius, visuals.bg_fill, visuals.bg_stroke); - let circle_x = lerp((rect.left() + radius)..=(rect.right() - radius), how_on); - let center = pos2(circle_x, rect.center().y); - ui.painter() - .circle(center, 0.75 * radius, visuals.bg_fill, visuals.fg_stroke); - } - - response -} - -fn space() -> impl Widget { - space_ui -} - -#[cfg(test)] -mod tests { - use super::*; - - impl Sequencer { - fn piano_roll(&self) -> &RwLock { - &self.e.piano_roll - } - } - - #[test] - fn basic() { - let s = Sequencer::default(); - assert!( - s.arranged_patterns.is_empty(), - "default sequencer has no arranged patterns" - ); - assert!(s.e.events.is_empty(), "default sequencer has no events"); - } - - #[test] - fn sequencer_translates_patterns_to_events() { - let mut s = Sequencer::default(); - - let (pid0, p0_note_count, p0_duration) = - s.piano_roll().write().unwrap().populate_pattern(0); - let (pid1, p1_note_count, p1_duration) = - s.piano_roll().write().unwrap().populate_pattern(1); - - assert!(s.arrange_pattern_append(&pid0).is_ok()); - assert_eq!(s.arranged_patterns.len(), 1, "arranging pattern works"); - assert_eq!( - p0_duration, - MusicalTime::new_with_bars(&TimeSignature::default(), 1), - "arranging pattern leads to correct pattern duration" - ); - - // One event for note-on, one for note-off = two events per note. - assert_eq!( - s.e.events.len(), - p0_note_count * 2, - "sequencer can schedule multiple simultaneous events" - ); - - assert!(s.arrange_pattern_append(&pid1).is_ok()); - assert_eq!( - s.arranged_patterns.len(), - 2, - "arranging multiple patterns works" - ); - - assert_eq!( - p0_duration + p1_duration, - MusicalTime::new_with_bars(&TimeSignature::default(), 2), - "arranging second pattern leads to correct pattern duration" - ); - assert_eq!( - s.e.events.len(), - p0_note_count * 2 + p1_note_count * 2, - "multiple arranged patterns produces expected number of events" - ); - } - - #[test] - fn rearrangement() { - // Start with empty sequencer - let mut s = Sequencer::default(); - assert_eq!(s.e.final_event_time, MusicalTime::START); - - // Add a pattern to the palette. - let (pid0, _, p0_duration) = s.piano_roll().write().unwrap().populate_pattern(0); - assert_eq!(p0_duration, MusicalTime::new_with_beats(4)); - - // Arrange that pattern at the cursor location. - let ap_uid0 = s.arrange_pattern_append(&pid0).unwrap(); - assert_eq!( - s.e.final_event_time, - MusicalTime::TIME_END_OF_FIRST_BEAT * 2 + MusicalTime::DURATION_WHOLE, - "Arranging a pattern properly sets the final event time" - ); - - // Move it to the second bar. - assert!(s.move_pattern(&ap_uid0, 1).is_ok()); - assert_eq!( - s.e.final_event_time, - MusicalTime::new_with_bars(&s.time_signature, 1) - + MusicalTime::TIME_END_OF_FIRST_BEAT * 2 - + MusicalTime::DURATION_WHOLE, - ); - } - - #[test] - fn shift_pattern() { - let mut s = SequencerBuilder::default().build().unwrap(); - let (puid, _, _) = s.piano_roll().write().unwrap().populate_pattern(0); - let apuid = s.arrange_pattern(&puid, 0).unwrap(); - assert_eq!( - s.arranged_pattern_by_uid(&apuid).unwrap().position, - MusicalTime::START - ); - - assert!(s.shift_arranged_pattern_right(&apuid).is_ok()); - assert_eq!( - s.arranged_pattern_by_uid(&apuid).unwrap().position, - MusicalTime::DURATION_WHOLE, - "shift right works" - ); - - assert!(s.shift_arranged_pattern_left(&apuid).is_ok()); - assert_eq!( - s.arranged_pattern_by_uid(&apuid).unwrap().position, - MusicalTime::START, - "nondegenerate shift left works" - ); - - assert!(s.shift_arranged_pattern_left(&apuid).is_ok()); - assert_eq!( - s.arranged_pattern_by_uid(&apuid).unwrap().position, - MusicalTime::START, - "degenerate shift left is a no-op" - ); - } - - #[test] - fn removing_arranged_pattern_works() { - let mut s = SequencerBuilder::default().build().unwrap(); - let (puid0, _, _) = s.piano_roll().write().unwrap().populate_pattern(0); - - let uid0 = s.arrange_pattern(&puid0, 0).unwrap(); - assert_eq!(s.arranged_patterns.len(), 1); - - s.remove_arranged_pattern(&uid0); - assert!(s.arranged_patterns.is_empty()); - - let (puid1, _, _) = s.piano_roll().write().unwrap().populate_pattern(1); - - let uid1 = s.arrange_pattern(&puid1, 0).unwrap(); - let uid0 = s.arrange_pattern(&puid0, 1).unwrap(); - assert_eq!(s.arranged_patterns.len(), 2); - - s.arranged_pattern_selection_set.click(&uid1, false); - s.remove_selected_arranged_patterns(); - assert_eq!(s.arranged_patterns.len(), 1); - - s.arranged_pattern_selection_set.click(&uid0, false); - s.remove_selected_arranged_patterns(); - assert!(s.arranged_patterns.is_empty()); - } - - #[test] - fn arranged_pattern_selection_works() { - let mut s = SequencerBuilder::default().build().unwrap(); - assert!(s.arranged_pattern_selection_set.is_empty()); - - let (puid0, _, _) = s.piano_roll().write().unwrap().populate_pattern(0); - let (puid1, _, _) = s.piano_roll().write().unwrap().populate_pattern(1); - - let uid0 = s.arrange_pattern(&puid0, 0).unwrap(); - let uid1 = s.arrange_pattern(&puid1, 1).unwrap(); - - assert!(s.arranged_pattern_selection_set.is_empty()); - - s.arranged_pattern_selection_set.click(&uid0, false); - assert_eq!(s.arranged_pattern_selection_set.len(), 1); - assert!(s.arranged_pattern_selection_set.contains(&uid0)); - assert!(!s.arranged_pattern_selection_set.contains(&uid1)); - - s.arranged_pattern_selection_set.click(&uid1, true); - assert_eq!(s.arranged_pattern_selection_set.len(), 2); - assert!(s.arranged_pattern_selection_set.contains(&uid0)); - assert!(s.arranged_pattern_selection_set.contains(&uid1)); - - s.arranged_pattern_selection_set.click(&uid1, true); - assert_eq!(s.arranged_pattern_selection_set.len(), 1); - assert!(s.arranged_pattern_selection_set.contains(&uid0)); - assert!(!s.arranged_pattern_selection_set.contains(&uid1)); - - s.arranged_pattern_selection_set.click(&uid1, false); - assert_eq!(s.arranged_pattern_selection_set.len(), 1); - assert!(!s.arranged_pattern_selection_set.contains(&uid0)); - assert!(s.arranged_pattern_selection_set.contains(&uid1)); - } -} diff --git a/src/mini/track.rs b/src/mini/track.rs deleted file mode 100644 index 2d3568dc..00000000 --- a/src/mini/track.rs +++ /dev/null @@ -1,1007 +0,0 @@ -// Copyright (c) 2023 Mike Tsao. All rights reserved. - -use super::{ - control_atlas::ControlAtlas, - control_router::ControlRouter, - entity_factory::EntityStore, - humidifier::Humidifier, - midi_router::MidiRouter, - piano_roll::PianoRoll, - sequencer::Sequencer, - widgets::{control, placeholder, track}, - DragDropManager, DragDropSource, Key, -}; -use anyhow::anyhow; -use eframe::{ - egui::{self, Frame, Layout, Margin, Ui}, - emath::Align, - epaint::{vec2, Color32, Stroke, Vec2}, -}; -use ensnare::{midi::prelude::*, prelude::*, traits::prelude::*, uid::IsUid}; -use serde::{Deserialize, Serialize}; -use std::{ - fmt::Display, - ops::Range, - option::Option, - sync::{Arc, RwLock}, -}; - -/// Identifies a [Track]. -#[derive(Copy, Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd, Hash)] -pub struct TrackUid(pub usize); -impl Default for TrackUid { - fn default() -> Self { - Self(1) - } -} -impl IsUid for TrackUid { - fn increment(&mut self) -> &Self { - self.0 += 1; - self - } -} -impl Display for TrackUid { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_fmt(format_args!("{}", self.0)) - } -} - -#[derive(Debug)] -pub enum TrackElementAction { - MoveDeviceLeft(usize), - MoveDeviceRight(usize), - RemoveDevice(usize), -} - -#[derive(Debug)] -pub enum TrackDetailAction {} - -#[allow(missing_docs)] -#[derive(Clone, Debug)] -pub enum TrackAction { - SetTitle(TrackTitle), - ToggleDisclosure, - NewDevice(TrackUid, Key), -} - -#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize)] -pub enum TrackType { - #[default] - Midi, - Audio, - Aux, -} - -#[derive(Serialize, Deserialize, Debug, Default)] -pub struct TrackFactory { - next_uid: TrackUid, -} -impl TrackFactory { - fn next_uid(&mut self) -> TrackUid { - let uid = self.next_uid; - self.next_uid.increment(); - uid - } - - pub fn midi(&mut self, piano_roll: &Arc>) -> Track { - let uid = self.next_uid(); - let title = TrackTitle(format!("MIDI {}", uid)); - - let mut t = Track { - uid, - title, - ty: TrackType::Midi, - ..Default::default() - }; - t.sequencer_mut().set_piano_roll(Arc::clone(piano_roll)); - - t - } - - pub fn audio(&mut self) -> Track { - let uid = self.next_uid(); - let title = TrackTitle(format!("Audio {}", uid)); - Track { - uid, - title, - ty: TrackType::Audio, - ..Default::default() - } - } - - pub fn aux(&mut self) -> Track { - let uid = self.next_uid(); - let title = TrackTitle(format!("Aux {}", uid)); - Track { - uid, - title, - ty: TrackType::Aux, - ..Default::default() - } - } -} -#[derive(Debug)] -pub struct TrackBuffer(pub [StereoSample; Self::LEN]); -impl TrackBuffer { - pub const LEN: usize = 64; -} -impl Default for TrackBuffer { - fn default() -> Self { - Self([StereoSample::default(); Self::LEN]) - } -} - -/// Newtype for track title string. -#[derive(Serialize, Deserialize, Debug, Clone)] -pub struct TrackTitle(pub String); -impl Default for TrackTitle { - fn default() -> Self { - Self("Untitled".to_string()) - } -} - -#[derive(Debug, Default, Serialize, Deserialize, Clone, Copy)] -pub enum TrackUiState { - #[default] - Collapsed, - Expanded, -} - -#[derive(Debug, Default)] -pub struct TrackEphemerals { - buffer: TrackBuffer, - is_sequencer_open: bool, - piano_roll: Arc>, - action: Option, - view_range: Range, - is_selected: bool, - ui_state: TrackUiState, -} - -/// A collection of instruments, effects, and controllers that combine to -/// produce a single source of audio. -#[derive(Serialize, Deserialize, Debug, Default)] -pub struct Track { - uid: TrackUid, - title: TrackTitle, - ty: TrackType, - - entity_store: EntityStore, - - sequencer: Sequencer, - midi_router: MidiRouter, - - /// [ControlAtlas] manages the sources of Control events. It generates - /// events but does not handle their routing. - control_atlas: ControlAtlas, - /// [ControlRouter] manages the destinations of Control events. It does not - /// generate events, but when events are generated, it knows where to route - /// them. - control_router: ControlRouter, - - controllers: Vec, - instruments: Vec, - effects: Vec, - - humidifier: Humidifier, - - #[serde(skip)] - e: TrackEphemerals, -} -impl Track { - #[allow(missing_docs)] - pub fn is_aux(&self) -> bool { - matches!(self.ty, TrackType::Aux) - } - - // TODO: for now the only way to add something new to a Track is to append it. - #[allow(missing_docs)] - pub fn append_entity(&mut self, entity: Box) -> anyhow::Result { - let uid = entity.uid(); - - // Some entities are hybrids, so they can appear in multiple lists. - // That's why we don't have if-else here. - if entity.as_controller().is_some() { - self.controllers.push(uid); - } - if entity.as_effect().is_some() { - self.effects.push(uid); - } - if entity.as_instrument().is_some() { - self.instruments.push(uid); - } - if entity.as_handles_midi().is_some() { - // TODO: for now, everyone's on channel 0 - self.midi_router.connect(uid, MidiChannel(0)); - } - - self.entity_store.add(entity) - } - - #[allow(missing_docs)] - pub fn remove_entity(&mut self, uid: &Uid) -> Option> { - if let Some(entity) = self.entity_store.remove(uid) { - if entity.as_controller().is_some() { - self.controllers.retain(|e| e != uid) - } - if entity.as_effect().is_some() { - self.effects.retain(|e| e != uid); - } - if entity.as_instrument().is_some() { - self.instruments.retain(|e| e != uid); - } - Some(entity) - } else { - None - } - } - - /// Returns the [Entity] having the given [Uid], if it exists. - pub fn entity(&self, uid: &Uid) -> Option<&Box> { - self.entity_store.get(uid) - } - - /// Returns the mutable [Entity] having the given [Uid], if it exists. - pub fn entity_mut(&mut self, uid: &Uid) -> Option<&mut Box> { - self.entity_store.get_mut(uid) - } - - fn button_states(index: usize, len: usize) -> (bool, bool) { - let left = index != 0; - let right = len > 1 && index != len - 1; - (left, right) - } - - /// Shows the detail view for the selected track. - // TODO: ordering should be controllers, instruments, then effects. Within - // those groups, the user can reorder as desired (but instrument order - // doesn't matter because they're all simultaneous) - #[must_use] - pub fn ui_detail(&mut self, ui: &mut Ui) -> Option { - let style = ui.visuals().widgets.inactive; - let action = None; - - ui.with_layout( - egui::Layout::top_down(egui::Align::LEFT).with_cross_justify(true), - |ui| { - let desired_size = Vec2::new(ui.available_width(), 256.0 - style.fg_stroke.width); - ui.set_min_size(desired_size); - ui.set_max_size(desired_size); - - ui.horizontal_centered(|ui| { - let desired_size = Vec2::new(384.0, ui.available_height()); - - let mut action = None; - - if let Some(a) = Self::add_track_element(ui, 0, false, false, false, |ui| { - ui.allocate_ui(vec2(256.0, ui.available_height()), |ui| { - self.sequencer.ui(ui); - }); - }) { - action = Some(a); - }; - - let len = self.controllers.len(); - for (index, uid) in self.controllers.iter().enumerate() { - let index = index + 1; - ui.allocate_ui(desired_size, |ui| { - let (show_left, show_right) = Self::button_states(index, len); - if let Some(a) = Self::add_track_element( - ui, - index, - show_left, - show_right, - true, - |ui| { - if let Some(e) = self.entity_store.get_mut(uid) { - e.ui(ui); - } - }, - ) { - action = Some(a); - }; - }); - } - let len = self.instruments.len(); - for (index, uid) in self.instruments.iter().enumerate() { - let index = index + 1; - ui.allocate_ui(desired_size, |ui| { - let (show_left, show_right) = Self::button_states(index, len); - if let Some(a) = Self::add_track_element( - ui, - index, - show_left, - show_right, - true, - |ui| { - if let Some(e) = self.entity_store.get_mut(uid) { - e.ui(ui); - } - }, - ) { - action = Some(a); - }; - }); - } - let len = self.effects.len(); - for (index, uid) in self.effects.iter().enumerate() { - let index = index + 1; - ui.allocate_ui(desired_size, |ui| { - let (show_left, show_right) = Self::button_states(index, len); - if let Some(a) = Self::add_track_element( - ui, - index, - show_left, - show_right, - true, - |ui| { - if let Some(e) = self.entity_store.get_mut(uid) { - e.ui(ui); - } - }, - ) { - action = Some(a); - }; - }); - } - }); - }, - ); - action - } - - fn add_track_element( - ui: &mut Ui, - index: usize, - show_left_button: bool, - show_right_button: bool, - show_delete_button: bool, - add_contents: impl FnOnce(&mut Ui), - ) -> Option { - let mut action = None; - let style = ui.visuals().widgets.inactive; - Frame::none() - .stroke(style.fg_stroke) - .inner_margin(Margin::same(2.0)) - .show(ui, |ui| { - ui.vertical(|ui| { - ui.allocate_ui(vec2(384.0, ui.available_height()), |ui| { - ui.with_layout(Layout::right_to_left(Align::TOP), |ui| { - if show_left_button && ui.button("<").clicked() { - action = Some(TrackElementAction::MoveDeviceLeft(index)); - } - if show_right_button && ui.button(">").clicked() { - action = Some(TrackElementAction::MoveDeviceRight(index)); - } - if show_delete_button && ui.button("x").clicked() { - action = Some(TrackElementAction::RemoveDevice(index)); - } - }); - ui.vertical(|ui| { - add_contents(ui); - }); - }); - }); - }); - action - } - - pub(crate) fn track_view_height(track_type: TrackType, ui_state: TrackUiState) -> f32 { - if matches!(track_type, TrackType::Aux) { - Self::device_view_height(ui_state) - } else { - Self::arrangement_view_height(ui_state) + Self::device_view_height(ui_state) - } - } - - const fn arrangement_view_height(_ui_state: TrackUiState) -> f32 { - 64.0 - } - - const fn device_view_height(ui_state: TrackUiState) -> f32 { - match ui_state { - TrackUiState::Collapsed => 32.0, - TrackUiState::Expanded => 96.0, - } - } - - /// Renders a MIDI [Track]'s arrangement view, which is an overview of some or - /// all of the track's project timeline. - fn ui_contents_midi(&mut self, ui: &mut Ui) { - let (_response, _action) = self.sequencer.ui_arrangement(ui, self.uid); - } - - /// Renders an audio [Track]'s arrangement view, which is an overview of some or - /// all of the track's project timeline. - fn ui_contents_audio(&mut self, ui: &mut Ui) { - ui.add(placeholder::wiggler()); - } - - #[must_use] - fn ui_device_view(&mut self, ui: &mut Ui) -> Option { - let mut action = None; - let mut drag_and_drop_action = None; - let desired_size = vec2(128.0, Self::device_view_height(self.e.ui_state)); - - ui.horizontal(|ui| { - if self.e.is_sequencer_open { - egui::Window::new("Sequencer") - .open(&mut self.e.is_sequencer_open) - .show(ui.ctx(), |ui| { - self.sequencer.ui(ui); - }); - } else { - Self::ui_device(ui, &mut self.sequencer, desired_size); - if ui.button("open").clicked() { - self.e.is_sequencer_open = !self.e.is_sequencer_open; - } - } - self.entity_store.iter_mut().for_each(|e| { - Self::ui_device(ui, e.as_mut(), desired_size); - }); - - let can_accept = if let Some(source) = DragDropManager::source() { - match source { - DragDropSource::NewDevice(_) => true, - DragDropSource::Pattern(_) => false, - DragDropSource::ControlTrip(_) => false, - } - } else { - false - }; - let r = DragDropManager::drop_target(ui, can_accept, |ui| { - ui.allocate_ui_with_layout( - desired_size, - Layout::centered_and_justified(egui::Direction::LeftToRight), - |ui| { - ui.label(if self.entity_store.is_empty() { - "Drag stuff here" - } else { - "+" - }) - }, - ); - }); - - // super::drag_drop::DragDropTarget::Track(self.uid), - - if DragDropManager::is_dropped(ui, &r.response) { - if let Some(source) = DragDropManager::source() { - match source { - DragDropSource::NewDevice(key) => { - drag_and_drop_action = Some(DragDropSource::NewDevice(key.clone())); - action = Some(TrackAction::NewDevice(self.uid, key.clone())); - DragDropManager::reset(); - } - DragDropSource::Pattern(_) => eprintln!( - "nope - I'm a device drop target, not a pattern target {:?}", - source - ), - DragDropSource::ControlTrip(_) => { - eprintln!("NOPE!") - } - } - } - } - }); - - action - } - - fn ui_device(ui: &mut Ui, entity: &mut dyn Entity, desired_size: Vec2) { - ui.allocate_ui(desired_size, |ui| { - ui.set_min_size(desired_size); - ui.set_max_size(desired_size); - Frame::default() - .stroke(Stroke { - width: 0.5, - color: Color32::DARK_GRAY, - }) - .inner_margin(2.0) - .show(ui, |ui| { - entity.ui(ui); - }); - }); - } - - #[allow(missing_docs)] - pub fn remove_selected_patterns(&mut self) { - self.sequencer.remove_selected_arranged_patterns(); - } - - #[allow(missing_docs)] - pub fn route_midi_message(&mut self, channel: MidiChannel, message: MidiMessage) { - if let Err(e) = self - .midi_router - .route(&mut self.entity_store, channel, message) - { - eprintln!("While routing: {e}"); - } - } - - #[allow(missing_docs)] - pub fn route_control_change(&mut self, uid: Uid, value: ControlValue) { - if let Err(e) = self.control_router.route( - &mut |target_uid, index, value| { - if let Some(e) = self.entity_store.get_mut(target_uid) { - if let Some(e) = e.as_controllable_mut() { - e.control_set_param_by_index(index, value); - } - } - }, - uid, - value, - ) { - eprintln!("While routing control change: {e}") - } - } - - pub(crate) fn set_title(&mut self, title: TrackTitle) { - self.title = title; - } - - #[allow(missing_docs)] - pub fn uid(&self) -> TrackUid { - self.uid - } - - pub(crate) fn ty(&self) -> TrackType { - self.ty - } - - #[allow(missing_docs)] - pub fn set_piano_roll(&mut self, piano_roll: Arc>) { - self.e.piano_roll = Arc::clone(&piano_roll); - self.sequencer.set_piano_roll(piano_roll); - } - - #[allow(missing_docs)] - pub fn sequencer_mut(&mut self) -> &mut Sequencer { - &mut self.sequencer - } - - /// Sets the wet/dry of an effect in the chain. - pub fn set_humidity(&mut self, effect_uid: Uid, humidity: Normal) -> anyhow::Result<()> { - if let Some(entity) = self.entity(&effect_uid) { - if entity.as_effect().is_some() { - self.humidifier.set_humidity_by_uid(effect_uid, humidity); - Ok(()) - } else { - Err(anyhow!("{effect_uid} is not an effect")) - } - } else { - Err(anyhow!("{effect_uid} not found")) - } - } - - pub(crate) fn calculate_max_entity_uid(&self) -> Option { - self.entity_store.calculate_max_entity_uid() - } - - /// Moves the indicated effect to a new position within the effects chain. - /// Zero is the first position. - pub fn move_effect(&mut self, uid: Uid, new_index: usize) -> anyhow::Result<()> { - if new_index >= self.effects.len() { - Err(anyhow!( - "Can't move {uid} to {new_index} when we have only {} items", - self.effects.len() - )) - } else if self.effects.contains(&uid) { - self.effects.retain(|e| e != &uid); - self.effects.insert(new_index, uid); - Ok(()) - } else { - Err(anyhow!("Effect {uid} not found")) - } - } - - /// Returns the [ControlRouter]. - pub fn control_router_mut(&mut self) -> &mut ControlRouter { - &mut self.control_router - } - - /// Returns an immutable reference to the internal buffer. - pub fn buffer(&self) -> &TrackBuffer { - &self.e.buffer - } - - /// Returns a writable version of the internal buffer. - pub fn buffer_mut(&mut self) -> &mut TrackBuffer { - &mut self.e.buffer - } - - /// Returns the [ControlAtlas]. - pub fn control_atlas_mut(&mut self) -> &mut ControlAtlas { - &mut self.control_atlas - } - - #[allow(missing_docs)] - pub fn action(&self) -> Option { - self.e.action.clone() - } - - #[allow(missing_docs)] - pub fn set_is_selected(&mut self, selected: bool) { - self.e.is_selected = selected; - } - - #[allow(missing_docs)] - pub fn set_ui_state(&mut self, ui_state: TrackUiState) { - self.e.ui_state = ui_state; - } -} -impl GeneratesToInternalBuffer for Track { - fn generate_batch_values(&mut self, len: usize) -> usize { - if len > self.e.buffer.0.len() { - eprintln!( - "requested {} samples but buffer is only len {}", - len, - self.e.buffer.0.len() - ); - return 0; - } - - if !self.is_aux() { - // We're a regular track. Start with a fresh buffer and let each - // instrument do its thing. - self.e.buffer.0.fill(StereoSample::SILENCE); - } else { - // We're an aux track. We leave the internal buffer as-is, with the - // expectation that the caller has already filled it with the signal - // we should be processing. - } - - for uid in self.instruments.iter() { - if let Some(e) = self.entity_store.get_mut(uid) { - if let Some(e) = e.as_instrument_mut() { - // Note that we're expecting everyone to ADD to the buffer, - // not to overwrite! TODO: convert all instruments to have - // internal buffers - e.generate_batch_values(&mut self.e.buffer.0); - } - } - } - - // TODO: change this trait to operate on batches. - for uid in self.effects.iter() { - if let Some(e) = self.entity_store.get_mut(uid) { - if let Some(e) = e.as_effect_mut() { - let humidity = self.humidifier.get_humidity_by_uid(uid); - if humidity == Normal::zero() { - continue; - } - for sample in self.e.buffer.0.iter_mut() { - *sample = self.humidifier.transform_audio( - humidity, - *sample, - e.transform_audio(*sample), - ); - } - } - } - } - - // See #146 TODO - at this point we might want to gather any events - // produced during the effects stage. - - self.e.buffer.0.len() - } - - fn values(&self) -> &[StereoSample] { - &self.e.buffer.0 - } -} -impl Ticks for Track { - fn tick(&mut self, tick_count: usize) { - self.entity_store.tick(tick_count); - } -} -impl Configurable for Track { - fn update_sample_rate(&mut self, sample_rate: SampleRate) { - self.sequencer.update_sample_rate(sample_rate); - self.control_atlas.update_sample_rate(sample_rate); - self.entity_store.update_sample_rate(sample_rate); - } - - fn update_tempo(&mut self, tempo: Tempo) { - self.sequencer.update_tempo(tempo); - self.control_atlas.update_tempo(tempo); - self.entity_store.update_tempo(tempo); - } - - fn update_time_signature(&mut self, time_signature: TimeSignature) { - self.sequencer.update_time_signature(time_signature); - self.control_atlas.update_time_signature(time_signature); - self.entity_store.update_time_signature(time_signature); - } -} - -// TODO: I think this is wrong and misguided. If MIDI messages are handled by -// Track, then each Track needs to record who's receiving on which channel, and -// messages can't be sent from a device on one track to one on a different -// track. While that could make parallelism easier, it doesn't seem intuitively -// correct, because in a real studio you'd be able to hook up MIDI cables -// independently of audio cables. -#[cfg(never)] -impl HandlesMidi for Track { - fn handle_midi_message( - &mut self, - channel: MidiChannel, - message: MidiMessage, - messages_fn: &mut dyn FnMut(Uid, MidiChannel, MidiMessage), - ) { - for e in self.controllers.iter_mut() { - e.handle_midi_message(channel, &message, messages_fn); - } - for e in self.instruments.iter_mut() { - e.handle_midi_message(channel, &message, messages_fn); - } - } -} -impl Controls for Track { - fn update_time(&mut self, range: &Range) { - self.sequencer.update_time(range); - self.control_atlas.update_time(range); - self.entity_store.update_time(range); - } - - fn work(&mut self, control_events_fn: &mut ControlEventsFn) { - // Create a place to hold MIDI messages that we need to route. - let mut midi_events = Vec::default(); - - // Peek at incoming events before handing them to control_events_fn. - let mut handler = |uid, event| { - match event { - // We need to route MIDI messages to all eligible Entities in - // this Track, so we save them up. - EntityEvent::Midi(channel, message) => { - midi_events.push((channel, message)); - } - EntityEvent::Control(_) => {} - } - control_events_fn(uid, event); - }; - - // Let everyone work and possibly generate messages. - self.sequencer.work(&mut handler); - self.control_atlas.work(&mut handler); - self.entity_store.work(&mut handler); - - // We've accumulated all the MIDI messages. Route them to our own - // MidiRouter. They've already been forwarded to the caller via - // control_events_fn. - midi_events.into_iter().for_each(|(channel, message)| { - let _ = self - .midi_router - .route(&mut self.entity_store, channel, message); - }); - } - - fn is_finished(&self) -> bool { - self.sequencer.is_finished() - && self.control_atlas.is_finished() - && self.entity_store.is_finished() - } - - fn play(&mut self) { - self.sequencer.play(); - self.entity_store.play(); - } - - fn stop(&mut self) { - self.sequencer.stop(); - self.entity_store.stop(); - } - - fn skip_to_start(&mut self) { - self.sequencer.skip_to_start(); - self.entity_store.skip_to_start(); - } - - fn is_performing(&self) -> bool { - self.sequencer.is_performing() || self.entity_store.is_performing() - } -} -impl Serializable for Track { - fn after_deser(&mut self) { - self.sequencer.after_deser(); - self.entity_store.after_deser(); - } -} -impl DisplaysInTimeline for Track { - fn set_view_range(&mut self, view_range: &Range) { - self.sequencer.set_view_range(view_range); - self.e.view_range = view_range.clone(); - } -} -impl Displays for Track { - fn ui(&mut self, ui: &mut egui::Ui) -> egui::Response { - self.e.action = None; - - // The inner_margin() should be half of the Frame stroke width to leave - // room for it. Thanks vikrinox on the egui Discord. - Frame::default() - .inner_margin(Margin::same(0.5)) - .stroke(Stroke { - width: 1.0, - color: { - if self.e.is_selected { - Color32::YELLOW - } else { - Color32::DARK_GRAY - } - }, - }) - .show(ui, |ui| { - ui.horizontal(|ui| { - ui.set_min_height(Self::track_view_height(self.ty, self.e.ui_state)); - - // The `Response` is based on the title bar, so - // clicking/dragging on the title bar affects the `Track` as a - // whole. - let response = ui.add(track::title_bar(&mut self.title.0)); - - // Take up all the space we're given, even if we can't fill - // it with widget content. - ui.set_min_size(ui.available_size()); - - // The frames shouldn't have space between them. - ui.style_mut().spacing.item_spacing = Vec2::ZERO; - - // Build the track content with the device view beneath it. - ui.vertical(|ui| { - // Only MIDI/audio tracks have content. - if !matches!(self.ty, TrackType::Aux) { - // Reserve space for the device view. - ui.set_max_height(Self::arrangement_view_height(self.e.ui_state)); - - // Draw the arrangement view. - Frame::default() - .inner_margin(Margin::same(0.5)) - .outer_margin(Margin::same(0.5)) - .stroke(Stroke { - width: 1.0, - color: Color32::DARK_GRAY, - }) - .show(ui, |ui| { - ui.set_min_size(ui.available_size()); - match self.ty { - TrackType::Midi => self.ui_contents_midi(ui), - TrackType::Audio => self.ui_contents_audio(ui), - _ => panic!(), - } - ui.add(control::atlas( - &mut self.control_atlas, - &mut self.control_router, - self.e.view_range.clone(), - )); - }); - } - - // Now the device view. - Frame::default() - .inner_margin(Margin::same(0.5)) - .outer_margin(Margin::same(0.5)) - .stroke(Stroke { - width: 1.0, - color: Color32::DARK_GRAY, - }) - .show(ui, |ui| { - if let Some(track_action) = self.ui_device_view(ui) { - self.e.action = Some(track_action); - } - }); - }); - response - }) - .inner - }) - .inner - } -} - -#[cfg(test)] -mod tests { - use super::*; - use groove_toys::{ - ToyControllerAlwaysSendsMidiMessage, ToyEffect, ToyInstrument, ToyInstrumentParams, - }; - - #[test] - fn basic_track_operations() { - let mut t = Track::default(); - assert!(t.controllers.is_empty()); - assert!(t.effects.is_empty()); - assert!(t.instruments.is_empty()); - - // Create an instrument and add it to a track. - let mut instrument = ToyInstrument::new_with(&ToyInstrumentParams::default()); - instrument.set_uid(Uid(1)); - let id1 = t.append_entity(Box::new(instrument)).unwrap(); - - // Add a second instrument to the track. - let mut instrument = ToyInstrument::new_with(&ToyInstrumentParams::default()); - instrument.set_uid(Uid(2)); - let id2 = t.append_entity(Box::new(instrument)).unwrap(); - - assert_ne!(id1, id2, "Don't forget to assign UIDs!"); - - assert_eq!( - t.instruments[0], id1, - "first appended entity should be at index 0" - ); - assert_eq!( - t.instruments[1], id2, - "second appended entity should be at index 1" - ); - assert_eq!( - t.instruments.len(), - 2, - "there should be exactly as many entities as added" - ); - - let instrument = t.remove_entity(&id1).unwrap(); - assert_eq!(instrument.uid(), id1, "removed the right instrument"); - assert_eq!(t.instruments.len(), 1, "removed exactly one instrument"); - assert_eq!( - t.instruments[0], id2, - "the remaining instrument should be the one we left" - ); - assert!( - t.entity_store.get(&id1).is_none(), - "it should be gone from the store" - ); - - let mut effect = ToyEffect::default(); - effect.set_uid(Uid(3)); - let effect_id1 = t.append_entity(Box::new(effect)).unwrap(); - let mut effect = ToyEffect::default(); - effect.set_uid(Uid(4)); - let effect_id2 = t.append_entity(Box::new(effect)).unwrap(); - - assert_eq!(t.effects[0], effect_id1); - assert_eq!(t.effects[1], effect_id2); - assert!(t.move_effect(effect_id1, 1).is_ok()); - assert_eq!( - t.effects[0], effect_id2, - "After moving effects, id2 should be first" - ); - assert_eq!(t.effects[1], effect_id1); - } - - // We expect that a MIDI message will be routed to the eligible Entities in - // the same Track, and forwarded to the work() caller, presumably to decide - // whether to send it to other destination(s) such as external MIDI - // interfaces. - #[test] - fn midi_messages_sent_to_caller_and_sending_track_instruments() { - let mut t = Track::default(); - - let mut sender = ToyControllerAlwaysSendsMidiMessage::default(); - sender.set_uid(Uid(2001)); - let _sender_id = t.append_entity(Box::new(sender)).unwrap(); - - let mut receiver = ToyInstrument::new_with(&ToyInstrumentParams::default()); - receiver.set_uid(Uid(2002)); - let counter = Arc::clone(receiver.received_count_mutex()); - let _receiver_id = t.append_entity(Box::new(receiver)).unwrap(); - - let mut external_midi_messages = 0; - t.play(); - t.work(&mut |_uid, _event| { - external_midi_messages += 1; - }); - - if let Ok(c) = counter.lock() { - assert_eq!( - *c, 1, - "The receiving instrument in the track should have received the message" - ); - }; - - assert_eq!( - external_midi_messages, 1, - "After one work(), one MIDI message should have emerged for external processing" - ); - } -} diff --git a/src/mini/transport.rs b/src/mini/transport.rs index 773de89e..95703263 100644 --- a/src/mini/transport.rs +++ b/src/mini/transport.rs @@ -2,8 +2,8 @@ use derive_builder::Builder; use eframe::egui::Ui; -use ensnare::prelude::*; -use ensnare::traits::{ +use ensnare_core::prelude::*; +use ensnare_core::traits::{ Configurable, ControlEventsFn, Controls, Displays, HandlesMidi, Serializable, }; use ensnare_proc_macros::{Control, IsController, Uid}; diff --git a/src/mini/widgets/audio.rs b/src/mini/widgets/audio.rs deleted file mode 100644 index 19cb99d3..00000000 --- a/src/mini/widgets/audio.rs +++ /dev/null @@ -1,227 +0,0 @@ -// Copyright (c) 2023 Mike Tsao. All rights reserved. - -use crate::mini::rng::Rng; -use eframe::{ - egui::{self, Sense}, - emath::RectTransform, - epaint::{pos2, Color32, Rect, RectShape, Rounding, Stroke}, -}; -use ensnare::prelude::*; -use ensnare::traits::Displays; -use spectrum_analyzer::{scaling::divide_by_N_sqrt, FrequencyLimit}; - -/// A fixed-size circular buffer for use by audio widgets. -#[derive(Debug, Default)] -pub struct CircularSampleBuffer { - buffer: Vec, - cursor: usize, - rng: Rng, -} -impl CircularSampleBuffer { - /// Creates a new [CircularSampleBuffer] of the given size. - pub fn new(size: usize) -> Self { - let mut r = Self { - buffer: Vec::with_capacity(size), - cursor: Default::default(), - rng: Rng::default(), - }; - r.buffer.resize(size, Sample::SILENCE); - r - } - - /// Returns the start of the buffer in memory and the cursor position. It is - /// the caller's responsibility to figure out the boundaries of the buffer - /// using the cursor value. - pub fn get(&self) -> (&[Sample], usize) { - (&self.buffer, self.cursor) - } - - /// Adds a slice of [Sample]s to the buffer, overwriting what's already there. - pub fn push(&mut self, new_samples: &[Sample]) { - let src_len = new_samples.len(); - let dst_len = self.buffer.len(); - if src_len > dst_len { - panic!("Error: tried to push too much data into circular buffer"); - } - let d = &mut self.buffer; - - // Copy as much of the src as we can with the first memcpy. - let available_dst_len = dst_len - self.cursor; - let part_1_len = src_len.min(available_dst_len); - d[self.cursor..(self.cursor + part_1_len)].copy_from_slice(&new_samples[0..part_1_len]); - self.cursor += part_1_len; - if self.cursor >= dst_len { - self.cursor = 0; - } - - // If needed, copy the rest with a second memcpy. - if part_1_len < src_len { - let part_2_len = src_len - part_1_len; - d[0..part_2_len].copy_from_slice(&new_samples[part_1_len..]); - self.cursor += part_2_len; - - // This could happen if self.cursor was at the max position and - // src_len == dst_len - if self.cursor >= dst_len { - self.cursor = 0; - } - } - } - - /// TODO remove - temp for development - pub fn add_some_noise(&mut self) { - let new_samples: Vec = (0..8) - .map(|_| Sample::from(Normal::from(self.rng.0.rand_u64() as f64 / u64::MAX as f64))) - .collect(); - self.push(&new_samples); - } - - /// Does a quick-and-dirty FFT of the sample buffer, producing a Vec - /// that is suitable for an unlabeled visualization. If you want labels, - /// then do this transformation yourself so you can display the Hz bucket - /// labels. - pub fn analyze_spectrum( - &self, - ) -> anyhow::Result, spectrum_analyzer::error::SpectrumAnalyzerError> { - let samples: Vec = self.buffer.iter().map(|x| x.0 as f32).collect(); - let hann_window = spectrum_analyzer::windows::hann_window(&samples); - let spectrum = spectrum_analyzer::samples_fft_to_spectrum( - &hann_window, - 44100, - FrequencyLimit::All, - Some(÷_by_N_sqrt), - )?; - Ok(spectrum.data().iter().map(|(_hz, val)| val.val()).collect()) - } -} - -/// Wraps a [TimeDomain] as a [Widget](eframe::egui::Widget). -pub fn time_domain(samples: &[Sample], start: usize) -> impl eframe::egui::Widget + '_ { - move |ui: &mut eframe::egui::Ui| TimeDomain::new(samples, start).ui(ui) -} - -/// Wraps a [FrequencyDomain] as a [Widget](eframe::egui::Widget). -pub fn frequency_domain(values: &[f32]) -> impl eframe::egui::Widget + '_ { - move |ui: &mut eframe::egui::Ui| FrequencyDomain::new(values).ui(ui) -} - -/// Creates 256 samples of noise. -pub fn init_random_samples() -> [Sample; 256] { - let mut r = [Sample::default(); 256]; - let mut rng = Rng::default(); - for s in &mut r { - let value = rng.0.rand_float().fract() * 2.0 - 1.0; - *s = Sample::from(value); - } - r -} - -/// Displays a series of [Sample]s in the time domain. That's a fancy way of -/// saying it shows the amplitudes. -#[derive(Debug)] -pub struct TimeDomain<'a> { - samples: &'a [Sample], - start: usize, -} -impl<'a> TimeDomain<'a> { - fn new(samples: &'a [Sample], start: usize) -> Self { - Self { samples, start } - } -} -impl<'a> Displays for TimeDomain<'a> { - fn ui(&mut self, ui: &mut egui::Ui) -> egui::Response { - let (response, painter) = - ui.allocate_painter(ui.available_size_before_wrap(), Sense::hover()); - - let to_screen = RectTransform::from_to( - Rect::from_x_y_ranges( - 0.0..=self.samples.len() as f32, - Sample::MAX.0 as f32..=Sample::MIN.0 as f32, - ), - response.rect, - ); - let mut shapes = Vec::default(); - - shapes.push(eframe::epaint::Shape::Rect(RectShape { - rect: response.rect, - rounding: Rounding::same(3.0), - fill: Color32::DARK_BLUE, - stroke: Stroke { - width: 2.0, - color: Color32::YELLOW, - }, - })); - - for i in 0..self.samples.len() { - let cursor = (self.start + i) % self.samples.len(); - let sample = self.samples[cursor]; - shapes.push(eframe::epaint::Shape::LineSegment { - points: [ - to_screen * pos2(i as f32, Sample::MIN.0 as f32), - to_screen * pos2(i as f32, sample.0 as f32), - ], - stroke: Stroke { - width: 1.0, - color: Color32::YELLOW, - }, - }) - } - - painter.extend(shapes); - response - } -} - -/// Displays a series of [Sample]s in the frequency domain. Or, to put it -/// another way, shows a spectrum analysis of a clip. -#[derive(Debug)] -pub struct FrequencyDomain<'a> { - values: &'a [f32], -} -impl<'a> FrequencyDomain<'a> { - fn new(values: &'a [f32]) -> Self { - Self { values } - } -} -impl<'a> Displays for FrequencyDomain<'a> { - fn ui(&mut self, ui: &mut egui::Ui) -> egui::Response { - let (response, painter) = - ui.allocate_painter(ui.available_size_before_wrap(), Sense::hover()); - - let buf_min = 0.0; - let buf_max = 1.0; - - #[allow(unused_variables)] - let to_screen = RectTransform::from_to( - Rect::from_x_y_ranges(0.0..=self.values.len() as f32, buf_max..=buf_min), - response.rect, - ); - let mut shapes = Vec::default(); - - shapes.push(eframe::epaint::Shape::Rect(RectShape { - rect: response.rect, - rounding: Rounding::same(3.0), - fill: Color32::DARK_GREEN, - stroke: Stroke { - width: 2.0, - color: Color32::YELLOW, - }, - })); - - for (i, value) in self.values.iter().enumerate() { - shapes.push(eframe::epaint::Shape::LineSegment { - points: [ - to_screen * pos2(i as f32, buf_min), - to_screen * pos2(i as f32, *value), - ], - stroke: Stroke { - width: 1.0, - color: Color32::YELLOW, - }, - }) - } - - painter.extend(shapes); - response - } -} diff --git a/src/mini/widgets/control.rs b/src/mini/widgets/control.rs deleted file mode 100644 index 27bca78a..00000000 --- a/src/mini/widgets/control.rs +++ /dev/null @@ -1,274 +0,0 @@ -// Copyright (c) 2023 Mike Tsao. All rights reserved. - -use crate::{ - mini::{ - control_router::ControlRouter, ControlAtlas, ControlTrip, ControlTripBuilder, - ControlTripPath, DragDropManager, DragDropSource, - }, - EntityFactory, -}; -use eframe::{ - egui::{self, Layout, Sense}, - emath::RectTransform, - epaint::{pos2, vec2, Color32, Rect, Stroke}, -}; -use ensnare::prelude::*; -use ensnare::traits::{Displays, DisplaysInTimeline, HasUid}; -use std::ops::Range; - -/// Wraps a [ControlAtlas] as a [Widget](eframe::egui::Widget). -pub fn atlas<'a>( - control_atlas: &'a mut ControlAtlas, - control_router: &'a mut ControlRouter, - view_range: Range, -) -> impl eframe::egui::Widget + 'a { - move |ui: &mut eframe::egui::Ui| Atlas::new(control_atlas, control_router, view_range).ui(ui) -} - -#[derive(Debug)] -struct Atlas<'a> { - control_atlas: &'a mut ControlAtlas, - control_router: &'a mut ControlRouter, - view_range: Range, -} -impl<'a> Atlas<'a> { - fn new( - control_atlas: &'a mut ControlAtlas, - control_router: &'a mut ControlRouter, - view_range: Range, - ) -> Self { - Self { - control_atlas, - control_router, - view_range, - } - } -} -impl<'a> DisplaysInTimeline for Atlas<'a> { - fn set_view_range(&mut self, view_range: &std::ops::Range) { - self.view_range = view_range.clone(); - } -} -impl<'a> Displays for Atlas<'a> { - fn ui(&mut self, ui: &mut egui::Ui) -> egui::Response { - // This push_id() was needed to avoid an ID conflict. I think it is - // because we're drawing widgets on top of each other, but I'm honestly - // not sure. - ui.push_id(ui.next_auto_id(), |ui| { - let (_id, rect) = ui.allocate_space(vec2(ui.available_width(), 64.0)); - let response = ui - .allocate_ui_at_rect(rect, |ui| { - let mut remove_uid = None; - self.control_atlas.trips_mut().iter_mut().for_each(|t| { - ui.allocate_ui_at_rect(rect, |ui| { - ui.add(trip(t, self.control_router, self.view_range.clone())); - - // Draw the trip controls. - if ui.is_enabled() { - // TODO: I don't know why this isn't flush with - // the right side of the component. - let controls_rect = Rect::from_points(&[ - rect.right_top(), - pos2( - rect.right() - - ui.ctx().style().spacing.interact_size.x * 2.0, - rect.top(), - ), - ]); - ui.allocate_ui_at_rect(controls_rect, |ui| { - ui.allocate_ui_with_layout( - ui.available_size(), - Layout::right_to_left(eframe::emath::Align::Center), - |ui| { - if ui.button("x").clicked() { - remove_uid = Some(t.uid()); - } - // TODO: this will be what you drag - // to things you want this trip to - // control - DragDropManager::drag_source( - ui, - ui.next_auto_id(), - DragDropSource::ControlTrip(t.uid()), - |ui| { - ui.label("S"); - }, - ); - }, - ); - }); - } - }); - }); - if let Some(uid) = remove_uid { - self.control_atlas.remove_trip(uid); - } - }) - .response; - if ui.is_enabled() { - response.context_menu(|ui| { - if ui.button("Add trip").clicked() { - ui.close_menu(); - let mut trip = ControlTripBuilder::default() - .random(MusicalTime::START) - .build() - .unwrap(); - trip.set_uid(EntityFactory::global().mint_uid()); - self.control_atlas.add_trip(trip); - } - }) - } else { - response - } - }) - .inner - } -} - -/// Wraps a [ControlTrip] as a [Widget](eframe::egui::Widget). -fn trip<'a>( - trip: &'a mut ControlTrip, - control_router: &'a mut ControlRouter, - view_range: Range, -) -> impl eframe::egui::Widget + 'a { - move |ui: &mut eframe::egui::Ui| Trip::new(trip, control_router, view_range).ui(ui) -} - -#[derive(Debug)] -struct Trip<'a> { - control_trip: &'a mut ControlTrip, - control_router: &'a mut ControlRouter, - view_range: Range, -} -impl<'a> Trip<'a> { - fn new( - control_trip: &'a mut ControlTrip, - control_router: &'a mut ControlRouter, - view_range: Range, - ) -> Self { - Self { - control_trip, - control_router, - view_range, - } - } -} -impl<'a> Displays for Trip<'a> { - fn ui(&mut self, ui: &mut egui::Ui) -> egui::Response { - let (response, painter) = ui.allocate_painter(ui.available_size(), Sense::click()); - let to_screen = RectTransform::from_to( - Rect::from_x_y_ranges( - self.view_range.start.total_units() as f32 - ..=self.view_range.end.total_units() as f32, - ControlValue::MAX.0 as f32..=ControlValue::MIN.0 as f32, - ), - response.rect, - ); - - // The first step always starts at the left of the view range. - let mut pos = to_screen - * pos2( - MusicalTime::START.total_units() as f32, - if let Some(step) = self.control_trip.steps_mut().first() { - step.value.0 as f32 - } else { - 0.0 - }, - ); - let stroke = if ui.is_enabled() { - ui.ctx().style().visuals.widgets.active.bg_stroke - } else { - ui.ctx().style().visuals.widgets.inactive.bg_stroke - }; - let steps_len = self.control_trip.steps().len(); - self.control_trip - .steps_mut() - .iter_mut() - .enumerate() - .for_each(|(index, step)| { - // Get the next step position, adjusting if it's the last one. - let second_pos = if index + 1 == steps_len { - let value = pos.y; - // Last step. Extend to end of view range. - let mut tmp_pos = - to_screen * pos2(self.view_range.end.total_units() as f32, 0.0); - tmp_pos.y = value; - tmp_pos - } else { - // Not last step. Get the actual value. - to_screen * pos2(step.time.total_units() as f32, step.value.0 as f32) - }; - - // If we're hovering over this step, highlight it. - let stroke = if response.hovered() { - if let Some(hover_pos) = ui.ctx().pointer_interact_pos() { - if hover_pos.x >= pos.x && hover_pos.x < second_pos.x { - if response.clicked() { - let from_screen = to_screen.inverse(); - let hover_pos_local = from_screen * hover_pos; - step.value = ControlValue::from(hover_pos_local.y); - } else if response.secondary_clicked() { - step.path = step.path.next(); - } - - Stroke { - width: stroke.width * 2.0, - color: Color32::YELLOW, - } - } else { - stroke - } - } else { - stroke - } - } else { - stroke - }; - - // Draw according to the step type. - match step.path { - ControlTripPath::None => {} - ControlTripPath::Flat => { - painter.line_segment([pos, pos2(pos.x, second_pos.y)], stroke); - painter.line_segment([pos2(pos.x, second_pos.y), second_pos], stroke); - } - ControlTripPath::Linear => { - painter.line_segment([pos, second_pos], stroke); - } - ControlTripPath::Logarithmic => todo!(), - ControlTripPath::Exponential => todo!(), - } - pos = second_pos; - }); - - if ui.is_enabled() { - let label = - if let Some(links) = self.control_router.control_links(self.control_trip.uid()) { - let link_texts = links.iter().fold(Vec::default(), |mut v, (uid, index)| { - // TODO: this can be a descriptive list of controlled things - v.push(format!("{uid}-{index:?} ")); - v - }); - link_texts.join("/") - } else { - String::from("none") - }; - if ui - .allocate_ui_at_rect(response.rect, |ui| ui.button(&label)) - .inner - .clicked() - { - // TODO: this is incomplete. It's a placeholder while I figure - // out the best way to present this information (it might - // actually be DnD rather than menu-driven). - self.control_router.link_control( - self.control_trip.uid(), - Uid(234), - ControlIndex(456), - ); - } - } - - response - } -} diff --git a/src/mini/widgets/controllers.rs b/src/mini/widgets/controllers.rs deleted file mode 100644 index 531bb5d1..00000000 --- a/src/mini/widgets/controllers.rs +++ /dev/null @@ -1,117 +0,0 @@ -// Copyright (c) 2023 Mike Tsao. All rights reserved. - -use crate::mini::{ESSequencer, Note}; -use eframe::{ - egui::{style::WidgetVisuals, Sense}, - emath::RectTransform, - epaint::{pos2, vec2, Rect, RectShape, Shape}, -}; -use ensnare::prelude::*; -use ensnare::traits::{Displays, DisplaysInTimeline}; -use std::ops::Range; - -/// Wraps an [ESSequencer] as a [Widget](eframe::egui::Widget). -pub fn es_sequencer( - sequencer: &mut ESSequencer, - view_range: Range, -) -> impl eframe::egui::Widget + '_ { - move |ui: &mut eframe::egui::Ui| SequencerWidget::new(sequencer, view_range).ui(ui) -} - -#[derive(Debug)] -struct SequencerWidget<'a> { - sequencer: &'a mut ESSequencer, - view_range: Range, -} -impl<'a> SequencerWidget<'a> { - fn new(sequencer: &'a mut ESSequencer, view_range: Range) -> Self { - Self { - sequencer, - view_range, - } - } - - fn shape_for_note( - &self, - to_screen: &RectTransform, - visuals: &WidgetVisuals, - note: &Note, - ) -> Shape { - Shape::Rect(RectShape { - rect: Rect::from_two_pos( - to_screen * pos2(note.range.start.total_units() as f32, note.key as f32), - to_screen * pos2(note.range.end.total_units() as f32, note.key as f32), - ), - rounding: visuals.rounding, - fill: visuals.bg_fill, - stroke: visuals.fg_stroke, - }) - } -} -impl<'a> DisplaysInTimeline for SequencerWidget<'a> { - fn set_view_range(&mut self, view_range: &std::ops::Range) { - self.view_range = view_range.clone(); - } -} -impl<'a> Displays for SequencerWidget<'a> { - fn ui(&mut self, ui: &mut eframe::egui::Ui) -> eframe::egui::Response { - ui.allocate_ui(vec2(ui.available_width(), 64.0), |ui| { - let (response, painter) = ui.allocate_painter(ui.available_size(), Sense::click()); - let x_range_f32 = self.view_range.start.total_units() as f32 - ..=self.view_range.end.total_units() as f32; - let y_range = i8::MAX as f32..=u8::MIN as f32; - let local_space_rect = Rect::from_x_y_ranges(x_range_f32, y_range); - let to_screen = RectTransform::from_to(local_space_rect, response.rect); - let from_screen = to_screen.inverse(); - - // Check whether we edited the sequence - if response.clicked() { - if let Some(click_pos) = ui.ctx().pointer_interact_pos() { - let local_pos = from_screen * click_pos; - let time = MusicalTime::new_with_units(local_pos.x as usize).quantized(); - let key = local_pos.y as u8; - let note = Note::new_with(key, time, MusicalTime::DURATION_QUARTER); - eprintln!("Saw a click at {time}, note {note:?}"); - self.sequencer.toggle_note(note); - self.sequencer.calculate_events(); - } - } - - let visuals = if ui.is_enabled() { - ui.ctx().style().visuals.widgets.active - } else { - ui.ctx().style().visuals.widgets.inactive - }; - - // Generate all the note shapes - let note_shapes: Vec = self - .sequencer - .notes() - .iter() - .map(|note| self.shape_for_note(&to_screen, &visuals, note)) - .collect(); - - // Generate all the pattern note shapes - let pattern_shapes: Vec = self.sequencer.patterns().iter().fold( - Vec::default(), - |mut v, (position, pattern)| { - pattern.notes().iter().for_each(|note| { - let note = Note { - key: note.key, - range: (note.range.start + *position)..(note.range.end + *position), - }; - v.push(self.shape_for_note(&to_screen, &visuals, ¬e)); - }); - v - }, - ); - - // Paint all the shapes - painter.extend(note_shapes); - painter.extend(pattern_shapes); - - response - }) - .inner - } -} diff --git a/src/mini/widgets/core.rs b/src/mini/widgets/core.rs deleted file mode 100644 index 75b8f1f6..00000000 --- a/src/mini/widgets/core.rs +++ /dev/null @@ -1,46 +0,0 @@ -// Copyright (c) 2023 Mike Tsao. All rights reserved. - -use crate::mini::Transport; -use eframe::{ - egui::{Label, Layout, RichText, TextStyle}, - emath::Align, - epaint::vec2, -}; -use ensnare::traits::Displays; - -/// Wraps a [Transport] as a [Widget](eframe::egui::Widget). -pub fn transport(transport: &mut Transport) -> impl eframe::egui::Widget + '_ { - move |ui: &mut eframe::egui::Ui| TransportWidget::new(transport).ui(ui) -} - -#[derive(Debug)] -struct TransportWidget<'a> { - transport: &'a mut Transport, -} -impl<'a> TransportWidget<'a> { - fn new(transport: &'a mut Transport) -> Self { - Self { transport } - } -} -impl<'a> Displays for TransportWidget<'a> { - fn ui(&mut self, ui: &mut eframe::egui::Ui) -> eframe::egui::Response { - ui.allocate_ui(vec2(72.0, 20.0), |ui| { - ui.set_min_width(128.0); - ui.with_layout(Layout::right_to_left(Align::Center), |ui| { - ui.add(Label::new( - RichText::new(format!("{:0.2}", self.transport.tempo)) - .text_style(TextStyle::Monospace), - )); - }); - }) - .response - | ui.allocate_ui(vec2(72.0, 20.0), |ui| { - ui.set_min_width(128.0); - ui.add(Label::new( - RichText::new(format!("{}", self.transport.current_time())) - .text_style(TextStyle::Monospace), - )); - }) - .response - } -} diff --git a/src/mini/widgets/mod.rs b/src/mini/widgets/mod.rs deleted file mode 100644 index b7d56987..00000000 --- a/src/mini/widgets/mod.rs +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright (c) 2023 Mike Tsao. All rights reserved. - -use ensnare::midi::MidiNote; - -/// Contains widgets that help visualize audio. -pub mod audio; - -/// Contains widgets related to automation/control. -pub mod control; - -/// Contains widgets that support Controller views. -pub mod controllers; - -/// Various widgets used throughout the system. -pub mod core; - -/// Contains widgets related to [Pattern](crate::mini::piano_roll::Pattern)s and -/// [PianoRoll](crate::mini::piano_roll::PianoRoll). -pub mod pattern; - -/// Contains widgets that are useful as placeholders during development. -pub mod placeholder; - -/// Contains widgets that help draw timeline views. -pub mod timeline; - -/// Contains widgets that help draw tracks. -pub mod track; - -/// A range that's useful for arranging MIDI notes along an egui axis. Note that -/// this is in reverse order, because vertically-oriented piano rolls show the -/// highest notes at the top of the screen. -pub const MIDI_NOTE_F32_RANGE: std::ops::RangeInclusive = - MidiNote::MAX as u8 as f32..=MidiNote::MIN as u8 as f32; - -/// A range that covers all MIDI note values in ascending order. -pub const MIDI_NOTE_U8_RANGE: std::ops::RangeInclusive = - MidiNote::MIN as u8..=MidiNote::MAX as u8; diff --git a/src/mini/widgets/pattern.rs b/src/mini/widgets/pattern.rs deleted file mode 100644 index 7c1af323..00000000 --- a/src/mini/widgets/pattern.rs +++ /dev/null @@ -1,279 +0,0 @@ -// Copyright (c) 2023 Mike Tsao. All rights reserved. - -use crate::mini::{DragDropManager, DragDropSource, Note, Pattern, PatternUid, SelectionSet}; -use eframe::{ - egui::{Id as EguiId, Response, Ui}, - emath::RectTransform, - epaint::{pos2, RectShape, Shape}, -}; -use ensnare::traits::Displays; -use ensnare::{midi::MidiNote, prelude::*}; -use std::collections::HashMap; - -use super::MIDI_NOTE_F32_RANGE; - -/// Wraps an [Icon] as a [Widget](eframe::egui::Widget). -pub fn icon( - duration: MusicalTime, - notes: &[Note], - is_selected: bool, -) -> impl eframe::egui::Widget + '_ { - move |ui: &mut eframe::egui::Ui| { - Icon::new() - .duration(duration) - .notes(notes) - .is_selected(is_selected) - .ui(ui) - } -} - -/// Wraps a [DraggableIcon] as a [Widget](eframe::egui::Widget). -pub fn draggable_icon() -> impl eframe::egui::Widget { - move |ui: &mut eframe::egui::Ui| DraggableIcon::new().ui(ui) -} - -/// Wraps a [Grid] as a [Widget](eframe::egui::Widget). -pub fn grid(duration: MusicalTime) -> impl eframe::egui::Widget { - move |ui: &mut eframe::egui::Ui| Grid::default().duration(duration).ui(ui) -} - -/// Wraps a [Carousel] as a [Widget](eframe::egui::Widget). -pub fn carousel<'a>( - pattern_uids: &'a [PatternUid], - uids_to_patterns: &'a HashMap, - selection_set: &'a mut SelectionSet, -) -> impl eframe::egui::Widget + 'a { - move |ui: &mut eframe::egui::Ui| { - Carousel::new(pattern_uids, uids_to_patterns, selection_set).ui(ui) - } -} - -/// Displays an iconic representation of a sequence of [Note]s (that might be in -/// a [Pattern](crate::mini::piano_roll::Pattern)). Intended to be a -/// drag-and-drop source. -#[derive(Debug, Default)] -pub struct Icon<'a> { - duration: MusicalTime, - notes: &'a [Note], - is_selected: bool, -} -impl<'a> Icon<'a> { - /// Creates a new [Icon]. - pub fn new() -> Self { - Default::default() - } - /// Sets the duration of the pattern implied by the notes. - pub fn duration(mut self, duration: MusicalTime) -> Self { - self.duration = duration; - self - } - /// Sets the sequence of [Note]s that determine the icon's appearance. - pub fn notes(mut self, notes: &'a [Note]) -> Self { - self.notes = notes; - self - } - /// Sets whether this widget is selected in the UI. - pub fn is_selected(mut self, is_selected: bool) -> Self { - self.is_selected = is_selected; - self - } -} -impl<'a> Displays for Icon<'a> { - fn ui(&mut self, ui: &mut Ui) -> Response { - let desired_size = ui.spacing().interact_size.y * eframe::egui::vec2(3.0, 3.0); - let (rect, response) = ui.allocate_exact_size(desired_size, eframe::egui::Sense::click()); - - let visuals = if ui.is_enabled() { - ui.ctx().style().visuals.widgets.active - } else { - ui.ctx().style().visuals.widgets.inactive - }; - - if self.is_selected { - ui.painter() - .rect(rect, visuals.rounding, visuals.bg_fill, visuals.fg_stroke); - } else { - ui.painter().rect( - rect, - visuals.rounding, - visuals.weak_bg_fill, - visuals.bg_stroke, - ); - } - let to_screen = RectTransform::from_to( - eframe::epaint::Rect::from_x_y_ranges( - MusicalTime::START.total_parts() as f32..=self.duration.total_parts() as f32, - 128.0..=0.0, - ), - rect, - ); - for note in self.notes { - let key = note.key as f32; - let p1 = to_screen * eframe::epaint::pos2(note.range.start.total_parts() as f32, key); - let mut p2 = to_screen * eframe::epaint::pos2(note.range.end.total_parts() as f32, key); - - // Even very short notes should be visible. - if p1.x == p2.x { - p2.x += 1.0; - } - ui.painter().line_segment([p1, p2], visuals.fg_stroke); - } - response - } -} - -/// Displays a simple representation of a [Pattern]. Intended to be a -/// drag-and-drop source. This is needed in the short term because egui doesn't -/// have an easy way to make a widget both clickable and a drag source. -#[derive(Debug, Default)] -pub struct DraggableIcon {} -impl DraggableIcon { - /// Creates a new [DraggableIcon]. - pub fn new() -> Self { - Default::default() - } -} -impl Displays for DraggableIcon { - fn ui(&mut self, ui: &mut Ui) -> Response { - let desired_size = ui.spacing().interact_size.y * eframe::egui::vec2(3.0, 1.0); - let (rect, response) = - ui.allocate_exact_size(desired_size, eframe::egui::Sense::click_and_drag()); - - let visuals = if ui.is_enabled() { - ui.ctx().style().visuals.widgets.active - } else { - ui.ctx().style().visuals.widgets.inactive - }; - - ui.painter().rect( - rect, - visuals.rounding, - visuals.weak_bg_fill, - visuals.bg_stroke, - ); - - response - } -} - -/// Displays a row of selectable icons, each with a drag source. -#[derive(Debug)] -pub struct Carousel<'a> { - pattern_uids: &'a [PatternUid], - uids_to_patterns: &'a HashMap, - selection_set: &'a mut SelectionSet, -} -impl<'a> Carousel<'a> { - /// Creates a new [Carousel]. - pub fn new( - pattern_uids: &'a [PatternUid], - uids_to_patterns: &'a HashMap, - selection_set: &'a mut SelectionSet, - ) -> Self { - Self { - pattern_uids, - uids_to_patterns, - selection_set, - } - } -} -impl<'a> Displays for Carousel<'a> { - fn ui(&mut self, ui: &mut Ui) -> Response { - ui.horizontal_top(|ui| { - let icon_width = ui.available_width() / self.pattern_uids.len() as f32; - ui.set_max_width(ui.available_width()); - ui.set_height(64.0); - self.pattern_uids.iter().for_each(|pattern_uid| { - ui.vertical(|ui| { - ui.set_max_width(icon_width); - if let Some(pattern) = self.uids_to_patterns.get(pattern_uid) { - if ui - .add(icon( - pattern.duration(), - pattern.notes(), - self.selection_set.contains(pattern_uid), - )) - .clicked() - { - self.selection_set.click(pattern_uid, false); - }; - } - let dd_id = EguiId::new("piano roll").with(pattern_uid); - DragDropManager::drag_source( - ui, - dd_id, - DragDropSource::Pattern(*pattern_uid), - |ui| { - ui.add(draggable_icon()); - }, - ); - }); - }); - }) - .response - } -} - -/// An egui widget that draws a grid in -/// [PianoRoll](crate::mini::piano_roll::PianoRoll)'s pattern-editing view. -#[derive(Debug, Default)] -pub struct Grid { - /// The extent of the [Pattern](crate::mini::piano_roll::Pattern) to be - /// edited. - duration: MusicalTime, -} -impl Grid { - fn duration(mut self, duration: MusicalTime) -> Self { - self.duration = duration; - self - } -} -impl Displays for Grid { - fn ui(&mut self, ui: &mut eframe::egui::Ui) -> eframe::egui::Response { - let desired_size = ui.available_size(); - let (rect, response) = ui.allocate_exact_size(desired_size, eframe::egui::Sense::hover()); - let to_screen = RectTransform::from_to( - eframe::epaint::Rect::from_x_y_ranges( - MusicalTime::START.total_parts() as f32..=self.duration.total_parts() as f32, - MIDI_NOTE_F32_RANGE, - ), - rect, - ); - let visuals = ui.ctx().style().visuals.widgets.noninteractive; - - let mut shapes = vec![Shape::Rect(RectShape::filled( - rect, - visuals.rounding, - visuals.bg_fill, - ))]; - - for part in 0..self.duration.total_parts() { - let x = part as f32; - let stroke = if part % 16 == 0 { - visuals.fg_stroke - } else { - visuals.bg_stroke - }; - shapes.push(Shape::LineSegment { - points: [to_screen * pos2(x, 0.0), to_screen * pos2(x, 127.0)], - stroke, - }); - } - for key in MidiNote::MIN as u8..MidiNote::MAX as u8 { - let left = to_screen * pos2(MusicalTime::START.total_parts() as f32, key as f32); - let right = to_screen * pos2(self.duration.total_parts() as f32, key as f32); - let stroke = if (key - MidiNote::C0 as u8) % 12 == 0 { - visuals.fg_stroke - } else { - visuals.bg_stroke - }; - shapes.push(Shape::LineSegment { - points: [left, right], - stroke, - }) - } - ui.painter().extend(shapes); - - response - } -} diff --git a/src/mini/widgets/placeholder.rs b/src/mini/widgets/placeholder.rs deleted file mode 100644 index 39aa35ee..00000000 --- a/src/mini/widgets/placeholder.rs +++ /dev/null @@ -1,72 +0,0 @@ -// Copyright (c) 2023 Mike Tsao. All rights reserved. - -use eframe::{ - egui::Sense, - emath, - epaint::{pos2, vec2, Color32, Pos2, Rect, Shape, Stroke}, -}; -use ensnare::traits::Displays; - -/// Wraps a [Wiggler] as a [Widget](eframe::egui::Widget). -pub fn wiggler() -> impl eframe::egui::Widget { - move |ui: &mut eframe::egui::Ui| Wiggler::new().ui(ui) -} - -/// A placeholder widget that fills available space with an animation. -#[derive(Debug, Default)] -pub struct Wiggler {} -impl Wiggler { - #[allow(missing_docs)] - pub fn new() -> Self { - Default::default() - } -} -impl Displays for Wiggler { - fn ui(&mut self, ui: &mut eframe::egui::Ui) -> eframe::egui::Response { - ui.ctx().request_repaint(); - - let color = if ui.visuals().dark_mode { - Color32::from_additive_luminance(196) - } else { - Color32::from_black_alpha(240) - }; - - let (response, painter) = - ui.allocate_painter(vec2(ui.available_width(), 64.0), Sense::click()); - - let time = ui.input(|i| i.time); - let to_screen = emath::RectTransform::from_to( - Rect::from_x_y_ranges(0.0..=1.0, -1.0..=1.0), - response.rect, - ); - - let mut shapes = vec![]; - - for &mode in &[2, 3, 5] { - let mode = mode as f64; - let n = 120; - let speed = 1.5; - - let points: Vec = (0..=n) - .map(|i| { - let t = i as f64 / (n as f64); - let amp = (time * speed * mode).sin() / mode; - let y = amp * (t * std::f64::consts::TAU / 2.0 * mode).sin(); - to_screen * pos2(t as f32, y as f32) - }) - .collect(); - - let thickness = 10.0 / mode as f32; - shapes.push(Shape::line(points, Stroke::new(thickness, color))); - } - - shapes.push(Shape::LineSegment { - points: [to_screen * pos2(0.0, 1.0), to_screen * pos2(1.0, 1.0)], - stroke: Stroke { width: 1.0, color }, - }); - - painter.extend(shapes); - - response - } -} diff --git a/src/mini/widgets/timeline.rs b/src/mini/widgets/timeline.rs deleted file mode 100644 index aedd761b..00000000 --- a/src/mini/widgets/timeline.rs +++ /dev/null @@ -1,437 +0,0 @@ -// Copyright (c) 2023 Mike Tsao. All rights reserved. - -use super::{control::atlas, controllers::es_sequencer}; -use crate::mini::{ - control_router::ControlRouter, ControlAtlas, DragDropEvent, DragDropManager, DragDropSource, - ESSequencer, TrackUid, -}; -use eframe::{ - egui::{self, vec2, Response, Ui}, - emath::{Align2, RectTransform}, - epaint::{pos2, FontId, Rect, RectShape, Shape}, -}; -use ensnare::prelude::*; -use ensnare::traits::{Displays, DisplaysInTimeline}; -use std::ops::Range; -use strum::EnumCount; -use strum_macros::{EnumCount as EnumCountMacro, FromRepr}; - -/// Wraps a [Timeline] as a [Widget](eframe::egui::Widget). Mutates the given view_range. -pub fn timeline<'a>( - track_uid: TrackUid, - sequencer: &'a mut ESSequencer, - control_atlas: &'a mut ControlAtlas, - control_router: &'a mut ControlRouter, - range: Range, - view_range: Range, - focused: FocusedComponent, -) -> impl eframe::egui::Widget + 'a { - move |ui: &mut eframe::egui::Ui| { - Timeline::new(track_uid, sequencer, control_atlas, control_router) - .range(range) - .view_range(view_range) - .focused(focused) - .ui(ui) - } -} - -/// Wraps a [Legend] as a [Widget](eframe::egui::Widget). Mutates the given view_range. -pub fn legend(view_range: &mut std::ops::Range) -> impl eframe::egui::Widget + '_ { - move |ui: &mut eframe::egui::Ui| Legend::new(view_range).ui(ui) -} - -/// Wraps a [Grid] as a [Widget](eframe::egui::Widget). -pub fn grid( - range: std::ops::Range, - view_range: std::ops::Range, -) -> impl eframe::egui::Widget { - move |ui: &mut eframe::egui::Ui| Grid::default().range(range).view_range(view_range).ui(ui) -} - -/// Wraps an [EmptySpace] as a [Widget](eframe::egui::Widget). -pub fn empty_space( - range: std::ops::Range, - view_range: std::ops::Range, -) -> impl eframe::egui::Widget { - move |ui: &mut eframe::egui::Ui| EmptySpace::new().range(range).view_range(view_range).ui(ui) -} - -/// An egui widget that draws a legend on the horizontal axis of the timeline -/// view. -#[derive(Debug)] -pub struct Legend<'a> { - /// The GUI view's time range. - view_range: &'a mut Range, -} -impl<'a> Legend<'a> { - fn new(view_range: &'a mut std::ops::Range) -> Self { - Self { view_range } - } - - fn steps(view_range: &std::ops::Range) -> std::iter::StepBy> { - let beat_count = view_range.end.total_beats() - view_range.start.total_beats(); - let step = (beat_count as f32).log10().round() as usize; - (view_range.start.total_beats()..view_range.end.total_beats()).step_by(step * 2) - } -} -impl<'a> Displays for Legend<'a> { - fn ui(&mut self, ui: &mut eframe::egui::Ui) -> eframe::egui::Response { - let desired_size = vec2(ui.available_width(), ui.spacing().interact_size.y); - let (rect, response) = ui.allocate_exact_size(desired_size, eframe::egui::Sense::click()); - let to_screen = RectTransform::from_to( - eframe::epaint::Rect::from_x_y_ranges( - self.view_range.start.total_beats() as f32 - ..=self.view_range.end.total_beats() as f32, - rect.top()..=rect.bottom(), - ), - rect, - ); - - let font_id = FontId::proportional(12.0); - for beat in Self::steps(self.view_range) { - let beat_plus_one = beat + 1; - let pos = to_screen * pos2(beat as f32, rect.top()); - ui.painter().text( - pos, - Align2::CENTER_TOP, - format!("{beat_plus_one}"), - font_id.clone(), - ui.style().noninteractive().text_color(), - ); - } - ui.painter().line_segment( - [rect.left_bottom(), rect.right_bottom()], - ui.style().noninteractive().fg_stroke, - ); - - response.context_menu(|ui| { - if ui.button("Start x2").clicked() { - self.view_range.start = self.view_range.start * 2; - ui.close_menu(); - } - if ui.button("Start x0.5").clicked() { - self.view_range.start = self.view_range.start / 2; - ui.close_menu(); - } - if ui.button("Start +4").clicked() { - self.view_range.start += MusicalTime::new_with_beats(4); - ui.close_menu(); - } - }) - } -} - -/// An egui widget that draws a grid in the timeline view. -#[derive(Debug, Default)] -pub struct Grid { - /// The timeline's full time range. - range: Range, - - /// The GUI view's time range. - view_range: Range, -} -impl Grid { - fn range(mut self, range: std::ops::Range) -> Self { - self.range = range.clone(); - self - } - fn view_range(mut self, view_range: std::ops::Range) -> Self { - self.set_view_range(&view_range); - self - } -} -impl DisplaysInTimeline for Grid { - fn set_view_range(&mut self, view_range: &std::ops::Range) { - self.view_range = view_range.clone(); - } -} -impl Displays for Grid { - fn ui(&mut self, ui: &mut eframe::egui::Ui) -> eframe::egui::Response { - let desired_size = vec2(ui.available_width(), 64.0); - let (rect, response) = ui.allocate_exact_size(desired_size, eframe::egui::Sense::hover()); - let to_screen = RectTransform::from_to( - eframe::epaint::Rect::from_x_y_ranges( - self.view_range.start.total_beats() as f32 - ..=self.view_range.end.total_beats() as f32, - 0.0..=1.0, - ), - rect, - ); - let visuals = ui.ctx().style().visuals.widgets.noninteractive; - - let mut shapes = vec![Shape::Rect(RectShape::filled( - rect, - visuals.rounding, - visuals.bg_fill, - ))]; - - for x in Legend::steps(&self.view_range) { - shapes.push(Shape::LineSegment { - points: [ - to_screen * pos2(x as f32, 0.0), - to_screen * pos2(x as f32, 1.0), - ], - stroke: visuals.fg_stroke, - }); - } - ui.painter().extend(shapes); - - response - } -} - -/// An egui widget that displays nothing in the timeline view. This is useful as -/// a DnD target. -#[derive(Debug, Default)] -pub struct EmptySpace { - view_range: Range, - range: Range, -} -#[allow(missing_docs)] -impl EmptySpace { - pub fn new() -> Self { - Default::default() - } - pub fn view_range(mut self, view_range: Range) -> Self { - self.set_view_range(&view_range); - self - } - pub fn range(mut self, range: Range) -> Self { - self.range = range; - self - } -} -impl DisplaysInTimeline for EmptySpace { - fn set_view_range(&mut self, view_range: &std::ops::Range) { - self.view_range = view_range.clone(); - } -} -impl Displays for EmptySpace { - fn ui(&mut self, ui: &mut Ui) -> Response { - ui.set_min_height(ui.available_height()); - - let full_range_beats = - (self.view_range.end.total_beats() - self.view_range.start.total_beats() - 1) as f32; - let range_beats = - (self.range.end.total_beats() - self.range.start.total_beats() - 1) as f32; - let range_as_pct = range_beats / full_range_beats; - let desired_size = vec2(ui.available_width() * range_as_pct, ui.available_height()); - let (rect, response) = - ui.allocate_exact_size(desired_size, eframe::egui::Sense::click_and_drag()); - - let visuals = if ui.is_enabled() { - ui.ctx().style().visuals.widgets.active - } else { - ui.ctx().style().visuals.widgets.noninteractive - }; - - // skip interaction - ui.painter() - .rect(rect, visuals.rounding, visuals.bg_fill, visuals.bg_stroke); - ui.painter() - .line_segment([rect.right_top(), rect.left_bottom()], visuals.fg_stroke); - ui.painter() - .line_segment([rect.left_top(), rect.right_bottom()], visuals.fg_stroke); - response - } -} - -#[derive(Debug, Default, Copy, Clone, EnumCountMacro, FromRepr)] -#[allow(missing_docs)] -pub enum FocusedComponent { - #[default] - Sequencer, - ControlAtlas, -} -impl FocusedComponent { - /// Returns the next enum, wrapping to the start if necessary. - pub fn next(&self) -> Self { - FocusedComponent::from_repr((*self as usize + 1) % FocusedComponent::COUNT).unwrap() - } -} - -/// Draws the content area of a Timeline, which is the view of a [Track]. -#[derive(Debug)] -struct Timeline<'a> { - track_uid: TrackUid, - - /// The full timespan of the project. - range: Range, - - /// The part of the timeline that is viewable. - view_range: Range, - - /// Which component is currently enabled, - focused: FocusedComponent, - - control_atlas: &'a mut ControlAtlas, - control_router: &'a mut ControlRouter, - sequencer: &'a mut ESSequencer, -} -impl<'a> DisplaysInTimeline for Timeline<'a> { - fn set_view_range(&mut self, view_range: &std::ops::Range) { - self.view_range = view_range.clone(); - } -} -impl<'a> Displays for Timeline<'a> { - fn ui(&mut self, ui: &mut egui::Ui) -> egui::Response { - let mut from_screen = RectTransform::identity(Rect::NOTHING); - let can_accept = self.check_drag_source(); - let response = DragDropManager::drop_target(ui, can_accept, |ui| { - let desired_size = vec2(ui.available_width(), 64.0); - let (_id, rect) = ui.allocate_space(desired_size); - from_screen = RectTransform::from_to( - rect, - Rect::from_x_y_ranges( - self.view_range.start.total_units() as f32 - ..=self.view_range.end.total_units() as f32, - rect.top()..=rect.bottom(), - ), - ); - - // What's going on here? To correctly capture Sense events, we - // need to draw only the UI that we consider active, or enabled, - // or focused, because egui does not seem to like widgets being - // drawn on top of each other. So we first draw the non-focused - // UI components, but wrapped in add_enabled_ui(false) so that - // egui won't try to sense them. Then we draw the one focused - // component normally. - ui.add_enabled_ui(false, |ui| { - self.ui_not_focused(ui, rect, self.focused); - }); - self.ui_focused(ui, rect, self.focused) - }) - .response; - if DragDropManager::is_dropped(ui, &response) { - if let Some(pointer_pos) = ui.ctx().pointer_interact_pos() { - let time_pos = from_screen * pointer_pos; - let time = MusicalTime::new_with_units(time_pos.x as usize); - if let Some(source) = DragDropManager::source() { - let event = match source { - DragDropSource::NewDevice(key) => { - Some(DragDropEvent::AddDeviceToTrack(key, self.track_uid)) - } - DragDropSource::Pattern(pattern_uid) => Some( - DragDropEvent::AddPatternToTrack(pattern_uid, self.track_uid, time), - ), - DragDropSource::ControlTrip(_uid) => None, - }; - if let Some(event) = event { - DragDropManager::enqueue_event(event); - } - } - } else { - eprintln!("Dropped on timeline at unknown position"); - } - } - response - } -} -impl<'a> Timeline<'a> { - pub fn new( - track_uid: TrackUid, - sequencer: &'a mut ESSequencer, - control_atlas: &'a mut ControlAtlas, - control_router: &'a mut ControlRouter, - ) -> Self { - Self { - track_uid, - range: Default::default(), - view_range: Default::default(), - focused: Default::default(), - sequencer, - control_atlas, - control_router, - } - } - fn range(mut self, range: Range) -> Self { - self.range = range; - self - } - - fn view_range(mut self, view_range: Range) -> Self { - self.set_view_range(&view_range); - self - } - - fn focused(mut self, component: FocusedComponent) -> Self { - self.focused = component; - self - } - - // Draws the Timeline component that is currently focused. - fn ui_focused( - &mut self, - ui: &mut egui::Ui, - rect: Rect, - component: FocusedComponent, - ) -> egui::Response { - match component { - FocusedComponent::ControlAtlas => { - ui.allocate_ui_at_rect(rect, |ui| { - ui.add(atlas( - self.control_atlas, - self.control_router, - self.view_range.clone(), - )) - }) - .inner - } - FocusedComponent::Sequencer => { - ui.allocate_ui_at_rect(rect, |ui| { - ui.add(es_sequencer(self.sequencer, self.view_range.clone())) - }) - .inner - } - } - } - - // Draws the Timeline components that are not currently focused. It's up to - // the caller to wrap in ui.add_enabled_ui(). - fn ui_not_focused( - &mut self, - ui: &mut egui::Ui, - rect: Rect, - which: FocusedComponent, - ) -> egui::Response { - // The Grid is always disabled and drawn first. - let mut response = ui - .allocate_ui_at_rect(rect, |ui| { - ui.add(grid(self.range.clone(), self.view_range.clone())) - }) - .inner; - - // Now go through and draw the components that are *not* enabled. - if !matches!(which, FocusedComponent::ControlAtlas) { - response |= ui - .allocate_ui_at_rect(rect, |ui| { - ui.add(atlas( - self.control_atlas, - self.control_router, - self.view_range.clone(), - )) - }) - .inner; - } - if !matches!(which, FocusedComponent::Sequencer) { - response |= ui - .allocate_ui_at_rect(rect, |ui| { - ui.add(es_sequencer(self.sequencer, self.view_range.clone())) - }) - .inner; - } - response - } - - // Looks at what's being dragged, if anything, and updates any state needed - // to handle it. Returns whether we are interested in this drag source. - fn check_drag_source(&mut self) -> bool { - if let Some(source) = DragDropManager::source() { - if matches!(source, DragDropSource::Pattern(_)) { - self.focused = FocusedComponent::Sequencer; - return true; - } - } - false - } -} diff --git a/src/mini/widgets/track.rs b/src/mini/widgets/track.rs deleted file mode 100644 index a4a3735f..00000000 --- a/src/mini/widgets/track.rs +++ /dev/null @@ -1,63 +0,0 @@ -// Copyright (c) 2023 Mike Tsao. All rights reserved. - -use eframe::{ - egui::{Frame, Margin, Sense, TextFormat}, - emath::Align, - epaint::{text::LayoutJob, vec2, Color32, FontId, Shape, Stroke, TextShape}, -}; -use ensnare::traits::Displays; -use std::f32::consts::PI; - -/// Wraps a [TitleBar] as a [Widget](eframe::egui::Widget). -pub fn title_bar(title: &mut String) -> impl eframe::egui::Widget + '_ { - move |ui: &mut eframe::egui::Ui| TitleBar::new(title).ui(ui) -} - -/// An egui widget that draws a [Track]'s sideways title bar. -#[derive(Debug)] -pub struct TitleBar<'a> { - title: &'a mut String, -} -impl<'a> Displays for TitleBar<'a> { - fn ui(&mut self, ui: &mut eframe::egui::Ui) -> eframe::egui::Response { - let available_size = vec2(16.0, ui.available_height()); - ui.set_min_size(available_size); - Frame::default() - .outer_margin(Margin::same(1.0)) - .inner_margin(Margin::same(0.0)) - .fill(Color32::DARK_GRAY) - .show(ui, |ui| { - ui.allocate_ui(available_size, |ui| { - let mut job = LayoutJob::default(); - job.append( - self.title.as_str(), - 1.0, - TextFormat { - color: Color32::YELLOW, - font_id: FontId::proportional(12.0), - valign: Align::Center, - ..Default::default() - }, - ); - let galley = ui.ctx().fonts(|f| f.layout_job(job)); - let (response, painter) = ui.allocate_painter(available_size, Sense::click()); - let t = Shape::Text(TextShape { - pos: response.rect.left_bottom(), - galley, - underline: Stroke::default(), - override_text_color: None, - angle: 2.0 * PI * 0.75, - }); - painter.add(t); - response - }) - .inner - }) - .inner - } -} -impl<'a> TitleBar<'a> { - fn new(title: &'a mut String) -> Self { - Self { title } - } -} diff --git a/src/panels/audio_panel.rs b/src/panels/audio_panel.rs index 1f66cbcb..1a4a082e 100644 --- a/src/panels/audio_panel.rs +++ b/src/panels/audio_panel.rs @@ -2,9 +2,10 @@ use crossbeam_channel::{Receiver, Sender}; use eframe::egui::{CollapsingHeader, Ui}; -use ensnare::prelude::*; -use ensnare::traits::{Displays, HasSettings}; -use groove_audio::{AudioInterfaceEvent, AudioInterfaceInput, AudioQueue, AudioStreamService}; +use ensnare_core::core::AudioQueue; +use ensnare_core::prelude::*; +use ensnare_core::traits::{Displays, HasSettings}; +use groove_audio::{AudioInterfaceEvent, AudioInterfaceInput, AudioStreamService}; use serde::{Deserialize, Serialize}; use std::{ fmt::Debug, diff --git a/src/panels/control_panel.rs b/src/panels/control_panel.rs index 168826b6..e7ff408c 100644 --- a/src/panels/control_panel.rs +++ b/src/panels/control_panel.rs @@ -1,8 +1,7 @@ // Copyright (c) 2023 Mike Tsao. All rights reserved. -use crate::mini::{widgets::core::transport, Transport}; use eframe::egui::Ui; -use ensnare::traits::Displays; +use ensnare_core::{time::Transport, traits::Displays, widgets::core::transport}; use std::path::PathBuf; /// Actions the user might take via the control panel. diff --git a/src/panels/legacy/audio_panel.rs b/src/panels/legacy/audio_panel.rs index 32e8be75..61475a34 100644 --- a/src/panels/legacy/audio_panel.rs +++ b/src/panels/legacy/audio_panel.rs @@ -3,7 +3,7 @@ use crate::panels::{audio_panel::AudioSettings, AudioPanelEvent}; use crossbeam_channel::{Receiver, Sender}; use eframe::egui::{CollapsingHeader, Ui}; -use ensnare::core::StereoSample; +use ensnare_core::core::StereoSample; use groove_audio::{AudioInterfaceEvent, AudioInterfaceInput, AudioQueue, AudioStreamService}; use groove_core::{traits::gui::Displays, SAMPLE_BUFFER_SIZE}; use std::{ diff --git a/src/panels/legacy/thing_browser.rs b/src/panels/legacy/thing_browser.rs index 4334de1c..3f67894d 100644 --- a/src/panels/legacy/thing_browser.rs +++ b/src/panels/legacy/thing_browser.rs @@ -78,8 +78,7 @@ impl EntityBrowser { } /// [EntityBrowser] shows assets in a tree view. -#[derive(Clone, Debug, Default)] -#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +#[derive(Clone, Debug, Default, Deserialize, Serialize)] pub struct EntityBrowserNode { depth: usize, thing_type: EntityType, diff --git a/src/panels/midi_panel.rs b/src/panels/midi_panel.rs index eda7460e..d5540ab7 100644 --- a/src/panels/midi_panel.rs +++ b/src/panels/midi_panel.rs @@ -2,7 +2,7 @@ use crossbeam_channel::{Receiver, Sender}; use eframe::egui::{CollapsingHeader, ComboBox, Ui}; -use ensnare::{midi::prelude::*, traits::prelude::*}; +use ensnare_core::{midi::prelude::*, traits::prelude::*}; use ensnare_midi_interface::{ MidiInterfaceEvent, MidiInterfaceInput, MidiInterfaceService, MidiPortDescriptor, }; diff --git a/src/panels/orchestrator_panel.rs b/src/panels/orchestrator_panel.rs index 33b65bf7..67e67a34 100644 --- a/src/panels/orchestrator_panel.rs +++ b/src/panels/orchestrator_panel.rs @@ -1,13 +1,18 @@ -use crate::mini::{ - ChannelPair, EntityFactory, Key, Orchestrator, OrchestratorAction, OrchestratorBuilder, - SelectionSet, TrackUid, -}; +// Copyright (c) 2023 Mike Tsao. All rights reserved. + use anyhow::{anyhow, Result}; use crossbeam_channel::{Receiver, Sender}; use eframe::egui::Ui; -use ensnare::midi::{MidiChannel, MidiMessage}; -use ensnare::prelude::*; -use ensnare::traits::{Configurable, Controls, Displays, HandlesMidi, Serializable}; +use ensnare_core::{ + core::ChannelPair, + entities::EntityFactory, + midi::{MidiChannel, MidiMessage}, + orchestration::{Orchestrator, OrchestratorAction, OrchestratorBuilder}, + prelude::*, + selection_set::SelectionSet, + track::TrackUid, + traits::prelude::*, +}; use std::{ path::PathBuf, sync::{Arc, Mutex, MutexGuard}, @@ -42,7 +47,7 @@ pub enum OrchestratorInput { /// Delete the selected arranged patterns. TrackPatternRemoveSelected, /// Add a new entity to the selected track. - TrackAddEntity(Key), + TrackAddEntity(EntityKey), /// Sets the tempo. Tempo(Tempo), diff --git a/src/panels/palette_panel.rs b/src/panels/palette_panel.rs index debd6ec8..3eee1993 100644 --- a/src/panels/palette_panel.rs +++ b/src/panels/palette_panel.rs @@ -1,15 +1,17 @@ -use eframe::egui::{Id as EguiId, Ui}; -use ensnare::traits::Displays; +// Copyright (c) 2023 Mike Tsao. All rights reserved. -use crate::mini::{ - {DragDropManager, DragDropSource}, {EntityFactory, Key}, +use eframe::egui::{Id as EguiId, Ui}; +use ensnare_core::{ + drag_drop::{DragDropManager, DragDropSource}, + prelude::*, + traits::prelude::*, }; /// Actions that [PalettePanel] can generate. #[derive(Debug)] pub enum PaletteAction { /// Requests a new entity of type [Key]. - NewEntity(Key), + NewEntity(EntityKey), } /// A tree view of devices that can be placed in tracks. diff --git a/tests/automation.rs b/tests/automation.rs deleted file mode 100644 index 83fab7d9..00000000 --- a/tests/automation.rs +++ /dev/null @@ -1,169 +0,0 @@ -// Copyright (c) 2023 Mike Tsao. All rights reserved. - -use ensnare::{prelude::*, traits::prelude::*}; -use groove::{ - mini::{ - register_factory_entities, ControlStepBuilder, ControlTripBuilder, ControlTripPath, Key, - OrchestratorBuilder, PatternBuilder, - }, - EntityFactory, -}; -use groove_core::generators::Waveform; -use groove_entities::controllers::{LfoController, LfoControllerParams}; -use std::path::PathBuf; - -// Demonstrates the control (automation) system. -#[test] -fn demo_automation() { - let mut orchestrator = OrchestratorBuilder::default() - .title(Some("Automation".to_string())) - .build() - .unwrap(); - orchestrator.update_tempo(Tempo(128.0)); - - let factory = register_factory_entities(EntityFactory::default()); - - // Add the lead pattern to the PianoRoll. - let scale_pattern_uid = { - let mut piano_roll = orchestrator.piano_roll_mut(); - piano_roll.insert( - PatternBuilder::default() - .note_sequence( - vec![ - 60, 255, 62, 255, 64, 255, 65, 255, 67, 255, 69, 255, 71, 255, 72, 255, - ], - None, - ) - .build() - .unwrap(), - ) - }; - - // Arrange the lead pattern in a new MIDI track's Sequencer. - let track_uid = orchestrator.new_midi_track().unwrap(); - let track = orchestrator.get_track_mut(&track_uid).unwrap(); - let _ = track.sequencer_mut().arrange_pattern(&scale_pattern_uid, 0); - - // Add a synth to play the pattern. - let synth_uid = track - .append_entity(factory.new_entity(&Key::from("toy-synth")).unwrap()) - .unwrap(); - - // Add an LFO that will control a synth parameter. - let lfo_uid = { - let mut lfo = Box::new(LfoController::new_with(&LfoControllerParams { - frequency: FrequencyHz(2.0), - waveform: Waveform::Sine, - })); - lfo.set_uid(factory.mint_uid()); - track.append_entity(lfo).unwrap() - }; - - let pan_param_index = { - // This would have been a little easier if Orchestrator or Track had a - // way to query param names, but I'm not sure how often that will - // happen. - factory - .new_entity(&Key::from("toy-synth")) - .unwrap() - .as_controllable() - .unwrap() - .control_index_for_name("dca-pan") - .unwrap() - }; - - // Link the LFO to the synth's pan. - track - .control_router_mut() - .link_control(lfo_uid, synth_uid, pan_param_index); - - // https://doc.rust-lang.org/std/path/struct.PathBuf.html example - let output_path: PathBuf = [env!("CARGO_TARGET_TMPDIR"), "automation.wav"] - .iter() - .collect(); - assert!(orchestrator.write_to_file(&output_path).is_ok()); -} - -#[test] -fn demo_control_trips() { - let mut orchestrator = OrchestratorBuilder::default() - .title(Some("Automation".to_string())) - .build() - .unwrap(); - orchestrator.update_tempo(Tempo(128.0)); - - let factory = register_factory_entities(EntityFactory::default()); - - // Add the lead pattern to the PianoRoll. - let scale_pattern_uid = { - let mut piano_roll = orchestrator.piano_roll_mut(); - piano_roll.insert( - PatternBuilder::default() - .note_sequence( - vec![ - 60, 255, 62, 255, 64, 255, 65, 255, 67, 255, 69, 255, 71, 255, 72, 255, - ], - None, - ) - .build() - .unwrap(), - ) - }; - - // Arrange the lead pattern in a new MIDI track's Sequencer. - let track_uid = orchestrator.new_midi_track().unwrap(); - let track = orchestrator.get_track_mut(&track_uid).unwrap(); - let _ = track.sequencer_mut().arrange_pattern(&scale_pattern_uid, 0); - - // Add a synth to play the pattern. - let synth_uid = track - .append_entity(factory.new_entity(&Key::from("toy-synth")).unwrap()) - .unwrap(); - - // Figure how out to identify the parameter we want to control. - let pan_param_index = { - factory - .new_entity(&Key::from("toy-synth")) - .unwrap() - .as_controllable() - .unwrap() - .control_index_for_name("dca-pan") - .unwrap() - }; - - // Add a ControlTrip that ramps from zero to max over the desired amount of time. - let control_atlas = track.control_atlas_mut(); - let mut trip = ControlTripBuilder::default() - .step( - ControlStepBuilder::default() - .value(ControlValue::MIN) - .time(MusicalTime::START) - .path(ControlTripPath::Linear) - .build() - .unwrap(), - ) - .step( - ControlStepBuilder::default() - .value(ControlValue::MAX) - .time(MusicalTime::new_with_beats(4)) - .path(ControlTripPath::Flat) - .build() - .unwrap(), - ) - .build() - .unwrap(); - let trip_uid = factory.mint_uid(); - trip.set_uid(trip_uid); - control_atlas.add_trip(trip); - - // Hook up that ControlTrip to the pan parameter. - track - .control_router_mut() - .link_control(trip_uid, synth_uid, pan_param_index); - - // https://doc.rust-lang.org/std/path/struct.PathBuf.html example - let output_path: PathBuf = [env!("CARGO_TARGET_TMPDIR"), "control-trips.wav"] - .iter() - .collect(); - assert!(orchestrator.write_to_file(&output_path).is_ok()); -} diff --git a/tests/aux_bus.rs b/tests/aux_bus.rs deleted file mode 100644 index 114e38ab..00000000 --- a/tests/aux_bus.rs +++ /dev/null @@ -1,100 +0,0 @@ -// Copyright (c) 2023 Mike Tsao. All rights reserved. - -use ensnare::core::Normal; -use groove::{ - mini::{register_factory_entities, Key, OrchestratorBuilder, PatternBuilder}, - EntityFactory, -}; -use std::path::PathBuf; - -// Demonstrates use of aux buses. -#[test] -fn aux_bus() { - let mut orchestrator = OrchestratorBuilder::default() - .title(Some("Auxiliary Buses".to_string())) - .build() - .unwrap(); - let factory = register_factory_entities(EntityFactory::default()); - - let track_uid_1 = orchestrator.new_midi_track().unwrap(); - let track_uid_2 = orchestrator.new_midi_track().unwrap(); - let aux_track_uid = orchestrator.new_aux_track().unwrap(); - - let synth_pattern_uid_1 = { - let mut piano_roll = orchestrator.piano_roll_mut(); - piano_roll.insert( - PatternBuilder::default() - .note_sequence( - vec![ - 60, 255, 62, 255, 64, 255, 65, 255, 67, 255, 69, 255, 71, 255, 72, 255, - ], - None, - ) - .build() - .unwrap(), - ) - }; - - let synth_pattern_uid_2 = { - let mut piano_roll = orchestrator.piano_roll_mut(); - piano_roll.insert( - PatternBuilder::default() - .note_sequence( - vec![ - 84, 255, 83, 255, 81, 255, 79, 255, 77, 255, 76, 255, 74, 255, 72, 255, - ], - None, - ) - .build() - .unwrap(), - ) - }; - - let _synth_uid_1 = { - let track = orchestrator.get_track_mut(&track_uid_1).unwrap(); - let _ = track - .sequencer_mut() - .arrange_pattern(&synth_pattern_uid_1, 0); - - // Even though we want the effect to be placed after the instrument in - // the audio chain, we can add the effect before we add the instrument. - // This is because the processing order is always controllers, - // instruments, effects. - track - .append_entity(factory.new_entity(&Key::from("gain")).unwrap()) - .unwrap(); - track - .append_entity(factory.new_entity(&Key::from("welsh-synth")).unwrap()) - .unwrap() - }; - let _synth_uid_2 = { - let track = orchestrator.get_track_mut(&track_uid_2).unwrap(); - let _ = track - .sequencer_mut() - .arrange_pattern(&synth_pattern_uid_2, 0); - track - .append_entity(factory.new_entity(&Key::from("gain")).unwrap()) - .unwrap(); - track - .append_entity(factory.new_entity(&Key::from("toy-synth")).unwrap()) - .unwrap() - }; - let _effect_uid_1 = { - let track = orchestrator.get_track_mut(&aux_track_uid).unwrap(); - track - .append_entity(factory.new_entity(&Key::from("gain")).unwrap()) - .unwrap(); - track - .append_entity(factory.new_entity(&Key::from("reverb")).unwrap()) - .unwrap() - }; - - let _ = orchestrator.send_to_aux(track_uid_1, aux_track_uid, Normal::from(1.0)); - let _ = orchestrator.send_to_aux(track_uid_2, aux_track_uid, Normal::from(1.0)); - - // https://doc.rust-lang.org/std/path/struct.PathBuf.html example - let output_path: PathBuf = [env!("CARGO_TARGET_TMPDIR"), "aux-bus.wav"] - .iter() - .collect(); - assert!(orchestrator.write_to_file(&output_path).is_ok()); -} diff --git a/tests/entity_validator.rs b/tests/entity_validator.rs deleted file mode 100644 index ea5ae415..00000000 --- a/tests/entity_validator.rs +++ /dev/null @@ -1,132 +0,0 @@ -// Copyright (c) 2023 Mike Tsao. All rights reserved. - -use ensnare::{prelude::*, traits::prelude::*}; -use groove::{ - mini::{register_factory_entities, Key}, - EntityFactory, -}; - -#[test] -fn entity_validator_production_entities() { - if EntityFactory::initialize(register_factory_entities(EntityFactory::default())).is_err() { - panic!("Couldn't set EntityFactory once_cell"); - } - validate_factory_entities(); -} - -fn validate_factory_entities() { - for key in EntityFactory::global().keys() { - if let Some(mut entity) = EntityFactory::global().new_entity(key) { - validate_entity(key, &mut entity); - } else { - panic!("Couldn't create entity with {key}, but EntityFactory said it existed!"); - } - } -} - -fn validate_entity(key: &Key, entity: &mut Box) { - assert_ne!(entity.uid(), Uid(0), "New entity should have a nonzero Uid"); - assert!( - entity.uid().0 > EntityFactory::MAX_RESERVED_UID, - "New entity should have a Uid above {}, but the one for {key} was {}", - EntityFactory::MAX_RESERVED_UID, - entity.uid() - ); - validate_configurable(key, entity); - validate_entity_type(key, entity); -} - -fn validate_configurable(key: &Key, entity: &mut Box) { - const TEST_SAMPLE_RATE: SampleRate = SampleRate(1111111); - entity.update_tempo(Tempo(1234.5678)); - entity.update_time_signature(TimeSignature::new_with(127, 128).unwrap()); - entity.update_sample_rate(TEST_SAMPLE_RATE); - - // This caused lots of things to fail and has me rethinking why Configurable - // needed sample_rate() as such a widespread trait method. TODO - if false { - assert!( - entity.sample_rate().0 > 0, - "Entity {key}'s default sample rate should be nonzero" - ); - assert_eq!( - entity.sample_rate(), - SampleRate::DEFAULT, - "Entity {key}'s default sample rate should equal the default of {}", - SampleRate::DEFAULT_SAMPLE_RATE - ); - entity.update_sample_rate(TEST_SAMPLE_RATE); - assert_eq!( - entity.sample_rate(), - TEST_SAMPLE_RATE, - "Entity {key}'s sample rate should change once set" - ); - } -} - -fn validate_entity_type(key: &Key, entity: &mut Box) { - let mut is_something = false; - if let Some(e) = entity.as_controller_mut() { - is_something = true; - validate_controller(e); - validate_extreme_tempo_and_time_signature(key, e); - } - if let Some(e) = entity.as_instrument_mut() { - is_something = true; - validate_instrument(e); - validate_extreme_sample_rates(key, entity); - } - if let Some(e) = entity.as_effect_mut() { - is_something = true; - validate_effect(e); - validate_extreme_sample_rates(key, entity); - } - assert!( - is_something, - "Entity {key} is neither a controller, nor an instrument, nor an effect!" - ); -} - -fn validate_extreme_sample_rates(key: &Key, entity: &mut Box) { - assert!(entity.as_instrument().is_some() || entity.as_effect().is_some()); - - entity.update_sample_rate(SampleRate(1)); - exercise_instrument_or_effect(key, entity); - entity.update_sample_rate(SampleRate(7)); - exercise_instrument_or_effect(key, entity); - entity.update_sample_rate(SampleRate(441)); - exercise_instrument_or_effect(key, entity); - entity.update_sample_rate(SampleRate(1024 * 1024)); - exercise_instrument_or_effect(key, entity); - entity.update_sample_rate(SampleRate(1024 * 1024 * 1024)); - exercise_instrument_or_effect(key, entity); -} - -// This doesn't assert anything. We are looking to make sure the entity doesn't -// blow up with weird sample rates. -fn exercise_instrument_or_effect(_key: &Key, entity: &mut Box) { - let mut buffer = [StereoSample::SILENCE; 64]; - if let Some(e) = entity.as_instrument_mut() { - e.generate_batch_values(&mut buffer); - buffer.iter_mut().for_each(|s| { - e.tick(1); - *s = e.value(); - }); - } - if let Some(e) = entity.as_effect_mut() { - buffer.iter_mut().for_each(|s| *s = e.transform_audio(*s)); - } -} - -fn validate_extreme_tempo_and_time_signature(_key: &Key, _e: &mut dyn IsController) {} - -fn validate_effect(_e: &mut dyn IsEffect) {} - -fn validate_instrument(_e: &mut dyn IsInstrument) {} - -fn validate_controller(e: &mut dyn IsController) { - assert!( - !e.is_performing(), - "A new Controller should not be performing" - ); -} diff --git a/tests/integration.rs b/tests/integration.rs deleted file mode 100644 index bab6e13b..00000000 --- a/tests/integration.rs +++ /dev/null @@ -1,107 +0,0 @@ -// Copyright (c) 2023 Mike Tsao. All rights reserved. - -// use ensnare::prelude::*; -// use groove_core::{util::tests::TestOnlyPaths, SAMPLE_BUFFER_SIZE}; -// use groove_orchestration::helpers::IOHelper; -// use groove_utils::{PathType, Paths}; -// use std::{fs::File, io::prelude::*, path::Path, time::Instant}; - -#[cfg(obsolete)] -#[test] -fn project_loads_and_parses() { - let mut paths = Paths::default(); - paths.push_hive(&Paths::hive(PathType::Test)); - - let path = Path::new("kitchen-sink.json"); - let json = paths - .search_and_read_to_string(path) - .unwrap_or_else(|err| panic!("loading JSON failed: {:?}", err)); - let song_settings = SongSettings::new_from_json5(json.as_str()) - .unwrap_or_else(|err| panic!("parsing settings for {} failed: {:?}", path.display(), err)); - let mut orchestrator = song_settings - .instantiate(&paths, false) - .unwrap_or_else(|err| panic!("instantiation failed: {:?}", err)); - orchestrator.update_sample_rate(SampleRate::DEFAULT); - let mut sample_buffer = [StereoSample::SILENCE; SAMPLE_BUFFER_SIZE]; - if let Ok(samples) = orchestrator.run(&mut sample_buffer) { - assert!( - !samples.is_empty(), - "Orchestrator reported successful performance, but performance is empty." - ); - - assert!( - samples - .iter() - .any(|sample| { *sample != StereoSample::SILENCE }), - "Performance contains only silence." - ); - } else { - panic!("run failed") - } -} - -#[cfg(obsolete)] -#[test] -#[ignore = "orchestrator - control_message_for_index is incomplete. re-enable when macroized"] -fn spit_out_perf_data() { - let mut paths = Paths::default(); - paths.push_hive(&Paths::hive(PathType::Test)); - - let path = Path::new("perf-1.json5"); - let contents = paths - .search_and_read_to_string(path) - .unwrap_or_else(|err| panic!("loading project failed: {:?}", err)); - let song_settings = SongSettings::new_from_json5(contents.as_str()) - .unwrap_or_else(|err| panic!("parsing settings for {} failed: {:?}", path.display(), err)); - let mut orchestrator = song_settings - .instantiate(&paths, false) - .unwrap_or_else(|err| panic!("instantiation failed: {:?}", err)); - - let start_instant = Instant::now(); - let mut samples = [StereoSample::SILENCE; SAMPLE_BUFFER_SIZE]; - let performance = orchestrator - .run_performance(&mut samples, false) - .unwrap_or_else(|err| panic!("performance failed: {:?}", err)); - let elapsed = start_instant.elapsed(); - let frame_count = performance.worker.len(); - - let mut out_path = TestOnlyPaths::writable_out_path(); - out_path.push("perf-output.txt"); - let mut file = File::create(out_path).unwrap(); - let output = format!( - "Elapsed : {:0.3}s\n\ -Frames : {}\n\ -Frames/msec: {:.2?} (goal >{:.2?})\n\ -usec/frame : {:.2?} (goal <{:.2?})", - elapsed.as_secs_f32(), - frame_count, - frame_count as f32 / start_instant.elapsed().as_millis() as f32, - performance.sample_rate.value() as f32 / 1000.0, - start_instant.elapsed().as_micros() as f32 / frame_count as f32, - 1000000.0 / performance.sample_rate.value() as f32 - ); - let _ = file.write(output.as_bytes()); - - let mut path = TestOnlyPaths::data_path(); - path.push("perf-1.wav"); - assert!(IOHelper::send_performance_to_file(&performance, &path).is_ok()); -} - -#[cfg(obsolete)] -#[test] -fn patching_to_device_with_no_input_fails_with_proper_error() { - let mut paths = Paths::default(); - paths.push_hive(&Paths::hive(PathType::Test)); - - let path = Path::new("instruments-have-no-inputs.json5"); - let contents = paths - .search_and_read_to_string(path) - .unwrap_or_else(|err| panic!("loading project failed: {:?}", err)); - let song_settings = SongSettings::new_from_json5(contents.as_str()) - .unwrap_or_else(|err| panic!("parsing settings for {} failed: {:?}", path.display(), err)); - let r = song_settings.instantiate(&paths, false); - assert_eq!( - r.unwrap_err().to_string(), - "Input device doesn't transform audio and can't be patched from output device" - ); -} diff --git a/tests/sidechain.rs b/tests/sidechain.rs deleted file mode 100644 index 5cf0100f..00000000 --- a/tests/sidechain.rs +++ /dev/null @@ -1,106 +0,0 @@ -// Copyright (c) 2023 Mike Tsao. All rights reserved. - -use ensnare::{midi::prelude::*, prelude::*}; -use groove::{ - mini::{register_factory_entities, Key, Note, OrchestratorBuilder, PatternBuilder}, - EntityFactory, -}; -use std::path::PathBuf; - -// Demonstrates sidechaining (which could be considered a kind of automation, -// but it's important enough to put top-level and make sure it's a good -// experience and not merely possible). -#[test] -fn demo_sidechaining() { - let mut orchestrator = OrchestratorBuilder::default() - .title(Some("Sidechaining".to_string())) - .build() - .unwrap(); - - let factory = register_factory_entities(EntityFactory::default()); - - // Add the sidechain source track. - let sidechain_pattern_uid = { - let mut piano_roll = orchestrator.piano_roll_mut(); - piano_roll.insert( - PatternBuilder::default() - .note_sequence( - vec![ - 35, 255, 255, 255, 35, 255, 255, 255, 35, 255, 255, 255, 35, 255, 255, 255, - ], - None, - ) - .build() - .unwrap(), - ) - }; - let sidechain_track_uid = orchestrator.new_midi_track().unwrap(); - let track = orchestrator.get_track_mut(&sidechain_track_uid).unwrap(); - let _ = track - .sequencer_mut() - .arrange_pattern(&sidechain_pattern_uid, 0); - let _drumkit_uid = track - .append_entity(factory.new_entity(&Key::from("drumkit")).unwrap()) - .unwrap(); - // This turns the chain's audio output into Control events. - let signal_passthrough_uid = track - .append_entity( - factory - .new_entity(&Key::from("signal-amplitude-inverted-passthrough")) - .unwrap(), - ) - .unwrap(); - // In this demo, we don't want to hear the kick track. - let _mute_uid = track - .append_entity(factory.new_entity(&Key::from("mute")).unwrap()) - .unwrap(); - - // Add the lead track that we want to duck. - let lead_pattern_uid = { - let mut piano_roll = orchestrator.piano_roll_mut(); - piano_roll.insert( - PatternBuilder::default() - .note(Note { - key: MidiNote::C4 as u8, - range: MusicalTime::START..MusicalTime::new_with_beats(4), - }) - .build() - .unwrap(), - ) - }; - let lead_track_uid = orchestrator.new_midi_track().unwrap(); - let track = orchestrator.get_track_mut(&lead_track_uid).unwrap(); - let _ = track.sequencer_mut().arrange_pattern(&lead_pattern_uid, 0); - let _synth_uid = track - .append_entity(factory.new_entity(&Key::from("toy-synth")).unwrap()) - .unwrap(); - let gain_uid = track - .append_entity(factory.new_entity(&Key::from("gain")).unwrap()) - .unwrap(); - - let gain_ceiling_param_index = { - factory - .new_entity(&Key::from("gain")) - .unwrap() - .as_controllable() - .unwrap() - .control_index_for_name("ceiling") - .unwrap() - }; - - // Link the sidechain control to the synth's gain. Note that the track with - // the controllable device, not the track with the controlling device, is - // the right one to contain the link. - let track = orchestrator.get_track_mut(&lead_track_uid).unwrap(); - track.control_router_mut().link_control( - signal_passthrough_uid, - gain_uid, - gain_ceiling_param_index, - ); - - // https://doc.rust-lang.org/std/path/struct.PathBuf.html example - let output_path: PathBuf = [env!("CARGO_TARGET_TMPDIR"), "sidechaining.wav"] - .iter() - .collect(); - assert!(orchestrator.write_to_file(&output_path).is_ok()); -} diff --git a/tests/song_editing.rs b/tests/song_editing.rs deleted file mode 100644 index 3d767e11..00000000 --- a/tests/song_editing.rs +++ /dev/null @@ -1,128 +0,0 @@ -// Copyright (c) 2023 Mike Tsao. All rights reserved. - -use ensnare::prelude::*; -use groove::{ - mini::{register_factory_entities, Key, Note, OrchestratorBuilder, PatternBuilder}, - EntityFactory, -}; -use std::path::PathBuf; - -#[test] -fn edit_song() { - let mut orchestrator = OrchestratorBuilder::default() - .title(Some("Simple Song (Edits)".to_string())) - .build() - .unwrap(); - let factory = register_factory_entities(EntityFactory::default()); - - // Create two MIDI tracks. - let rhythm_track_uid = orchestrator.new_midi_track().unwrap(); - let lead_track_uid = orchestrator.new_midi_track().unwrap(); - - // Prepare the rhythm track first. Create a rhythm pattern, add it to the - // PianoRoll, and then manipulate it. If we were really doing this in Rust - // code, it would be simpler to create, manipulate, and then add, rather - // than create, add, and manipulate, because PianoRoll takes ownership. But - // in a DAW, we expect that PianoRoll's GUI will do the pattern - // manipulation, so we're modeling that flow. This requires a bit of scoping - // to satisfy the borrow checker. - let drum_pattern = PatternBuilder::default().build().unwrap(); - let drum_pattern_uid = { - let mut piano_roll = orchestrator.piano_roll_mut(); - piano_roll.insert(drum_pattern) - }; - { - let mut piano_roll = orchestrator.piano_roll_mut(); - let drum_pattern = piano_roll.get_pattern_mut(&drum_pattern_uid).unwrap(); - - let mut note = Note { - key: 60, - range: MusicalTime::START..(MusicalTime::START + MusicalTime::DURATION_HALF), - }; - // Add to the pattern. - drum_pattern.add_note(note.clone()); - // Wait, no, didn't want to do that. - drum_pattern.remove_note(¬e); - // It should be a kick. Change and then re-add. - note.key = 35; - drum_pattern.add_note(note.clone()); - - // We don't have to keep removing/re-adding to edit notes. If we can - // describe them, then we can edit them within the pattern. - let note = drum_pattern.change_note_key(¬e.clone(), 39).unwrap(); - let note = drum_pattern - .move_note( - ¬e.clone(), - note.range.start + MusicalTime::DURATION_BREVE, - ) - .unwrap(); - let _ = drum_pattern - .move_and_resize_note( - ¬e.clone(), - MusicalTime::START, - MusicalTime::DURATION_SIXTEENTH, - ) - .unwrap(); - } - - // Pattern is good; add an instrument to the track. - let rhythm_track = orchestrator.get_track_mut(&rhythm_track_uid).unwrap(); - let _drumkit_uid = rhythm_track - .append_entity(factory.new_entity(&Key::from("drumkit")).unwrap()) - .unwrap(); - - // Arrange the drum pattern. - let _ = rhythm_track - .sequencer_mut() - .arrange_pattern(&drum_pattern_uid, 0); - - // Now set up the lead track. We need a pattern; we'll whip up something - // quickly because we already showed the editing process while making the - // drum pattern. - let lead_pattern = PatternBuilder::default() - .note_sequence( - vec![ - 60, 255, 62, 255, 64, 255, 65, 255, 67, 255, 69, 255, 71, 255, 72, 255, - ], - None, - ) - .build() - .unwrap(); - let lead_pattern_uid = { - let mut piano_roll = orchestrator.piano_roll_mut(); - piano_roll.insert(lead_pattern) - }; - - let lead_track = orchestrator.get_track_mut(&lead_track_uid).unwrap(); - let welsh_synth_uid = lead_track - .append_entity(factory.new_entity(&Key::from("welsh-synth")).unwrap()) - .unwrap(); - - // Hmmm, we don't like the sound of that synth; let's replace it with another. - lead_track.remove_entity(&welsh_synth_uid); - let _toy_synth_uid = lead_track - .append_entity(factory.new_entity(&Key::from("toy-synth")).unwrap()) - .unwrap(); - - // That's better, but it needs an effect. - let _lead_reverb_uid = lead_track - .append_entity(factory.new_entity(&Key::from("reverb")).unwrap()) - .unwrap(); - // And another. - let lead_gain_uid = lead_track - .append_entity(factory.new_entity(&Key::from("gain")).unwrap()) - .unwrap(); - // Sounds better if gain is first in chain. - let _ = lead_track.move_effect(lead_gain_uid, 0); - - // Arrange the lead pattern. - let _ = lead_track - .sequencer_mut() - .arrange_pattern(&lead_pattern_uid, 0); - - // https://doc.rust-lang.org/std/path/struct.PathBuf.html example - let output_path: PathBuf = [env!("CARGO_TARGET_TMPDIR"), "simple-song-with-edits.wav"] - .iter() - .collect(); - assert!(orchestrator.write_to_file(&output_path).is_ok()); -} diff --git a/tests/song_programming.rs b/tests/song_programming.rs deleted file mode 100644 index 1047b212..00000000 --- a/tests/song_programming.rs +++ /dev/null @@ -1,120 +0,0 @@ -// Copyright (c) 2023 Mike Tsao. All rights reserved. - -use ensnare::prelude::*; -use ensnare::traits::Configurable; -use groove::{ - mini::{register_factory_entities, Key, OrchestratorBuilder, PatternBuilder}, - EntityFactory, Orchestrator, -}; -use std::path::PathBuf; - -fn set_up_drum_track(o: &mut Orchestrator, factory: &EntityFactory) { - // Add the drum pattern to the PianoRoll. - // We need to scope piano_roll to satisfy the borrow checker. - let drum_pattern_uid = { - let mut piano_roll = o.piano_roll_mut(); - piano_roll.insert( - PatternBuilder::default() - .note_sequence( - vec![ - 35, 255, 255, 255, 35, 255, 255, 255, 35, 255, 255, 255, 35, 255, 255, 255, - ], - None, - ) - .note_sequence( - vec![ - 255, 255, 255, 255, 39, 255, 255, 255, 255, 255, 255, 255, 39, 255, 255, - 255, - ], - None, - ) - .note_sequence( - vec![ - // Bug: if we do note on every 16th, we get only the first one - 42, 255, 42, 255, 42, 255, 42, 255, 42, 255, 42, 255, 42, 255, 42, 255, - ], - None, - ) - .build() - .unwrap(), - ) - }; - - // Arrange the drum pattern in a new MIDI track's Sequencer. By default, the - // Sequencer emits events on MIDI channel 0. - let track_uid = o.new_midi_track().unwrap(); - let track = o.get_track_mut(&track_uid).unwrap(); - let _ = track.sequencer_mut().arrange_pattern(&drum_pattern_uid, 0); - - // Add the drumkit instrument to the track. By default, it listens on MIDI channel 0. - let _drumkit_uid = track - .append_entity(factory.new_entity(&Key::from("drumkit")).unwrap()) - .unwrap(); - - // Add an effect to the track's effect chain. - let filter_uid = track - .append_entity( - factory - .new_entity(&Key::from("filter-low-pass-24db")) - .unwrap(), - ) - .unwrap(); - let _ = track.set_humidity(filter_uid, Normal::from(0.2)); -} - -fn set_up_lead_track(o: &mut Orchestrator, factory: &EntityFactory) { - // Add the lead pattern to the PianoRoll. - let scale_pattern_uid = { - let mut piano_roll = o.piano_roll_mut(); - piano_roll.insert( - PatternBuilder::default() - .note_sequence( - vec![ - 60, 255, 62, 255, 64, 255, 65, 255, 67, 255, 69, 255, 71, 255, 72, 255, - ], - None, - ) - .build() - .unwrap(), - ) - }; - - // Arrange the lead pattern in a new MIDI track's Sequencer. - let track_uid = o.new_midi_track().unwrap(); - let track = o.get_track_mut(&track_uid).unwrap(); - let _ = track.sequencer_mut().arrange_pattern(&scale_pattern_uid, 0); - - // Add a synth to play the pattern. - let _synth_uid = track - .append_entity(factory.new_entity(&Key::from("welsh-synth")).unwrap()) - .unwrap(); - - // Make the synth sound better. - let reverb_uid = track - .append_entity(factory.new_entity(&Key::from("reverb")).unwrap()) - .unwrap(); - let _ = track.set_humidity(reverb_uid, Normal::from(0.2)); -} - -// Demonstrates making a song in Rust. We assume that we knew what the song is -// from the start, so there is no editing -- just programming. Compare the -// edit_song() test, which demonstrates adding elements, changing them, and -// removing them, as you'd expect a GUI DAW to do. -#[test] -fn program_song() { - let mut orchestrator = OrchestratorBuilder::default() - .title(Some("Simple Song".to_string())) - .build() - .unwrap(); - orchestrator.update_tempo(Tempo(128.0)); - let factory = register_factory_entities(EntityFactory::default()); - - set_up_drum_track(&mut orchestrator, &factory); - set_up_lead_track(&mut orchestrator, &factory); - - // https://doc.rust-lang.org/std/path/struct.PathBuf.html example - let output_path: PathBuf = [env!("CARGO_TARGET_TMPDIR"), "simple-song.wav"] - .iter() - .collect(); - assert!(orchestrator.write_to_file(&output_path).is_ok()); -} diff --git a/toys/Cargo.toml b/toys/Cargo.toml index e37f5e27..c51d868f 100644 --- a/toys/Cargo.toml +++ b/toys/Cargo.toml @@ -8,7 +8,7 @@ publish = false [dependencies] eframe = { version = "0.22" } -ensnare = { path = "../../ensnare" } +ensnare-core = { path = "../../ensnare/core" } ensnare-proc-macros = { path = "../../ensnare/proc-macros" } groove-core = { path = "../core" } groove-proc-macros = { path = "../proc-macros" } diff --git a/toys/src/controllers.rs b/toys/src/controllers.rs deleted file mode 100644 index 4198a228..00000000 --- a/toys/src/controllers.rs +++ /dev/null @@ -1,62 +0,0 @@ -// Copyright (c) 2023 Mike Tsao. All rights reserved. - -use ensnare::{midi::prelude::*, prelude::*, traits::prelude::*}; -use ensnare_proc_macros::{IsController, Uid}; -use serde::{Deserialize, Serialize}; - -#[derive(Debug, Default, Uid, IsController, Serialize, Deserialize)] -pub struct ToyControllerAlwaysSendsMidiMessage { - uid: Uid, - - #[serde(skip)] - midi_note: u8, - - #[serde(skip)] - is_performing: bool, -} -impl Displays for ToyControllerAlwaysSendsMidiMessage {} -impl HandlesMidi for ToyControllerAlwaysSendsMidiMessage {} -impl Controls for ToyControllerAlwaysSendsMidiMessage { - fn update_time(&mut self, _range: &std::ops::Range) {} - - fn work(&mut self, control_events_fn: &mut ControlEventsFn) { - if self.is_performing { - control_events_fn( - self.uid, - EntityEvent::Midi( - MidiChannel(0), - MidiMessage::NoteOn { - key: u7::from(self.midi_note), - vel: u7::from(127), - }, - ), - ); - self.midi_note += 1; - if self.midi_note > 127 { - self.midi_note = 1; - } - } - } - - fn is_finished(&self) -> bool { - false - } - - fn play(&mut self) { - self.is_performing = true; - } - - fn stop(&mut self) { - self.is_performing = false; - } - - fn skip_to_start(&mut self) { - todo!() - } - - fn is_performing(&self) -> bool { - self.is_performing - } -} -impl Configurable for ToyControllerAlwaysSendsMidiMessage {} -impl Serializable for ToyControllerAlwaysSendsMidiMessage {} diff --git a/toys/src/effects.rs b/toys/src/effects.rs index c4bf0bb1..4711d227 100644 --- a/toys/src/effects.rs +++ b/toys/src/effects.rs @@ -1,7 +1,7 @@ // Copyright (c) 2023 Mike Tsao. All rights reserved. use eframe::egui; -use ensnare::{prelude::*, traits::prelude::*}; +use ensnare_core::{prelude::*, traits::prelude::*}; use ensnare_proc_macros::{Control, IsEffect, Params, Uid}; use serde::{Deserialize, Serialize}; use std::fmt::Debug; diff --git a/toys/src/instruments.rs b/toys/src/instruments.rs index 1de6c373..4d208143 100644 --- a/toys/src/instruments.rs +++ b/toys/src/instruments.rs @@ -5,147 +5,22 @@ use eframe::{ emath::Align, epaint::{pos2, Color32, Rect, Rounding, Stroke}, }; -use ensnare::{ +use ensnare_core::{ + generators::{Envelope, EnvelopeParams, Oscillator, OscillatorParams, Waveform}, instruments::Synthesizer, midi::prelude::*, + modulators::{Dca, DcaParams}, prelude::*, traits::{prelude::*, GeneratesEnvelope}, voices::{VoiceCount, VoiceStore}, }; use ensnare_proc_macros::{Control, IsInstrument, Params, Uid}; -use groove_core::{ - generators::{Envelope, EnvelopeParams, Oscillator, OscillatorParams, Waveform}, - Dca, DcaParams, -}; use serde::{Deserialize, Serialize}; use std::{ fmt::Debug, sync::{Arc, Mutex}, }; -#[derive(Debug, Default)] -pub struct ToyInstrumentEphemerals { - sample: StereoSample, - pub is_playing: bool, - pub received_count: Arc>, - pub debug_messages: Vec, -} - -/// An [IsInstrument](groove_core::traits::IsInstrument) that uses a default -/// Oscillator to produce sound. Its "envelope" is just a boolean that responds -/// to MIDI NoteOn/NoteOff. [Controllable](groove_core::traits::Controllable) by -/// two parameters: Oscillator waveform and frequency. -#[derive(Debug, Control, IsInstrument, Params, Uid, Serialize, Deserialize)] -pub struct ToyInstrument { - uid: Uid, - - #[control] - #[params] - fake_value: Normal, - - oscillator: Oscillator, - - #[control] - #[params] - dca: Dca, - - #[serde(skip)] - e: ToyInstrumentEphemerals, -} -impl Generates for ToyInstrument { - fn value(&self) -> StereoSample { - self.e.sample - } - - fn generate_batch_values(&mut self, values: &mut [StereoSample]) { - for value in values { - self.tick(1); - *value = self.value(); - } - } -} -impl Configurable for ToyInstrument { - fn sample_rate(&self) -> SampleRate { - self.oscillator.sample_rate() - } - - fn update_sample_rate(&mut self, sample_rate: SampleRate) { - self.oscillator.update_sample_rate(sample_rate); - } -} -impl Ticks for ToyInstrument { - fn tick(&mut self, tick_count: usize) { - self.oscillator.tick(tick_count); - self.e.sample = if self.e.is_playing { - self.dca - .transform_audio_to_stereo(Sample::from(self.oscillator.value())) - } else { - StereoSample::SILENCE - }; - } -} -impl HandlesMidi for ToyInstrument { - fn handle_midi_message( - &mut self, - _channel: MidiChannel, - message: MidiMessage, - _: &mut MidiMessagesFn, - ) { - self.e.debug_messages.push(message); - if let Ok(mut received_count) = self.e.received_count.lock() { - *received_count += 1; - } - - match message { - MidiMessage::NoteOn { key, vel: _ } => { - self.e.is_playing = true; - self.oscillator.set_frequency(key.into()); - } - MidiMessage::NoteOff { key: _, vel: _ } => { - self.e.is_playing = false; - } - _ => {} - } - } -} -impl Serializable for ToyInstrument {} -impl ToyInstrument { - pub fn new_with(params: &ToyInstrumentParams) -> Self { - Self { - uid: Default::default(), - fake_value: params.fake_value(), - oscillator: Oscillator::new_with(&OscillatorParams::default_with_waveform( - Waveform::Sine, - )), - dca: Dca::new_with(¶ms.dca), - e: Default::default(), - } - } - - // TODO: when we have a more specific control param type, we can do a real - // into/from - #[allow(dead_code)] - fn waveform(&self) -> f32 { - match self.oscillator.waveform() { - Waveform::Sawtooth => -1.0, - Waveform::Square => 1.0, - _ => 0.0, - } - } - - pub fn set_fake_value(&mut self, fake_value: Normal) { - self.fake_value = fake_value; - } - - pub fn fake_value(&self) -> Normal { - self.fake_value - } - - pub fn received_count_mutex(&self) -> &Arc> { - &self.e.received_count - } -} - /// Another [IsInstrument](groove_core::traits::IsInstrument) that was designed /// for black-box debugging. #[derive(Debug, Control, IsInstrument, Params, Uid, Serialize, Deserialize)] @@ -242,399 +117,8 @@ impl DebugSynth { } } -#[derive(Debug, Default, Control, IsInstrument, Params, Uid, Serialize, Deserialize)] -pub struct ToySynth { - uid: Uid, - - #[params] - voice_count: VoiceCount, - - #[control] - #[params] - waveform: Waveform, - - #[control] - #[params] - envelope: Envelope, - - #[control] - #[params] - dca: Dca, - - // TODO: this skip is a can of worms. I don't know whether we want to - // serialize everything, or manually reconstitute everything. Maybe the - // right answer is to expect that every struct gets serialized, but everyone - // should be #[serde(skip)]ing at the leaf-field level. - #[serde(skip)] - inner: Synthesizer, - - #[serde(skip)] - max_signal: Normal, -} -impl Serializable for ToySynth {} -impl Generates for ToySynth { - fn value(&self) -> StereoSample { - self.inner.value() - } - - fn generate_batch_values(&mut self, values: &mut [StereoSample]) { - // TODO: temp hack to avoid the pain of figuring out how deal with - // Synthesizer sharing a single DCA across voices. - for voice in self.inner.voices_mut() { - voice.dca.set_pan(self.dca.pan()); - } - self.inner.generate_batch_values(values); - self.update_max(); - } -} -impl HandlesMidi for ToySynth { - fn handle_midi_message( - &mut self, - channel: MidiChannel, - message: MidiMessage, - midi_messages_fn: &mut MidiMessagesFn, - ) { - self.inner - .handle_midi_message(channel, message, midi_messages_fn) - } -} -impl Ticks for ToySynth { - fn tick(&mut self, tick_count: usize) { - self.inner.tick(tick_count); - - self.update_max(); - } -} -impl Configurable for ToySynth { - fn sample_rate(&self) -> SampleRate { - self.inner.sample_rate() - } - fn update_sample_rate(&mut self, sample_rate: SampleRate) { - self.inner.update_sample_rate(sample_rate) - } -} -impl ToySynth { - pub fn new_with(params: &ToySynthParams) -> Self { - let voice_store = VoiceStore::::new_with_voice(params.voice_count(), || { - ToyVoice::new_with(params.waveform(), ¶ms.envelope) - }); - Self { - uid: Default::default(), - voice_count: params.voice_count(), - waveform: params.waveform(), - envelope: Envelope::new_with(¶ms.envelope), - dca: Dca::new_with(¶ms.dca), - inner: Synthesizer::::new_with(Box::new(voice_store)), - max_signal: Normal::minimum(), - } - } - - fn update_max(&mut self) { - let value = Normal::from(Sample::from(self.value()).0); - if value > self.max_signal { - self.max_signal = value; - } - } - - pub fn degrade_max(&mut self, factor: f64) { - self.max_signal *= factor; - } - - pub fn voice_count(&self) -> VoiceCount { - self.voice_count - } - - pub fn set_voice_count(&mut self, voice_count: VoiceCount) { - self.voice_count = voice_count; - } - - pub fn waveform(&self) -> Waveform { - self.waveform - } - - pub fn set_waveform(&mut self, waveform: Waveform) { - self.waveform = waveform; - } - - pub fn envelope(&self) -> &Envelope { - &self.envelope - } - - pub fn set_envelope(&mut self, envelope: Envelope) { - self.envelope = envelope; - } -} - -#[derive(Debug, Default)] -struct ToyVoice { - oscillator: Oscillator, - envelope: Envelope, - dca: Dca, - value: StereoSample, -} -impl IsStereoSampleVoice for ToyVoice {} -impl IsVoice for ToyVoice {} -impl PlaysNotes for ToyVoice { - fn is_playing(&self) -> bool { - !self.envelope.is_idle() - } - - fn note_on(&mut self, key: u7, _velocity: u7) { - self.envelope.trigger_attack(); - self.oscillator.set_frequency(key.into()); - } - - fn aftertouch(&mut self, _velocity: u7) { - todo!() - } - - fn note_off(&mut self, _velocity: u7) { - self.envelope.trigger_release() - } -} -impl Generates for ToyVoice { - fn value(&self) -> StereoSample { - self.value - } - - #[allow(unused_variables)] - fn generate_batch_values(&mut self, values: &mut [StereoSample]) { - todo!() - } -} -impl Ticks for ToyVoice { - fn tick(&mut self, tick_count: usize) { - self.oscillator.tick(tick_count); - self.envelope.tick(tick_count); - self.value = self.dca.transform_audio_to_stereo( - (self.oscillator.value().value() * self.envelope.value().value()).into(), - ); - } -} -impl Configurable for ToyVoice { - fn sample_rate(&self) -> SampleRate { - self.oscillator.sample_rate() - } - - fn update_sample_rate(&mut self, sample_rate: SampleRate) { - self.oscillator.update_sample_rate(sample_rate); - self.envelope.update_sample_rate(sample_rate); - } -} -impl ToyVoice { - fn new_with(waveform: Waveform, envelope: &EnvelopeParams) -> Self { - Self { - oscillator: Oscillator::new_with(&OscillatorParams::default_with_waveform(waveform)), - envelope: Envelope::new_with(envelope), - dca: Dca::default(), - value: Default::default(), - } - } -} - -/// Produces a constant audio signal. Used for ensuring that a known signal -/// value gets all the way through the pipeline. -#[derive(Debug, Default, Control, IsInstrument, Params, Uid, Serialize, Deserialize)] -pub struct ToyAudioSource { - uid: Uid, - - // This should be a Normal, but we use this audio source for testing edge - // conditions. Thus we need to let it go out of range. - #[control] - #[params] - level: ParameterType, - - #[serde(skip)] - sample_rate: SampleRate, -} -impl Serializable for ToyAudioSource {} -impl Generates for ToyAudioSource { - fn value(&self) -> StereoSample { - StereoSample::from(self.level) - } - - #[allow(unused_variables)] - fn generate_batch_values(&mut self, values: &mut [StereoSample]) { - let sample = StereoSample::from(self.level); - values.fill(sample); - } -} -impl Configurable for ToyAudioSource { - fn sample_rate(&self) -> SampleRate { - self.sample_rate - } - - fn update_sample_rate(&mut self, sample_rate: SampleRate) { - self.sample_rate = sample_rate; - } -} -impl Ticks for ToyAudioSource { - fn tick(&mut self, _tick_count: usize) {} -} -impl HandlesMidi for ToyAudioSource {} -#[allow(dead_code)] -impl ToyAudioSource { - pub const TOO_LOUD: SampleType = 1.1; - pub const LOUD: SampleType = 1.0; - pub const MEDIUM: SampleType = 0.5; - pub const SILENT: SampleType = 0.0; - pub const QUIET: SampleType = -1.0; - pub const TOO_QUIET: SampleType = -1.1; - - pub fn new_with(params: &ToyAudioSourceParams) -> Self { - Self { - level: params.level(), - ..Default::default() - } - } - - pub fn new_always_loud() -> Self { - Self::new_with(&ToyAudioSourceParams { level: Self::LOUD }) - } - - pub fn new_always_medium() -> Self { - Self::new_with(&ToyAudioSourceParams { - level: Self::MEDIUM, - }) - } - - pub fn new_always_quiet() -> Self { - Self::new_with(&ToyAudioSourceParams { level: Self::QUIET }) - } - - pub fn level(&self) -> f64 { - self.level - } - - pub fn set_level(&mut self, level: ParameterType) { - self.level = level; - } -} - -fn indicator(value: Normal) -> impl egui::Widget + 'static { - move |ui: &mut egui::Ui| indicator_ui(ui, value) -} - -fn indicator_ui(ui: &mut egui::Ui, value: Normal) -> egui::Response { - let desired_size = egui::vec2(2.0, 16.0); - let (rect, response) = - ui.allocate_exact_size(desired_size, egui::Sense::focusable_noninteractive()); - - if ui.is_rect_visible(rect) { - ui.painter().rect( - rect, - Rounding::default(), - Color32::BLACK, - Stroke { - width: 1.0, - color: Color32::DARK_GRAY, - }, - ); - let sound_rect = Rect::from_two_pos( - rect.left_bottom(), - pos2( - rect.right(), - rect.bottom() - rect.height() * value.value_as_f32(), - ), - ); - ui.painter().rect( - sound_rect, - Rounding::default(), - Color32::YELLOW, - Stroke { - width: 1.0, - color: Color32::YELLOW, - }, - ); - } - - response -} - -impl Displays for ToyInstrument { - fn ui(&mut self, ui: &mut Ui) -> egui::Response { - ui.label(self.name()) - } -} - impl Displays for DebugSynth { fn ui(&mut self, ui: &mut Ui) -> egui::Response { ui.label(self.name()) } } -impl Displays for ToySynth { - fn ui(&mut self, ui: &mut Ui) -> egui::Response { - let height = ui.available_height(); - ui.set_min_size(ui.available_size()); - ui.set_max_size(ui.available_size()); - if height <= 32.0 { - self.show_small(ui) - } else if height <= 128.0 { - self.show_medium(ui) - } else { - self.show_full(ui) - } - } -} -impl ToySynth { - fn show_small(&mut self, ui: &mut Ui) -> egui::Response { - let response = ui - .horizontal(|ui| { - ui.with_layout(Layout::left_to_right(Align::Center), |ui| { - ui.label("ToySynth") - }) - .inner - | ui.with_layout(Layout::right_to_left(Align::Center), |ui| { - ui.add(indicator(self.max_signal)) - }) - .inner - }) - .inner; - self.degrade_max(0.95); - response - } - fn show_medium(&mut self, ui: &mut Ui) -> egui::Response { - ui.label("ToySynth MEDIUM!"); - let value = Normal::from(0.5); - ui.add(indicator(value)) - } - fn show_full(&mut self, ui: &mut Ui) -> egui::Response { - ui.label("ToySynth LARGE!!!!"); - let value = Normal::from(0.8); - ui.add(indicator(value)) - } -} - -impl Displays for ToyAudioSource { - fn ui(&mut self, ui: &mut Ui) -> egui::Response { - ui.label(self.name()) - } -} - -#[cfg(test)] -pub mod tests { - use crate::{instruments::ToyInstrumentParams, ToyInstrument}; - use ensnare::{prelude::*, traits::prelude::*}; - use groove_core::DcaParams; - - // TODO: restore tests that test basic trait behavior, then figure out how - // to run everyone implementing those traits through that behavior. For now, - // this one just tests that a generic instrument doesn't panic when accessed - // for non-consecutive time slices. - #[test] - fn sources_audio_random_access() { - let mut instrument = ToyInstrument::new_with(&ToyInstrumentParams { - fake_value: Normal::from(0.42), - dca: DcaParams { - gain: Default::default(), - pan: Default::default(), - }, - }); - let mut rng = oorandom::Rand32::new(0); - - for _ in 0..100 { - instrument.tick(rng.rand_range(1..10) as usize); - let _ = instrument.value(); - } - } -} diff --git a/toys/src/lib.rs b/toys/src/lib.rs index 6e9861e3..fcc6079b 100644 --- a/toys/src/lib.rs +++ b/toys/src/lib.rs @@ -4,13 +4,8 @@ //! implement [Entity] traits, usually in a simple fashion. They aren't likely //! to be useful in real music prduction. -pub use controllers::ToyControllerAlwaysSendsMidiMessage; pub use effects::{ToyEffect, ToyEffectParams}; -pub use instruments::{ - DebugSynth, DebugSynthParams, ToyAudioSource, ToyAudioSourceParams, ToyInstrument, - ToyInstrumentParams, ToySynth, ToySynthParams, -}; +pub use instruments::{DebugSynth, DebugSynthParams}; -mod controllers; mod effects; mod instruments;