diff options
author | Silas Bartha <[email protected]> | 2024-09-09 18:44:52 -0400 |
---|---|---|
committer | Silas Bartha <[email protected]> | 2024-09-09 18:44:52 -0400 |
commit | 1e80d0684ddbcd3d995a05e572fafb181263758c (patch) | |
tree | c0b6b9366a00fe5148a691faac7e0cc8569e98a7 /src |
Initial Commit
Diffstat (limited to 'src')
-rw-r--r-- | src/assets.rs | 130 | ||||
-rw-r--r-- | src/lib.rs | 31 |
2 files changed, 161 insertions, 0 deletions
diff --git a/src/assets.rs b/src/assets.rs new file mode 100644 index 0000000..58a6ed3 --- /dev/null +++ b/src/assets.rs @@ -0,0 +1,130 @@ +use std::{ + io::{self, Cursor}, + sync::Arc, +}; + +use async_channel::{Receiver, TryRecvError}; +use bevy::{ + asset::{io::Reader, AssetLoader, AsyncReadExt, LoadContext}, + audio::Source, + prelude::*, + tasks::AsyncComputeTaskPool, +}; +use itertools::Itertools; +use rustysynth::{MidiFile, MidiFileSequencer, SoundFont, Synthesizer, SynthesizerSettings}; + +/// MIDI audio asset +#[derive(Asset, TypePath)] +pub struct MidiAudio { + /// MIDI file data + pub midi: Vec<u8>, +} + +/// AssetLoader for MIDI files (.mid/.midi) +#[derive(Default, Debug)] +pub struct MidiAssetLoader; + +impl AssetLoader for MidiAssetLoader { + type Asset = MidiAudio; + + type Settings = (); + + type Error = io::Error; + + async fn load<'a>( + &'a self, + reader: &'a mut Reader<'_>, + _settings: &'a Self::Settings, + _load_context: &'a mut LoadContext<'_>, + ) -> Result<Self::Asset, Self::Error> { + let mut bytes = vec![]; + reader.read_to_end(&mut bytes).await?; + Ok(MidiAudio { midi: bytes }) + } +} + +/// Decoder for MIDI file playback +pub struct MidiDecoder { + sample_rate: usize, + stream: Receiver<f32>, +} + +impl MidiDecoder { + /// 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<u8>, soundfont: Arc<SoundFont>) -> Self { + let mut midi = Cursor::new(midi); + let sample_rate = 44100_usize; + let (tx, rx) = async_channel::bounded::<f32>(sample_rate * 2); + 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<f32> = vec![0_f32; sample_rate]; + let mut right: Vec<f32> = 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(e) = tx.send(*value).await { + error!("{e}"); + }; + } + } + tx.close(); + }) + .detach(); + Self { + sample_rate, + stream: rx, + } + } +} + +impl Iterator for MidiDecoder { + type Item = f32; + + fn next(&mut self) -> Option<Self::Item> { + match self.stream.try_recv() { + Ok(value) => Some(value), + Err(e) => match e { + TryRecvError::Empty => Some(0.0), + TryRecvError::Closed => None, + }, + } + } +} + +impl Source for MidiDecoder { + fn current_frame_len(&self) -> Option<usize> { + None + } + + fn channels(&self) -> u16 { + 2 + } + + fn sample_rate(&self) -> u32 { + self.sample_rate as u32 + } + + fn total_duration(&self) -> Option<std::time::Duration> { + None + } +} + +impl Decodable for MidiAudio { + type Decoder = MidiDecoder; + + type DecoderItem = <MidiDecoder as Iterator>::Item; + + fn decoder(&self) -> Self::Decoder { + MidiDecoder::new(self.midi.clone(), crate::SOUNDFONT.get().unwrap().clone()) + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..2906d61 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,31 @@ +#![warn(missing_docs)] + +//! A plugin which adds MIDI file and soundfont audio support to the [bevy](https://crates.io/crates/bevy) engine via [rustysynth](https://crates.io/crates/rustysynth). + +use bevy::{audio::AddAudioSource, prelude::*}; +use rustysynth::SoundFont; +use std::{ + io::Read, + sync::{Arc, OnceLock}, +}; + +mod assets; +pub use assets::*; + +pub(crate) static SOUNDFONT: OnceLock<Arc<SoundFont>> = OnceLock::new(); + +/// This plugin configures the soundfont used for playback and registers MIDI assets. +#[derive(Default, Debug)] +pub struct RustySynthPlugin<R: Read + Send + Sync + Clone + 'static> { + /// Reader for soundfont data. A default is not provided since soundfonts can be quite large. + pub soundfont: R, +} + +impl<R: Read + Send + Sync + Clone + 'static> Plugin for RustySynthPlugin<R> { + fn build(&self, app: &mut App) { + let _ = SOUNDFONT.set(Arc::new( + SoundFont::new(&mut self.soundfont.clone()).unwrap(), + )); + app.add_audio_source::<MidiAudio>().init_asset::<MidiAudio>().init_asset_loader::<MidiAssetLoader>(); + } +} |