diff --git a/audio/src/program.rs b/audio/src/program.rs index ba5ab4e..9b57173 100644 --- a/audio/src/program.rs +++ b/audio/src/program.rs @@ -23,11 +23,11 @@ pub struct Program { /// The name of the preset. pub preset_name: String, #[serde(skip)] - pub(crate) chorus: f32, + pub chorus: f32, #[serde(skip)] - pub(crate) pan: f32, + pub pan: f32, #[serde(skip)] - pub(crate) reverb: f32, + pub reverb: f32, } impl Clone for Program { diff --git a/common/src/effect.rs b/common/src/effect.rs index 7a40384..fced40b 100644 --- a/common/src/effect.rs +++ b/common/src/effect.rs @@ -1,5 +1,5 @@ -mod effect_type; -pub use effect_type::EffectType; +pub(crate) mod effect_type; +pub(crate) mod valueless_effect_type; use serde::{Deserialize, Serialize}; use std::cmp::Ordering; @@ -10,7 +10,7 @@ pub struct Effect { /// The time of the event in PPQ. pub time: u64, /// The type of effect. - pub effect: EffectType, + pub effect: effect_type::EffectType, } impl Ord for Effect { diff --git a/common/src/effect/effect_type.rs b/common/src/effect/effect_type.rs index ce7050f..3da0383 100644 --- a/common/src/effect/effect_type.rs +++ b/common/src/effect/effect_type.rs @@ -27,14 +27,57 @@ pub enum EffectType { } impl EffectType { - /// Returns true if the effect values are valid. - pub fn valid(&self) -> bool { + pub fn increment(&mut self) -> bool { match self { - Self::Reverb(value) | Self::Chorus(value) => *value < 1000, - Self::Pan(value) => *value >= -500 && *value <= 500, - Self::PitchBend(value) => *value <= 16383, - Self::ChannelPressure(value) => *value <= 127, - Self::PolyphonicKeyPressure { key, value } => *key <= 127 && *value <= 127, - } + Self::Reverb(value) | Self::Chorus(value) | Self::PitchBend(value) => { + if *value > 0 { + *value -= 1; + return true + } + }, + Self::Pan(value) => { + if *value > -500 { + *value -= 1; + return true + } + } + Self::ChannelPressure(value) | Self::PolyphonicKeyPressure { key: _, value } => { + if *value > 0 { + *value -= 1; + return true + } + } + } + false + } + + pub fn decrement(&mut self) -> bool { + match self { + Self::Reverb(value) | Self::Chorus(value) => { + if *value < 1000 { + *value += 1; + return true + } + }, + Self::Pan(value) => { + if *value < 500 { + *value += 1; + return true + } + } + Self::PitchBend(value) => { + if *value < 16383 { + *value += 1; + return true + } + } + Self::ChannelPressure(value) | Self::PolyphonicKeyPressure { key: _, value } => { + if *value < 127 { + *value += 1; + return true + } + } + } + false } } diff --git a/common/src/effect/valueless_effect_type.rs b/common/src/effect/valueless_effect_type.rs new file mode 100644 index 0000000..83662b9 --- /dev/null +++ b/common/src/effect/valueless_effect_type.rs @@ -0,0 +1,40 @@ +use serde::{Deserialize, Serialize}; +use super::effect_type::EffectType; + +/// A hashable EffectType. +#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, Hash, Deserialize, Serialize)] +pub enum ValuelessEffectType { + #[default] + Chorus, + Reverb, + Pan, + PitchBend, + ChannelPressure, + PolyphonicKeyPressure, +} + +impl From for ValuelessEffectType { + fn from(value: EffectType) -> Self { + match value { + EffectType::Chorus(_) => Self::Chorus, + EffectType::Reverb(_) => Self::Reverb, + EffectType::Pan(_) => Self::Pan, + EffectType::PitchBend(_) => Self::PitchBend, + EffectType::ChannelPressure(_) => Self::ChannelPressure, + EffectType::PolyphonicKeyPressure { key: _, value: _ } => Self::PolyphonicKeyPressure, + } + } +} + +impl ValuelessEffectType { + pub fn eq(&self, value: &EffectType) -> bool { + match value { + EffectType::Chorus(_) => *self == Self::Chorus, + EffectType::Reverb(_) => *self == Self::Reverb, + EffectType::Pan(_) => *self == Self::Pan, + EffectType::PitchBend(_) => *self == Self::PitchBend, + EffectType::ChannelPressure(_) => *self == Self::ChannelPressure, + EffectType::PolyphonicKeyPressure { key: _, value: _ } => *self == Self::PolyphonicKeyPressure, + } + } +} \ No newline at end of file diff --git a/common/src/lib.rs b/common/src/lib.rs index f2b9da4..d6e9fc3 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -27,7 +27,7 @@ mod paths_state; mod state; pub mod time; pub mod view; -pub use effect::{Effect, EffectType}; +pub use effect::{Effect, effect_type::EffectType, valueless_effect_type::ValuelessEffectType}; pub use event::Event; pub use index::Index; mod indexed_values; @@ -35,7 +35,7 @@ pub use indexed_values::IndexedValues; pub use input_state::InputState; pub use midi_track::MidiTrack; pub use music::*; -pub use note::{Note, MAX_NOTE, MIN_NOTE, NOTE_NAMES}; +pub use note::{Note, MAX_NOTE, MIN_NOTE, NOTE_NAMES, MIDDLE_C}; pub use panel_type::PanelType; pub use paths::Paths; pub use state::State; diff --git a/common/src/panel_type.rs b/common/src/panel_type.rs index 0387656..49e934d 100644 --- a/common/src/panel_type.rs +++ b/common/src/panel_type.rs @@ -12,4 +12,5 @@ pub enum PanelType { ExportSettings, Quit, Links, + Effects, } diff --git a/common/src/state.rs b/common/src/state.rs index 5781bb6..c8634be 100644 --- a/common/src/state.rs +++ b/common/src/state.rs @@ -1,3 +1,4 @@ +use crate::ValuelessEffectType; use crate::music_panel_field::MusicPanelField; use crate::{ EditMode, Index, IndexedEditModes, IndexedValues, InputState, Music, PanelType, PianoRollMode, @@ -30,6 +31,7 @@ pub struct State { pub edit_mode: IndexedEditModes, /// The current selection. pub selection: Selection, + pub effect_types: IndexedValues, /// If true, there are unsaved changes. #[serde(skip_serializing, skip_deserializing)] pub unsaved_changes: bool, @@ -54,6 +56,17 @@ impl State { let piano_roll_mode = PianoRollMode::Time; let edit_mode = EditMode::indexed(); let selection = Selection::default(); + let effect_types = IndexedValues::new( + 0, + [ + ValuelessEffectType::Chorus, + ValuelessEffectType::Pan, + ValuelessEffectType::Reverb, + ValuelessEffectType::PitchBend, + ValuelessEffectType::ChannelPressure, + ValuelessEffectType::PolyphonicKeyPressure, + ], + ); Self { music, view, @@ -65,6 +78,7 @@ impl State { piano_roll_mode, edit_mode, selection, + effect_types, unsaved_changes: false, } } diff --git a/data/config.ini b/data/config.ini index 81f4660..f3f26fa 100644 --- a/data/config.ini +++ b/data/config.ini @@ -208,6 +208,13 @@ DiscordUrl = {"keys": ["2"]} GitHubUrl = {"keys": ["3"]} CloseLinksPanel = {"keys": ["Escape"]} +# Effects. +NextEffect = {"keys": ["Down"], "dt": 10} +PreviousEffect = {"keys": ["Up"], "dt": 10} +IncrementEffectValue = {"keys": ["Right"], "dt": 2} +DecrementEffectValue = {"keys": ["Left"], "dt": 2} +DeleteEffect = {"keys": ["Delete"]} + # Qwerty note input. C = {"keys": ["A"]} CSharp = {"keys": ["Q"]} diff --git a/input/src/input_event.rs b/input/src/input_event.rs index 0617ad1..b1f705d 100644 --- a/input/src/input_event.rs +++ b/input/src/input_event.rs @@ -130,6 +130,12 @@ pub enum InputEvent { DiscordUrl, GitHubUrl, CloseLinksPanel, + // Effects panel. + NextEffect, + PreviousEffect, + IncrementEffectValue, + DecrementEffectValue, + DeleteEffect, // Qwerty note input. C, CSharp, diff --git a/io/src/effects_panel.rs b/io/src/effects_panel.rs new file mode 100644 index 0000000..dbaea32 --- /dev/null +++ b/io/src/effects_panel.rs @@ -0,0 +1,115 @@ +use crate::panel::*; +use common::{EffectType, Effect, ValuelessEffectType, MIDDLE_C}; +use text::EFFECT_NAME_KEYS; + +#[derive(Default)] +pub(crate) struct EffectsPanel { +} + +impl EffectsPanel { + /// Increment or decrement the effect type index. + fn cycle_effect_type(state: &mut State, up: bool) -> Option { + let s0 = state.clone(); + state.effect_types.index.increment(up); + Some(Snapshot::from_states(s0, state)) + } + + fn increment_effect_value(state: &mut State, conn: &Conn, up: bool) -> Option { + let s0 = state.clone(); + let ve = state.effect_types.get(); + match state.music.get_selected_track_mut() { + Some(track) => match track.effects.iter_mut().filter(|e| e.time == state.time.cursor && ve.eq(&e.effect)).next() { + Some(effect) => { + if (up && effect.effect.increment()) || (!up && effect.effect.decrement()) { + Some(Snapshot::from_states(s0, state)) + } + else { + None + } + } + // Add a new effect. + None => { + let program = &conn.state.programs[&track.channel]; + // Get a new effect type. + let effect_type = match ve { + ValuelessEffectType::Chorus => EffectType::Chorus(program.chorus as u16), + ValuelessEffectType::Pan => EffectType::Pan(program.pan as i16), + ValuelessEffectType::Reverb => EffectType::Reverb(program.reverb as u16), + ValuelessEffectType::PitchBend => EffectType::PitchBend(0), + ValuelessEffectType::ChannelPressure => EffectType::ChannelPressure(0), + ValuelessEffectType::PolyphonicKeyPressure => EffectType::PolyphonicKeyPressure { key: MIDDLE_C, value: 0 } + }; + // Get a new effect. + track.effects.push(Effect { time: state.time.cursor, effect: effect_type }); + Some(Snapshot::from_states(s0, state)) + } + } + None => None + } + } +} + +impl Panel for EffectsPanel { + fn update( + &mut self, + state: &mut State, + conn: &mut Conn, + input: &Input, + tts: &mut TTS, + text: &Text, + _: &mut PathsState, + ) -> Option { + // Cycle the selected input event. + if input.happened(&InputEvent::NextEffect) { + Self::cycle_effect_type(state, true) + } + else if input.happened(&InputEvent::PreviousEffect) { + Self::cycle_effect_type(state, false) + } + else if input.happened(&InputEvent::IncrementEffectValue) { + Self::increment_effect_value(state, conn, true) + } + else if input.happened(&InputEvent::DecrementEffectValue) { + Self::increment_effect_value(state, conn, false) + } + else if input.happened(&InputEvent::DeleteEffect) { + let s0 = state.clone(); + let ve = state.effect_types.get(); + match state.music.get_selected_track_mut() { + Some(track) => { + let has_effect = track.effects.iter().filter(|e| e.time == state.time.cursor && ve.eq(&e.effect)).next().is_some(); + track.effects.retain(|e| e.time != state.time.cursor || !ve.eq(&e.effect)); + if has_effect { + Some(Snapshot::from_states(s0, state)) + } + else { + None + } + } + None => None + } + } + else { + None + } + } + + fn on_disable_abc123(&mut self, _: &mut State, _: &mut Conn) {} + + fn update_abc123( + &mut self, + _: &mut State, + _: &Input, + _: &mut Conn, + ) -> (Option, bool) { + (None, false) + } + + fn allow_alphanumeric_input(&self, _: &State, _: &Conn) -> bool { + false + } + + fn allow_play_music(&self) -> bool { + true + } +} \ No newline at end of file diff --git a/io/src/lib.rs b/io/src/lib.rs index 9d63794..8392e83 100644 --- a/io/src/lib.rs +++ b/io/src/lib.rs @@ -50,6 +50,7 @@ mod abc123; mod export_settings_panel; mod quit_panel; use quit_panel::QuitPanel; +mod effects_panel; mod links_panel; mod popup; use links_panel::LinksPanel; diff --git a/text/src/lib.rs b/text/src/lib.rs index 448bd17..5bbf642 100644 --- a/text/src/lib.rs +++ b/text/src/lib.rs @@ -12,7 +12,9 @@ use std::path::Path; pub use value_map::ValueMap; mod tts_string; use common::config::parse; -use common::{EditMode, EffectType, Event, Paths, PianoRollMode, Time, MIN_NOTE, PPQ_F, PPQ_U}; +use common::{ + EditMode, Effect, EffectType, Event, Paths, PianoRollMode, Time, MIN_NOTE, PPQ_F, PPQ_U, +}; use csv::Reader; use hashbrown::HashMap; use ini::Ini; @@ -147,6 +149,15 @@ const KEYCODE_LOOKUPS: [&str; 121] = [ "Menu", "Unknown", ]; +/// The lookup keys of the name of each effect type. +pub const EFFECT_NAME_KEYS: [&str; 6] = [ + "EFFECT_TYPE_CHORUS", + "EFFECT_TYPE_PAN", + "EFFECT_TYPE_REVERB", + "EFFECT_TYPE_PITCH_BEND", + "EFFECT_TYPE_CHANNEL_PRESSURE", + "EFFECT_TYPE_POLYPHONIC_KEY_PRESSURE", +]; type TextMap = HashMap; @@ -337,22 +348,37 @@ impl Text { self.get_with_values("ERROR", &[error]) } + /// Returns the name of a note or event. pub fn get_event_name(&self, event: &Event<'_>) -> String { match event { - Event::Effect { effect, index: _ } => self.get(match effect.effect { - EffectType::Chorus(_) => "EFFECT_TYPE_CHORUS", - EffectType::Pan(_) => "EFFECT_TYPE_PAN", - EffectType::Reverb(_) => "EFFECT_TYPE_REVERB", - EffectType::PitchBend(_) => "EFFECT_TYPE_PITCH_BEND", - EffectType::ChannelPressure(_) => "EFFECT_TYPE_CHANNEL_PRESSURE", - EffectType::PolyphonicKeyPressure { key: _, value: _ } => { - "EFFECT_TYPE_POLYPHONIC_KEY_PRESSURE" - } - }), + Event::Effect { effect, index: _ } => self.get_effect_name(effect).to_string(), Event::Note { note, index: _ } => note.get_name().to_string(), } } + /// Returns the name of an effect type. + pub fn get_effect_name(&self, effect: &Effect) -> &str { + self.get_ref(match effect.effect { + EffectType::Chorus(_) => "EFFECT_TYPE_CHORUS", + EffectType::Pan(_) => "EFFECT_TYPE_PAN", + EffectType::Reverb(_) => "EFFECT_TYPE_REVERB", + EffectType::PitchBend(_) => "EFFECT_TYPE_PITCH_BEND", + EffectType::ChannelPressure(_) => "EFFECT_TYPE_CHANNEL_PRESSURE", + EffectType::PolyphonicKeyPressure { key: _, value: _ } => { + "EFFECT_TYPE_POLYPHONIC_KEY_PRESSURE" + } + }) + } + + /// Returns the width of the longest effect name. + pub fn get_effect_name_max_width(&self) -> usize { + *EFFECT_NAME_KEYS + .map(|s| self.text[s].len()) + .iter() + .max() + .unwrap() + } + /// Returns the name of the note. pub fn get_note_name(&self, note: u8) -> &str { &self.note_names[(note - MIN_NOTE) as usize]