From 86d578c587be1874ff7923c4c1056cadd3a46130 Mon Sep 17 00:00:00 2001 From: Silas Bartha Date: Thu, 19 Sep 2024 16:05:28 -0400 Subject: Added basic note-sequence source support and removed multithreading dependency --- Cargo.toml | 4 +- src/assets.rs | 130 +++++++++++++++++++++++++++++++++++++++++----------------- src/lib.rs | 10 +++-- 3 files changed, 102 insertions(+), 42 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index ecde1f0..f190879 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "bevy_rustysynth" description = "A plugin which adds MIDI file and soundfont audio support to the bevy engine via rustysynth." -version = "0.1.2" +version = "0.2.0" edition = "2021" license = "0BSD OR MIT OR Apache-2.0" @@ -14,7 +14,7 @@ rodio = "0.19" [dependencies.bevy] version = "0.14" default-features = false -features = ["bevy_audio", "bevy_asset", "multi_threaded"] +features = ["bevy_audio", "bevy_asset"] [features] default = ["hl4mgm"] diff --git a/src/assets.rs b/src/assets.rs index 8aab74d..7747531 100644 --- a/src/assets.rs +++ b/src/assets.rs @@ -1,23 +1,53 @@ use std::{ io::{self, Cursor}, sync::Arc, + time::Duration, }; -use async_channel::{Receiver, SendError, TryRecvError, TrySendError}; +use async_channel::{Receiver, TryRecvError}; use bevy::{ asset::{io::Reader, AssetLoader, AsyncReadExt, LoadContext}, audio::Source, prelude::*, - tasks::{AsyncComputeTaskPool, Task}, + tasks::AsyncComputeTaskPool, }; use itertools::Itertools; use rustysynth::{MidiFile, MidiFileSequencer, SoundFont, Synthesizer, SynthesizerSettings}; +/// Represents a single MIDI note in a sequence +#[derive(Clone, Debug)] +pub struct MidiNote { + /// Channel to play the note on + pub channel: i32, + /// Preset (instrument) to play the note with (see GM spec.) + pub preset: i32, + /// Key to play (60 is middle C) + pub key: i32, + /// Velocity to play note at + pub velocity: i32, + /// Duration to play note for + pub duration: Duration, +} + +impl Default for MidiNote { + fn default() -> Self { + Self { + channel: 0, + preset: 0, + key: 60, + velocity: 100, + duration: Duration::from_secs(1), + } + } +} + /// MIDI audio asset -#[derive(Asset, TypePath)] -pub struct MidiAudio { - /// MIDI file data - pub midi: Vec, +#[derive(Asset, TypePath, Clone, Debug)] +pub enum MidiAudio { + /// Plays audio from a MIDI file + File(Vec), + /// Plays a simple sequence of notes + Sequence(Vec), } /// AssetLoader for MIDI files (.mid/.midi) @@ -39,7 +69,7 @@ impl AssetLoader for MidiAssetLoader { ) -> Result { let mut bytes = vec![]; reader.read_to_end(&mut bytes).await?; - Ok(MidiAudio { midi: bytes }) + Ok(MidiAudio::File(bytes)) } fn extensions(&self) -> &[&str] { @@ -48,51 +78,77 @@ impl AssetLoader for MidiAssetLoader { } /// Decoder for MIDI file playback -pub struct MidiDecoder { +pub struct MidiFileDecoder { sample_rate: usize, stream: Receiver, - _task: Task<()>, } -impl MidiDecoder { +impl MidiFileDecoder { /// Construct and begin a new MIDI sequencer with the given MIDI data and soundfont. /// /// The sequencer will push at most 1 second's worth of audio ahead, allowing the decoder to /// be paused without endlessly backing up data forever. - pub fn new(midi: Vec, soundfont: Arc) -> Self { - let mut midi = Cursor::new(midi); + pub fn new(midi: MidiAudio, soundfont: Arc) -> Self { let sample_rate = 44100_usize; let (tx, rx) = async_channel::bounded::(sample_rate * 2); - let task = AsyncComputeTaskPool::get() - .spawn(async move { - let midi = Arc::new(MidiFile::new(&mut midi).expect("Failed to read midi file.")); - let settings = SynthesizerSettings::new(sample_rate as i32); - let synthesizer = - Synthesizer::new(&soundfont, &settings).expect("Failed to create synthesizer."); - let mut sequencer = MidiFileSequencer::new(synthesizer); - sequencer.play(&midi, true); - - let mut left: Vec = vec![0_f32; sample_rate]; - let mut right: Vec = vec![0_f32; sample_rate]; - while !sequencer.end_of_sequence() { - sequencer.render(&mut left, &mut right); - for value in left.iter().interleave(right.iter()) { - if let Err(_) = tx.send(*value).await { - return; - }; + AsyncComputeTaskPool::get().spawn(async move { + let settings = SynthesizerSettings::new(sample_rate as i32); + let mut synthesizer = + Synthesizer::new(&soundfont, &settings).expect("Failed to create synthesizer."); + + match midi { + MidiAudio::File(midi_data) => { + let mut sequencer = MidiFileSequencer::new(synthesizer); + let mut midi_data = Cursor::new(midi_data); + let midi = + Arc::new(MidiFile::new(&mut midi_data).expect("Failed to read midi file.")); + sequencer.play(&midi, false); + let mut left: Vec = vec![0_f32; sample_rate]; + let mut right: Vec = vec![0_f32; sample_rate]; + while !sequencer.end_of_sequence() { + sequencer.render(&mut left, &mut right); + for value in left.iter().interleave(right.iter()) { + if let Err(_) = tx.send(*value).await { + return; + }; + } } } - tx.close(); - }); + MidiAudio::Sequence(sequence) => { + for MidiNote { + channel, + preset, + key, + velocity, + duration, + } in sequence.iter() + { + synthesizer.process_midi_message(*channel, 0b1100_0000, *preset, 0); + synthesizer.note_on(*channel, *key, *velocity); + let note_length = (sample_rate as f32 * duration.as_secs_f32()) as usize; + let mut left: Vec = vec![0_f32; note_length]; + let mut right: Vec = vec![0_f32; note_length]; + synthesizer.render(&mut left, &mut right); + for value in left.iter().interleave(right.iter()) { + if let Err(_) = tx.send(*value).await { + return; + }; + } + synthesizer.note_off(*channel, *key); + } + } + } + + tx.close(); + }).detach(); Self { - _task: task, sample_rate, stream: rx, } } } -impl Iterator for MidiDecoder { +impl Iterator for MidiFileDecoder { type Item = f32; fn next(&mut self) -> Option { @@ -106,7 +162,7 @@ impl Iterator for MidiDecoder { } } -impl Source for MidiDecoder { +impl Source for MidiFileDecoder { fn current_frame_len(&self) -> Option { None } @@ -125,11 +181,11 @@ impl Source for MidiDecoder { } impl Decodable for MidiAudio { - type Decoder = MidiDecoder; + type Decoder = MidiFileDecoder; - type DecoderItem = ::Item; + type DecoderItem = ::Item; fn decoder(&self) -> Self::Decoder { - MidiDecoder::new(self.midi.clone(), crate::SOUNDFONT.get().unwrap().clone()) + MidiFileDecoder::new(self.clone(), crate::SOUNDFONT.get().unwrap().clone()) } } diff --git a/src/lib.rs b/src/lib.rs index a15f3b8..0a9123e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -13,7 +13,7 @@ mod assets; pub use assets::*; #[cfg(feature = "hl4mgm")] -pub (crate) static HL4MGM: &[u8] = include_bytes!("./embedded_assets/hl4mgm.sf2"); +pub(crate) static HL4MGM: &[u8] = include_bytes!("./embedded_assets/hl4mgm.sf2"); pub(crate) static SOUNDFONT: OnceLock> = OnceLock::new(); @@ -27,7 +27,9 @@ pub struct RustySynthPlugin { #[cfg(feature = "hl4mgm")] impl Default for RustySynthPlugin> { fn default() -> Self { - Self { soundfont: Cursor::new(HL4MGM) } + Self { + soundfont: Cursor::new(HL4MGM), + } } } @@ -36,6 +38,8 @@ impl Plugin for RustySynthPlugin { let _ = SOUNDFONT.set(Arc::new( SoundFont::new(&mut self.soundfont.clone()).unwrap(), )); - app.add_audio_source::().init_asset::().init_asset_loader::(); + app.add_audio_source::() + .init_asset::() + .init_asset_loader::(); } } -- cgit v1.2.3