Skip to content

Commit 5775093

Browse files
catilactychedelia
andauthored
Midi pt2 (#85)
Co-authored-by: charlotte 🌸 <charlotte.c.mcelwain@gmail.com>
1 parent c82ff83 commit 5775093

File tree

12 files changed

+290
-87
lines changed

12 files changed

+290
-87
lines changed

Cargo.lock

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

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ web-sys = { version = "0.3", features = ["Window"] }
4949

5050
[dev-dependencies]
5151
glfw = "0.60.0"
52+
rand = "0.10.0"
5253

5354
[target.'cfg(target_os = "linux")'.dev-dependencies]
5455
glfw = { version = "0.60.0", features = ["wayland"] }

crates/processing_core/src/error.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,4 +42,6 @@ pub enum ProcessingError {
4242
ShaderCompilationError(String),
4343
#[error("Shader not found")]
4444
ShaderNotFound,
45+
#[error("MIDI port {0} not found")]
46+
MidiPortNotFound(usize),
4547
}

crates/processing_midi/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ edition = "2024"
55

66
[dependencies]
77
bevy = { workspace = true }
8+
processing_core = { workspace = true }
89
bevy_midi = { git = "https://github.com/BlackPhlox/bevy_midi", branch = "latest" }
910

10-
1111
[lints]
1212
workspace = true

crates/processing_midi/src/lib.rs

Lines changed: 95 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,109 @@
11
use bevy::prelude::*;
22
use bevy_midi::prelude::*;
33

4+
use processing_core::app_mut;
5+
use processing_core::error::{self, Result};
6+
47
pub struct MidiPlugin;
58

9+
pub const NOTE_ON: u8 = 0b1001_0000;
10+
pub const NOTE_OFF: u8 = 0b1000_0000;
11+
612
impl Plugin for MidiPlugin {
713
fn build(&self, app: &mut App) {
14+
// TODO: Update `bevy_midi` to treat connections as entities
15+
// in order to support hot-plugging
816
app.insert_resource(MidiOutputSettings {
9-
port_name: "output",
10-
})
11-
.add_plugins(MidiOutputPlugin);
17+
port_name: "libprocessing output",
18+
});
19+
20+
app.add_plugins(MidiOutputPlugin);
21+
}
22+
}
23+
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)),
1231
}
1332
}
1433

15-
pub fn connect(_port: usize) {
16-
// we need to work with the ECS
17-
// do we pass a MidiCommand to Bevy?
34+
pub fn disconnect(output: Res<MidiOutput>) -> Result<()> {
35+
output.disconnect();
36+
Ok(())
1837
}
1938

20-
pub fn disconnect() {}
21-
pub fn refresh_ports() {}
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()
48+
.enumerate()
49+
.map(|(i, (name, _))| format!("{}: {}", i, name))
50+
.collect())
51+
}
52+
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
55+
56+
std::thread::sleep(std::time::Duration::from_millis(duration));
57+
58+
output.send([NOTE_OFF, note, 127].into()); // Note off, channel 1, max velocity
2259

23-
pub fn play_notes() {}
60+
Ok(())
61+
}
62+
63+
#[cfg(not(target_arch = "wasm32"))]
64+
pub fn midi_refresh_ports() -> error::Result<()> {
65+
app_mut(|app| {
66+
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);
73+
Ok(())
74+
})
75+
}
76+
77+
#[cfg(not(target_arch = "wasm32"))]
78+
pub fn midi_list_ports() -> error::Result<Vec<String>> {
79+
app_mut(|app| {
80+
let world = app.world_mut();
81+
world.run_system_cached(list_ports).unwrap()
82+
})
83+
}
84+
85+
#[cfg(not(target_arch = "wasm32"))]
86+
pub fn midi_connect(port: usize) -> error::Result<()> {
87+
app_mut(|app| {
88+
let world = app.world_mut();
89+
world.run_system_cached_with(connect, port).unwrap()
90+
})
91+
}
92+
93+
#[cfg(not(target_arch = "wasm32"))]
94+
pub fn midi_disconnect() -> error::Result<()> {
95+
app_mut(|app| {
96+
let world = app.world_mut();
97+
world.run_system_cached(disconnect).unwrap()
98+
})
99+
}
100+
101+
#[cfg(not(target_arch = "wasm32"))]
102+
pub fn midi_play_notes(note: u8, duration: u64) -> error::Result<()> {
103+
app_mut(|app| {
104+
let world = app.world_mut();
105+
world
106+
.run_system_cached_with(play_notes, (note, duration))
107+
.unwrap()
108+
})
109+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
from processing import *
2+
import random
3+
4+
def setup():
5+
size(800, 600)
6+
7+
# Refresh midi port list, print available ports, and connect to first one
8+
midi_refresh_ports()
9+
for port in midi_list_ports():
10+
print(port)
11+
midi_connect(0)
12+
13+
def draw():
14+
background(220)
15+
16+
fill(255, 0, 100)
17+
stroke(1)
18+
stroke_weight(2)
19+
rect(100, 100, 200, 150)
20+
21+
# pick a random note value, and duration value for that note
22+
# then send the midi command
23+
note = random.randint(57,68)
24+
note_duration = random.randint(25, 250)
25+
midi_play_notes(note, note_duration)
26+
27+
# TODO: this should happen implicitly on module load somehow
28+
run()

crates/processing_pyo3/src/lib.rs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,14 @@ mod glfw;
1212
mod gltf;
1313
mod graphics;
1414
pub(crate) mod material;
15+
mod midi;
1516
pub(crate) mod shader;
1617
#[cfg(feature = "webcam")]
1718
mod webcam;
1819

1920
use graphics::{Geometry, Graphics, Image, Light, Topology, get_graphics, get_graphics_mut};
2021
use material::Material;
22+
2123
use pyo3::{
2224
exceptions::PyRuntimeError,
2325
prelude::*,
@@ -94,6 +96,11 @@ fn processing(m: &Bound<'_, PyModule>) -> PyResult<()> {
9496
m.add_function(wrap_pyfunction!(metallic, m)?)?;
9597
m.add_function(wrap_pyfunction!(emissive, m)?)?;
9698
m.add_function(wrap_pyfunction!(unlit, m)?)?;
99+
m.add_function(wrap_pyfunction!(midi_connect, m)?)?;
100+
m.add_function(wrap_pyfunction!(midi_disconnect, m)?)?;
101+
m.add_function(wrap_pyfunction!(midi_refresh_ports, m)?)?;
102+
m.add_function(wrap_pyfunction!(midi_list_ports, m)?)?;
103+
m.add_function(wrap_pyfunction!(midi_play_notes, m)?)?;
97104

98105
#[cfg(feature = "webcam")]
99106
{
@@ -589,3 +596,24 @@ fn create_webcam(
589596
) -> PyResult<webcam::Webcam> {
590597
webcam::Webcam::new(width, height, framerate)
591598
}
599+
600+
#[pyfunction]
601+
fn midi_connect(port: usize) -> PyResult<()> {
602+
midi::connect(port)
603+
}
604+
#[pyfunction]
605+
fn midi_disconnect() -> PyResult<()> {
606+
midi::disconnect()
607+
}
608+
#[pyfunction]
609+
fn midi_refresh_ports() -> PyResult<()> {
610+
midi::refresh_ports()
611+
}
612+
#[pyfunction]
613+
fn midi_list_ports() -> PyResult<Vec<String>> {
614+
midi::list_ports()
615+
}
616+
#[pyfunction]
617+
fn midi_play_notes(note: u8, duration: u64) -> PyResult<()> {
618+
midi::play_notes(note, duration)
619+
}

crates/processing_pyo3/src/midi.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
use processing::prelude::*;
2+
use pyo3::{exceptions::PyRuntimeError, prelude::*};
3+
4+
pub fn connect(port: usize) -> PyResult<()> {
5+
midi_connect(port).map_err(|e| PyRuntimeError::new_err(format!("{e}")))
6+
}
7+
pub fn disconnect() -> PyResult<()> {
8+
midi_disconnect().map_err(|e| PyRuntimeError::new_err(format!("{e}")))
9+
}
10+
pub fn refresh_ports() -> PyResult<()> {
11+
midi_refresh_ports().map_err(|e| PyRuntimeError::new_err(format!("{e}")))
12+
}
13+
pub fn list_ports() -> PyResult<Vec<String>> {
14+
midi_list_ports().map_err(|e| PyRuntimeError::new_err(format!("{e}")))
15+
}
16+
pub fn play_notes(note: u8, duration: u64) -> PyResult<()> {
17+
midi_play_notes(note, duration).map_err(|e| PyRuntimeError::new_err(format!("{e}")))
18+
}

crates/processing_render/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ raw-window-handle = "0.6"
2121
half = "2.7"
2222
crossbeam-channel = "0.5"
2323
processing_core = { workspace = true }
24+
processing_midi = { workspace = true }
2425

2526
[build-dependencies]
2627
wesl = { workspace = true, features = ["package"] }

crates/processing_render/src/lib.rs

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -46,13 +46,11 @@ impl Plugin for ProcessingRenderPlugin {
4646
let has_sketch_file = config
4747
.get(ConfigKey::SketchFileName)
4848
.is_some_and(|f| !f.is_empty());
49-
if has_sketch_file {
50-
if let Some(sketch_path) = config.get(ConfigKey::SketchRootPath) {
51-
app.register_asset_source(
52-
"sketch_directory",
53-
AssetSourceBuilder::platform_default(sketch_path, None),
54-
);
55-
}
49+
if has_sketch_file && let Some(sketch_path) = config.get(ConfigKey::SketchRootPath) {
50+
app.register_asset_source(
51+
"sketch_directory",
52+
AssetSourceBuilder::platform_default(sketch_path, None),
53+
);
5654
}
5755

5856
if has_sketch_file {

0 commit comments

Comments
 (0)