Skip to content

Commit 46f9855

Browse files
authored
Merge pull request #4 from projectM-visualizer/pm-thread-safety
Wrap projectM instance access in a mutex
2 parents 58856fb + ee25f78 commit 46f9855

File tree

5 files changed

+80
-45
lines changed

5 files changed

+80
-45
lines changed

Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ rust-version = "1.68.2"
1010
libc = "*"
1111
sdl2 = "0.35"
1212
projectm-rs = "1.0.5"
13+
# projectm-rs = { path = "../projectm-rs" }
1314
#projectm-rs = { git = "https://github.com/projectM-visualizer/projectm-rs" }
1415

1516
# gl = "0.14.0"

src/app.rs

+11-3
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
use std::sync::{Arc, Mutex};
2+
13
use projectm_rs::core::{ProjectMHandle, Projectm};
24
use sdl2::video::GLProfile;
35

@@ -7,9 +9,12 @@ pub mod main_loop;
79
pub mod playlist;
810
pub mod video;
911

12+
/// Thread-safe wrapper around the projectM instance.
13+
pub type ProjectMWrapped = Arc<Mutex<ProjectMHandle>>;
14+
1015
/// Application state
1116
pub struct App {
12-
pm: ProjectMHandle,
17+
pm: ProjectMWrapped,
1318
playlist: projectm_rs::playlist::Playlist,
1419
sdl_context: sdl2::Sdl,
1520
window: sdl2::video::Window,
@@ -65,11 +70,14 @@ impl App {
6570
let (width, height) = window.drawable_size(); // highDPI aware
6671
Projectm::set_window_size(pm, width.try_into().unwrap(), height.try_into().unwrap());
6772

73+
// create a mutex to protect the projectM instance
74+
let pm = Arc::new(Mutex::new(pm));
75+
6876
// initialize audio
69-
let audio = audio::Audio::new(&sdl_context, pm);
77+
let audio = audio::Audio::new(&sdl_context, pm.clone());
7078

7179
Self {
72-
pm,
80+
pm: pm.clone(),
7381
playlist,
7482
sdl_context,
7583
window,

src/app/audio.rs

+51-35
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
use projectm_rs::core::ProjectMHandle;
22
use sdl2::audio::{AudioCallback, AudioDevice, AudioSpecDesired};
3+
use std::sync::Arc;
4+
use std::sync::Mutex;
35

46
use super::config::FrameRate;
7+
use super::ProjectMWrapped;
58

69
type AudioDeviceIndex = u32;
710
type SampleFormat = f32; // format of audio samples
@@ -16,12 +19,13 @@ pub struct Audio {
1619
device_index: AudioDeviceIndex,
1720
is_capturing: bool,
1821
frame_rate: Option<FrameRate>,
19-
capturing_device: Option<AudioDevice<AudioCaptureCallback>>,
20-
projectm: ProjectMHandle,
22+
capturing_device: Option<Box<AudioDevice<AudioCaptureCallback>>>,
23+
projectm: ProjectMWrapped,
2124
}
2225

26+
/// Wrapper around the audio subsystem to capture audio and pass it to projectM.
2327
impl Audio {
24-
pub fn new(sdl_context: &sdl2::Sdl, projectm: ProjectMHandle) -> Self {
28+
pub fn new(sdl_context: &sdl2::Sdl, projectm: ProjectMWrapped) -> Self {
2529
let audio_subsystem = sdl_context.audio().unwrap();
2630

2731
Self {
@@ -52,7 +56,8 @@ impl Audio {
5256
}
5357
}
5458

55-
pub fn set_device(&mut self, device_index: AudioDeviceIndex) {
59+
/// Start capturing audio from device_index.
60+
pub fn capture_device(&mut self, device_index: AudioDeviceIndex) {
5661
self.stop_audio_capture();
5762
self.device_index = device_index;
5863
self.begin_audio_capture();
@@ -64,12 +69,14 @@ impl Audio {
6469
.expect("could not get audio device")
6570
}
6671

72+
/// Select a new audio device and start capturing audio from it.
6773
pub fn open_next_device(&mut self) {
6874
let device_list = self.get_device_list();
6975
let current_device_index = self.device_index;
7076

71-
let next_device_index = current_device_index + 1 % device_list.len() as AudioDeviceIndex;
72-
self.set_device(next_device_index);
77+
let next_device_index = (current_device_index + 1) % device_list.len() as AudioDeviceIndex;
78+
println!("Opening next device: {}", next_device_index);
79+
self.capture_device(next_device_index);
7380
}
7481

7582
fn get_device_list(&self) -> Vec<AudioCaptureDevice> {
@@ -96,66 +103,75 @@ impl Audio {
96103
let sample_rate: u32 = 44100;
97104
let frame_rate = self.frame_rate.unwrap();
98105

99-
// should be enough for 1 frame
100-
let buffer_size = (sample_rate / frame_rate) as u16;
106+
// how many samples to capture at a time
107+
// should be enough for 1 frame or less
108+
// should not be larger than max_samples / channels
109+
let max_samples: usize = projectm_rs::core::Projectm::pcm_get_max_samples()
110+
.try_into()
111+
.unwrap();
112+
let samples_per_frame = (sample_rate / frame_rate) as usize;
113+
let buffer_size = std::cmp::min(max_samples / 2, samples_per_frame);
114+
println!("Buffer size: {}", buffer_size);
101115

102116
let desired_spec = AudioSpecDesired {
103117
freq: Some(sample_rate.try_into().unwrap()),
104118
channels: Some(2),
105-
samples: Some(buffer_size),
119+
samples: Some(buffer_size.try_into().unwrap()),
106120
};
107121

108-
let audio_device = self
122+
// open audio device for capture
123+
let device_name = self.get_current_device_name();
124+
let audio_device = match self
109125
.audio_subsystem // sdl
110-
.open_capture(None, &desired_spec, |_spec| {
111-
println!(
112-
"Beginning audio capture for device {}",
113-
self.get_current_device_name()
114-
);
126+
.open_capture(device_name.as_str(), &desired_spec, |_spec| {
127+
println!("Beginning audio capture for device {}", device_name);
115128

116129
// print spec
117130
println!("Audio Spec: {:?}", _spec);
118131

119132
// return callback fn
120133
AudioCaptureCallback {
121-
pm: self.projectm,
122-
// spec,
123-
// buffer_size,
124-
// buffer: vec![0; buffer_size as usize],
125-
// position: 0,
134+
pm: self.projectm.clone(),
126135
}
127-
})
128-
.unwrap();
136+
}) {
137+
Ok(device) => device,
138+
Err(e) => {
139+
println!("Error opening audio device: {}", e);
140+
return;
141+
}
142+
};
143+
144+
// start capturing
145+
audio_device.resume();
129146

130147
// take ownership of device
131-
self.capturing_device = Some(audio_device);
148+
self.capturing_device = Some(Box::new(audio_device));
132149
self.is_capturing = true;
133-
134-
// play device
135-
self.capturing_device.as_mut().unwrap().resume();
136150
}
137151

138152
pub fn stop_audio_capture(&mut self) {
139153
let current_device_name = self.get_current_device_name();
140154
println!("Stopping audio capture for device {}", current_device_name);
141155

142156
println!(
143-
"current capture device: {:?}",
157+
"Current capture device status: {:?}",
144158
self.capturing_device.as_ref().unwrap().status()
145159
);
146160

161+
// take ownership of device
162+
// capture device will be dropped when this function returns
163+
// and the audio callback will stop being called
164+
let device = self.capturing_device.take().unwrap();
165+
device.pause();
166+
147167
self.is_capturing = false;
148-
// drop(self.capturing_device); // stop capturing
149-
self.capturing_device = None;
150168
}
151169
}
152170

153171
struct AudioCaptureCallback {
154-
pm: ProjectMHandle,
155-
// spec: sdl2::audio::AudioSpec,
156-
// buffer_size: SampleFormat,
157-
// buffer: Vec<u8>,
158-
// position: usize,
172+
// we need to keep a reference to the projectm instance to
173+
// add the audio data to it
174+
pm: Arc<Mutex<ProjectMHandle>>,
159175
}
160176
unsafe impl Send for AudioCaptureCallback {}
161177
unsafe impl Sync for AudioCaptureCallback {}
@@ -166,7 +182,7 @@ impl AudioCallback for AudioCaptureCallback {
166182
// we are receiving some chunk of audio data
167183
// we need to pass it to projectm
168184
fn callback(&mut self, out: &mut [SampleFormat]) {
169-
let pm = self.pm;
185+
let pm = *self.pm.lock().unwrap();
170186
projectm_rs::core::Projectm::pcm_add_float(pm, out.to_vec(), 2);
171187
}
172188
}

src/app/config.rs

+8-5
Original file line numberDiff line numberDiff line change
@@ -39,35 +39,38 @@ impl Default for Config {
3939

4040
impl App {
4141
pub fn load_config(&self, config: &Config) {
42+
let pm = *self.pm.lock().unwrap();
43+
4244
// load presets if provided
4345
if let Some(preset_path) = &config.preset_path {
4446
self.add_preset_path(preset_path);
4547
}
4648

4749
// set frame rate if provided
4850
if let Some(frame_rate) = config.frame_rate {
49-
Projectm::set_fps(self.pm, frame_rate)
51+
Projectm::set_fps(pm, frame_rate)
5052
}
5153

5254
// load textures if provided
5355
if let Some(texture_path) = &config.texture_path {
5456
let mut paths: Vec<String> = Vec::new();
5557
paths.push(texture_path.into());
56-
Projectm::set_texture_search_paths(self.pm, &paths, 1);
58+
Projectm::set_texture_search_paths(pm, &paths, 1);
5759
}
5860

5961
// set beat sensitivity if provided
6062
if let Some(beat_sensitivity) = config.beat_sensitivity {
61-
Projectm::set_beat_sensitivity(self.pm, beat_sensitivity);
63+
Projectm::set_beat_sensitivity(pm, beat_sensitivity);
6264
}
6365

6466
// set preset duration if provided
6567
if let Some(preset_duration) = config.preset_duration {
66-
Projectm::set_preset_duration(self.pm, preset_duration);
68+
Projectm::set_preset_duration(pm, preset_duration);
6769
}
6870
}
6971

7072
pub fn get_frame_rate(&self) -> FrameRate {
71-
Projectm::get_fps(self.pm)
73+
let pm = *self.pm.lock().unwrap();
74+
Projectm::get_fps(pm)
7275
}
7376
}

src/app/main_loop.rs

+9-2
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,11 @@ impl App {
8181
// Next audio capture input device (ctl-I, cmd-I)
8282
Event::KeyUp {
8383
keycode: Some(Keycode::I),
84-
keymod: sdl2::keyboard::Mod::LCTRLMOD,
84+
keymod:
85+
sdl2::keyboard::Mod::LCTRLMOD
86+
| sdl2::keyboard::Mod::RCTRLMOD
87+
| sdl2::keyboard::Mod::LGUIMOD
88+
| sdl2::keyboard::Mod::RGUIMOD,
8589
..
8690
} => {
8791
self.audio.open_next_device();
@@ -97,7 +101,10 @@ impl App {
97101
dummy_audio::generate_random_audio_data(self.pm);
98102

99103
// render a frame
100-
Projectm::render_frame(self.pm);
104+
{
105+
let pm = *self.pm.lock().unwrap();
106+
Projectm::render_frame(pm);
107+
}
101108

102109
// swap buffers
103110
self.window.gl_swap_window();

0 commit comments

Comments
 (0)