Skip to content
Merged
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
373 changes: 182 additions & 191 deletions Cargo.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion crates/processing_midi/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ edition = "2024"
[dependencies]
bevy = { workspace = true }
processing_core = { workspace = true }
bevy_midi = { git = "https://github.com/BlackPhlox/bevy_midi", branch = "latest" }
nannou_midi = { git = "https://github.com/nannou-org/nannou", branch = "bevy-refactor" }

[lints]
workspace = true
161 changes: 117 additions & 44 deletions crates/processing_midi/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,75 +1,98 @@
use bevy::prelude::*;
use bevy_midi::prelude::*;
use nannou_midi::{MidiOutput, MidiOutputStream, MidiPort, MidiPortDirection};

use processing_core::app_mut;
use processing_core::error::{self, Result};

pub use nannou_midi::{MidiData, MidiMessage};

pub struct MidiPlugin;

pub const NOTE_ON: u8 = 0b1001_0000;
pub const NOTE_OFF: u8 = 0b1000_0000;

#[derive(Resource, Default)]
struct ActiveOutput(Option<Entity>);

impl Plugin for MidiPlugin {
fn build(&self, app: &mut App) {
// TODO: Update `bevy_midi` to treat connections as entities
// in order to support hot-plugging
app.insert_resource(MidiOutputSettings {
port_name: "libprocessing output",
});

app.add_plugins(MidiOutputPlugin);
app.init_resource::<ActiveOutput>();
app.add_plugins(nannou_midi::MidiPlugin);
}
}

pub fn connect(In(port): In<usize>, output: Res<MidiOutput>) -> Result<()> {
match output.ports().get(port) {
Some((_, p)) => {
output.connect(p.clone());
Ok(())
}
None => Err(error::ProcessingError::MidiPortNotFound(port)),
}
}

pub fn disconnect(output: Res<MidiOutput>) -> Result<()> {
output.disconnect();
Ok(())
fn sorted_output_ports(world: &mut World) -> Vec<(Entity, String)> {
let mut q = world.query::<(Entity, &Name, &MidiPort)>();
let mut ports: Vec<(Entity, String)> = q
.iter(world)
.filter(|(_, _, p)| p.direction == MidiPortDirection::Output)
.map(|(e, n, _)| (e, n.as_str().to_string()))
.collect();
ports.sort_by(|a, b| a.1.cmp(&b.1));
ports
}

pub fn refresh_ports(output: Res<MidiOutput>) -> Result<()> {
output.refresh_ports();
Ok(())
}

pub fn list_ports(output: Res<MidiOutput>) -> Result<Vec<String>> {
Ok(output
.ports()
.iter()
pub fn list_ports(world: &mut World) -> Result<Vec<String>> {
Ok(sorted_output_ports(world)
.into_iter()
.enumerate()
.map(|(i, (name, _))| format!("{}: {}", i, name))
.map(|(i, (_, name))| format!("{i}: {name}"))
.collect())
}

pub fn play_notes(In((note, duration)): In<(u8, u64)>, output: Res<MidiOutput>) -> Result<()> {
output.send([NOTE_ON, note, 127].into()); // Note on, channel 1, max velocity
pub fn connect(In(port): In<usize>, world: &mut World) -> Result<()> {
let port_entity = sorted_output_ports(world)
.get(port)
.map(|(e, _)| *e)
.ok_or(error::ProcessingError::MidiPortNotFound(port))?;

let previous = world.resource::<ActiveOutput>().0;
if let Some(e) = previous
&& let Ok(entity) = world.get_entity_mut(e)
{
entity.despawn();
}

std::thread::sleep(std::time::Duration::from_millis(duration));
let connection = world
.spawn((
Name::new("libprocessing output"),
MidiOutput {
port: Some(port_entity),
},
))
.id();
world.resource_mut::<ActiveOutput>().0 = Some(connection);
Ok(())
}

output.send([NOTE_OFF, note, 127].into()); // Note off, channel 1, max velocity
pub fn disconnect(world: &mut World) -> Result<()> {
let entity = world.resource_mut::<ActiveOutput>().0.take();
if let Some(e) = entity
&& let Ok(entity_mut) = world.get_entity_mut(e)
{
entity_mut.despawn();
}
Ok(())
}

pub fn send_message(In(msg): In<MidiMessage>, world: &mut World) -> Result<()> {
let entity = world
.resource::<ActiveOutput>()
.0
.ok_or(error::ProcessingError::MidiPortNotFound(usize::MAX))?;
if let Some(mut stream) = world.get_mut::<MidiOutputStream>(entity) {
stream.send(msg);
}
Ok(())
}

#[cfg(not(target_arch = "wasm32"))]
pub fn midi_refresh_ports() -> error::Result<()> {
app_mut(|app| {
let world = app.world_mut();
world.run_system_cached(refresh_ports).unwrap()
})?;
// run the `PreUpdate` schedule to let `bevy_midi` process it's callbacks and update the ports list
// TODO: race condition is still present here in theory
app_mut(|app| {
app.world_mut().run_schedule(PreUpdate);
world
.run_system_cached(nannou_midi::native::enumerate_midi_ports)
.unwrap();
Ok(())
})
}
Expand All @@ -86,7 +109,12 @@ pub fn midi_list_ports() -> error::Result<Vec<String>> {
pub fn midi_connect(port: usize) -> error::Result<()> {
app_mut(|app| {
let world = app.world_mut();
world.run_system_cached_with(connect, port).unwrap()
world.run_system_cached_with(connect, port).unwrap()?;
// Materialize the MidiOutputStream component before the caller sends.
world
.run_system_cached(nannou_midi::native::open_midi_outputs)
.unwrap();
Ok(())
})
}

Expand All @@ -98,12 +126,57 @@ pub fn midi_disconnect() -> error::Result<()> {
})
}

#[cfg(not(target_arch = "wasm32"))]
pub fn midi_note_on(note: u8, velocity: u8) -> error::Result<()> {
app_mut(|app| {
let world = app.world_mut();
world
.run_system_cached_with(send_message, MidiMessage::from([NOTE_ON, note, velocity]))
.unwrap()?;
world
.run_system_cached(nannou_midi::native::send_midi_messages)
.unwrap();
Ok(())
})
}

#[cfg(not(target_arch = "wasm32"))]
pub fn midi_note_off(note: u8) -> error::Result<()> {
app_mut(|app| {
let world = app.world_mut();
world
.run_system_cached_with(send_message, MidiMessage::from([NOTE_OFF, note, 0]))
.unwrap()?;
world
.run_system_cached(nannou_midi::native::send_midi_messages)
.unwrap();
Ok(())
})
}

#[cfg(not(target_arch = "wasm32"))]
pub fn midi_play_notes(note: u8, duration: u64) -> error::Result<()> {
app_mut(|app| {
let world = app.world_mut();
world
.run_system_cached_with(play_notes, (note, duration))
.unwrap()
.run_system_cached_with(send_message, MidiMessage::from([NOTE_ON, note, 127]))
.unwrap()?;
world
.run_system_cached(nannou_midi::native::send_midi_messages)
.unwrap();
Ok(())
})?;

std::thread::sleep(std::time::Duration::from_millis(duration));

app_mut(|app| {
let world = app.world_mut();
world
.run_system_cached_with(send_message, MidiMessage::from([NOTE_OFF, note, 127]))
.unwrap()?;
world
.run_system_cached(nannou_midi::native::send_midi_messages)
.unwrap();
Ok(())
})
}
8 changes: 8 additions & 0 deletions crates/processing_pyo3/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1947,6 +1947,14 @@ mod mewnala {
fn midi_play_notes(note: u8, duration: u64) -> PyResult<()> {
midi::play_notes(note, duration)
}
#[pyfunction]
fn midi_note_on(note: u8, velocity: u8) -> PyResult<()> {
midi::note_on(note, velocity)
}
#[pyfunction]
fn midi_note_off(note: u8) -> PyResult<()> {
midi::note_off(note)
}

#[pyfunction]
fn key_is_down(key_code: u32) -> PyResult<bool> {
Expand Down
6 changes: 6 additions & 0 deletions crates/processing_pyo3/src/midi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,9 @@ pub fn list_ports() -> PyResult<Vec<String>> {
pub fn play_notes(note: u8, duration: u64) -> PyResult<()> {
midi_play_notes(note, duration).map_err(|e| PyRuntimeError::new_err(format!("{e}")))
}
pub fn note_on(note: u8, velocity: u8) -> PyResult<()> {
midi_note_on(note, velocity).map_err(|e| PyRuntimeError::new_err(format!("{e}")))
}
pub fn note_off(note: u8) -> PyResult<()> {
midi_note_off(note).map_err(|e| PyRuntimeError::new_err(format!("{e}")))
}
4 changes: 2 additions & 2 deletions crates/processing_render/src/surface.rs
Original file line number Diff line number Diff line change
Expand Up @@ -303,7 +303,7 @@ pub fn create_surface_x11(
HandleError::Unavailable,
));
}
let display_ptr = NonNull::new(display_handle as *mut c_void).unwrap();
let display_ptr = NonNull::new(display_handle as *mut std::ffi::c_void).unwrap();
let display = XlibDisplayHandle::new(Some(display_ptr), 0); // screen 0

spawn_surface(
Expand Down Expand Up @@ -331,7 +331,7 @@ pub fn create_surface_web(
if window_handle == 0 {
return Err(error::ProcessingError::InvalidWindowHandle);
}
let canvas_ptr = NonNull::new(window_handle as *mut c_void).unwrap();
let canvas_ptr = NonNull::new(window_handle as *mut std::ffi::c_void).unwrap();
let window = WebCanvasWindowHandle::new(canvas_ptr.cast());
let display = WebDisplayHandle::new();

Expand Down
Loading
Loading