diff --git a/Cargo.toml b/Cargo.toml index ae575af..4437181 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "midix" -version = "4.0.0-alpha" +version = "4.0.0-alpha.1" authors = ["dsgallups "] edition = "2024" description = "MIDI structures designed for humans" @@ -15,51 +15,22 @@ exclude = ["assets/*"] [features] default = ["std"] -all = ["std", "bevy_resources", "web"] -std = ["thiserror/std", "num_enum/std", "crossbeam-channel/std"] +std = ["thiserror/std"] web = ["bevy_platform/web"] -bevy_resources = [ - "bevy", - "dep:midir", - "dep:tinyaudio", - "dep:itertools", - "dep:rustysynth", - "dep:crossbeam-channel", -] -bevy = ["bevy_resources", "bevy/bevy_log", "std", "bevy/bevy_asset"] -debug = ["bevy_resources"] -example = [ - "bevy_resources", - "std", - "bevy/bevy_color", - "bevy/bevy_core_pipeline", - "bevy/bevy_ui", - "bevy/bevy_ui_picking_backend", - "bevy/bevy_winit", - "bevy/bevy_window", - "bevy/x11", - # note that the wasm example freezes since when this feature is not enabled! TODO - "bevy/multi_threaded", -] +bevy = ["dep:bevy"] +bevy_asset = ["bevy/bevy_asset"] serde = ["dep:serde"] [dependencies.bevy] -version = "0.17.0-rc.1" +version = "0.17.0-rc" optional = true default-features = false -# features = ["async_executor", "bevy_log", "bevy_state", "bevy_asset", "std"] [dependencies] -num_enum = { version = "0.7.3", default-features = false } thiserror = { version = "2.0", default-features = false } # Bevy feature deps -crossbeam-channel = { version = "0.5.15", optional = true, default-features = false } -midir = { version = "0.10", optional = true } -tinyaudio = { version = "1.1.0", optional = true } -itertools = { version = "0.14.0", optional = true } -rustysynth = { version = "1.3.5", optional = true } -bevy_platform = { version = "0.17.0-rc.1", default-features = false, features = [ +bevy_platform = { version = "0.17.0-rc", optional = true, default-features = false, features = [ "alloc", ] } serde = {version = "1.0", features = ["derive"], optional = true} @@ -110,3 +81,28 @@ pretty_assertions = { default-features = false, features = [ # [[example]] # name = "scale" # required-features = ["example"] + +# debug = ["bevy_resources"] +# example = [ +# "bevy_resources", +# "std", +# "bevy/bevy_color", +# "bevy/bevy_core_pipeline", +# "bevy/bevy_ui", +# "bevy/bevy_ui_picking_backend", +# "bevy/bevy_winit", +# "bevy/bevy_window", +# "bevy/x11", +# # note that the wasm example freezes since when this feature is not enabled! TODO +# "bevy/multi_threaded", +# ] +# + +# bevy_resources = [ +# "bevy", +# "dep:midir", +# "dep:tinyaudio", +# "dep:itertools", +# "dep:rustysynth", +# "dep:crossbeam-channel", +# ] diff --git a/src/bevy/asset/midi_file.rs b/src/bevy/asset/midi_file.rs deleted file mode 100644 index 0bcd23d..0000000 --- a/src/bevy/asset/midi_file.rs +++ /dev/null @@ -1,251 +0,0 @@ -#![doc = r#" -Asset types - -TODO -"#] -#![allow(dead_code)] -#![allow(unused_variables)] - -use bevy::{ - asset::{AssetLoader, LoadContext, io::Reader}, - prelude::*, -}; - -use crate::{ - events::LiveEvent, - file::ParsedMidiFile as Mf, - prelude::{FormatType, Timed, Timing}, - reader::ReaderError, -}; - -use crate::bevy::song::MidiSong; - -/// Sound font asset. Wraps a midix MidiFile -/// -/// TODO(before v4: do not wrap midix MidiFile) -#[derive(Asset, TypePath)] -pub struct MidiFile { - inner: Mf<'static>, -} - -impl MidiFile { - /// Create a new midifile with the given inner midix MidiFile - pub fn new(file: Mf<'static>) -> Self { - Self { inner: file } - } - - /// Get a reference to the inner midifile - pub fn inner(&self) -> &Mf<'static> { - &self.inner - } - - /// uses owned self to make a song sendable to the synth - pub fn into_song(self) -> MidiSong { - self.inner.into() - } - /// uses reference to make a song - pub fn to_song(&self) -> MidiSong { - (&self.inner).into() - } -} - -impl<'a> From> for MidiSong { - fn from(midi: Mf<'a>) -> Self { - let mut commands = Vec::new(); - let tracks = midi.tracks(); - - // is Some if the tempo is set for the whole file. - // None if the format is sequentially independent - let file_tempo = match midi.format_type() { - FormatType::SequentiallyIndependent => None, - FormatType::Simultaneous | FormatType::SingleMultiChannel => { - let first_track = tracks.first().unwrap(); - Some(first_track.info().tempo) - } - }; - - for track in tracks { - let track_tempo = file_tempo.unwrap_or(track.info().tempo); - let micros_per_quarter_note = track_tempo.micros_per_quarter_note(); - - let (micros_per_tick, offset_in_micros) = match midi.header().timing() { - Timing::Smpte(v) => { - //µs_per_tick = 1 000 000 / (fps × ticks_per_frame) - //FPS is −24/−25/−29/−30 in the high byte of division; - // ticks per frame is the low byte. - - let frames_per_second = v.fps().as_division() as u32; - let ticks_per_frame = v.ticks_per_frame() as u32; - let ticks_per_second = frames_per_second * ticks_per_frame; - let micros_per_tick = 1_000_000. / ticks_per_second as f64; - - //NOTE: if the file header uses smpte, that overrides any track smpte offset. - let offset_in_micros = track - .info() - .smpte_offset - .as_ref() - .map(|offset| { - if offset.fps != v.fps() { - warn!( - "Header's fps({}) does not align with track's fps({}). \ - The file's fps will override the track's!", - v.fps().as_f64(), - offset.fps.as_f64() - ); - } - offset.as_micros_with_override(v.fps()) - }) - .unwrap_or(0.); - - (micros_per_tick, offset_in_micros) - } - Timing::TicksPerQuarterNote(tpqn) => { - // µs_per_tick = tempo_meta / TPQN - // micro_seconds/quarternote * quarternote_per_tick (1/ticks per qn) - let micros_per_tick = - micros_per_quarter_note as f64 / tpqn.ticks_per_quarter_note() as f64; - - let offset_in_micros = track - .info() - .smpte_offset - .as_ref() - .map(|offset| offset.as_micros()) - .unwrap_or(0.); - - (micros_per_tick, offset_in_micros) - } - }; - - for event in track.events() { - match event.event() { - LiveEvent::ChannelVoice(cv) => { - let tick = event.accumulated_ticks(); - let micros = micros_per_tick * tick as f64; - - commands.push(Timed::new(micros as u64, *cv)); - } - _ => { - //idk - } - } - } - } - MidiSong::new(commands) - } -} - -impl<'a> From<&Mf<'a>> for MidiSong { - fn from(midi: &Mf<'a>) -> Self { - let mut commands = Vec::new(); - let tracks = midi.tracks(); - - let ticks_per_qn = midi.header().timing().ticks_per_quarter_note().unwrap(); - - // is Some if the tempo is set for the whole file. - // None if the format is sequentially independent - let file_tempo = match midi.format_type() { - FormatType::SequentiallyIndependent => None, - FormatType::Simultaneous | FormatType::SingleMultiChannel => { - let first_track = tracks.first().unwrap(); - Some(first_track.info().tempo) - } - }; - - for track in tracks { - let track_tempo = file_tempo.unwrap_or(track.info().tempo); - let micros_per_quarter_note = track_tempo.micros_per_quarter_note(); - - let (micros_per_tick, offset_in_micros) = match midi.header().timing() { - Timing::Smpte(v) => { - //µs_per_tick = 1 000 000 / (fps × ticks_per_frame) - //FPS is −24/−25/−29/−30 in the high byte of division; - // ticks per frame is the low byte. - - let frames_per_second = v.fps().as_division() as u32; - let ticks_per_frame = v.ticks_per_frame() as u32; - let ticks_per_second = frames_per_second * ticks_per_frame; - let micros_per_tick = 1_000_000. / ticks_per_second as f64; - - //NOTE: if the file header uses smpte, that overrides any track smpte offset. - let offset_in_micros = track - .info() - .smpte_offset - .as_ref() - .map(|offset| { - if offset.fps != v.fps() { - warn!( - "Header's fps({}) does not align with track's fps({}). \ - The file's fps will override the track's!", - v.fps().as_f64(), - offset.fps.as_f64() - ); - } - offset.as_micros_with_override(v.fps()) - }) - .unwrap_or(0.); - - (micros_per_tick, offset_in_micros) - } - Timing::TicksPerQuarterNote(tpqn) => { - // µs_per_tick = tempo_meta / TPQN - // micro_seconds/quarternote * quarternote_per_tick (1/ticks per qn) - let micros_per_tick = - micros_per_quarter_note as f64 / tpqn.ticks_per_quarter_note() as f64; - - let offset_in_micros = track - .info() - .smpte_offset - .as_ref() - .map(|offset| offset.as_micros()) - .unwrap_or(0.); - - (micros_per_tick, offset_in_micros) - } - }; - - for event in track.events() { - match event.event() { - LiveEvent::ChannelVoice(cv) => { - let tick = event.accumulated_ticks(); - let micros = micros_per_tick * tick as f64; - - commands.push(Timed::new(micros as u64, *cv)); - } - _ => { - //idk - } - } - } - } - MidiSong::new(commands) - } -} - -/// Loader for sound fonts -#[derive(Default)] -pub struct MidiFileLoader; - -impl AssetLoader for MidiFileLoader { - type Asset = MidiFile; - type Settings = (); - type Error = ReaderError; - async fn load( - &self, - reader: &mut dyn Reader, - _settings: &(), - _load_context: &mut LoadContext<'_>, - ) -> Result { - let mut bytes = Vec::new(); - reader.read_to_end(&mut bytes).await.unwrap(); - - let inner = Mf::parse(bytes)?; - - let res = MidiFile::new(inner); - - Ok(res) - } - - fn extensions(&self) -> &[&str] { - &["mid"] - } -} diff --git a/src/bevy/asset/mod.rs b/src/bevy/asset/mod.rs deleted file mode 100644 index 3aaa003..0000000 --- a/src/bevy/asset/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -//! asset types -mod midi_file; -pub use midi_file::*; -mod sound_font; -pub use sound_font::*; diff --git a/src/bevy/asset/sound_font.rs b/src/bevy/asset/sound_font.rs deleted file mode 100644 index 1968e59..0000000 --- a/src/bevy/asset/sound_font.rs +++ /dev/null @@ -1,67 +0,0 @@ -#![doc = r#" -Asset types - -TODO -"#] - -use alloc::sync::Arc; -use bevy_platform::prelude::*; -use thiserror::Error; - -use bevy::{ - asset::{AssetLoader, LoadContext, io::Reader}, - prelude::*, -}; -use rustysynth::SoundFont as Sf; - -/// Sound font asset -#[derive(Asset, TypePath)] -pub struct SoundFont { - pub(crate) file: Arc, -} - -impl SoundFont { - /// Create a new - fn new(file: &mut &[u8]) -> Self { - let sf = Sf::new(file).unwrap(); - Self { file: Arc::new(sf) } - } -} -/// Possible errors that can be produced by [`CustomAssetLoader`] -#[derive(Debug, Error)] -pub enum SoundFontLoadError { - /// An [IO](std::io) Error - #[error("Could not load asset: {0}")] - Io(#[from] std::io::Error), -} - -/// Loader for sound fonts -#[derive(Default)] -pub struct SoundFontLoader; - -impl AssetLoader for SoundFontLoader { - type Asset = SoundFont; - type Settings = (); - type Error = SoundFontLoadError; - async fn load( - &self, - reader: &mut dyn Reader, - _settings: &(), - _load_context: &mut LoadContext<'_>, - ) -> Result { - let mut bytes = Vec::new(); - info!( - "Loading bytes...this might take a while. If taking too long, run with --release or with opt-level = 3!" - ); - reader.read_to_end(&mut bytes).await?; - - info!("Loaded!"); - let res = SoundFont::new(&mut bytes.as_slice()); - - Ok(res) - } - - fn extensions(&self) -> &[&str] { - &["custom"] - } -} diff --git a/src/bevy/input/connection.rs b/src/bevy/input/connection.rs deleted file mode 100644 index a956cd2..0000000 --- a/src/bevy/input/connection.rs +++ /dev/null @@ -1,61 +0,0 @@ -use crate::events::{FromLiveEventBytes, LiveEvent}; -use bevy::prelude::*; -use crossbeam_channel::{Receiver, TryRecvError}; -use midir::MidiInputPort; - -use super::MidiInputError; - -/// An [`Event`] for incoming midi data. -#[derive(Event, Debug)] -pub struct MidiData { - /// Returns the timestamp of the data - pub stamp: Option, - - /// The underlying message of the event - pub message: LiveEvent<'static>, -} -pub(crate) struct MidiInputConnection { - data: Receiver, - conn: midir::MidiInputConnection<()>, -} - -impl MidiInputConnection { - pub fn new( - midir_input: midir::MidiInput, - port: &MidiInputPort, - port_name: &str, - ) -> Result { - let (sender, receiver) = crossbeam_channel::unbounded::(); - - let conn = midir_input.connect( - port, - port_name, - { - move |timestamp, data, _| { - let Ok(message) = LiveEvent::from_bytes(data) else { - return; - }; - sender - .send(MidiData { - stamp: Some(timestamp), - message, - }) - .unwrap(); - } - }, - (), - )?; - - Ok(Self { - data: receiver, - conn, - }) - } - pub fn read(&self) -> Result { - self.data.try_recv() - } - pub fn close(self) -> midir::MidiInput { - let (listener, _) = self.conn.close(); - listener - } -} diff --git a/src/bevy/input/error.rs b/src/bevy/input/error.rs deleted file mode 100644 index dc75ff5..0000000 --- a/src/bevy/input/error.rs +++ /dev/null @@ -1,36 +0,0 @@ -use bevy::prelude::*; -use midir::{ConnectError, ConnectErrorKind}; // XXX: do we expose this? -use thiserror::Error; - -/// The [`Error`] type for midi input operations, accessible as an [`Event`]. -#[derive(Debug, Event, Error)] -pub enum MidiInputError { - /// There was something wrong connecting to the input - #[error("Couldn't reconnect to input port: {0}")] - ConnectionError(ConnectErrorKind), - - /// The port, passed by id, was not found. - #[error("Port not found (id: {0}")] - PortNotFound(String), - - /// Something happened when refreshing the port statuses - #[error("Couldn't refersh input ports")] - PortRefreshError, - /// Invalid state - #[error("Invalid State: {0}")] - InvalidState(String), -} -impl MidiInputError { - pub(crate) fn invalid(msg: impl ToString) -> Self { - Self::InvalidState(msg.to_string()) - } - pub(crate) fn port_not_found(id: impl Into) -> Self { - Self::PortNotFound(id.into()) - } -} - -impl From> for MidiInputError { - fn from(value: ConnectError) -> Self { - Self::ConnectionError(value.kind()) - } -} diff --git a/src/bevy/input/mod.rs b/src/bevy/input/mod.rs deleted file mode 100644 index 675c6e0..0000000 --- a/src/bevy/input/mod.rs +++ /dev/null @@ -1,243 +0,0 @@ -#![doc = r#" -a plugin and types for handling MIDI input -"#] -use crossbeam_channel::TryRecvError; - -mod connection; -pub use connection::*; - -mod error; -pub use error::*; - -mod plugin; -pub use plugin::*; - -use bevy::prelude::*; -use midir::MidiInputPort; - -use crate::bevy::settings::MidiSettings; - -// you can't actually have multiple MidiInputs on one device, it's really strange. -enum MidiInputState { - Listening(midir::MidiInput), - Active(MidiInputConnection), -} -/// SAFETY: This applies to linux alsa. -/// -/// There is only one instance of MidiInput at any time using this crate. -/// -/// However, this may not satisfy the requirements for safety. If another instance of -/// MidiInput exists in the external program, then UB is possible. -/// -/// Therefore, the assumption is, that when using this crate, that the user -/// will NOT instantiate another [`midir::MidiInput`] at any point while -/// [`MidiInput`] has been inserted as a resource -unsafe impl Sync for MidiInputState {} -unsafe impl Send for MidiInputState {} - -/// The central resource for interacting with midi inputs -/// -/// `MidiInput` does many things: -/// - Fetches a list of ports with connected midi devices -/// - Allows one to connect to a particular midi device and read output -/// - Close that connection and search for other devices -#[derive(Resource)] -pub struct MidiInput { - settings: MidiSettings, - state: Option, - ports: Vec, -} - -/// SAFETY: -/// -/// `JsValue`s in WASM cannot be `Send`: -/// -/// Quote: -/// > The JsValue type wraps a slab/heap of js objects which is managed by -/// > the wasm-bindgen shim, and everything here is not actually able to cross -/// > any thread boundaries. -/// -/// Therefore, `MidiOutput` nor `MidiInput` should not be able to implement Send and Sync. -/// -/// HOWEVER: Because the main scheduler does not run on worker threads, it is safe, -/// for the wasm target, to implement Send (until this issue is resolved.) -/// -#[cfg(all(target_arch = "wasm32", target_os = "unknown", feature = "web"))] -unsafe impl Send for MidiInput {} -/// SAFETY: -/// -/// See [`MidiInput`]'s Send implementation -#[cfg(all(target_arch = "wasm32", target_os = "unknown", feature = "web"))] -unsafe impl Sync for MidiInput {} - -impl MidiInput { - /// Creates a new midi input with the provided settings. This is done automatically - /// by [`MidiInputPlugin`]. - pub fn new(settings: MidiSettings) -> Self { - let listener = match midir::MidiInput::new(settings.client_name) { - Ok(input) => input, - Err(e) => { - panic!("Error initializing midi input! {e:?}"); - } - }; - let ports = listener.ports(); - Self { - state: Some(MidiInputState::Listening(listener)), - settings, - ports, - } - } - - /// Return a list of ports updated since calling [`MidiInput::new`] or - /// [`MidiInput::refresh_ports`] - pub fn ports(&self) -> &[MidiInputPort] { - &self.ports - } - /// Attempts to connects to the port at the given index returned by [`MidiInput::ports`] - /// - /// # Errors - /// - If already connected to a device - /// - If the index is out of bounds - /// - An input connection cannot be established - pub fn connect_to_index(&mut self, index: usize) -> Result<(), MidiInputError> { - if self - .state - .as_ref() - .is_none_or(|s| matches!(s, MidiInputState::Active(_))) - { - return Err(MidiInputError::invalid( - "Cannot connect: not currently active!", - )); - } - let Some(port) = self.ports.get(index) else { - return Err(MidiInputError::port_not_found( - "Port was not found at {index}!", - )); - }; - - let MidiInputState::Listening(listener) = self.state.take().unwrap() else { - unreachable!() - }; - - self.state = Some(MidiInputState::Active( - MidiInputConnection::new(listener, port, self.settings.port_name).unwrap(), - )); - Ok(()) - } - - /// A method you should call if [`MidiInput::is_listening`] and [`MidiInput::is_active`] are both false. - pub fn reset(&mut self) { - let listener = match midir::MidiInput::new(self.settings.client_name) { - Ok(input) => input, - Err(e) => { - error!("Failed to reset listening state! {e:?}"); - return; - } - }; - self.state = Some(MidiInputState::Listening(listener)); - } - /// Attempts to connects to the passed port - /// - /// # Errors - /// - If already connected to a device - /// - An input connection cannot be established - pub fn connect_to_port(&mut self, port: &MidiInputPort) -> Result<(), MidiInputError> { - if self - .state - .as_ref() - .is_none_or(|s| matches!(s, MidiInputState::Active(_))) - { - return Err(MidiInputError::invalid( - "Cannot connect: not currently active!", - )); - } - let MidiInputState::Listening(listener) = self.state.take().unwrap() else { - unreachable!() - }; - - self.state = Some(MidiInputState::Active( - MidiInputConnection::new(listener, port, self.settings.port_name).unwrap(), - )); - Ok(()) - } - /// Attempts to connects to the passed port - /// - /// # Errors - /// - If already connected to a device - /// - If the port ID cannot be currently found - /// - Note that this case can occur if you have not refreshed ports - /// and the device is no longer available. - /// - An input connection cannot be established - pub fn connect_to_id(&mut self, id: String) -> Result<(), MidiInputError> { - if self - .state - .as_ref() - .is_none_or(|s| matches!(s, MidiInputState::Active(_))) - { - return Err(MidiInputError::invalid( - "Cannot connect: not currently active!", - )); - } - let MidiInputState::Listening(listener) = self.state.take().unwrap() else { - unreachable!() - }; - let Some(port) = listener.find_port_by_id(id.clone()) else { - return Err(MidiInputError::port_not_found(id)); - }; - self.state = Some(MidiInputState::Active( - MidiInputConnection::new(listener, &port, self.settings.port_name).unwrap(), - )); - Ok(()) - } - /// True if a device is currently connected - pub fn is_active(&self) -> bool { - self.state - .as_ref() - .is_some_and(|s| matches!(s, MidiInputState::Active(_))) - } - - /// True if input is waiting to connect to a device - pub fn is_listening(&self) -> bool { - self.state - .as_ref() - .is_some_and(|s| matches!(s, MidiInputState::Listening(_))) - } - - /// Refreshes the available port list - /// - /// Does nothing if [`MidiInput::is_active`] is true - pub fn refresh_ports(&mut self) { - let Some(MidiInputState::Listening(listener)) = &self.state else { - return; - }; - self.ports = listener.ports(); - } - - /// Disconnects from the active device - /// - /// Does nothing if the [`MidiInput::is_listening`] is true. - pub fn disconnect(&mut self) { - if self - .state - .as_ref() - .is_none_or(|s| matches!(s, MidiInputState::Listening(_))) - { - return; - } - let MidiInputState::Active(conn) = self.state.take().unwrap() else { - unreachable!() - }; - let listener = conn.close(); - self.state = Some(MidiInputState::Listening(listener)); - } - - /// will return data if connected. Note, this CONSUMES the event. - /// - /// You will need to propagate this data out to other systems if need be. - pub fn read(&self) -> Result { - let Some(MidiInputState::Active(conn)) = &self.state else { - return Err(TryRecvError::Disconnected); - }; - conn.read() - } -} diff --git a/src/bevy/input/plugin.rs b/src/bevy/input/plugin.rs deleted file mode 100644 index 59a1258..0000000 --- a/src/bevy/input/plugin.rs +++ /dev/null @@ -1,21 +0,0 @@ -use crate::bevy::settings::MidiSettings; - -use super::MidiInput; -use bevy::prelude::*; - -#[doc = r#" -Inserts [`MidiInput`] as a resource. - -See [`MidiSettings`] for configuration options. -"#] -#[derive(Clone, Copy, Debug, Default)] -pub struct MidiInputPlugin { - /// The settings to apply to [`MidiInput`] on instantiation. - pub settings: MidiSettings, -} - -impl Plugin for MidiInputPlugin { - fn build(&self, app: &mut App) { - app.insert_resource(MidiInput::new(self.settings)); - } -} diff --git a/src/bevy/mod.rs b/src/bevy/mod.rs deleted file mode 100644 index b923042..0000000 --- a/src/bevy/mod.rs +++ /dev/null @@ -1,150 +0,0 @@ -#![doc = r#" -Bevy plugin that uses [`midix`](https://crates.io/crates/midix), -[`midir`](https://github.com/Boddlnagg/midir), and a [`rustysynth`](https://github.com/sinshu/rustysynth) fork to play midi sounds! - -Read from MIDI devices, MIDI files, and programmable input, and output to user audio with a soundfont! - -## Features -- Enable `web` for WASM compatibility - -## Example -```rust, no_run -use std::time::Duration; -use bevy_platform::prelude::*;use bevy::{ - log::{Level, LogPlugin}, - prelude::*, -}; -use midix::prelude::*; -fn main() { - App::new() - .add_plugins(( - DefaultPlugins.set(LogPlugin { - level: Level::INFO, - ..default() - }), - MidiPlugin { - input: None, - ..Default::default() - }, - )) - .add_systems(Startup, load_sf2) - .add_systems(Update, scale_me) - .run(); -} -/// Take a look here for some soundfonts: -/// -/// -fn load_sf2(asset_server: Res, mut synth: ResMut) { - synth.use_soundfont(asset_server.load("soundfont.sf2")); -} - -struct Scale { - timer: Timer, - current_key: Key, - note_on: bool, - forward: bool, - incremented_by: u8, - max_increment: u8, -} - -impl Scale { - pub fn calculate_next_key(&mut self) { - if self.forward { - if self.incremented_by == self.max_increment { - self.forward = false; - self.incremented_by -= 1; - self.current_key -= 1; - } else { - self.incremented_by += 1; - self.current_key += 1; - } - } else if self.incremented_by == 0 { - self.forward = true; - self.incremented_by += 1; - self.current_key += 1; - } else { - self.incremented_by -= 1; - self.current_key -= 1; - } - } -} - -impl Default for Scale { - fn default() -> Self { - let timer = Timer::new(Duration::from_millis(200), TimerMode::Repeating); - Scale { - timer, - current_key: Key::new(Note::C, Octave::new(2)), - note_on: true, - forward: true, - incremented_by: 0, - max_increment: 11, - } - } -} - -fn scale_me(synth: Res, time: Res