Added basic note-sequence source support and removed multithreading dependency

This commit is contained in:
Silas Bartha 2024-09-19 16:05:28 -04:00
parent 8e98b888a4
commit 86d578c587
Signed by: soaos
GPG Key ID: 9BD3DCC0D56A09B2
3 changed files with 101 additions and 41 deletions

View File

@ -1,7 +1,7 @@
[package] [package]
name = "bevy_rustysynth" name = "bevy_rustysynth"
description = "A plugin which adds MIDI file and soundfont audio support to the bevy engine via 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" edition = "2021"
license = "0BSD OR MIT OR Apache-2.0" license = "0BSD OR MIT OR Apache-2.0"
@ -14,7 +14,7 @@ rodio = "0.19"
[dependencies.bevy] [dependencies.bevy]
version = "0.14" version = "0.14"
default-features = false default-features = false
features = ["bevy_audio", "bevy_asset", "multi_threaded"] features = ["bevy_audio", "bevy_asset"]
[features] [features]
default = ["hl4mgm"] default = ["hl4mgm"]

View File

@ -1,23 +1,53 @@
use std::{ use std::{
io::{self, Cursor}, io::{self, Cursor},
sync::Arc, sync::Arc,
time::Duration,
}; };
use async_channel::{Receiver, SendError, TryRecvError, TrySendError}; use async_channel::{Receiver, TryRecvError};
use bevy::{ use bevy::{
asset::{io::Reader, AssetLoader, AsyncReadExt, LoadContext}, asset::{io::Reader, AssetLoader, AsyncReadExt, LoadContext},
audio::Source, audio::Source,
prelude::*, prelude::*,
tasks::{AsyncComputeTaskPool, Task}, tasks::AsyncComputeTaskPool,
}; };
use itertools::Itertools; use itertools::Itertools;
use rustysynth::{MidiFile, MidiFileSequencer, SoundFont, Synthesizer, SynthesizerSettings}; 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 /// MIDI audio asset
#[derive(Asset, TypePath)] #[derive(Asset, TypePath, Clone, Debug)]
pub struct MidiAudio { pub enum MidiAudio {
/// MIDI file data /// Plays audio from a MIDI file
pub midi: Vec<u8>, File(Vec<u8>),
/// Plays a simple sequence of notes
Sequence(Vec<MidiNote>),
} }
/// AssetLoader for MIDI files (.mid/.midi) /// AssetLoader for MIDI files (.mid/.midi)
@ -39,7 +69,7 @@ impl AssetLoader for MidiAssetLoader {
) -> Result<Self::Asset, Self::Error> { ) -> Result<Self::Asset, Self::Error> {
let mut bytes = vec![]; let mut bytes = vec![];
reader.read_to_end(&mut bytes).await?; reader.read_to_end(&mut bytes).await?;
Ok(MidiAudio { midi: bytes }) Ok(MidiAudio::File(bytes))
} }
fn extensions(&self) -> &[&str] { fn extensions(&self) -> &[&str] {
@ -48,30 +78,31 @@ impl AssetLoader for MidiAssetLoader {
} }
/// Decoder for MIDI file playback /// Decoder for MIDI file playback
pub struct MidiDecoder { pub struct MidiFileDecoder {
sample_rate: usize, sample_rate: usize,
stream: Receiver<f32>, stream: Receiver<f32>,
_task: Task<()>,
} }
impl MidiDecoder { impl MidiFileDecoder {
/// Construct and begin a new MIDI sequencer with the given MIDI data and soundfont. /// 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 /// 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. /// be paused without endlessly backing up data forever.
pub fn new(midi: Vec<u8>, soundfont: Arc<SoundFont>) -> Self { pub fn new(midi: MidiAudio, soundfont: Arc<SoundFont>) -> Self {
let mut midi = Cursor::new(midi);
let sample_rate = 44100_usize; let sample_rate = 44100_usize;
let (tx, rx) = async_channel::bounded::<f32>(sample_rate * 2); let (tx, rx) = async_channel::bounded::<f32>(sample_rate * 2);
let task = AsyncComputeTaskPool::get() AsyncComputeTaskPool::get().spawn(async move {
.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 settings = SynthesizerSettings::new(sample_rate as i32);
let synthesizer = let mut synthesizer =
Synthesizer::new(&soundfont, &settings).expect("Failed to create synthesizer."); Synthesizer::new(&soundfont, &settings).expect("Failed to create synthesizer.");
let mut sequencer = MidiFileSequencer::new(synthesizer);
sequencer.play(&midi, true);
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<f32> = vec![0_f32; sample_rate]; let mut left: Vec<f32> = vec![0_f32; sample_rate];
let mut right: Vec<f32> = vec![0_f32; sample_rate]; let mut right: Vec<f32> = vec![0_f32; sample_rate];
while !sequencer.end_of_sequence() { while !sequencer.end_of_sequence() {
@ -82,17 +113,42 @@ impl MidiDecoder {
}; };
} }
} }
}
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<f32> = vec![0_f32; note_length];
let mut right: Vec<f32> = 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(); tx.close();
}); }).detach();
Self { Self {
_task: task,
sample_rate, sample_rate,
stream: rx, stream: rx,
} }
} }
} }
impl Iterator for MidiDecoder { impl Iterator for MidiFileDecoder {
type Item = f32; type Item = f32;
fn next(&mut self) -> Option<Self::Item> { fn next(&mut self) -> Option<Self::Item> {
@ -106,7 +162,7 @@ impl Iterator for MidiDecoder {
} }
} }
impl Source for MidiDecoder { impl Source for MidiFileDecoder {
fn current_frame_len(&self) -> Option<usize> { fn current_frame_len(&self) -> Option<usize> {
None None
} }
@ -125,11 +181,11 @@ impl Source for MidiDecoder {
} }
impl Decodable for MidiAudio { impl Decodable for MidiAudio {
type Decoder = MidiDecoder; type Decoder = MidiFileDecoder;
type DecoderItem = <MidiDecoder as Iterator>::Item; type DecoderItem = <MidiFileDecoder as Iterator>::Item;
fn decoder(&self) -> Self::Decoder { fn decoder(&self) -> Self::Decoder {
MidiDecoder::new(self.midi.clone(), crate::SOUNDFONT.get().unwrap().clone()) MidiFileDecoder::new(self.clone(), crate::SOUNDFONT.get().unwrap().clone())
} }
} }

View File

@ -27,7 +27,9 @@ pub struct RustySynthPlugin<R: Read + Send + Sync + Clone + 'static> {
#[cfg(feature = "hl4mgm")] #[cfg(feature = "hl4mgm")]
impl Default for RustySynthPlugin<Cursor<&[u8]>> { impl Default for RustySynthPlugin<Cursor<&[u8]>> {
fn default() -> Self { fn default() -> Self {
Self { soundfont: Cursor::new(HL4MGM) } Self {
soundfont: Cursor::new(HL4MGM),
}
} }
} }
@ -36,6 +38,8 @@ impl<R: Read + Send + Sync + Clone + 'static> Plugin for RustySynthPlugin<R> {
let _ = SOUNDFONT.set(Arc::new( let _ = SOUNDFONT.set(Arc::new(
SoundFont::new(&mut self.soundfont.clone()).unwrap(), SoundFont::new(&mut self.soundfont.clone()).unwrap(),
)); ));
app.add_audio_source::<MidiAudio>().init_asset::<MidiAudio>().init_asset_loader::<MidiAssetLoader>(); app.add_audio_source::<MidiAudio>()
.init_asset::<MidiAudio>()
.init_asset_loader::<MidiAssetLoader>();
} }
} }