diff --git a/Cargo.lock b/Cargo.lock index 676be54..e36e0b0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1874,7 +1874,7 @@ dependencies = [ "cc", "cfg-if", "constant_time_eq", - "cpufeatures", + "cpufeatures 0.2.17", ] [[package]] @@ -1898,7 +1898,7 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" dependencies = [ - "objc2 0.6.3", + "objc2 0.6.4", ] [[package]] @@ -2048,6 +2048,17 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "chacha20" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "rand_core 0.10.0", +] + [[package]] name = "clang-sys" version = "1.8.1" @@ -2336,6 +2347,15 @@ dependencies = [ "libc", ] +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + [[package]] name = "crc32fast" version = "1.5.0" @@ -2460,14 +2480,14 @@ checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" [[package]] name = "dispatch2" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" dependencies = [ "bitflags 2.11.0", "block2 0.6.2", "libc", - "objc2 0.6.3", + "objc2 0.6.4", ] [[package]] @@ -2899,6 +2919,7 @@ dependencies = [ "cfg-if", "libc", "r-efi", + "rand_core 0.10.0", "wasip2", "wasip3", ] @@ -4071,9 +4092,9 @@ dependencies = [ [[package]] name = "objc2" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c2599ce0ec54857b29ce62166b0ed9b4f6f1a70ccc9a71165b6154caca8c05" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" dependencies = [ "objc2-encode", ] @@ -4103,7 +4124,7 @@ dependencies = [ "bitflags 2.11.0", "block2 0.6.2", "libc", - "objc2 0.6.3", + "objc2 0.6.4", "objc2-cloud-kit 0.3.2", "objc2-core-data 0.3.2", "objc2-core-foundation", @@ -4135,7 +4156,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" dependencies = [ "bitflags 2.11.0", - "objc2 0.6.3", + "objc2 0.6.4", "objc2-foundation 0.3.2", ] @@ -4169,7 +4190,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa" dependencies = [ "bitflags 2.11.0", - "objc2 0.6.3", + "objc2 0.6.4", "objc2-foundation 0.3.2", ] @@ -4181,7 +4202,7 @@ checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ "bitflags 2.11.0", "dispatch2", - "objc2 0.6.3", + "objc2 0.6.4", ] [[package]] @@ -4192,7 +4213,7 @@ checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" dependencies = [ "bitflags 2.11.0", "dispatch2", - "objc2 0.6.3", + "objc2 0.6.4", "objc2-core-foundation", "objc2-io-surface", ] @@ -4215,7 +4236,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006" dependencies = [ - "objc2 0.6.3", + "objc2 0.6.4", "objc2-foundation 0.3.2", ] @@ -4238,7 +4259,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" dependencies = [ "bitflags 2.11.0", - "objc2 0.6.3", + "objc2 0.6.4", "objc2-core-foundation", "objc2-core-graphics", ] @@ -4250,7 +4271,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d425caf1df73233f29fd8a5c3e5edbc30d2d4307870f802d18f00d83dc5141a6" dependencies = [ "bitflags 2.11.0", - "objc2 0.6.3", + "objc2 0.6.4", "objc2-core-foundation", "objc2-core-graphics", "objc2-io-surface", @@ -4282,7 +4303,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" dependencies = [ "bitflags 2.11.0", - "objc2 0.6.3", + "objc2 0.6.4", "objc2-core-foundation", ] @@ -4304,7 +4325,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" dependencies = [ "bitflags 2.11.0", - "objc2 0.6.3", + "objc2 0.6.4", "objc2-core-foundation", ] @@ -4352,7 +4373,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" dependencies = [ "bitflags 2.11.0", - "objc2 0.6.3", + "objc2 0.6.4", "objc2-foundation 0.3.2", ] @@ -4710,8 +4731,8 @@ version = "0.1.0" dependencies = [ "bevy", "glfw", - "processing_midi", "processing_render", + "rand 0.10.0", ] [[package]] @@ -4723,14 +4744,6 @@ dependencies = [ "processing", ] -[[package]] -name = "processing_midi" -version = "0.1.0" -dependencies = [ - "bevy", - "bevy_midi", -] - [[package]] name = "processing_pyo3" version = "0.1.0" @@ -4747,13 +4760,13 @@ name = "processing_render" version = "0.1.0" dependencies = [ "bevy", + "bevy_midi", "crossbeam-channel", "half", "js-sys", "lyon", - "objc2 0.6.3", + "objc2 0.6.4", "objc2-app-kit 0.3.2", - "processing_midi", "raw-window-handle", "thiserror 2.0.18", "tracing", @@ -4927,6 +4940,7 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc266eb313df6c5c09c1c7b1fbe2510961e5bcd3add930c1e31f7ed9da0feff8" dependencies = [ + "chacha20", "getrandom 0.4.1", "rand_core 0.10.0", ] @@ -7085,18 +7099,18 @@ checksum = "6df3dc4292935e51816d896edcd52aa30bc297907c26167fec31e2b0c6a32524" [[package]] name = "zerocopy" -version = "0.8.39" +version = "0.8.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db6d35d663eadb6c932438e763b262fe1a70987f9ae936e60158176d710cae4a" +checksum = "a789c6e490b576db9f7e6b6d661bcc9799f7c0ac8352f56ea20193b2681532e5" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.39" +version = "0.8.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4122cd3169e94605190e77839c9a40d40ed048d305bfdc146e7df40ab0f3e517" +checksum = "f65c489a7071a749c849713807783f70672b28094011623e200cb86dcb835953" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index d873fef..2af1ae6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,15 +25,14 @@ bevy = { git = "https://github.com/bevyengine/bevy", branch = "main" } processing = { path = "." } processing_pyo3 = { path = "crates/processing_pyo3" } processing_render = { path = "crates/processing_render" } -processing_midi = { path = "crates/processing_midi" } [dependencies] bevy = { workspace = true } processing_render = { workspace = true } -processing_midi = { workspace = true } [dev-dependencies] glfw = "0.60.0" +rand = "0.10.0" [target.'cfg(target_os = "linux")'.dev-dependencies] glfw = { version = "0.60.0", features = ["wayland"] } diff --git a/crates/processing_midi/Cargo.toml b/crates/processing_midi/Cargo.toml deleted file mode 100644 index 7c087a5..0000000 --- a/crates/processing_midi/Cargo.toml +++ /dev/null @@ -1,12 +0,0 @@ -[package] -name = "processing_midi" -version = "0.1.0" -edition = "2024" - -[dependencies] -bevy = { workspace = true } -bevy_midi = { git = "https://github.com/BlackPhlox/bevy_midi", branch = "latest" } - - -[lints] -workspace = true diff --git a/crates/processing_midi/src/lib.rs b/crates/processing_midi/src/lib.rs deleted file mode 100644 index 3e6186f..0000000 --- a/crates/processing_midi/src/lib.rs +++ /dev/null @@ -1,25 +0,0 @@ -use bevy::prelude::*; -use bevy_midi::prelude::*; - -pub struct MidiPlugin; - -impl Plugin for MidiPlugin { - fn build(&self, app: &mut App) { - app.insert_resource(MidiOutputSettings { - port_name: "output", - }) - .add_plugins(MidiOutputPlugin); - } -} - -enum MidiCommand {} - -pub fn connect(port: usize) { - // we need to work with the ECS - // do we pass a MidiCommand to Bevy? -} - -pub fn disconnect() {} -pub fn refresh_ports() {} - -pub fn play_notes() {} diff --git a/crates/processing_pyo3/examples/midi.py b/crates/processing_pyo3/examples/midi.py new file mode 100644 index 0000000..1af87d1 --- /dev/null +++ b/crates/processing_pyo3/examples/midi.py @@ -0,0 +1,26 @@ +from processing import * +import random + +def setup(): + size(800, 600) + + # Refresh midi port list, and connect to first one + midi_refresh_ports() + midi_connect(0) + +def draw(): + background(220) + + fill(255, 0, 100) + stroke(1) + stroke_weight(2) + rect(100, 100, 200, 150) + + # pick a random note value, and duration value for that note + # then send the midi command + note = random.randint(57,68) + note_duration = random.randint(25, 250) + midi_play_notes(note, note_duration) + +# TODO: this should happen implicitly on module load somehow +run() diff --git a/crates/processing_pyo3/src/lib.rs b/crates/processing_pyo3/src/lib.rs index f67b645..8982c04 100644 --- a/crates/processing_pyo3/src/lib.rs +++ b/crates/processing_pyo3/src/lib.rs @@ -12,9 +12,11 @@ mod glfw; mod gltf; mod graphics; pub(crate) mod material; +mod midi; use graphics::{Geometry, Graphics, Image, Light, Topology, get_graphics, get_graphics_mut}; use material::Material; + use pyo3::{ exceptions::PyRuntimeError, prelude::*, @@ -22,8 +24,8 @@ use pyo3::{ }; use std::ffi::{CStr, CString}; -use gltf::Gltf; use bevy::log::warn; +use gltf::Gltf; use std::env; /// Get a shared ref to the Graphics context, or return Ok(()) if not yet initialized. @@ -78,6 +80,10 @@ fn processing(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_function(wrap_pyfunction!(metallic, m)?)?; m.add_function(wrap_pyfunction!(emissive, m)?)?; m.add_function(wrap_pyfunction!(unlit, m)?)?; + 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_play_notes, m)?)?; Ok(()) } @@ -544,3 +550,24 @@ fn emissive(module: &Bound<'_, PyModule>, args: &Bound<'_, PyTuple>) -> PyResult fn unlit(module: &Bound<'_, PyModule>) -> PyResult<()> { graphics!(module).unlit() } + +#[pyfunction] +#[pyo3(pass_module)] +fn midi_connect(module: &Bound<'_, PyModule>, port: usize) -> PyResult<()> { + midi::connect(port) +} +#[pyfunction] +#[pyo3(pass_module)] +fn midi_disconnect(module: &Bound<'_, PyModule>) -> PyResult<()> { + midi::disconnect() +} +#[pyfunction] +#[pyo3(pass_module)] +fn midi_refresh_ports(module: &Bound<'_, PyModule>) -> PyResult<()> { + midi::refresh_ports() +} +#[pyfunction] +#[pyo3(pass_module)] +fn midi_play_notes(module: &Bound<'_, PyModule>, note: u8, duration: u64) -> PyResult<()> { + midi::play_notes(note, duration) +} diff --git a/crates/processing_pyo3/src/midi.rs b/crates/processing_pyo3/src/midi.rs new file mode 100644 index 0000000..2f88b50 --- /dev/null +++ b/crates/processing_pyo3/src/midi.rs @@ -0,0 +1,15 @@ +use processing::prelude::*; +use pyo3::{exceptions::PyRuntimeError, prelude::*}; + +pub fn connect(port: usize) -> PyResult<()> { + midi_connect(port).map_err(|e| PyRuntimeError::new_err(format!("{e}"))) +} +pub fn disconnect() -> PyResult<()> { + midi_disconnect().map_err(|e| PyRuntimeError::new_err(format!("{e}"))) +} +pub fn refresh_ports() -> PyResult<()> { + midi_refresh_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}"))) +} diff --git a/crates/processing_render/Cargo.toml b/crates/processing_render/Cargo.toml index 64a976d..a479eb7 100644 --- a/crates/processing_render/Cargo.toml +++ b/crates/processing_render/Cargo.toml @@ -20,7 +20,7 @@ tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } half = "2.7" crossbeam-channel = "0.5" -processing_midi = { workspace = true } +bevy_midi = { git = "https://github.com/BlackPhlox/bevy_midi", branch = "latest" } [target.'cfg(target_os = "macos")'.dependencies] objc2 = { version = "0.6", default-features = false } diff --git a/crates/processing_render/src/lib.rs b/crates/processing_render/src/lib.rs index b603247..ae136b0 100644 --- a/crates/processing_render/src/lib.rs +++ b/crates/processing_render/src/lib.rs @@ -6,6 +6,7 @@ mod graphics; pub mod image; pub mod light; pub mod material; +pub mod midi; pub mod render; pub mod sketch; mod surface; @@ -18,7 +19,6 @@ use config::*; #[cfg(not(target_arch = "wasm32"))] use bevy::log::tracing_subscriber; use bevy::render::RenderPlugin; -use bevy::render::settings::{RenderCreation, WgpuSettings}; use bevy::{ app::{App, AppExit}, asset::{AssetEventSystems, io::AssetSourceBuilder}, @@ -36,8 +36,6 @@ use crate::{ surface::SurfacePlugin, }; -use processing_midi::MidiPlugin; - static IS_INIT: OnceLock<()> = OnceLock::new(); thread_local! { @@ -281,7 +279,7 @@ fn create_app(config: Config) -> App { geometry::GeometryPlugin, LightPlugin, material::MaterialPlugin, - MidiPlugin, + midi::MidiPlugin, )); app.add_systems(First, (clear_transient_meshes, activate_cameras)) .add_systems( @@ -1394,3 +1392,37 @@ pub fn gltf_light(gltf_entity: Entity, index: usize) -> error::Result { .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(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(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(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(midi::play_notes, (note, duration)) + .unwrap() + }) +} diff --git a/crates/processing_render/src/midi.rs b/crates/processing_render/src/midi.rs new file mode 100644 index 0000000..ce66df4 --- /dev/null +++ b/crates/processing_render/src/midi.rs @@ -0,0 +1,45 @@ +use crate::error::Result; +use bevy::prelude::*; + +use bevy_midi::prelude::*; + +pub struct MidiPlugin; + +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); + } +} + +pub fn connect(In(port): In, output: Res) -> Result<()> { + if let Some((_, port)) = output.ports().get(port) { + output.connect(port.clone()); + } + Ok(()) +} + +pub fn disconnect(output: Res) -> Result<()> { + output.disconnect(); + Ok(()) +} + +pub fn refresh_ports(output: Res) -> Result<()> { + output.refresh_ports(); + Ok(()) +} + +pub fn play_notes(In((note, duration)): In<(u8, u64)>, output: Res) -> Result<()> { + output.send([0b1001_0000, 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 + + Ok(()) +} diff --git a/examples/midi.rs b/examples/midi.rs index 2a001fa..962574b 100644 --- a/examples/midi.rs +++ b/examples/midi.rs @@ -4,6 +4,8 @@ use glfw::GlfwContext; use processing::prelude::*; use processing_render::render::command::DrawCommand; +use rand::prelude::*; + fn main() { match sketch() { Ok(_) => { @@ -28,7 +30,10 @@ fn sketch() -> error::Result<()> { let surface = glfw_ctx.create_surface(width, height, scale_factor)?; let graphics = graphics_create(surface, width, height, TextureFormat::Rgba16Float)?; - processing_midi::connect(0); + midi_refresh_ports()?; + midi_connect(0)?; + + let mut rng = rand::rng(); while glfw_ctx.poll_events() { graphics_begin_draw(graphics)?; @@ -45,6 +50,11 @@ fn sketch() -> error::Result<()> { )?; graphics_end_draw(graphics)?; + + let note = rng.random_range(57..68); + let note_duration = rng.random_range(25..250); + midi_play_notes(note, note_duration)?; } + Ok(()) }