Added bevy_kira_audio support

This commit is contained in:
Silas Bartha 2025-03-09 13:23:16 -04:00
parent f87de10085
commit a0640daf80
7 changed files with 2777 additions and 175 deletions

View File

@ -1,24 +0,0 @@
name: Rust
on:
push:
branches: [ "master" ]
pull_request:
branches: [ "master" ]
env:
CARGO_TERM_COLOR: always
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Dependencies
run: sudo apt-get update; sudo apt-get install --no-install-recommends libasound2-dev libudev-dev
- name: Build
run: cargo build --verbose
- name: Run tests
run: cargo test --verbose

2
.gitignore vendored
View File

@ -1 +1,3 @@
/target /target
.vscode
.cargo

2517
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,10 +1,10 @@
[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.3.0" version = "0.4.0"
edition = "2021" edition = "2021"
license = "0BSD OR MIT OR Apache-2.0" license = "0BSD OR MIT OR Apache-2.0"
repository = "https://git.exvacuum.dev/bevy_rustysynth" repository = "https://git.soaos.dev/bevy_rustysynth"
[dependencies] [dependencies]
rustysynth = "1.3" rustysynth = "1.3"
@ -15,8 +15,14 @@ rodio = "0.20"
[dependencies.bevy] [dependencies.bevy]
version = "0.15" version = "0.15"
default-features = false default-features = false
features = ["bevy_audio", "bevy_asset"] features = ["bevy_asset"]
[dependencies.bevy_kira_audio]
version = "0.22"
optional = true
[features] [features]
default = ["hl4mgm"] default = ["hl4mgm", "bevy_audio"]
kira = ["dep:bevy_kira_audio"]
bevy_audio = ["bevy/bevy_audio"]
hl4mgm = [] hl4mgm = []

View File

@ -7,11 +7,13 @@
A plugin which adds MIDI file and soundfont audio support to the bevy engine via rustysynth. A plugin which adds MIDI file and soundfont audio support to the bevy engine via rustysynth.
From version 0.4, the crate has undergone significant rewrites, and now works with the default `bevy_audio` backend (`bevy_audio` feature) OR [`bevy_kira_audio`](https://github.com/NiklasEi/bevy_kira_audio) (`kira` feature)
## Compatibility ## Compatibility
| Crate Version | Bevy Version | | Crate Version | Bevy Version |
|--- |--- | |--- |--- |
| 0.3 | 0.15 | | 0.3-0.4 | 0.15 |
| 0.1-0.2 | 0.14 | | 0.1-0.2 | 0.14 |
## Installation ## Installation
@ -19,13 +21,13 @@ A plugin which adds MIDI file and soundfont audio support to the bevy engine via
### crates.io ### crates.io
```toml ```toml
[dependencies] [dependencies]
bevy_rustysynth = "0.2" bevy_rustysynth = "0.4"
``` ```
### Using git URL in Cargo.toml ### Using git URL in Cargo.toml
```toml ```toml
[dependencies.bevy_rustysynth] [dependencies.bevy_rustysynth]
git = "https://github.com/exvacuum/bevy_rustysynth.git" git = "https://git.soaos.dev/bevy_rustysynth.git"
``` ```
## Usage ## Usage
@ -47,8 +49,10 @@ fn main() {
} }
``` ```
Then you can load and play a MIDI like any other audio file: Then you can load and play a MIDI like any other audio file:
### `bevy_audio` Example
```rs ```rs
let midi_handle = asset_server.load::<MidiAudio>("example.mid"); let midi_handle = asset_server.load::<MidiAudioSource>("example.mid");
commands.spawn(AudioSourceBundle { commands.spawn(AudioSourceBundle {
source: midi_handle, source: midi_handle,
@ -56,6 +60,13 @@ commands.spawn(AudioSourceBundle {
}); });
``` ```
### `bevy_kira_audio` Example
```rs
let midi_handle = asset_server.load::<AudioSource>("example.mid");
audio.play(midi_handle);
```
## License ## License
This crate is licensed under your choice of 0BSD, Apache-2.0, or MIT license. This crate is licensed under your choice of 0BSD, Apache-2.0, or MIT license.

View File

@ -1,19 +1,16 @@
use std::{ use std::{
io::{self, Cursor}, io::{self, Cursor}, sync::Arc, time::Duration
sync::Arc,
time::Duration,
};
use async_channel::{Receiver, TryRecvError};
use bevy::{
asset::{io::Reader, AssetLoader, LoadContext},
audio::Source,
prelude::*,
tasks::AsyncComputeTaskPool,
}; };
#[cfg(feature = "kira")]
use std::future::Future;
#[cfg(feature = "bevy_audio")]
use bevy::prelude::*;
use bevy::asset::{io::Reader, AssetLoader, LoadContext};
use itertools::Itertools; use itertools::Itertools;
use rustysynth::{MidiFile, MidiFileSequencer, SoundFont, Synthesizer, SynthesizerSettings}; use rustysynth::{MidiFile, MidiFileSequencer, SoundFont, Synthesizer, SynthesizerSettings};
use crate::SOUNDFONT;
/// Represents a single MIDI note in a sequence /// Represents a single MIDI note in a sequence
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct MidiNote { pub struct MidiNote {
@ -44,82 +41,57 @@ impl Default for MidiNote {
} }
} }
/// MIDI audio asset
#[derive(Asset, TypePath, Clone, Debug)]
pub enum MidiAudio {
/// Plays audio from a MIDI file
File(Vec<u8>),
/// Plays a simple sequence of notes
Sequence(Vec<MidiNote>),
}
/// AssetLoader for MIDI files (.mid/.midi) /// AssetLoader for MIDI files (.mid/.midi)
#[derive(Default, Debug)] #[derive(Default, Debug)]
pub struct MidiAssetLoader; pub struct MidiAssetLoader;
impl AssetLoader for MidiAssetLoader {
type Asset = MidiAudio;
type Settings = ();
type Error = io::Error;
async fn load(
&self,
reader: &mut dyn Reader,
_settings: &Self::Settings,
_load_context: &mut LoadContext<'_>,
) -> Result<Self::Asset, Self::Error> {
let mut bytes = vec![];
reader.read_to_end(&mut bytes).await?;
Ok(MidiAudio::File(bytes))
}
fn extensions(&self) -> &[&str] {
&["mid", "midi"]
}
}
/// Decoder for MIDI file playback /// Decoder for MIDI file playback
pub struct MidiFileDecoder { pub struct MidiFileDecoder {
sample_rate: usize, sample_rate: usize,
stream: Receiver<f32>, data: Vec<f32>,
#[cfg(feature = "bevy_audio")]
index: usize,
} }
impl MidiFileDecoder { impl MidiFileDecoder {
/// Construct and begin a new MIDI sequencer with the given MIDI data and soundfont. /// Construct and render a MIDI sequence with the given MIDI data and soundfont.
/// pub async fn new(midi_data: Vec<u8>, soundfont: Arc<SoundFont>) -> Self {
/// 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: MidiAudio, soundfont: Arc<SoundFont>) -> Self {
let sample_rate = 44100_usize; let sample_rate = 44100_usize;
let (tx, rx) = async_channel::bounded::<f32>(sample_rate * 2);
AsyncComputeTaskPool::get()
.spawn(async move {
let settings = SynthesizerSettings::new(sample_rate as i32); let settings = SynthesizerSettings::new(sample_rate as i32);
let mut synthesizer = let synthesizer =
Synthesizer::new(&soundfont, &settings).expect("Failed to create synthesizer."); Synthesizer::new(&soundfont, &settings).expect("Failed to create synthesizer.");
match midi { let mut data = Vec::new();
MidiAudio::File(midi_data) => {
let mut sequencer = MidiFileSequencer::new(synthesizer); let mut sequencer = MidiFileSequencer::new(synthesizer);
let mut midi_data = Cursor::new(midi_data); let mut midi_data = Cursor::new(midi_data);
let midi = Arc::new( let midi = Arc::new(MidiFile::new(&mut midi_data).expect("Failed to read midi file."));
MidiFile::new(&mut midi_data).expect("Failed to read midi file."),
);
sequencer.play(&midi, false); 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() {
sequencer.render(&mut left, &mut right); sequencer.render(&mut left, &mut right);
for value in left.iter().interleave(right.iter()) { for value in left.iter().interleave(right.iter()) {
if let Err(_) = tx.send(*value).await { data.push(*value);
return;
};
} }
} }
Self {
sample_rate,
data,
#[cfg(feature = "bevy_audio")]
index: 0,
} }
MidiAudio::Sequence(sequence) => { }
/// Render a MIDI sequence with the given soundfont.
pub fn new_sequence(midi_sequence: Vec<MidiNote>, soundfont: Arc<SoundFont>) -> Self {
let sample_rate = 44100_usize;
let settings = SynthesizerSettings::new(sample_rate as i32);
let mut synthesizer =
Synthesizer::new(&soundfont, &settings).expect("Failed to create synthesizer.");
let mut data = Vec::new();
for MidiNote { for MidiNote {
channel, channel,
preset, preset,
@ -127,53 +99,45 @@ impl MidiFileDecoder {
key, key,
velocity, velocity,
duration, duration,
} in sequence.iter() } in midi_sequence.iter()
{ {
synthesizer.process_midi_message(*channel, 0xB0, 0x00, *bank); synthesizer.process_midi_message(*channel, 0xB0, 0x00, *bank);
synthesizer.process_midi_message(*channel, 0xC0, *preset, 0); synthesizer.process_midi_message(*channel, 0xC0, *preset, 0);
synthesizer.note_on(*channel, *key, *velocity); synthesizer.note_on(*channel, *key, *velocity);
let note_length = let note_length = (sample_rate as f32 * duration.as_secs_f32()) as usize;
(sample_rate as f32 * duration.as_secs_f32()) as usize;
let mut left: Vec<f32> = vec![0_f32; note_length]; let mut left: Vec<f32> = vec![0_f32; note_length];
let mut right: Vec<f32> = vec![0_f32; note_length]; let mut right: Vec<f32> = vec![0_f32; note_length];
for (left, right) in left.chunks_mut(sample_rate).zip(right.chunks_mut(sample_rate)) { for (left, right) in left
.chunks_mut(sample_rate)
.zip(right.chunks_mut(sample_rate))
{
synthesizer.render(left, right); synthesizer.render(left, right);
for value in left.iter().interleave(right.iter()) { for value in left.iter().interleave(right.iter()) {
if let Err(_) = tx.send(*value).await { data.push(*value);
return;
};
} }
} }
synthesizer.note_off(*channel, *key); synthesizer.note_off(*channel, *key);
} }
}
}
tx.close();
})
.detach();
Self { Self {
sample_rate, sample_rate,
stream: rx, data,
#[cfg(feature = "bevy_audio")]
index: 0,
} }
} }
} }
impl Iterator for MidiFileDecoder { #[cfg(all(feature = "bevy_audio", not(feature = "kira")))]
type Item = f32; /// Asset containing MIDI file data to be used as a `Decodable` audio source
#[derive(Asset, TypePath, Debug)]
pub struct MidiAudio(Vec<u8>);
fn next(&mut self) -> Option<Self::Item> { #[cfg(all(feature = "bevy_audio", not(feature = "kira")))]
match self.stream.try_recv() { mod bevy_audio {
Ok(value) => Some(value), use super::*;
Err(e) => match e { use bevy::audio::{Decodable, Source};
TryRecvError::Empty => Some(0.0),
TryRecvError::Closed => None,
},
}
}
}
impl Source for MidiFileDecoder { impl Source for MidiFileDecoder {
fn current_frame_len(&self) -> Option<usize> { fn current_frame_len(&self) -> Option<usize> {
None None
} }
@ -189,14 +153,133 @@ impl Source for MidiFileDecoder {
fn total_duration(&self) -> Option<std::time::Duration> { fn total_duration(&self) -> Option<std::time::Duration> {
None None
} }
} }
impl Decodable for MidiAudio { impl Decodable for MidiAudio {
type Decoder = MidiFileDecoder; type Decoder = MidiFileDecoder;
type DecoderItem = <MidiFileDecoder as Iterator>::Item; type DecoderItem = <MidiFileDecoder as Iterator>::Item;
fn decoder(&self) -> Self::Decoder { fn decoder(&self) -> Self::Decoder {
MidiFileDecoder::new(self.clone(), crate::SOUNDFONT.get().unwrap().clone()) bevy::tasks::block_on(MidiFileDecoder::new(self.0.clone(), SOUNDFONT.get().unwrap().clone()))
}
}
impl AssetLoader for MidiAssetLoader {
type Asset = MidiAudio;
type Settings = ();
type Error = io::Error;
async fn load(
&self,
reader: &mut dyn Reader,
_settings: &Self::Settings,
_load_context: &mut LoadContext<'_>,
) -> Result<Self::Asset, Self::Error> {
let mut bytes = vec![];
reader.read_to_end(&mut bytes).await?;
Ok(MidiAudio(bytes))
}
fn extensions(&self) -> &[&str] {
&["mid", "midi"]
}
}
impl Iterator for MidiFileDecoder {
type Item = f32;
fn next(&mut self) -> Option<Self::Item> {
let result = self.data.get(self.index).copied();
self.index += 1;
result
}
}
}
#[cfg(all(feature = "kira", not(feature = "bevy_audio")))]
/// Extensions For Rendering MIDI Audio
pub trait MidiAudioExtensions {
/// Renders MIDI audio from orovided data
fn from_midi_file(data: Vec<u8>) -> impl Future<Output = Self> + Send;
/// Renders MIDI audio from provided note sequence
fn from_midi_sequence(sequence: Vec<MidiNote>) -> impl Future<Output = Self> + Send;
}
#[cfg(all(feature = "kira", not(feature = "bevy_audio")))]
mod kira {
use super::*;
use bevy_kira_audio::{
prelude::{Frame, StaticSoundData, StaticSoundSettings},
AudioSource,
};
impl AssetLoader for MidiAssetLoader {
type Asset = AudioSource;
type Settings = ();
type Error = io::Error;
async fn load(
&self,
reader: &mut dyn Reader,
_settings: &Self::Settings,
_load_context: &mut LoadContext<'_>,
) -> Result<Self::Asset, Self::Error> {
let mut bytes = vec![];
reader.read_to_end(&mut bytes).await?;
Ok(AudioSource::from_midi_file(bytes).await)
}
fn extensions(&self) -> &[&str] {
&["mid", "midi"]
}
}
impl MidiAudioExtensions for AudioSource {
async fn from_midi_file(data: Vec<u8>) -> Self {
let decoder = MidiFileDecoder::new(data, SOUNDFONT.get().unwrap().clone()).await;
let frames = decoder
.data
.chunks(2)
.map(|sample| Frame::new(sample[0], sample[1]))
.collect::<Arc<[_]>>();
let sample_rate = decoder.sample_rate as u32;
let settings = StaticSoundSettings {
..Default::default()
};
AudioSource {
sound: StaticSoundData {
sample_rate,
frames,
settings,
slice: None,
},
}
}
async fn from_midi_sequence(sequence: Vec<MidiNote>) -> Self {
let decoder = MidiFileDecoder::new_sequence(sequence, SOUNDFONT.get().unwrap().clone());
let frames = decoder
.data
.chunks(2)
.map(|sample| Frame::new(sample[0], sample[1]))
.collect::<Arc<[_]>>();
let sample_rate = decoder.sample_rate as u32;
let settings = StaticSoundSettings {
..Default::default()
};
AudioSource {
sound: StaticSoundData {
sample_rate,
frames,
settings,
slice: None,
},
}
}
} }
} }

View File

@ -1,13 +1,20 @@
#![warn(missing_docs)] #![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). //! 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::*}; #[cfg(all(feature = "bevy_audio", feature = "kira"))]
compile_error!("Cannot compile with both bevy_audio and kira features enabled simultaneously. Please disable one of these features");
use bevy::prelude::*;
use rustysynth::SoundFont; use rustysynth::SoundFont;
use std::{ use std::{
io::{Cursor, Read}, io::Read,
sync::{Arc, OnceLock}, sync::{Arc, OnceLock},
}; };
#[cfg(feature = "hl4mgm")]
use std::io::Cursor;
#[cfg(feature = "bevy_audio")]
use bevy::audio::AddAudioSource;
mod assets; mod assets;
pub use assets::*; pub use assets::*;
@ -38,8 +45,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>() app.init_asset_loader::<MidiAssetLoader>();
.init_asset::<MidiAudio>() #[cfg(feature = "bevy_audio")]
.init_asset_loader::<MidiAssetLoader>(); app.init_asset::<MidiAudio>().add_audio_source::<MidiAudio>();
} }
} }