Skip to content
This repository was archived by the owner on Sep 25, 2025. It is now read-only.
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
622 changes: 590 additions & 32 deletions Cargo.lock

Large diffs are not rendered by default.

8 changes: 8 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ bevy = [
"dep:itertools",
"dep:rustysynth",
"dep:crossbeam-channel",
"dep:bevy_seedling",
"dep:firewheel",
]
debug = ["bevy"]
example = [
Expand Down Expand Up @@ -63,6 +65,8 @@ midir = { version = "0.10", optional = true }
tinyaudio = { version = "1.1.0", optional = true }
itertools = { version = "0.14.0", optional = true }
rustysynth = { version = "1.3.5", optional = true }
bevy_seedling = { version = "0.4.3", optional = true }
firewheel = { version = "0.4.3", optional = true }
bevy_platform = { version = "0.16", default-features = false, features = [
"alloc",
] }
Expand Down Expand Up @@ -113,3 +117,7 @@ required-features = ["example"]
[[example]]
name = "scale"
required-features = ["example"]

[[example]]
name = "firewheel"
required-features = ["example"]
Binary file removed assets/Africa.mid
Binary file not shown.
211 changes: 211 additions & 0 deletions examples/firewheel/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
//! Simple example demonstrating MIDI synthesis using the firewheel backend

use bevy::prelude::{Val::*, *};
use midix::{
bevy::firewheel::{FirewheelMidiPlugin, MidiCommands, MidiSoundfont},
prelude::*,
};

fn main() {
App::new()
.add_plugins((
DefaultPlugins,
// Add bevy_seedling's audio plugin
bevy_seedling::SeedlingPlugin::default(),
// Add our MIDI plugin
FirewheelMidiPlugin,
))
.add_systems(Startup, setup)
.add_systems(Update, (play_scale, keyboard_input))
.run();
}

/// Set up the MIDI synthesizer
fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
// Load a soundfont file
info!("Setting up!");
let soundfont = asset_server.load("8bitsf.SF2");

// Spawn a MIDI synthesizer entity
commands.spawn((
MidiSoundfont(soundfont),
MidiCommands::default(),
Name::new("MIDI Synthesizer"),
));

// Spawn a camera
commands.spawn(Camera2d);

// Instructions
commands.spawn((
Node {
position_type: PositionType::Absolute,
width: Percent(100.),
height: Percent(100.),
top: Px(10.0),
left: Px(10.0),
..default()
},
Text::new(
"Firewheel MIDI Example\n\
Press A-K keys to play notes\n\
Press Space to play a scale\n\
Press +/- to adjust volume\n\
Press Escape to stop all notes",
),
TextFont::from_font_size(40.),
));
}

/// Play a C major scale when space is pressed
fn play_scale(
keyboard: Res<ButtonInput<KeyCode>>,
mut query: Query<&mut MidiCommands>,
time: Res<Time>,
mut timer: Local<Option<Timer>>,
mut note_index: Local<usize>,
mut playing: Local<bool>,
) {
// Toggle scale playback with space
if keyboard.just_pressed(KeyCode::Space) {
*playing = !*playing;
*note_index = 0;

// Stop any playing notes when toggling off
if !*playing {
for mut commands in &mut query {
for i in 0..127 {
commands.send(ChannelVoiceMessage::new(
Channel::One,
VoiceEvent::note_off(
Key::from_databyte(i).unwrap(),
Velocity::new_unchecked(0),
),
));
}
}
}
}

if !*playing {
return;
}

// Initialize timer if needed
let timer = timer.get_or_insert_with(|| Timer::from_seconds(0.3, TimerMode::Repeating));
timer.tick(time.delta());

if timer.just_finished() {
let scale = [60, 62, 64, 65, 67, 69, 71, 72]; // C major scale

for mut commands in &mut query {
// Turn off previous note
if *note_index > 0 {
let prev_note = scale[(*note_index - 1) % scale.len()];
commands.send(ChannelVoiceMessage::new(
Channel::One,
VoiceEvent::note_off(
Key::from_databyte(prev_note).unwrap(),
Velocity::new_unchecked(0),
),
));
}

// Play current note
let note = scale[*note_index % scale.len()];
commands.send(ChannelVoiceMessage::new(
Channel::One,
VoiceEvent::note_on(
Key::from_databyte(note).unwrap(),
Velocity::new_unchecked(80),
),
));

*note_index += 1;

// Stop after playing the scale twice
if *note_index >= scale.len() * 2 {
*playing = false;

commands.send(ChannelVoiceMessage::new(
Channel::One,
VoiceEvent::note_off(
Key::from_databyte(scale[(*note_index - 1) % scale.len()]).unwrap(),
Velocity::new_unchecked(0),
),
));
}
}
}
}

/// Handle keyboard input for playing notes
fn keyboard_input(keyboard: Res<ButtonInput<KeyCode>>, mut query: Query<&mut MidiCommands>) {
// Map keyboard keys to MIDI notes (C4 to B4)
let key_to_note = [
(KeyCode::KeyA, 60), // C4
(KeyCode::KeyW, 61), // C#4
(KeyCode::KeyS, 62), // D4
(KeyCode::KeyE, 63), // D#4
(KeyCode::KeyD, 64), // E4
(KeyCode::KeyF, 65), // F4
(KeyCode::KeyT, 66), // F#4
(KeyCode::KeyG, 67), // G4
(KeyCode::KeyY, 68), // G#4
(KeyCode::KeyH, 69), // A4
(KeyCode::KeyU, 70), // A#4
(KeyCode::KeyJ, 71), // B4
(KeyCode::KeyK, 72), // C5
];

for mut commands in &mut query {
// Handle note on/off for each key
for (key, note) in key_to_note {
if keyboard.just_pressed(key) {
commands.send(ChannelVoiceMessage::new(
Channel::One,
VoiceEvent::note_on(
Key::from_databyte(note).unwrap(),
Velocity::new_unchecked(0),
),
));
}
if keyboard.just_released(key) {
commands.send(ChannelVoiceMessage::new(
Channel::One,
VoiceEvent::note_off(
Key::from_databyte(note).unwrap(),
Velocity::new_unchecked(0),
),
));
}
}

// Panic button - stop all notes
if keyboard.just_pressed(KeyCode::Escape) {
for i in 0..127 {
commands.send(ChannelVoiceMessage::new(
Channel::One,
VoiceEvent::note_off(
Key::from_databyte(i).unwrap(),
Velocity::new_unchecked(0),
),
));
}
}
}
}

// /// Handle volume control
// fn volume_control(keyboard: Res<ButtonInput<KeyCode>>, mut query: Query<&mut MidiSynthConfig>) {
// for mut config in &mut query {
// if keyboard.just_pressed(KeyCode::Equal) || keyboard.just_pressed(KeyCode::NumpadAdd) {
// config.volume = (config.volume + 0.1).min(1.0);
// info!("Volume: {:.1}", config.volume);
// }
// if keyboard.just_pressed(KeyCode::Minus) || keyboard.just_pressed(KeyCode::NumpadSubtract) {
// config.volume = (config.volume - 0.1).max(0.0);
// info!("Volume: {:.1}", config.volume);
// }
// }
// }
30 changes: 30 additions & 0 deletions src/bevy/firewheel/components.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
use crate::prelude::{ChannelVoiceMessage, SoundFont};
use bevy::prelude::*;

/// Component for sending MIDI commands to a synthesizer node
#[derive(Component, Default)]
pub struct MidiCommands {
/// Queue of MIDI commands to send
pub queue: Vec<ChannelVoiceMessage>,
}

impl MidiCommands {
/// Add a MIDI command to the queue
pub fn send(&mut self, command: ChannelVoiceMessage) {
self.queue.push(command);
}

/// Add multiple MIDI commands to the queue
pub fn send_batch(&mut self, commands: impl IntoIterator<Item = ChannelVoiceMessage>) {
self.queue.extend(commands);
}

/// Take all commands, leaving the queue empty
pub fn take(&mut self) -> Vec<ChannelVoiceMessage> {
std::mem::take(&mut self.queue)
}
}

/// Component that specifies which soundfont to use for a MIDI synth
#[derive(Component)]
pub struct MidiSoundfont(pub Handle<SoundFont>);
113 changes: 113 additions & 0 deletions src/bevy/firewheel/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
use std::sync::Arc;

use bevy::prelude::*;
use bevy_seedling::{node::Events, prelude::*};
use firewheel::event::NodeEventType;

use crate::prelude::SoundFont;

mod components;
pub use components::*;
mod node;
pub use node::*;

/// Plugin for MIDI synthesis using Firewheel/bevy_seedling
pub struct FirewheelMidiPlugin;

impl Plugin for FirewheelMidiPlugin {
fn build(&self, app: &mut App) {
// Register our custom node type with bevy_seedling
// Since MidiSynthNode doesn't implement Diff/Patch, we use register_simple_node.
//
// This is mainly because we need more from rustysynth than is available.
app.register_simple_node::<MidiSynthNode>();

// Initialize soundfont assets
#[cfg(feature = "std")]
{
app.init_asset::<SoundFont>()
.init_asset_loader::<crate::bevy::asset::SoundFontLoader>();
}

// Add our systems
app.add_systems(Update, (spawn_midi_nodes, process_midi_commands).chain());
}
}

/// System that spawns MIDI synthesizer nodes for entities with soundfonts
#[allow(clippy::type_complexity)]
fn spawn_midi_nodes(
mut commands: Commands,
soundfont_assets: Res<Assets<SoundFont>>,
query: Query<(Entity, &MidiSoundfont), (Without<FirewheelNode>, With<MidiCommands>)>,
) {
for (entity, soundfont) in &query {
// Check if soundfont is loaded
let Some(soundfont_asset) = soundfont_assets.get(&soundfont.0) else {
continue;
};

// Get config or use defaults

let node = MidiSynthNode::new(Arc::clone(&soundfont_asset.file), true);

// Add the node and its configuration to the entity
// bevy_seedling will automatically handle node creation and connection
commands.entity(entity).insert(node);
}
}

/// System that processes MIDI commands and sends them to the audio nodes
fn process_midi_commands(mut query: Query<(&FirewheelNode, &mut MidiCommands, &mut Events)>) {
for (_, mut commands, mut events) in &mut query {
if commands.queue.is_empty() {
continue;
}

// Take all pending commands
let pending = commands.take();

// Send commands to the audio node as custom events
for command in pending {
events.push(NodeEventType::Custom(Box::new(command)));
}
}
}

// /// Extension trait for Commands to easily spawn MIDI synths
// pub trait MidiCommandsExt {
// /// Spawn a MIDI synthesizer with the given soundfont
// fn spawn_midi_synth(&mut self, soundfont: Handle<SoundFont>) -> EntityCommands<'_>;

// /// Spawn a MIDI synthesizer with custom configuration
// fn spawn_midi_synth_with_config(
// &mut self,
// soundfont: Handle<SoundFont>,
// config: MidiSynthConfig,
// ) -> Entity;
// }

// impl MidiCommandsExt for Commands<'_, '_> {
// fn spawn_midi_synth(&mut self, soundfont: Handle<SoundFont>) -> EntityCommands<'_> {
// self.spawn((
// MidiSoundfont(soundfont),
// MidiCommands::default(),
// MidiSynthConfig::default(),
// Name::new("MIDI Synthesizer"),
// ))
// }

// fn spawn_midi_synth_with_config(
// &mut self,
// soundfont: Handle<SoundFont>,
// config: MidiSynthConfig,
// ) -> Entity {
// self.spawn((
// MidiSoundfont(soundfont),
// MidiCommands::default(),
// config,
// Name::new("MIDI Synthesizer"),
// ))
// .id()
// }
// }
Loading