Skip to content

Commit f0eb8ad

Browse files
committed
Various improvements to widgets
1 parent 6779a0b commit f0eb8ad

File tree

10 files changed

+137
-42
lines changed

10 files changed

+137
-42
lines changed

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ members = [
1111
"core",
1212
"interactive",
1313
"keyboard",
14+
"live",
1415
"midi",
1516
"midi-file",
1617
"midi-live",

core/src/sig.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1101,6 +1101,7 @@ where
11011101
}
11021102
}
11031103

1104+
/// Call a supplied function on the `SigT` implementation inside `self`.
11041105
pub fn with_inner<T, F>(&self, mut f: F) -> T
11051106
where
11061107
F: FnMut(&S) -> T,

live/Cargo.toml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
[package]
2+
name = "caw_live"
3+
version = "0.1.0"
4+
edition = "2024"
5+
description = "Helpers for live-coding performances with CAW"
6+
7+
[dependencies]
8+
caw_core = { version = "0.5", path = "../core" }
9+
caw_player = { version = "0.7", path = "../player" }
10+
lazy_static = "1.5"

live/src/lib.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
use caw_core::{Sig, SigBoxedVar, Stereo, StereoPair, svf32};
2+
use caw_player::play_stereo_default;
3+
use lazy_static::lazy_static;
4+
use std::sync::Mutex;
5+
6+
lazy_static! {
7+
static ref OUT: StereoPair<Sig<SigBoxedVar<f32>>> =
8+
Stereo::new_fn(|| svf32(0.0));
9+
static ref INITIALIZED: Mutex<bool> = Mutex::new(false);
10+
}
11+
12+
pub fn live_stereo() -> StereoPair<Sig<SigBoxedVar<f32>>> {
13+
let mut initialized = INITIALIZED.lock().unwrap();
14+
if *initialized {
15+
return OUT.clone();
16+
}
17+
*initialized = true;
18+
let player = play_stereo_default(OUT.clone()).unwrap();
19+
std::mem::forget(player);
20+
OUT.clone()
21+
}

midi-udp-widgets-app-lib/examples/midi_udp_widgets_app_lib_knob.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use caw_midi_udp_widgets_app_lib::knob;
44
use caw_modules::*;
55

66
fn sig() -> Sig<impl SigT<Item = f32>> {
7-
oscillator(waveform::Saw, 40.0 + 40.0 * knob().title("freq").build())
7+
oscillator(waveform::Saw, 40.0 + 40.0 * knob("freq").build())
88
.build()
99
.zip(oscillator(waveform::Saw, 40.1).build())
1010
.map(|(a, b)| (a + b) / 10.0)

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

Lines changed: 67 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -3,29 +3,53 @@ use caw_midi::{MidiController01, MidiMessagesT};
33
use caw_midi_udp::{MidiLiveUdp, MidiLiveUdpChannel};
44
use lazy_static::lazy_static;
55
use std::{
6+
collections::HashMap,
67
net::SocketAddr,
78
process::{Child, Command},
9+
sync::Mutex,
810
};
911

1012
// For now this library only supports midi channel 0
1113
const CHANNEL: u8 = 0;
1214

1315
lazy_static! {
16+
// A stream of MIDI events received by the UDP/IP server. It owns the UDP/IP server.
1417
static ref SIG: Sig<SigShared<MidiLiveUdpChannel>> =
1518
sig_shared(MidiLiveUdp::new_unspecified().unwrap().channel(CHANNEL).0);
19+
20+
// Used to prevent multiple windows from opening at the same time with the same name. Note that
21+
// when using with evcxr this seems to get cleared when a cell is recomputed, but this is still
22+
// valuable in the context of stereo signals, where a function is evaluated once for the left
23+
// channel and once for the right channel. In such a case, this prevents each knob openned by
24+
// that function from being openned twice.
25+
static ref KNOBS_BY_TITLE: Mutex<HashMap<String, Sig<SigShared<Knob>>>> =
26+
Mutex::new(HashMap::new());
27+
28+
/// MIDI controller indices are allocated dynamically and this global tracks the value of the
29+
/// next controller index to allocate.
30+
static ref NEXT_CONTROLLER_INDEX: Mutex<u8> = Mutex::new(0);
1631
}
1732

33+
/// Returns the IP address of the server that will receive MIDI events.
1834
fn sig_server_local_socket_address() -> SocketAddr {
1935
SIG.0.with_inner(|midi_live_udp_channel| {
2036
midi_live_udp_channel.server.local_socket_address().unwrap()
2137
})
2238
}
2339

40+
/// Allocates and returns unique MIDI controller value.
41+
fn alloc_controller() -> u8 {
42+
let mut next_controller_index = NEXT_CONTROLLER_INDEX.lock().unwrap();
43+
let controller_index = *next_controller_index;
44+
*next_controller_index += 1;
45+
controller_index
46+
}
47+
2448
const PROGRAM_NAME: &str = "caw_midi_udp_widgets_app";
2549

2650
pub struct Knob {
2751
controller: u8,
28-
title: Option<String>,
52+
title: String,
2953
initial_value_01: f32,
3054
sensitivity: f32,
3155
process: Option<Child>,
@@ -34,35 +58,44 @@ pub struct Knob {
3458

3559
impl Knob {
3660
pub fn new(
37-
controller: u8,
38-
title: Option<String>,
61+
title: String,
3962
initial_value_01: f32,
4063
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)
64+
) -> Sig<SigShared<Self>> {
65+
let mut knobs_by_title = KNOBS_BY_TITLE.lock().unwrap();
66+
if let Some(knob) = knobs_by_title.get(&title) {
67+
knob.clone()
68+
} else {
69+
let controller = alloc_controller();
70+
let sig = SIG
71+
.clone()
72+
.controllers()
73+
.get_with_initial_value_01(controller, initial_value_01);
74+
let mut s = Self {
75+
controller,
76+
title: title.clone(),
77+
initial_value_01,
78+
sensitivity,
79+
process: None,
80+
sig,
81+
};
82+
let child = match s.command().spawn() {
83+
Ok(child) => child,
84+
Err(e) => panic!(
85+
"{} (make sure `{}` is in your PATH",
86+
e, PROGRAM_NAME
87+
),
88+
};
89+
s.process = Some(child);
90+
let s = sig_shared(s);
91+
knobs_by_title.insert(title, s.clone());
92+
s
93+
}
6194
}
6295

63-
fn command(&self) -> anyhow::Result<Command> {
96+
fn command(&self) -> Command {
6497
let mut command = Command::new(PROGRAM_NAME);
65-
let mut args = vec![
98+
let args = vec![
6699
"knob".to_string(),
67100
"--server".to_string(),
68101
sig_server_local_socket_address().to_string(),
@@ -74,12 +107,11 @@ impl Knob {
74107
format!("{}", self.initial_value_01),
75108
"--sensitivity".to_string(),
76109
format!("{}", self.sensitivity),
110+
"--title".to_string(),
111+
self.title.clone(),
77112
];
78-
if let Some(title) = self.title.as_ref() {
79-
args.extend(["--title".to_string(), title.clone()]);
80-
}
81113
command.args(args);
82-
Ok(command)
114+
command
83115
}
84116
}
85117

@@ -94,30 +126,25 @@ impl SigT for Knob {
94126
mod builder {
95127
use super::Knob;
96128
use caw_builder_proc_macros::builder;
97-
use caw_core::Sig;
129+
use caw_core::{Sig, SigShared};
98130

99131
builder! {
100-
#[constructor = "knob"]
132+
#[constructor = "knob_"]
101133
#[constructor_doc = "A visual knob in a new window"]
102134
#[generic_setter_type_name = "X"]
103135
#[build_fn = "Knob::new"]
104-
#[build_ty = "Sig<Knob>"]
136+
#[build_ty = "Sig<SigShared<Knob>>"]
105137
pub struct Props {
106-
#[default = 0]
107-
controller: u8,
108-
#[default = None]
109-
title_opt: Option<String>,
138+
title: String,
110139
#[default = 0.5]
111140
initial_value_01: f32,
112141
#[default = 0.2]
113142
sensitivity: f32,
114143
}
115144
}
116145

117-
impl Props {
118-
pub fn title(self, title: impl Into<String>) -> Self {
119-
self.title_opt(Some(title.into()))
120-
}
146+
pub fn knob(title: impl Into<String>) -> Props {
147+
knob_(title.into())
121148
}
122149
}
123150

midi-udp-widgets-app/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "caw_midi_udp_widgets_app"
3-
version = "0.1.1"
3+
version = "0.1.2"
44
edition = "2024"
55
description = "App for launching widgets that communicate with a caw synthesizer by sending midi commands over UDP"
66
authors = ["Stephen Sherratt <[email protected]>"]

midi-udp-widgets-app/README.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
![CAW Logo](../assets/logo.png)
2+
3+
# caw_midi_udp_widgets_app
4+
5+
[![Version](https://img.shields.io/crates/v/caw_midi_udp_widgets_app.svg)](https://crates.io/crates/caw_midi_udp_widgets_app)
6+
[![Documentation](https://docs.rs/caw_midi_udp_widgets_app/badge.svg)](https://docs.rs/caw_midi_udp_widgets_app)
7+
[![test](https://github.com/gridbugs/caw/actions/workflows/test.yml/badge.svg)](https://github.com/gridbugs/caw/actions/workflows/test.yml)
8+
[![dependency status](https://deps.rs/repo/github/gridbugs/caw/status.svg)](https://deps.rs/repo/github/gridbugs/caw)
9+
10+
App for launching widgets that communicate with a caw synthesizer by sending
11+
midi commands over UDP. Sometimes it can be useful to pop up a graphical window
12+
and manually interact with a widget. This app can open windows containing
13+
widgets that can act as UDP clients, sending MIDI commands to a UDP server,
14+
such as is contained in the
15+
[caw_midi_udp](https://crates.io/crates/caw_midi_udp).
16+
17+
Part of the [CAW Synthesizer Framework](..).

midi-udp/Cargo.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@
22
name = "caw_midi_udp"
33
version = "0.1.0"
44
edition = "2024"
5+
description = "A caw signal of MIDI events backed by a UDP/IP server which can receive serialized MIDI events"
6+
authors = ["Stephen Sherratt <[email protected]>"]
7+
license = "MIT"
8+
homepage = "https://github.com/gridbugs/caw.git"
9+
repository = "https://github.com/gridbugs/caw.git"
10+
documentation = "https://docs.rs/caw_midi_udp"
511

612
[dependencies]
713
anyhow = "1.0"

midi-udp/README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
![CAW Logo](../assets/logo.png)
2+
3+
# caw_midi_udp
4+
5+
[![Version](https://img.shields.io/crates/v/caw_midi_udp.svg)](https://crates.io/crates/caw_midi_udp)
6+
[![Documentation](https://docs.rs/caw_midi_udp/badge.svg)](https://docs.rs/caw_midi_udp)
7+
[![test](https://github.com/gridbugs/caw/actions/workflows/test.yml/badge.svg)](https://github.com/gridbugs/caw/actions/workflows/test.yml)
8+
[![dependency status](https://deps.rs/repo/github/gridbugs/caw/status.svg)](https://deps.rs/repo/github/gridbugs/caw)
9+
10+
A caw signal of MIDI events backed by a UDP/IP server which can receive serialized MIDI events.
11+
12+
Part of the [CAW Synthesizer Framework](..).

0 commit comments

Comments
 (0)