diff --git a/.cargo/config.toml b/.cargo/config.toml index a627181..fa9631c 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,3 +1,3 @@ [env] # use pipewire for audio -SDL_AUDIODRIVER = "pipewire" +#SDL_AUDIODRIVER = "pipewire" diff --git a/Cargo.lock b/Cargo.lock index f6306b4..a4dd6f0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,7 +17,7 @@ version = "0.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f49d8fed880d473ea71efb9bf597651e77201bdd4893efe54c9e5d65ae04ce6f" dependencies = [ - "bitflags 2.6.0", + "bitflags", "cexpr", "clang-sys", "itertools", @@ -31,12 +31,6 @@ dependencies = [ "syn", ] -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - [[package]] name = "bitflags" version = "2.6.0" @@ -51,9 +45,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "cc" -version = "1.1.34" +version = "1.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b9470d453346108f93a59222a9a1a5724db32d0a4727b7ab7ace4b4d822dc9" +checksum = "c31a0499c1dc64f458ad13872de75c0eb7e3fdb0e67964610c914b034fc5956e" dependencies = [ "shlex", ] @@ -86,9 +80,9 @@ dependencies = [ [[package]] name = "cmake" -version = "0.1.51" +version = "0.1.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb1e43aa7fd152b1f968787f7dbcdeb306d1867ff373c69955211876c053f91a" +checksum = "c682c223677e0e5b6b7f63a64b9351844c3f1b1678a68b7ee617e30fb082620e" dependencies = [ "cc", ] @@ -152,15 +146,15 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.161" +version = "0.2.169" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1" +checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" [[package]] name = "libloading" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4" +checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34" dependencies = [ "cfg-if", "windows-targets", @@ -215,18 +209,18 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.89" +version = "1.0.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e" +checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" dependencies = [ "unicode-ident", ] [[package]] name = "projectm" -version = "2.0.2" +version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40bd522bb3162cb2aa72d899d7e77c197fdca2ca7b08d70001e6ca06015af2e6" +checksum = "58ea270346057465a68f573e4d1c35b2b30abeb479d3622e0c882ea7a6891516" dependencies = [ "libc", "projectm-sys", @@ -235,9 +229,9 @@ dependencies = [ [[package]] name = "projectm-sys" -version = "1.0.10" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d09dae2ff07b700fd48dcc2a58108865da9c2aa576dcd1bb05e1297e3f361886" +checksum = "6365f7261b468baa8f1d1e2ae98be4000514ab94733e8de37fdf03450602d10c" dependencies = [ "bindgen", "cmake", @@ -308,9 +302,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.8" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" dependencies = [ "aho-corasick", "memchr", @@ -337,11 +331,11 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] name = "sdl3" -version = "0.10.8" +version = "0.11.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "276a8d8f65325969aa59f1b10671bf450fc068401075085fa0c9f87c791ab6d7" +checksum = "3862047756c6f79a6acfc8a83bdd940d114725b78462e5fa7c746a0db46bfc3b" dependencies = [ - "bitflags 1.3.2", + "bitflags", "lazy_static", "libc", "sdl3-sys", @@ -355,9 +349,9 @@ checksum = "6cd923f539b7738e631de0c729f548da2d764cd45c3a480aaab65296dfa66e1f" [[package]] name = "sdl3-sys" -version = "0.1.1+SDL3-preview-3.1.6" +version = "0.1.3+SDL3-preview-3.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44d814a5030bcbeb04be50e8026cd538eee842005da844accc987880c191f5d2" +checksum = "5c47498b18bcdb59a4f59af2923b3c5b4c921482e6e3579a2ab19a0e2c813f56" dependencies = [ "cmake", "rpkg-config", @@ -372,9 +366,9 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "syn" -version = "2.0.87" +version = "2.0.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d" +checksum = "d53cbcb5a243bd33b7858b1d7f4aca2153490815872d86d955d6ea29f743c035" dependencies = [ "proc-macro2", "quote", @@ -383,9 +377,9 @@ dependencies = [ [[package]] name = "unicode-ident" -version = "1.0.13" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" +checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" [[package]] name = "wasi" diff --git a/Cargo.toml b/Cargo.toml index b1079de..1de8e57 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,9 +6,9 @@ rust-version = "1.68.2" [dependencies] libc = "*" -#projectm = { path = "../projectm-rs", version = "2.0.2", features = ["playlist"] } -projectm = { version = "2.0.2", features = ['playlist'] } -sdl3 = { version = "0.10.8", features = ["build-from-source"] } +#projectm = { path = "../projectm-rs", version = "3", features = ["playlist"] } +projectm = { version = "3", features = ['playlist'] } +sdl3 = { version = "0.11", features = ["build-from-source-static"] } #sdl3 = { path = "../../sdl3-rs", version = "0", features = ["build-from-source"] } rand = "0.8.5" include_dir = "0.7" diff --git a/src/app/audio.rs b/src/app/audio.rs index 5e9e6eb..622f9f1 100644 --- a/src/app/audio.rs +++ b/src/app/audio.rs @@ -1,30 +1,22 @@ use projectm::core::ProjectM; -use sdl3::audio::{ - AudioDevice, AudioDeviceID, AudioRecordingCallback, AudioSpec, AudioStreamWithCallback, -}; +use sdl3::audio::{AudioDevice, AudioDeviceID, AudioSpec, AudioStream}; use super::config::FrameRate; use super::ProjectMWrapped; -use std::rc::Rc; - -// let the user cycle through audio devices -type AudioDeviceIndex = usize; - -type SampleFormat = f32; // format of audio samples +type SampleFormat = f32; // Format of audio samples +const CHANNELS: u32 = 2; // Number of audio channels pub struct Audio { audio_subsystem: sdl3::AudioSubsystem, - - current_recording_device: Option, - capture_stream: Option>>, + recording_stream: Option>, is_capturing: bool, - frame_rate: Option, projectm: ProjectMWrapped, + current_device_id: Option, + current_device_name: Option, // Store device name for comparison } -/// Wrapper around the audio subsystem to capture audio and pass it to projectM. impl Audio { pub fn new(sdl_context: &sdl3::Sdl, projectm: ProjectMWrapped) -> Self { let audio_subsystem = sdl_context.audio().unwrap(); @@ -36,9 +28,10 @@ impl Audio { Self { is_capturing: false, audio_subsystem, - current_recording_device: None, frame_rate: None, - capture_stream: None, + current_device_id: None, + current_device_name: None, + recording_stream: None, projectm, } } @@ -46,10 +39,10 @@ impl Audio { pub fn init(&mut self, frame_rate: FrameRate) { self.list_devices(); - self.frame_rate = frame_rate.into(); + self.frame_rate = Some(frame_rate); #[cfg(not(feature = "dummy_audio"))] - self.begin_audio_capture(self.get_default_recording_device().id()); + self.begin_audio_recording(None); } pub fn list_devices(&self) { @@ -62,9 +55,55 @@ impl Audio { } /// Start capturing audio from device_id. - pub fn capture_device(&mut self, device_id: AudioDeviceID) { - self.stop_audio_capture(); - self.begin_audio_capture(device_id); + pub fn begin_audio_recording(&mut self, device_id: Option) { + // Stop capturing from current stream/device + self.stop_audio_recording(); + + let sample_rate: u32 = 44100; + + let desired_spec = AudioSpec { + freq: Some(sample_rate as i32), + channels: Some(CHANNELS.try_into().unwrap()), + format: Some(sdl3::audio::AudioFormat::f32_sys()), // Assuming F32SYS is the correct format + }; + + // Open audio device for recording (use default device if none specified) + let device_id = device_id + .or_else(|| Some(self.get_default_recording_device().id())) + .unwrap(); // Ensure device_id is Some + + let audio_stream = match AudioStream::open_device_stream(device_id, Some(&desired_spec)) { + Ok(stream) => stream, + Err(e) => { + println!("Failed to open audio stream: {}", e); + return; + } + }; + println!("Capturing audio from device {:?}", audio_stream); + + // Get the actual device ID and name from the stream + let actual_device_id = audio_stream.device_id(); + let actual_device_name = audio_stream.device_name(); + + if actual_device_id.is_none() { + println!("Failed to get device ID from audio stream: {:?}", audio_stream); + return; + } + + let actual_device_id = actual_device_id.unwrap(); + let actual_device_name = actual_device_name.unwrap_or_else(|| "unknown".to_string()); + + // Start capturing + if let Err(e) = audio_stream.resume() { + println!("Failed to start audio capture: {}", e); + return; + } + + // Take ownership of the stream and store device information + self.recording_stream = Some(Box::new(audio_stream)); + self.current_device_id = Some(actual_device_id); + self.current_device_name = Some(actual_device_name); + self.is_capturing = true; } fn get_default_recording_device(&self) -> AudioDevice { @@ -73,113 +112,110 @@ impl Audio { /// Select a new audio device and start capturing audio from it. pub fn open_next_device(&mut self) { + let current_device_name = self.recording_device_name(); + let device_list = self.get_device_list(); - let current_device = self.current_recording_device.as_ref().unwrap(); - let current_device_id = current_device.id(); - let current_device_index = device_list - .iter() - .position(|d| d.eq(¤t_device_id)) - .unwrap(); + + println!("Device list: {:?}", device_list); + println!("Current device name: {:?}", current_device_name); + + // Find the index of the current device by name + let current_device_index = current_device_name.as_ref().and_then(|name| { + device_list + .iter() + .position(|d| d.name() == *name) + }); + + let current_device_index = current_device_index.unwrap_or_else(|| { + println!("Current device not found in device list. Starting from the first device."); + 0 + }); + + // Select next device index let next_device_index = (current_device_index + 1) % device_list.len(); let next_device_id = device_list[next_device_index]; - self.capture_device(next_device_id); - } - fn get_device_list(&self) -> Vec { - let audio_subsystem = &self.audio_subsystem; + println!( + "Switching from device '{}' to '{}'", + current_device_name.unwrap_or_else(|| "unknown".to_string()), + next_device_id.name() + ); - audio_subsystem.audio_recording_device_ids().unwrap() + // Start capturing from next device + self.begin_audio_recording(Some(next_device_id)); } - pub fn begin_audio_capture(&mut self, device_id: AudioDeviceID) { - let sample_rate: u32 = 44100; - let frame_rate = self.frame_rate.unwrap(); - // how many samples to capture at a time - // should be enough for 1 frame or less - // should not be larger than max_samples / channels - let max_samples: usize = ProjectM::pcm_get_max_samples().try_into().unwrap(); - let samples_per_frame = (sample_rate / frame_rate) as usize; - let buffer_size = std::cmp::min(max_samples / 2, samples_per_frame); - println!("Capturing audio from device {}", device_id.name()); - println!("Buffer size: {}", buffer_size); + pub fn stop_audio_recording(&mut self) { + if let Some(stream) = self.recording_stream.take() { + // Retrieve the device name before dropping the stream + let current_device_name = stream.device_name().unwrap_or_else(|| "unknown".to_string()); - let desired_spec = AudioSpec { - freq: Some(sample_rate.try_into().unwrap()), - channels: Some(2), - format: Some(sdl3::audio::AudioFormat::f32_sys()), - }; + println!( + "Stopping audio capture for device {}", + current_device_name + ); - // open audio device for capture - let device = AudioDevice::new(device_id); - let audio_stream = device - // move this to open_recording_device - .open_recording_stream_with_callback( - &desired_spec, - AudioCaptureCallback { - pm: Rc::clone(&self.projectm), - }, - ) - .unwrap(); - - // start capturing - audio_stream - .resume() - .expect("Failed to start audio capture"); - - // take ownership of device - self.capture_stream = Some(Box::new(audio_stream)); - self.current_recording_device = Some(device); - self.is_capturing = true; + // The recording device will be closed when the stream is dropped + self.is_capturing = false; + drop(stream); + } } - pub fn recording_device_name(&self) -> Option { - self.current_recording_device - .as_ref() - .map(|device| device.name()) - } + /// Read all available audio samples from the recording stream and feed them to ProjectM. + /// This method should be called once per frame. + pub fn process_frame_samples(&mut self) { + if !self.is_capturing || self.recording_stream.is_none() { + return; + } - pub fn stop_audio_capture(&mut self) { - if self.current_recording_device.is_none() { + let stream = self.recording_stream.as_mut().unwrap(); + let available_bytes = stream.available_bytes(); + if available_bytes.is_err() || available_bytes.unwrap() == 0 { + // nothing to read return; } - let current_device_name = self.recording_device_name(); - println!( - "Stopping audio capture for device {}", - current_device_name.unwrap_or("unknown".to_string()) - ); + // Retrieve the maximum number of PCM samples ProjectM can handle + let max_samples: usize = ProjectM::pcm_get_max_samples() + .try_into() + .expect("Failed to convert max samples to usize"); + + // Allocate the sample buffer once to reuse in the loop + let mut sample_buf = vec![0.0f32; max_samples]; + + // Start the loop to read and process samples + loop { + // Attempt to read samples into the buffer + match stream.read_f32_samples(&mut sample_buf) { + Ok(samples_read) => { + if samples_read == 0 { + // No more data to read; exit the loop + break; + } + + // Add the read samples to ProjectM for processing + self.projectm + .pcm_add_float(&sample_buf[..samples_read], CHANNELS); + } + Err(e) => { + // Handle any read errors + println!("Failed to read audio samples: {}", e); + break; // Exit the loop on error + } + } + } - // take ownership of device - // capture device will be dropped when this function returns - // and the audio callback will stop being called - let device = self.capture_stream.take().unwrap(); - device.pause().expect("Failed to stop audio capture"); + } - self.is_capturing = false; - drop(device); + fn get_device_list(&self) -> Vec { + self.audio_subsystem.audio_recording_device_ids().unwrap_or_else(|e| { + println!("Failed to get audio device list: {}", e); + Vec::new() + }) } -} -struct AudioCaptureCallback { - // we need to keep a reference to the projectm instance to - // add the audio data to it - pm: ProjectMWrapped, -} -unsafe impl Send for AudioCaptureCallback {} -unsafe impl Sync for AudioCaptureCallback {} - -impl AudioRecordingCallback for AudioCaptureCallback { - // we are receiving some chunk of audio data - // we need to pass it to projectm - fn callback(&mut self, out: &[SampleFormat]) { - println!("Received {} samples", out.len()); - let mut out = out; - let max_samples = ProjectM::pcm_get_max_samples() as usize; - if (out.len() > max_samples) { - // remove some samples - out = &out[..max_samples]; - } - self.pm.pcm_add_float(out.to_vec(), 2); + pub fn recording_device_name(&self) -> Option { + self.current_device_name.clone() } -} +} \ No newline at end of file diff --git a/src/app/main_loop.rs b/src/app/main_loop.rs index eca0636..bc143ba 100644 --- a/src/app/main_loop.rs +++ b/src/app/main_loop.rs @@ -97,6 +97,10 @@ impl App { // generate random audio #[cfg(feature = "dummy_audio")] dummy_audio::generate_random_audio_data(&self.pm); + + // Feed audio data from capture device to projectM + #[cfg(not(feature = "dummy_audio"))] + self.audio.process_frame_samples(); // render a frame self.pm.render_frame(); diff --git a/src/dummy_audio.rs b/src/dummy_audio.rs index ed34045..83a177c 100644 --- a/src/dummy_audio.rs +++ b/src/dummy_audio.rs @@ -13,5 +13,5 @@ pub fn generate_random_audio_data(pm: &ProjectMWrapped) { } } - pm.pcm_add_int16(pcm_data, 2); + pm.pcm_add_int16(&pcm_data, 2); }