Skip to content

Commit af06f9b

Browse files
authored
Switch to nannou_midi. (#158)
1 parent d50b5c1 commit af06f9b

8 files changed

Lines changed: 470 additions & 269 deletions

File tree

Cargo.lock

Lines changed: 182 additions & 191 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/processing_midi/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ edition = "2024"
66
[dependencies]
77
bevy = { workspace = true }
88
processing_core = { workspace = true }
9-
bevy_midi = { git = "https://github.com/BlackPhlox/bevy_midi", branch = "latest" }
9+
nannou_midi = { git = "https://github.com/nannou-org/nannou", branch = "bevy-refactor" }
1010

1111
[lints]
1212
workspace = true

crates/processing_midi/src/lib.rs

Lines changed: 117 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,75 +1,98 @@
11
use bevy::prelude::*;
2-
use bevy_midi::prelude::*;
2+
use nannou_midi::{MidiOutput, MidiOutputStream, MidiPort, MidiPortDirection};
33

44
use processing_core::app_mut;
55
use processing_core::error::{self, Result};
66

7+
pub use nannou_midi::{MidiData, MidiMessage};
8+
79
pub struct MidiPlugin;
810

911
pub const NOTE_ON: u8 = 0b1001_0000;
1012
pub const NOTE_OFF: u8 = 0b1000_0000;
1113

14+
#[derive(Resource, Default)]
15+
struct ActiveOutput(Option<Entity>);
16+
1217
impl Plugin for MidiPlugin {
1318
fn build(&self, app: &mut App) {
14-
// TODO: Update `bevy_midi` to treat connections as entities
15-
// in order to support hot-plugging
16-
app.insert_resource(MidiOutputSettings {
17-
port_name: "libprocessing output",
18-
});
19-
20-
app.add_plugins(MidiOutputPlugin);
19+
app.init_resource::<ActiveOutput>();
20+
app.add_plugins(nannou_midi::MidiPlugin);
2121
}
2222
}
2323

24-
pub fn connect(In(port): In<usize>, output: Res<MidiOutput>) -> Result<()> {
25-
match output.ports().get(port) {
26-
Some((_, p)) => {
27-
output.connect(p.clone());
28-
Ok(())
29-
}
30-
None => Err(error::ProcessingError::MidiPortNotFound(port)),
31-
}
32-
}
33-
34-
pub fn disconnect(output: Res<MidiOutput>) -> Result<()> {
35-
output.disconnect();
36-
Ok(())
24+
fn sorted_output_ports(world: &mut World) -> Vec<(Entity, String)> {
25+
let mut q = world.query::<(Entity, &Name, &MidiPort)>();
26+
let mut ports: Vec<(Entity, String)> = q
27+
.iter(world)
28+
.filter(|(_, _, p)| p.direction == MidiPortDirection::Output)
29+
.map(|(e, n, _)| (e, n.as_str().to_string()))
30+
.collect();
31+
ports.sort_by(|a, b| a.1.cmp(&b.1));
32+
ports
3733
}
3834

39-
pub fn refresh_ports(output: Res<MidiOutput>) -> Result<()> {
40-
output.refresh_ports();
41-
Ok(())
42-
}
43-
44-
pub fn list_ports(output: Res<MidiOutput>) -> Result<Vec<String>> {
45-
Ok(output
46-
.ports()
47-
.iter()
35+
pub fn list_ports(world: &mut World) -> Result<Vec<String>> {
36+
Ok(sorted_output_ports(world)
37+
.into_iter()
4838
.enumerate()
49-
.map(|(i, (name, _))| format!("{}: {}", i, name))
39+
.map(|(i, (_, name))| format!("{i}: {name}"))
5040
.collect())
5141
}
5242

53-
pub fn play_notes(In((note, duration)): In<(u8, u64)>, output: Res<MidiOutput>) -> Result<()> {
54-
output.send([NOTE_ON, note, 127].into()); // Note on, channel 1, max velocity
43+
pub fn connect(In(port): In<usize>, world: &mut World) -> Result<()> {
44+
let port_entity = sorted_output_ports(world)
45+
.get(port)
46+
.map(|(e, _)| *e)
47+
.ok_or(error::ProcessingError::MidiPortNotFound(port))?;
48+
49+
let previous = world.resource::<ActiveOutput>().0;
50+
if let Some(e) = previous
51+
&& let Ok(entity) = world.get_entity_mut(e)
52+
{
53+
entity.despawn();
54+
}
5555

56-
std::thread::sleep(std::time::Duration::from_millis(duration));
56+
let connection = world
57+
.spawn((
58+
Name::new("libprocessing output"),
59+
MidiOutput {
60+
port: Some(port_entity),
61+
},
62+
))
63+
.id();
64+
world.resource_mut::<ActiveOutput>().0 = Some(connection);
65+
Ok(())
66+
}
5767

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

78+
pub fn send_message(In(msg): In<MidiMessage>, world: &mut World) -> Result<()> {
79+
let entity = world
80+
.resource::<ActiveOutput>()
81+
.0
82+
.ok_or(error::ProcessingError::MidiPortNotFound(usize::MAX))?;
83+
if let Some(mut stream) = world.get_mut::<MidiOutputStream>(entity) {
84+
stream.send(msg);
85+
}
6086
Ok(())
6187
}
6288

6389
#[cfg(not(target_arch = "wasm32"))]
6490
pub fn midi_refresh_ports() -> error::Result<()> {
6591
app_mut(|app| {
6692
let world = app.world_mut();
67-
world.run_system_cached(refresh_ports).unwrap()
68-
})?;
69-
// run the `PreUpdate` schedule to let `bevy_midi` process it's callbacks and update the ports list
70-
// TODO: race condition is still present here in theory
71-
app_mut(|app| {
72-
app.world_mut().run_schedule(PreUpdate);
93+
world
94+
.run_system_cached(nannou_midi::native::enumerate_midi_ports)
95+
.unwrap();
7396
Ok(())
7497
})
7598
}
@@ -86,7 +109,12 @@ pub fn midi_list_ports() -> error::Result<Vec<String>> {
86109
pub fn midi_connect(port: usize) -> error::Result<()> {
87110
app_mut(|app| {
88111
let world = app.world_mut();
89-
world.run_system_cached_with(connect, port).unwrap()
112+
world.run_system_cached_with(connect, port).unwrap()?;
113+
// Materialize the MidiOutputStream component before the caller sends.
114+
world
115+
.run_system_cached(nannou_midi::native::open_midi_outputs)
116+
.unwrap();
117+
Ok(())
90118
})
91119
}
92120

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

129+
#[cfg(not(target_arch = "wasm32"))]
130+
pub fn midi_note_on(note: u8, velocity: u8) -> error::Result<()> {
131+
app_mut(|app| {
132+
let world = app.world_mut();
133+
world
134+
.run_system_cached_with(send_message, MidiMessage::from([NOTE_ON, note, velocity]))
135+
.unwrap()?;
136+
world
137+
.run_system_cached(nannou_midi::native::send_midi_messages)
138+
.unwrap();
139+
Ok(())
140+
})
141+
}
142+
143+
#[cfg(not(target_arch = "wasm32"))]
144+
pub fn midi_note_off(note: u8) -> error::Result<()> {
145+
app_mut(|app| {
146+
let world = app.world_mut();
147+
world
148+
.run_system_cached_with(send_message, MidiMessage::from([NOTE_OFF, note, 0]))
149+
.unwrap()?;
150+
world
151+
.run_system_cached(nannou_midi::native::send_midi_messages)
152+
.unwrap();
153+
Ok(())
154+
})
155+
}
156+
101157
#[cfg(not(target_arch = "wasm32"))]
102158
pub fn midi_play_notes(note: u8, duration: u64) -> error::Result<()> {
103159
app_mut(|app| {
104160
let world = app.world_mut();
105161
world
106-
.run_system_cached_with(play_notes, (note, duration))
107-
.unwrap()
162+
.run_system_cached_with(send_message, MidiMessage::from([NOTE_ON, note, 127]))
163+
.unwrap()?;
164+
world
165+
.run_system_cached(nannou_midi::native::send_midi_messages)
166+
.unwrap();
167+
Ok(())
168+
})?;
169+
170+
std::thread::sleep(std::time::Duration::from_millis(duration));
171+
172+
app_mut(|app| {
173+
let world = app.world_mut();
174+
world
175+
.run_system_cached_with(send_message, MidiMessage::from([NOTE_OFF, note, 127]))
176+
.unwrap()?;
177+
world
178+
.run_system_cached(nannou_midi::native::send_midi_messages)
179+
.unwrap();
180+
Ok(())
108181
})
109182
}

crates/processing_pyo3/src/lib.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1947,6 +1947,14 @@ mod mewnala {
19471947
fn midi_play_notes(note: u8, duration: u64) -> PyResult<()> {
19481948
midi::play_notes(note, duration)
19491949
}
1950+
#[pyfunction]
1951+
fn midi_note_on(note: u8, velocity: u8) -> PyResult<()> {
1952+
midi::note_on(note, velocity)
1953+
}
1954+
#[pyfunction]
1955+
fn midi_note_off(note: u8) -> PyResult<()> {
1956+
midi::note_off(note)
1957+
}
19501958

19511959
#[pyfunction]
19521960
fn key_is_down(key_code: u32) -> PyResult<bool> {

crates/processing_pyo3/src/midi.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,9 @@ pub fn list_ports() -> PyResult<Vec<String>> {
1616
pub fn play_notes(note: u8, duration: u64) -> PyResult<()> {
1717
midi_play_notes(note, duration).map_err(|e| PyRuntimeError::new_err(format!("{e}")))
1818
}
19+
pub fn note_on(note: u8, velocity: u8) -> PyResult<()> {
20+
midi_note_on(note, velocity).map_err(|e| PyRuntimeError::new_err(format!("{e}")))
21+
}
22+
pub fn note_off(note: u8) -> PyResult<()> {
23+
midi_note_off(note).map_err(|e| PyRuntimeError::new_err(format!("{e}")))
24+
}

crates/processing_render/src/surface.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -303,7 +303,7 @@ pub fn create_surface_x11(
303303
HandleError::Unavailable,
304304
));
305305
}
306-
let display_ptr = NonNull::new(display_handle as *mut c_void).unwrap();
306+
let display_ptr = NonNull::new(display_handle as *mut std::ffi::c_void).unwrap();
307307
let display = XlibDisplayHandle::new(Some(display_ptr), 0); // screen 0
308308

309309
spawn_surface(
@@ -331,7 +331,7 @@ pub fn create_surface_web(
331331
if window_handle == 0 {
332332
return Err(error::ProcessingError::InvalidWindowHandle);
333333
}
334-
let canvas_ptr = NonNull::new(window_handle as *mut c_void).unwrap();
334+
let canvas_ptr = NonNull::new(window_handle as *mut std::ffi::c_void).unwrap();
335335
let window = WebCanvasWindowHandle::new(canvas_ptr.cast());
336336
let display = WebDisplayHandle::new();
337337

0 commit comments

Comments
 (0)