From a21fa2641fec0b1cd17012eb86f871dc3d3216c0 Mon Sep 17 00:00:00 2001 From: AlexanderARodin Date: Tue, 2 Jan 2024 04:18:02 +0300 Subject: [PATCH] added prelaminar MidiAudio and dependent --- src/base_domik_view.rs | 19 +- src/main.rs | 1 + src/midi_audio/audio_device_parameters.rs | 36 ++++ src/midi_audio/mod.rs | 96 ++++++++- src/midi_audio/render_holder.rs | 67 +++++++ src/midi_lib/midi_message.rs | 230 ++++++++++++++++++++++ src/midi_lib/midi_receiver.rs | 5 + src/midi_lib/midi_sequence.rs | 201 +++++++++++++++++++ src/midi_lib/mod.rs | 8 + src/root_app.rs | 2 +- 10 files changed, 647 insertions(+), 18 deletions(-) create mode 100644 src/midi_audio/audio_device_parameters.rs create mode 100644 src/midi_audio/render_holder.rs create mode 100644 src/midi_lib/midi_message.rs create mode 100644 src/midi_lib/midi_receiver.rs create mode 100644 src/midi_lib/midi_sequence.rs create mode 100644 src/midi_lib/mod.rs diff --git a/src/base_domik_view.rs b/src/base_domik_view.rs index b37a718..0e7eebc 100644 --- a/src/base_domik_view.rs +++ b/src/base_domik_view.rs @@ -2,6 +2,7 @@ const VERS: &str = "v0.0.3"; use crate::raadbg::log; +use crate::midi_audio::MidiAudio; pub struct BaseDomikView { pub title: String, @@ -19,18 +20,20 @@ impl BaseDomikView { pressed: false, } } - pub fn updateUI(&mut self, ui: &mut egui::Ui, example_text: &mut String) { + pub fn updateUI(&mut self, ui: &mut egui::Ui, midi_audio: &mut MidiAudio) { ui.label( format!("DoMiK {}", VERS) ); + ui.separator(); + ui.label( format!("device status: [active = {}]", midi_audio.is_active()) ); ui.horizontal( |ui| { - let btn = ui.button( "try to ??? TEXT" ); - ui.label( format!(" <{}>", self.pressed) ); - if btn.clicked(){ - log::simple("clicked with PRESSURE!!!"); - self.pressed = true; + let btn = ui.button("start"); + if btn.clicked() { + let _res = midi_audio.start(); + } + let btnStop = ui.button("stop"); + if btnStop.clicked() { + midi_audio.stop(); } }); - ui.text_edit_singleline(example_text); ui.separator(); - ui.label( format!("just edited: [{}]", example_text) ); } } diff --git a/src/main.rs b/src/main.rs index f19f0dd..880ab04 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,6 +3,7 @@ mod log_view; mod root_app; mod midi_audio; +mod midi_lib; use root_app::RootApp; mod base_domik_view; diff --git a/src/midi_audio/audio_device_parameters.rs b/src/midi_audio/audio_device_parameters.rs new file mode 100644 index 0000000..85cd223 --- /dev/null +++ b/src/midi_audio/audio_device_parameters.rs @@ -0,0 +1,36 @@ +use tinyaudio::prelude::OutputDeviceParameters; + +use crate::raadbg::log; + +pub struct AudioDeviceParameters { + pub sample_rate: usize, + pub block_size: usize, + pub blocks_count: usize, +} + +impl AudioDeviceParameters { + pub fn new() -> Self { + Self { + sample_rate: 44100, + block_size: 441, + blocks_count: 8 + } + } + pub fn get_output_device_parameters(&self) -> OutputDeviceParameters { + OutputDeviceParameters{ + sample_rate: self.sample_rate, + channels_count: 2, + channel_sample_count: self.block_size * self.blocks_count + } + } + pub fn get_tick_time(&self) -> f32 { + let res = 2. * (self.block_size as f32) / (self.sample_rate as f32); + log::simple( format!("tick time = {res}").as_str() ); + return res; + } +} +impl Default for AudioDeviceParameters { + fn default() -> Self { + Self::new() + } +} diff --git a/src/midi_audio/mod.rs b/src/midi_audio/mod.rs index a4b949e..3721959 100644 --- a/src/midi_audio/mod.rs +++ b/src/midi_audio/mod.rs @@ -1,24 +1,33 @@ use std::error::Error; -use tinyaudio::prelude::BaseAudioOutputDevice; - +use std::sync::{Arc,Mutex}; +use tinyaudio::prelude::{BaseAudioOutputDevice,run_output_device}; use crate::raadbg::log; +//use crate::midi_lib::MidiMessage; + +mod audio_device_parameters; +use audio_device_parameters::AudioDeviceParameters; +mod render_holder; +use render_holder::RenderHolder; +pub use render_holder::SoundRender as SoundRender; // // // // // // // // -// core +// CORE // // // // // // // // pub struct MidiAudio { - device: Option< - Box< dyn BaseAudioOutputDevice> - >, + params: AudioDeviceParameters, + device: Option< Box< dyn BaseAudioOutputDevice> >, + render_holder: Arc>, } impl MidiAudio { pub fn new() -> Self { let res = Self { + params: Default::default(), device: None, + render_holder: RenderHolder::new_arc_mutex(), }; log::create("MidiAudio"); return res; @@ -45,9 +54,8 @@ impl MidiAudio { }else{ log::info("MidiAudio", "starting"); } - Ok(()) - //self.refresh_tick_time(); - //self.run_device_loop() + self.refresh_tick_time(); + self.run_device_loop() } pub fn stop(&mut self) { self.device = None; @@ -68,4 +76,74 @@ impl MidiAudio { // internal interface // // // // // // // // +impl MidiAudio { + fn run_device_loop(&mut self) -> Result< (), Box> { + let params = self.params.get_output_device_parameters(); + let render_holder_clone = self.render_holder.clone(); + + let device = run_output_device( params, { + let render_holder = render_holder_clone; + let block_chunk = 2*self.params.block_size; + let mut left :Vec = vec![ 0_f32; self.params.block_size ]; + let mut right:Vec = vec![ 0_f32; self.params.block_size ]; + move |data: &mut [f32]| { + let mut render_holder_lock = render_holder.lock() + .expect("panic on locking render_holder_lock"); + for chunk in data.chunks_mut(block_chunk) { + render_holder_lock.render( &mut left, &mut right ); + for (i, l_sample) in left.iter().enumerate() { + chunk[i*2] = *l_sample; + chunk[i*2 + 1] = right[i]; + } + } + } + }); + + match device { + Err(e) => { + let errmsg = format!("{:?}",e); + log::error("MidiAudio", &errmsg); + return Err(e) + }, + Ok(running_device) => self.device = Some(running_device), + } + Ok(()) + } + + fn refresh_tick_time(&self) { + let mut holder_lock = self.render_holder.lock() + .expect("panic on lockin holder_lock"); + holder_lock.tick_time = self.params.get_tick_time(); + } +} + + +// // // // // // // // +// TESTS +// // // // // // // // + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn create_inactive() { + let audio = MidiAudio::new(); + assert!(!audio.is_active()); + } + #[test] + fn start_active() { + let mut audio = MidiAudio::new(); + let _ = audio.start(); + assert!(audio.is_active()); + } + #[test] + fn start_stop() { + let mut audio = MidiAudio::new(); + let _ = audio.start(); + assert!(audio.is_active()); + audio.stop(); + assert!(!audio.is_active()); + } +} diff --git a/src/midi_audio/render_holder.rs b/src/midi_audio/render_holder.rs new file mode 100644 index 0000000..07e392c --- /dev/null +++ b/src/midi_audio/render_holder.rs @@ -0,0 +1,67 @@ +use std::sync::{Arc,Mutex}; +use crate::raadbg::log; +use super::super::midi_lib::{MidiMessage,MidiReceiver,MidiSequence}; + + +pub trait SoundRender: MidiReceiver + Sync + Send { + fn render(&mut self, left: &mut [f32], right: &mut [f32]); + fn get_as_midi_receiver(&mut self) -> &mut dyn MidiReceiver; +} + +pub(crate) struct RenderHolder { + test_seq: MidiSequence, + pub(crate) tick_time: f32, + pub(crate) sound_render: Option< Arc> >, +} +impl RenderHolder { + pub fn new_arc_mutex() -> Arc> { + Arc::new(Mutex::new( Self::new() )) + } + pub fn new() -> Self { + let mut seq = MidiSequence::new(); + seq.push( 0.0, &MidiMessage::NoteOn( 1,90,80) ); + seq.push( 0.5, &MidiMessage::NoteOff(1,90,80) ); + seq.push( 0., &MidiMessage::NoteOn( 1,91,80) ); + seq.push( 0.5, &MidiMessage::NoteOff(1,91,80) ); + seq.push( 0., &MidiMessage::NoteOn( 1,92,80) ); + seq.push( 1., &MidiMessage::NoteOff(1,92,80) ); + seq.push( 1., &MidiMessage::NoteOff(1,92,80) ); + let res = Self{ + test_seq: seq, + tick_time: 0., + sound_render: None + }; + log::create("RenderHolder"); + return res; + } + + pub fn render(&mut self, left: &mut [f32], right: &mut [f32]) { + match &self.sound_render { + None => { + for sample in left { + *sample = 0_f32; + } + for sample in right { + *sample = 0_f32; + } + }, + Some(sound_render) => { + let mut locked_sound_render = sound_render.lock() + .expect("FATAL: can't lock SoundRender!"); + let midi_recevier: &mut dyn MidiReceiver = locked_sound_render.get_as_midi_receiver(); + self.test_seq.send_next_sequence( self.tick_time, midi_recevier ); + locked_sound_render.render(left, right); + if self.test_seq.is_finished() { + self.test_seq.restart(); + } + } + } + } +} + +impl Drop for RenderHolder { + fn drop(&mut self) { + log::on_drop("RenderHolder"); + } +} + diff --git a/src/midi_lib/midi_message.rs b/src/midi_lib/midi_message.rs new file mode 100644 index 0000000..e047bbd --- /dev/null +++ b/src/midi_lib/midi_message.rs @@ -0,0 +1,230 @@ + +pub struct MidiGeneral { + pub channel: i32, + pub command: i32, + pub data1: i32, + pub data2: i32 +} +impl Clone for MidiGeneral { + fn clone(&self) -> Self { + Self { + channel: self.channel, + command: self.command, + data1: self.data1, + data2: self.data2 + } + } +} + +pub enum MidiMessage { + General( MidiGeneral ), // channel, command, data1, data2 + NoteOn( i32, i32, i32 ), // channel, 0x90, key, velocity + NoteOff( i32, i32, i32 ), // channel, 0x80, key, velocity +} + +impl MidiMessage { + #[allow(dead_code)] + pub fn new(channel:i32, command:i32, data1:i32, data2:i32) -> Self { + Self::General( MidiGeneral { + channel, + command, + data1, + data2 + }) + } + pub fn from_midi_general( midi_general: &MidiGeneral ) -> Self { + Self::General( midi_general.clone() ) + } + #[allow(dead_code)] + pub fn to_general(&self) -> Self { + let midi_general = self.to_midi_general(); + Self::from_midi_general( &midi_general ) + } + + pub fn to_midi_general(&self) -> MidiGeneral { + match self { + Self::General( midi_general ) => { + midi_general.clone() + }, + Self::NoteOn( channel, key, velocity ) => { + MidiGeneral { + channel: *channel, + command: 0x90, + data1: *key, + data2: *velocity + } + }, + Self::NoteOff( channel, key, velocity) => { + MidiGeneral { + channel: *channel, + command: 0x80, + data1: *key, + data2: *velocity + } + }, + } + } + + pub fn get_parsed(&self) -> Self { + match self { + Self::General( midi_general ) => { + match midi_general.command { + 0x80 => { + Self::NoteOff( midi_general.channel, + midi_general.data1, + midi_general.data2 ) + }, + 0x90 => { + Self::NoteOn( midi_general.channel, + midi_general.data1, + midi_general.data2 ) + }, + _ => { + Self::General( midi_general.clone() ) + } + } + }, + Self::NoteOn( channel, key, velocity ) => { + Self::NoteOn( *channel, *key, *velocity ) + }, + Self::NoteOff( channel, key, velocity) => { + Self::NoteOff( *channel, *key, *velocity) + }, + } + } + +} + +impl Clone for MidiMessage { + fn clone(&self) -> Self { + let midi_general = self.to_midi_general(); + MidiMessage::from_midi_general(&midi_general) + .get_parsed() + } +} + + + + + + +// // // // // // // // +// // // // // // // // +// // // // // // // // +// // // // // // // // +#[cfg(test)] +mod test{ + use super::MidiMessage; + + #[test] + fn note_on_2general() { + let src_midi_msg = MidiMessage::NoteOn(1, 2, 3 ); + let dst_midi_msg = src_midi_msg.to_general(); + match dst_midi_msg { + MidiMessage::General( midi ) => { + assert!( midi.channel == 1, "wrong channel" ); + assert!( midi.command == 0x90, "wrong command" ); + assert!( midi.data1 == 2, "wrong key" ); + assert!( midi.data2 == 3, "wrong velocity" ); + }, + _ => { + assert!(false, "incorrect conversion"); + } + } + } + #[test] + fn note_off_2general() { + let src_midi_msg = MidiMessage::NoteOff(1, 2, 3); + let dst_midi_msg = src_midi_msg.to_general(); + match dst_midi_msg { + MidiMessage::General( midi ) => { + assert!( midi.channel == 1, "wrong channel" ); + assert!( midi.command == 0x80, "wrong command" ); + assert!( midi.data1 == 2, "wrong key" ); + assert!( midi.data2 == 3, "wrong velocity" ); + }, + _ => { + assert!(false, "incorrect conversion"); + } + } + } + #[test] + fn general_2general() { + let src_midi_msg = MidiMessage::new(1, 2, 3, 4); + let dst_midi_msg = src_midi_msg.to_general(); + match dst_midi_msg { + MidiMessage::General( midi ) => { + assert!( midi.channel == 1, "wrong channel" ); + assert!( midi.command == 2, "wrong command" ); + assert!( midi.data1 == 3, "wrong data1" ); + assert!( midi.data2 == 4, "wrong data2" ); + }, + _ => { + assert!(false, "incorrect conversion"); + } + } + } + + #[test] + fn parse_note_on() { + let src_midi_msg = MidiMessage::NoteOn( 1, 2, 3); + let dst_midi_msg = src_midi_msg.get_parsed(); + match dst_midi_msg { + MidiMessage::NoteOn( channel, data1, data2 ) => { + assert!( channel == 1, "wrong channel" ); + assert!( data1 == 2, "wrong key" ); + assert!( data2 == 3, "wrong velocity" ); + }, + _ => { + assert!(false, "incorrect conversion"); + } + } + } + #[test] + fn parse_note_off() { + let src_midi_msg = MidiMessage::NoteOff( 1, 2, 3 ); + let dst_midi_msg = src_midi_msg.get_parsed(); + match dst_midi_msg { + MidiMessage::NoteOff( channel, data1, data2 ) => { + assert!( channel == 1, "wrong channel" ); + assert!( data1 == 2, "wrong key" ); + assert!( data2 == 3, "wrong velocity" ); + }, + _ => { + assert!(false, "incorrect conversion"); + } + } + } + #[test] + fn parse_general_2note_off() { + let src_midi_msg = MidiMessage::new( 1, 0x80, 3, 4); + let dst_midi_msg = src_midi_msg.get_parsed(); + match dst_midi_msg { + MidiMessage::NoteOff( channel, data1, data2 ) => { + assert!( channel == 1, "wrong channel" ); + assert!( data1 == 3, "wrong key" ); + assert!( data2 == 4, "wrong velocity" ); + }, + _ => { + assert!(false, "incorrect conversion"); + } + } + } + #[test] + fn parse_general_2note_on() { + let src_midi_msg = MidiMessage::new( 1, 0x90, 3, 4); + let dst_midi_msg = src_midi_msg.get_parsed(); + match dst_midi_msg { + MidiMessage::NoteOn( channel, data1, data2 ) => { + assert!( channel == 1, "wrong channel" ); + assert!( data1 == 3, "wrong key" ); + assert!( data2 == 4, "wrong velocity" ); + }, + _ => { + assert!(false, "incorrect conversion"); + } + } + } + +} + diff --git a/src/midi_lib/midi_receiver.rs b/src/midi_lib/midi_receiver.rs new file mode 100644 index 0000000..eb009f2 --- /dev/null +++ b/src/midi_lib/midi_receiver.rs @@ -0,0 +1,5 @@ + +pub trait MidiReceiver { + fn reset(&mut self); + fn process_midi_command(&mut self, channel: i32, command: i32, data1: i32, data2: i32); +} diff --git a/src/midi_lib/midi_sequence.rs b/src/midi_lib/midi_sequence.rs new file mode 100644 index 0000000..3b67fb7 --- /dev/null +++ b/src/midi_lib/midi_sequence.rs @@ -0,0 +1,201 @@ +use super::{MidiMessage,MidiReceiver}; + + +pub struct MidiSequence { + current_index: usize, + elapsed_time: f32, + list: Vec, +} + +impl MidiSequence { + #[allow(dead_code)] + pub fn new() -> Self { + Self { + current_index: 0, + elapsed_time: 0_f32, + list: Vec::new() + } + } + + #[allow(dead_code)] + pub fn push(&mut self, delay: f32, msg: &MidiMessage) { + let len = self.list.len(); + let prev_time:f32 = match len { + 0 => { + 0_f32 + }, + _ => { + self.list[len - 1].time + } + }; + let new_value = TimedMidiMessage::new(prev_time+delay, msg.clone() ); + self.list.push( new_value ); + } + #[allow(dead_code)] + pub fn restart(&mut self) { + self.current_index = 0; + self.elapsed_time = 0_f32; + } + + #[allow(dead_code)] + pub fn send_next_sequence(&mut self, tick_time: f32, receiver: &mut dyn MidiReceiver) { + self.elapsed_time += tick_time; + for (i, tm_msg) in self.list.iter().enumerate() { + if i < self.current_index { + continue; + } + if self.elapsed_time < tm_msg.time { + break; + } + let midi = tm_msg.midi_msg.to_midi_general(); + receiver.process_midi_command( midi.channel, + midi.command, + midi.data1, + midi.data2 ); + self.current_index += 1; + } + } + + #[allow(dead_code)] + pub fn is_finished(&self) -> bool { + self.current_index >= self.list.len() + } + +} + + +struct TimedMidiMessage { + time: f32, + midi_msg: MidiMessage, +} +impl TimedMidiMessage { + #[allow(dead_code)] + fn new(time: f32, midi_msg: MidiMessage) -> Self { + Self { + time, + midi_msg + } + } +} + + + + +// // // // // // // // +// // // // // // // // +// // // // // // // // +// // // // // // // // +#[cfg(test)] +mod test{ + use super::MidiSequence; + use super::MidiMessage; + + #[test] + fn create() { + let seq = MidiSequence::new(); + assert!( seq.list.is_empty(), "is not empty"); + assert!( seq.current_index == 0, "wrong current_index"); + } + #[test] + fn push() { + let mut seq = MidiSequence::new(); + let a_note = MidiMessage::NoteOn(1,2,3); + seq.push( 0.5, &a_note ); + assert!( seq.list.len() == 1, "len must be 1"); + assert!( seq.list[0].time == 0.5, "time must be 0.5"); + seq.push( 1.2, &a_note ); + assert!( seq.list.len() == 2, "len must be 2"); + assert!( seq.list[1].time == 1.7, "time must be 1.7"); + } + + #[test] + fn restart() { + let mut seq = MidiSequence::new(); + seq.current_index = 666; + seq.restart(); + assert!( seq.current_index == 0, "wrong current_index"); + } +} + + +#[cfg(test)] +mod main_test{ + use super::MidiSequence; + use super::MidiMessage; + + struct ReceiverTest { + buf: Vec, + } + impl ReceiverTest { + fn new() -> Self { + Self { + buf: Vec::new() + } + } + } + impl super::MidiReceiver for ReceiverTest { + fn reset(&mut self) { + } + fn process_midi_command(&mut self, channel: i32, command: i32, data1: i32, data2: i32) { + let msg = MidiMessage::new( channel, command, data1, data2 ); + self.buf.push(msg); + println!("RECEIVER_TEST: got midi message"); + } + } + + #[test] + fn send_next_sequence() { + let mut seq = MidiSequence::new(); + let a_note = MidiMessage::NoteOn(1,2,3); + seq.push( 0.5, &a_note ); + assert!( seq.list.len() == 1, "len must be 1"); + assert!( seq.list[0].time == 0.5, "time must be 0.5"); + seq.push( 1.2, &a_note ); + assert!( seq.list.len() == 2, "len must be 2"); + assert!( seq.list[1].time == 1.7, "time must be 1.7"); + + let mut tst_rec = ReceiverTest::new(); + assert!( tst_rec.buf.is_empty(), "must be empty"); + seq.send_next_sequence( 999999., &mut tst_rec ); + assert!( tst_rec.buf.len() == 2, "must be some content but {}", tst_rec.buf.len()); + } + + #[test] + fn timing_1() { + let mut seq = MidiSequence::new(); + let c_on = MidiMessage::NoteOn(1,60,80); + let c_off = MidiMessage::NoteOff(1,60,80); + seq.push( 0., &c_on ); + seq.push( 0.5, &c_off ); + let d_on = MidiMessage::NoteOn(1,62,80); + let d_off = MidiMessage::NoteOff(1,62,80); + seq.push( 0., &d_on ); + seq.push( 1., &d_off ); + + let mut tst_rec = ReceiverTest::new(); + seq.send_next_sequence( 0., &mut tst_rec ); + assert!( tst_rec.buf.len() == 1, "A: must be some content but {}", tst_rec.buf.len()); + let midi = tst_rec.buf[0].to_midi_general(); + assert!( midi.channel == 1, "must be "); + assert!( midi.command == 0x90, "must be "); + assert!( midi.data1 == 60, "must be "); + assert!( midi.data2 == 80, "must be "); + + let mut tst_re2 = ReceiverTest::new(); + seq.send_next_sequence( 0., &mut tst_re2 ); + assert!( tst_re2.buf.len() == 0, "B: must be zero but {}", tst_re2.buf.len()); + seq.send_next_sequence( 0.5, &mut tst_re2 ); + assert!( tst_re2.buf.len() == 2, "B: must be TWO but {}", tst_re2.buf.len()); + + let mut tst_re3 = ReceiverTest::new(); + seq.send_next_sequence( 9., &mut tst_re3 ); + assert!( tst_re3.buf.len() == 1, "C: must be ONE but {}", tst_re3.buf.len()); + let mid3 = tst_re3.buf[0].to_midi_general(); + assert!( mid3.channel == 1, "must be "); + assert!( mid3.command == 0x80, "must be "); + assert!( mid3.data1 == 62, "must be "); + assert!( mid3.data2 == 80, "must be "); + } + +} + diff --git a/src/midi_lib/mod.rs b/src/midi_lib/mod.rs new file mode 100644 index 0000000..b9a474c --- /dev/null +++ b/src/midi_lib/mod.rs @@ -0,0 +1,8 @@ +pub mod midi_message; +pub use midi_message::MidiMessage as MidiMessage; + +pub mod midi_receiver; +pub use midi_receiver::MidiReceiver as MidiReceiver; + +pub mod midi_sequence; +pub use midi_sequence::*; diff --git a/src/root_app.rs b/src/root_app.rs index ce1270f..0cedc90 100644 --- a/src/root_app.rs +++ b/src/root_app.rs @@ -67,7 +67,7 @@ impl eframe::App for RootApp { }); egui::Window::new(self.base_domik_view.title.clone()).show( ctx, |ui| { - self.base_domik_view.updateUI( ui, &mut self.example_text ); + self.base_domik_view.updateUI( ui, &mut self.midi_audio ); }); egui::Window::new("logs").show( ctx, |ui| {