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
2 changes: 2 additions & 0 deletions crates/processing_core/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,6 @@ pub enum ProcessingError {
ShaderCompilationError(String),
#[error("Shader not found")]
ShaderNotFound,
#[error("MIDI port {0} not found")]
MidiPortNotFound(usize),
}
76 changes: 70 additions & 6 deletions crates/processing_midi/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
use bevy::prelude::*;
use bevy_midi::prelude::*;

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

pub struct MidiPlugin;

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

impl Plugin for MidiPlugin {
fn build(&self, app: &mut App) {
// TODO: Update `bevy_midi` to treat connections as entities
Expand All @@ -18,10 +22,13 @@ impl Plugin for MidiPlugin {
}

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

pub fn disconnect(output: Res<MidiOutput>) -> Result<()> {
Expand All @@ -34,12 +41,69 @@ pub fn refresh_ports(output: Res<MidiOutput>) -> Result<()> {
Ok(())
}

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

pub fn play_notes(In((note, duration)): In<(u8, u64)>, output: Res<MidiOutput>) -> Result<()> {
output.send([0b1001_0000, note, 127].into()); // Note on, channel 1, max velocity
output.send([NOTE_ON, note, 127].into()); // Note on, channel 1, max velocity

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

output.send([0b1000_0000, note, 127].into()); // Note on, channel 1, max velocity
output.send([NOTE_OFF, note, 127].into()); // Note off, channel 1, max velocity

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);
Ok(())
})
}

#[cfg(not(target_arch = "wasm32"))]
pub fn midi_list_ports() -> error::Result<Vec<String>> {
app_mut(|app| {
let world = app.world_mut();
world.run_system_cached(list_ports).unwrap()
})
}

#[cfg(not(target_arch = "wasm32"))]
pub fn midi_connect(port: usize) -> error::Result<()> {
app_mut(|app| {
let world = app.world_mut();
world.run_system_cached_with(connect, port).unwrap()
})
}

#[cfg(not(target_arch = "wasm32"))]
pub fn midi_disconnect() -> error::Result<()> {
app_mut(|app| {
let world = app.world_mut();
world.run_system_cached(disconnect).unwrap()
})
}

#[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()
})
}
4 changes: 3 additions & 1 deletion crates/processing_pyo3/examples/midi.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@
def setup():
size(800, 600)

# Refresh midi port list, and connect to first one
# Refresh midi port list, print available ports, and connect to first one
midi_refresh_ports()
for port in midi_list_ports():
print(port)
midi_connect(0)

def draw():
Expand Down
5 changes: 5 additions & 0 deletions crates/processing_pyo3/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ fn processing(m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_function(wrap_pyfunction!(midi_connect, m)?)?;
m.add_function(wrap_pyfunction!(midi_disconnect, m)?)?;
m.add_function(wrap_pyfunction!(midi_refresh_ports, m)?)?;
m.add_function(wrap_pyfunction!(midi_list_ports, m)?)?;
m.add_function(wrap_pyfunction!(midi_play_notes, m)?)?;

#[cfg(feature = "webcam")]
Expand Down Expand Up @@ -609,6 +610,10 @@ fn midi_refresh_ports() -> PyResult<()> {
midi::refresh_ports()
}
#[pyfunction]
fn midi_list_ports() -> PyResult<Vec<String>> {
midi::list_ports()
}
#[pyfunction]
fn midi_play_notes(note: u8, duration: u64) -> PyResult<()> {
midi::play_notes(note, duration)
}
3 changes: 3 additions & 0 deletions crates/processing_pyo3/src/midi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ pub fn disconnect() -> PyResult<()> {
pub fn refresh_ports() -> PyResult<()> {
midi_refresh_ports().map_err(|e| PyRuntimeError::new_err(format!("{e}")))
}
pub fn list_ports() -> PyResult<Vec<String>> {
midi_list_ports().map_err(|e| PyRuntimeError::new_err(format!("{e}")))
}
pub fn play_notes(note: u8, duration: u64) -> PyResult<()> {
midi_play_notes(note, duration).map_err(|e| PyRuntimeError::new_err(format!("{e}")))
}
40 changes: 0 additions & 40 deletions crates/processing_render/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1260,43 +1260,3 @@ pub fn gltf_light(gltf_entity: Entity, index: usize) -> error::Result<Entity> {
.unwrap()
})
}

#[cfg(not(target_arch = "wasm32"))]
pub fn midi_refresh_ports() -> error::Result<()> {
app_mut(|app| {
let world = app.world_mut();
world
.run_system_cached(processing_midi::refresh_ports)
.unwrap()
})
}

#[cfg(not(target_arch = "wasm32"))]
pub fn midi_connect(port: usize) -> error::Result<()> {
app_mut(|app| {
let world = app.world_mut();
world
.run_system_cached_with(processing_midi::connect, port)
.unwrap()
})
}

#[cfg(not(target_arch = "wasm32"))]
pub fn midi_disconnect() -> error::Result<()> {
app_mut(|app| {
let world = app.world_mut();
world
.run_system_cached(processing_midi::disconnect)
.unwrap()
})
}

#[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(processing_midi::play_notes, (note, duration))
.unwrap()
})
}
5 changes: 4 additions & 1 deletion examples/midi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,10 @@ fn sketch() -> error::Result<()> {
let graphics = graphics_create(surface, width, height, TextureFormat::Rgba16Float)?;

midi_refresh_ports()?;
midi_connect(0)?;
for port in midi_list_ports()? {
println!("{port}");
}
midi_connect(1)?;

let mut rng = rand::rng();

Expand Down
3 changes: 3 additions & 0 deletions src/prelude.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
pub use bevy::prelude::default;
pub use bevy::render::render_resource::TextureFormat;
pub use processing_core::{config::*, error};
pub use processing_midi::{
midi_connect, midi_disconnect, midi_list_ports, midi_play_notes, midi_refresh_ports,
};
pub use processing_render::{
render::command::{DrawCommand, StrokeCapMode, StrokeJoinMode},
*,
Expand Down