microwave 0.33.0

Make xenharmonic music and explore musical tunings.
use std::{fmt::Debug, hash::Hash, mem};

use tune::{
    note::Note,
    pitch::{Pitch, Pitched},
    scala::{KbmRoot, Scl},
    tuner::{AotTuner, JitTuner, PoolingMode, TunableSynth},
    tuning::{Scale, Tuning},
};

use crate::keypress::{IllegalState, KeypressTracker, LiftAction, PlaceAction};

pub struct TunableBackend<K, S> {
    tuner: Tuner<K, S>,
}

impl<K, S: TunableSynth> TunableBackend<K, S> {
    pub fn new(synth: S) -> Self {
        Self {
            tuner: Tuner::Aot {
                aot_tuner: AotTuner::start(synth),
                keypress_tracker: KeypressTracker::new(),
            },
        }
    }
}

enum Tuner<K, S> {
    Destroyed,
    Jit {
        jit_tuner: JitTuner<K, S>,
    },
    Aot {
        aot_tuner: AotTuner<i32, S>,
        keypress_tracker: KeypressTracker<K, i32>,
    },
}

impl<K: Copy + Eq + Hash + Debug + Send, S: TunableSynth> TunableBackend<K, S>
where
    S::Result: Debug,
{
    pub fn set_tuning(&mut self, tuning: (&Scl, KbmRoot)) {
        let synth = self.destroy_tuning();

        let lowest_key = tuning
            .find_by_pitch_sorted(Note::from_midi_number(-1).pitch())
            .approx_value;

        let highest_key = tuning
            .find_by_pitch_sorted(Note::from_midi_number(128).pitch())
            .approx_value;

        let mut aot_tuner = AotTuner::start(synth);

        let tuning = Tuning::<i32>::as_linear_mapping(tuning);
        let keys = lowest_key..highest_key;

        match aot_tuner.set_tuning(tuning, keys) {
            Ok(required_channels) => {
                if !aot_tuner.tuned() {
                    eprintln!("[WARNING] Cannot apply tuning. The tuning requires {required_channels} channels");
                }
            }
            Err(err) => {
                eprintln!("[WARNING] Cannot apply tuning: {err:?}");
            }
        }

        self.tuner = Tuner::Aot {
            aot_tuner,
            keypress_tracker: KeypressTracker::new(),
        };
    }

    pub fn set_no_tuning(&mut self) {
        let synth = self.destroy_tuning();
        let jit_tuner = JitTuner::start(synth, PoolingMode::Stop);
        self.tuner = Tuner::Jit { jit_tuner };
    }

    pub fn is_tuned(&self) -> bool {
        match &self.tuner {
            Tuner::Destroyed => false,
            Tuner::Jit { .. } => true,
            Tuner::Aot { aot_tuner, .. } => aot_tuner.tuned(),
        }
    }

    pub fn start(&mut self, id: K, degree: i32, pitch: Pitch, velocity: S::NoteAttr) {
        match &mut self.tuner {
            Tuner::Destroyed => {}
            Tuner::Jit { jit_tuner } => {
                jit_tuner.note_on(id, pitch, velocity);
            }
            Tuner::Aot {
                keypress_tracker,
                aot_tuner,
            } => match keypress_tracker.place_finger_at(id, degree) {
                Ok(PlaceAction::KeyPressed) => {
                    aot_tuner.note_on(degree, velocity);
                }
                Ok(PlaceAction::KeyAlreadyPressed) => {
                    aot_tuner.note_off(degree, S::NoteAttr::default());
                    aot_tuner.note_on(degree, velocity);
                }
                Err(id) => {
                    eprintln!(
                        "[WARNING] Key with ID {:?} not lifted before pressed again",
                        id,
                    );
                }
            },
        }
    }

    pub fn update_pitch(&mut self, id: K, degree: i32, pitch: Pitch, velocity: S::NoteAttr) {
        match &mut self.tuner {
            Tuner::Destroyed => {}
            Tuner::Jit { jit_tuner } => {
                jit_tuner.note_pitch(id, pitch);
            }
            Tuner::Aot {
                keypress_tracker,
                aot_tuner,
            } => match keypress_tracker.move_finger_to(&id, degree) {
                Ok((LiftAction::KeyReleased(released), _)) => {
                    aot_tuner.note_off(released, S::NoteAttr::default());
                    aot_tuner.note_on(degree, velocity);
                }
                Ok((LiftAction::KeyRemainsPressed, PlaceAction::KeyPressed)) => {
                    aot_tuner.note_on(degree, velocity);
                }
                Ok((LiftAction::KeyRemainsPressed, PlaceAction::KeyAlreadyPressed)) => {}
                Err(IllegalState) => {}
            },
        }
    }

    pub fn update_pressure(&mut self, id: K, pressure: S::NoteAttr) {
        match &mut self.tuner {
            Tuner::Destroyed => {}
            Tuner::Jit { jit_tuner } => {
                jit_tuner.note_attr(id, pressure);
            }
            Tuner::Aot {
                keypress_tracker,
                aot_tuner,
            } => {
                if let Some(&location) = keypress_tracker.location_of(&id) {
                    aot_tuner.note_attr(location, pressure);
                }
            }
        }
    }

    pub fn stop(&mut self, id: K, velocity: S::NoteAttr) {
        match &mut self.tuner {
            Tuner::Destroyed => {}
            Tuner::Jit { jit_tuner } => {
                jit_tuner.note_off(id, velocity);
            }
            Tuner::Aot {
                keypress_tracker,
                aot_tuner,
            } => match keypress_tracker.lift_finger(&id) {
                Ok(LiftAction::KeyReleased(location)) => {
                    aot_tuner.note_off(location, velocity);
                }
                Ok(LiftAction::KeyRemainsPressed) => {}
                Err(IllegalState) => {}
            },
        }
    }

    pub fn send_monophonic_message(&mut self, message_type: S::GlobalAttr) {
        match &mut self.tuner {
            Tuner::Destroyed => {}
            Tuner::Jit { jit_tuner } => {
                jit_tuner.global_attr(message_type);
            }
            Tuner::Aot { aot_tuner, .. } => {
                aot_tuner.global_attr(message_type);
            }
        }
    }

    pub fn is_aot(&self) -> bool {
        match self.tuner {
            Tuner::Destroyed | Tuner::Jit { .. } => false,
            Tuner::Aot { .. } => true,
        }
    }

    fn destroy_tuning(&mut self) -> S {
        let mut tuner = Tuner::Destroyed;
        mem::swap(&mut tuner, &mut self.tuner);

        match tuner {
            Tuner::Destroyed => unreachable!("Tuning already destroyed"),
            Tuner::Jit { jit_tuner } => jit_tuner.stop(),
            Tuner::Aot {
                mut aot_tuner,
                keypress_tracker,
            } => {
                for pressed_key in keypress_tracker.pressed_locations() {
                    aot_tuner.note_off(pressed_key, S::NoteAttr::default());
                }
                aot_tuner.stop()
            }
        }
    }
}

#[cfg(test)]
mod tests {
    use std::{cell::RefCell, rc::Rc};

    use assert_approx_eq::assert_approx_eq;
    use tune::{note::NoteLetter, pitch::Ratio, tuner::GroupBy};

    use super::*;

    struct FakeSynth {
        state: Rc<RefCell<CapturedState>>,
    }

    impl TunableSynth for FakeSynth {
        type Result = ();

        type NoteAttr = ();

        type GlobalAttr = ();

        fn num_channels(&self) -> usize {
            8
        }

        fn group_by(&self) -> GroupBy {
            GroupBy::Channel
        }

        fn notes_detune(
            &mut self,
            channel: usize,
            detuned_notes: &[(Note, Ratio)],
        ) -> Self::Result {
            self.state
                .borrow_mut()
                .notes_detunes
                .push((channel, detuned_notes.to_vec()));
        }

        fn note_on(
            &mut self,
            channel: usize,
            started_note: Note,
            _attr: Self::NoteAttr,
        ) -> Self::Result {
            self.state
                .borrow_mut()
                .note_ons
                .push((channel, started_note));
        }

        fn note_off(
            &mut self,
            _channel: usize,
            _stopped_note: Note,
            _attr: Self::NoteAttr,
        ) -> Self::Result {
        }

        fn note_attr(
            &mut self,
            _channel: usize,
            _affected_note: Note,
            _attr: Self::NoteAttr,
        ) -> Self::Result {
        }

        fn global_attr(&mut self, _attr: Self::GlobalAttr) -> Self::Result {}
    }

    #[derive(Default)]
    struct CapturedState {
        notes_detunes: Vec<(usize, Vec<(Note, Ratio)>)>,
        note_ons: Vec<(usize, Note)>,
    }

    #[test]
    fn tunable_backend_conserve_order_of_scale_items() {
        let state = Rc::new(RefCell::new(CapturedState::default()));
        let synth = FakeSynth {
            state: state.clone(),
        };

        let mut backend = TunableBackend::<usize, _>::new(synth);
        let (scl, kbm) = create_non_monotonous_tuning();

        backend.set_tuning((&scl, kbm));
        let fake_pitch = Pitch::from_hz(0.0);
        backend.start(0, 1, fake_pitch, ());
        backend.start(1, 2, fake_pitch, ());
        backend.start(2, 3, fake_pitch, ());
        backend.start(3, 4, fake_pitch, ());
        backend.start(4, 5, fake_pitch, ());

        let state = state.borrow();

        for ((channel, note_detunes), (expected_channel, expected_detune_cents)) in
            state.notes_detunes.iter().zip([(0, 0.0), (1, 50.0)])
        {
            assert_eq!(channel, &expected_channel);
            assert_eq!(note_detunes.len(), 1);
            assert_approx_eq!(note_detunes[0].1.as_cents(), expected_detune_cents);
        }

        assert_eq!(
            state.note_ons,
            [
                (0, Note::from_midi_number(63)),
                (0, Note::from_midi_number(64)),
                (1, Note::from_midi_number(63)),
                (0, Note::from_midi_number(65)),
                (0, Note::from_midi_number(66)),
            ]
        );
    }

    fn create_non_monotonous_tuning() -> (Scl, KbmRoot) {
        let scl = Scl::builder()
            .push_cents(100.0)
            .push_cents(200.0)
            .push_cents(150.0)
            .push_cents(300.0)
            .build()
            .unwrap();
        let kbm_root = KbmRoot::from(NoteLetter::D.in_octave(4));
        (scl, kbm_root)
    }
}