Skip to content

Commit 8922f7c

Browse files
committed
Add library for launching widgets as processes
1 parent ac8b1d0 commit 8922f7c

File tree

9 files changed

+293
-73
lines changed

9 files changed

+293
-73
lines changed

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ members = [
1818
"midi-udp",
1919
"midi-udp-client",
2020
"midi-udp-widgets-app",
21+
"midi-udp-widgets-app-lib",
2122
"modules",
2223
"patches",
2324
"player",

core/src/sig.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1100,6 +1100,14 @@ where
11001100
buf: Vec::new(),
11011101
}
11021102
}
1103+
1104+
pub fn with_inner<T, F>(&self, mut f: F) -> T
1105+
where
1106+
F: FnMut(&S) -> T,
1107+
{
1108+
let inner_cached = self.shared_cached_sig.read().unwrap();
1109+
f(&inner_cached.sig)
1110+
}
11031111
}
11041112

11051113
impl<S> Clone for SigShared<S>
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
[package]
2+
name = "caw_midi_udp_widgets_app_lib"
3+
version = "0.1.0"
4+
edition = "2024"
5+
6+
[dependencies]
7+
caw_midi_udp = { version = "0.1", path = "../midi-udp" }
8+
caw_core = { version = "0.5", path = "../core" }
9+
caw_midi = { version = "0.4", path = "../midi" }
10+
caw_builder_proc_macros = { version = "0.1", path = "../builder-proc-macros" }
11+
lazy_static = "1.5"
12+
anyhow = "1.0"
13+
14+
[dev-dependencies]
15+
env_logger = "0.11"
16+
caw_interactive = { path = "../interactive" }
17+
caw_modules = { version = "0.4", path = "../modules" }
18+
19+
[[example]]
20+
name = "midi_udp_widgets_app_lib_knob"
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
use caw_core::*;
2+
use caw_interactive::window::Window;
3+
use caw_midi_udp_widgets_app_lib::knob;
4+
use caw_modules::*;
5+
6+
fn sig() -> Sig<impl SigT<Item = f32>> {
7+
oscillator(waveform::Saw, 40.0 + 40.0 * knob().title("freq").build())
8+
.build()
9+
.zip(oscillator(waveform::Saw, 40.1).build())
10+
.map(|(a, b)| (a + b) / 10.0)
11+
}
12+
13+
fn main() -> anyhow::Result<()> {
14+
env_logger::init();
15+
let window = Window::builder().build();
16+
window.play_mono(sig(), Default::default())
17+
}
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
use caw_core::{Buf, Sig, SigShared, SigT, sig_shared};
2+
use caw_midi::{MidiController01, MidiMessagesT};
3+
use caw_midi_udp::{MidiLiveUdp, MidiLiveUdpChannel};
4+
use lazy_static::lazy_static;
5+
use std::{
6+
net::SocketAddr,
7+
process::{Child, Command},
8+
};
9+
10+
// For now this library only supports midi channel 0
11+
const CHANNEL: u8 = 0;
12+
13+
lazy_static! {
14+
static ref SIG: Sig<SigShared<MidiLiveUdpChannel>> =
15+
sig_shared(MidiLiveUdp::new_unspecified().unwrap().channel(CHANNEL).0);
16+
}
17+
18+
fn sig_server_local_socket_address() -> SocketAddr {
19+
SIG.0.with_inner(|midi_live_udp_channel| {
20+
midi_live_udp_channel.server.local_socket_address().unwrap()
21+
})
22+
}
23+
24+
const PROGRAM_NAME: &str = "caw_midi_udp_widgets_app";
25+
26+
pub struct Knob {
27+
controller: u8,
28+
title: Option<String>,
29+
initial_value_01: f32,
30+
sensitivity: f32,
31+
process: Option<Child>,
32+
sig: Sig<MidiController01<SigShared<MidiLiveUdpChannel>>>,
33+
}
34+
35+
impl Knob {
36+
pub fn new(
37+
controller: u8,
38+
title: Option<String>,
39+
initial_value_01: f32,
40+
sensitivity: f32,
41+
) -> Sig<Self> {
42+
let sig = SIG
43+
.clone()
44+
.controllers()
45+
.get_with_initial_value_01(controller, initial_value_01);
46+
let mut s = Self {
47+
controller,
48+
title,
49+
initial_value_01,
50+
sensitivity,
51+
process: None,
52+
sig,
53+
};
54+
let child = s
55+
.command()
56+
.unwrap()
57+
.spawn()
58+
.expect("Failed to launch process");
59+
s.process = Some(child);
60+
Sig(s)
61+
}
62+
63+
fn command(&self) -> anyhow::Result<Command> {
64+
let mut command = Command::new(PROGRAM_NAME);
65+
let mut args = vec![
66+
"knob".to_string(),
67+
"--server".to_string(),
68+
sig_server_local_socket_address().to_string(),
69+
"--channel".to_string(),
70+
format!("{}", CHANNEL),
71+
"--controller".to_string(),
72+
format!("{}", self.controller),
73+
"--initial-value".to_string(),
74+
format!("{}", self.initial_value_01),
75+
"--sensitivity".to_string(),
76+
format!("{}", self.sensitivity),
77+
];
78+
if let Some(title) = self.title.as_ref() {
79+
args.extend(["--title".to_string(), title.clone()]);
80+
}
81+
command.args(args);
82+
Ok(command)
83+
}
84+
}
85+
86+
impl SigT for Knob {
87+
type Item = f32;
88+
89+
fn sample(&mut self, ctx: &caw_core::SigCtx) -> impl Buf<Self::Item> {
90+
self.sig.sample(ctx)
91+
}
92+
}
93+
94+
mod builder {
95+
use super::Knob;
96+
use caw_builder_proc_macros::builder;
97+
use caw_core::Sig;
98+
99+
builder! {
100+
#[constructor = "knob"]
101+
#[constructor_doc = "A visual knob in a new window"]
102+
#[generic_setter_type_name = "X"]
103+
#[build_fn = "Knob::new"]
104+
#[build_ty = "Sig<Knob>"]
105+
pub struct Props {
106+
#[default = 0]
107+
controller: u8,
108+
#[default = None]
109+
title_opt: Option<String>,
110+
#[default = 0.5]
111+
initial_value_01: f32,
112+
#[default = 0.2]
113+
sensitivity: f32,
114+
}
115+
}
116+
117+
impl Props {
118+
pub fn title(self, title: impl Into<String>) -> Self {
119+
self.title_opt(Some(title.into()))
120+
}
121+
}
122+
}
123+
124+
pub use builder::knob;

midi-udp-widgets-app/src/main.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,9 @@ fn main() {
4141
sensitivity,
4242
} => {
4343
let client = MidiUdpClient::new(server).unwrap();
44-
let title = title.unwrap_or_else(|| "".to_string());
4544
let mut knob =
46-
Knob::new(title.as_str(), initial_value, sensitivity).unwrap();
45+
Knob::new(title.as_deref(), initial_value, sensitivity)
46+
.unwrap();
4747
loop {
4848
knob.tick().unwrap();
4949
client

midi-udp/src/lib.rs

Lines changed: 50 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
use caw_core::{Sig, SigT};
1+
use caw_core::{Buf, Sig, SigCtx, SigT};
22
use caw_midi::MidiMessages;
33
use midly::{MidiMessage, live::LiveEvent, num::u4};
44
use std::{
55
io,
6-
net::{ToSocketAddrs, UdpSocket},
6+
net::{Ipv4Addr, SocketAddr, ToSocketAddrs, UdpSocket},
77
};
88

99
const BUF_SIZE: usize = 256;
@@ -26,6 +26,14 @@ impl MidiLiveUdp {
2626
Ok(Self { socket, buf })
2727
}
2828

29+
pub fn new_unspecified() -> anyhow::Result<Self> {
30+
Self::new((Ipv4Addr::UNSPECIFIED, 0))
31+
}
32+
33+
pub fn local_socket_address(&self) -> anyhow::Result<SocketAddr> {
34+
Ok(self.socket.local_addr()?)
35+
}
36+
2937
fn recv_into_buf(&mut self) -> Result<bool, io::Error> {
3038
match self.socket.recv(&mut self.buf) {
3139
Ok(size) => {
@@ -63,34 +71,48 @@ impl MidiLiveUdp {
6371
}
6472
}
6573

66-
pub fn channel(
67-
mut self,
68-
channel: u8,
69-
) -> Sig<impl SigT<Item = MidiMessages>> {
70-
Sig::from_buf_fn(move |ctx, buf: &mut Vec<MidiMessages>| {
71-
// This is called once per frame (not once per sample). This will add an imperceptible
72-
// amount of latency (unless the output buffer is too large!), but reduce cpu usage.
73-
buf.resize_with(ctx.num_samples, Default::default);
74-
let mut midi_messages = MidiMessages::empty();
75-
loop {
76-
match self.recv_midi_event() {
77-
Err(e) => {
78-
log::warn!("IO error reading from UDP socket: {e}");
79-
break;
80-
}
81-
Ok(None) => break,
82-
Ok(Some(MidiEvent {
83-
channel: message_channel,
84-
message,
85-
})) => {
86-
if message_channel == channel {
87-
midi_messages.push(message);
88-
}
74+
pub fn channel(self, channel: u8) -> Sig<MidiLiveUdpChannel> {
75+
Sig(MidiLiveUdpChannel {
76+
channel: channel.into(),
77+
server: self,
78+
buf: Vec::new(),
79+
})
80+
}
81+
}
82+
83+
pub struct MidiLiveUdpChannel {
84+
pub channel: u4,
85+
pub server: MidiLiveUdp,
86+
buf: Vec<MidiMessages>,
87+
}
88+
89+
impl SigT for MidiLiveUdpChannel {
90+
type Item = MidiMessages;
91+
92+
fn sample(&mut self, ctx: &SigCtx) -> impl Buf<Self::Item> {
93+
// This is called once per frame (not once per sample). This will add an imperceptible
94+
// amount of latency (unless the output buffer is too large!), but reduce cpu usage.
95+
self.buf.resize_with(ctx.num_samples, Default::default);
96+
let mut midi_messages = MidiMessages::empty();
97+
loop {
98+
match self.server.recv_midi_event() {
99+
Err(e) => {
100+
log::warn!("IO error reading from UDP socket: {e}");
101+
break;
102+
}
103+
Ok(None) => break,
104+
Ok(Some(MidiEvent {
105+
channel: message_channel,
106+
message,
107+
})) => {
108+
if message_channel == self.channel {
109+
midi_messages.push(message);
89110
}
90111
}
91112
}
92-
// Only the first sample of each frame is populated with midi messages.
93-
buf[0] = midi_messages;
94-
})
113+
}
114+
// Only the first sample of each frame is populated with midi messages.
115+
self.buf[0] = midi_messages;
116+
&self.buf
95117
}
96118
}

midi/src/lib.rs

Lines changed: 43 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use caw_core::{sig_shared, Sig, SigShared, SigT};
1+
use caw_core::{sig_shared, Buf, Sig, SigCtx, SigShared, SigT};
22
use caw_keyboard::{KeyEvent, KeyEvents, Note, TONE_RATIO};
33
use midly::{num::u7, MidiMessage};
44
use smallvec::{smallvec, SmallVec};
@@ -73,6 +73,39 @@ where
7373
messages: Sig<SigShared<M>>,
7474
}
7575

76+
pub struct MidiController01<M>
77+
where
78+
M: SigT<Item = MidiMessages>,
79+
{
80+
index: u7,
81+
state: f32,
82+
messages: SigShared<M>,
83+
buf: Vec<f32>,
84+
}
85+
86+
impl<M> SigT for MidiController01<M>
87+
where
88+
M: SigT<Item = MidiMessages>,
89+
{
90+
type Item = f32;
91+
92+
fn sample(&mut self, ctx: &SigCtx) -> impl Buf<Self::Item> {
93+
self.buf.resize(ctx.num_samples, 0.0);
94+
let messages = self.messages.sample(ctx);
95+
for (out, messages) in self.buf.iter_mut().zip(messages.iter()) {
96+
for message in messages {
97+
if let MidiMessage::Controller { controller, value } = message {
98+
if controller == self.index {
99+
self.state = value.as_int() as f32 / 127.0;
100+
}
101+
}
102+
}
103+
*out = self.state;
104+
}
105+
&self.buf
106+
}
107+
}
108+
76109
impl<M> MidiControllers<M>
77110
where
78111
M: SigT<Item = MidiMessages>,
@@ -81,32 +114,24 @@ where
81114
&self,
82115
index: u8,
83116
initial_value: f32,
84-
) -> Sig<impl SigT<Item = f32>> {
85-
let index: u7 = index.into();
86-
let mut state = initial_value.clamp(0., 1.);
87-
self.messages.clone().map_mut(move |midi_messages| {
88-
for midi_message in midi_messages {
89-
if let MidiMessage::Controller { controller, value } =
90-
midi_message
91-
{
92-
if controller == index {
93-
state = value.as_int() as f32 / 127.0;
94-
}
95-
}
96-
}
97-
state
117+
) -> Sig<MidiController01<M>> {
118+
Sig(MidiController01 {
119+
index: index.into(),
120+
state: initial_value.clamp(0., 1.),
121+
messages: self.messages.clone().0,
122+
buf: Vec::new(),
98123
})
99124
}
100125

101-
pub fn get_01(&self, index: u8) -> Sig<impl SigT<Item = f32>> {
126+
pub fn get_01(&self, index: u8) -> Sig<MidiController01<M>> {
102127
self.get_with_initial_value_01(index, 0.0)
103128
}
104129

105-
pub fn volume(&self) -> Sig<impl SigT<Item = f32>> {
130+
pub fn volume(&self) -> Sig<MidiController01<M>> {
106131
self.get_01(7)
107132
}
108133

109-
pub fn modulation(&self) -> Sig<impl SigT<Item = f32>> {
134+
pub fn modulation(&self) -> Sig<MidiController01<M>> {
110135
self.get_01(1)
111136
}
112137
}

0 commit comments

Comments
 (0)