aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorLibravatar Silas Bartha <[email protected]>2024-09-09 18:44:52 -0400
committerLibravatar Silas Bartha <[email protected]>2024-09-09 18:44:52 -0400
commit1e80d0684ddbcd3d995a05e572fafb181263758c (patch)
treec0b6b9366a00fe5148a691faac7e0cc8569e98a7 /src
Initial Commit
Diffstat (limited to 'src')
-rw-r--r--src/assets.rs130
-rw-r--r--src/lib.rs31
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>();
+ }
+}