~nickbp/soundview

82bbe3750d6cd3f52b41f9a57c957584266687ca — Nick Parker 4 months ago 5ebfe87
Figure out threading model, with stubbed out processing/display
8 files changed, 166 insertions(+), 117 deletions(-)

M Cargo.lock
M Cargo.toml
D src/events.rs
A src/fourier.rs
M src/lib.rs
M src/main.rs
M src/recorder.rs
D src/render.rs
M Cargo.lock => Cargo.lock +21 -0
@@ 48,6 48,26 @@ dependencies = [
]

[[package]]
name = "crossbeam-channel"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06ed27e177f16d65f0f0c22a213e17c696ace5dd64b14258b52f9417ccb52db4"
dependencies = [
 "cfg-if",
 "crossbeam-utils",
]

[[package]]
name = "crossbeam-utils"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d82cfc11ce7f2c3faef78d8a684447b40d503d9681acebed6cb728d45940c4db"
dependencies = [
 "cfg-if",
 "lazy_static",
]

[[package]]
name = "git-testament"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"


@@ 257,6 277,7 @@ name = "soundview"
version = "0.1.0"
dependencies = [
 "anyhow",
 "crossbeam-channel",
 "git-testament",
 "sdl2",
 "tracing",

M Cargo.toml => Cargo.toml +1 -0
@@ 10,6 10,7 @@ repository = "https://sr.ht/~nickbp/soundview/"

[dependencies]
anyhow = "1.0"
crossbeam-channel = "0.5"
git-testament = "0.2"
sdl2 = "0.35"
tracing = "0.1"

D src/events.rs => src/events.rs +0 -56
@@ 1,56 0,0 @@
use anyhow::Result;
use tracing::{debug, info};

use sdl2::event::{Event, WindowEvent};
use sdl2::keyboard::{Keycode, Mod};
use sdl2::EventPump;

use crate::recorder::Recorder;

pub fn events_loop(mut event_pump: EventPump, recorder: Recorder) -> Result<()> {
    loop {
        let event = event_pump.wait_event_timeout(100);
        if let Some(event) = event {
            match event {
                Event::Quit {..} |
                // Esc, Q, Ctrl+C, Alt+F4: Quit
                Event::KeyDown { keycode: Some(Keycode::Escape), .. } => {
                    return Ok(());
                },
                Event::KeyDown { keycode: Some(Keycode::Q), .. } => {
                    return Ok(());
                },
                Event::KeyDown { keymod: Mod::LCTRLMOD | Mod::RCTRLMOD, keycode: Some(Keycode::C), .. } => {
                    return Ok(());
                },
                Event::KeyDown { keymod: Mod::LALTMOD | Mod::RALTMOD, keycode: Some(Keycode::F4), .. } => {
                    return Ok(());
                },

                // R, Space: Rotate
                Event::KeyDown { keycode: Some(Keycode::R) | Some(Keycode::Space), .. } => {
                    info!("rotate");
                },

                // A, D, I: Switch Audio Device Input
                Event::KeyDown { keycode: Some(Keycode::A) | Some(Keycode::D) | Some(Keycode::I), .. } => {
                    info!("next device");
                },

                Event::Window {win_event: WindowEvent::SizeChanged(x, y), ..} | Event::Window {win_event: WindowEvent::Resized(x,y), ..} => {
                    info!("resize: x={}, y={}", x, y);
                },

                Event::AudioDeviceAdded {iscapture: true, which, ..} => {
                    recorder.device_added(which)?;
                },
                Event::AudioDeviceRemoved {iscapture: true, which: idx, ..} => {
                    info!("capture device removed: idx={}", idx);
                },
                _ => {
                    debug!("ignored event: {:?}", event);
                }
            }
        }
    }
}

A src/fourier.rs => src/fourier.rs +20 -0
@@ 0,0 1,20 @@
use crossbeam_channel::{Receiver, Sender};
use tracing::{debug, warn};

pub fn process_audio_loop(recv_audio: Receiver<Vec<f32>>, send_processed: Sender<Vec<f32>>) {
    loop {
        match recv_audio.recv() {
            Ok(mut audio) => {
                audio.truncate(5); // TODO actual processing...
                if let Err(e) = send_processed.send(audio) {
                    warn!("Failed to send processed data: {}", e);
                }
            },
            Err(e) => {
                // This generally means that the input channel has closed
                debug!("exiting audio processing thread: {}", e);
                break;
            }
        }
    }
}

M src/lib.rs => src/lib.rs +1 -2
@@ 1,6 1,5 @@
pub mod events;
pub mod fourier;
pub mod hsl;
/// Utilities relating to setting up log output.
pub mod logging;
pub mod recorder;
pub mod render;

M src/main.rs => src/main.rs +98 -17
@@ 1,8 1,15 @@
use anyhow::{anyhow, Result};
use anyhow::{anyhow, bail, Result};
use git_testament::{git_testament, render_testament};
use tracing::{debug, info, warn};

use soundview::{events, logging, recorder, render};
use sdl2::event::{Event, WindowEvent};
use sdl2::keyboard::{Keycode, Mod};
use sdl2::pixels::Color;

use soundview::{fourier, logging, recorder};

use std::thread;
use std::time::Duration;

git_testament!(TESTAMENT);



@@ 36,25 43,99 @@ fn main() -> Result<()> {
        debug!("disabling screensaver");
        video_subsystem.disable_screen_saver();
    }
    let window = video_subsystem.window("soundview", 800, 600)
    let mut canvas = video_subsystem.window("soundview", 800, 600)
        .resizable()
        .build()
        .map_err(|e| anyhow!(e))?
        .into_canvas()
        .accelerated()
        .present_vsync()
        .build()
        .map_err(|e| anyhow!(e))?;

    // TODO run events handling and rendering in separate threads?
    render::render_loop(
        window.into_canvas()
            .accelerated()
            .present_vsync()
            .build()
            .map_err(|e| anyhow!(e))?
    )?;

    // TODO clean teardown after events_loop exits
    events::events_loop(
        sdl_context.event_pump().map_err(|e| anyhow!(e))?,
        recorder::Recorder::new(sdl_context.audio().map_err(|e| anyhow!(e))?)
    )?;
    let (send_audio, recv_audio) = crossbeam_channel::bounded::<Vec<f32>>(100);
    let (send_processed, recv_processed) = crossbeam_channel::bounded::<Vec<f32>>(100);

    let mut thread_handles = Vec::new();
    thread_handles.push(
        thread::Builder::new()
            .name("fourier".to_string())
            .spawn(move || {
                fourier::process_audio_loop(recv_audio, send_processed)
            })?
    );

    {
        // Ensure that rec (and send_audio) go out of scope when we're exiting,
        // so that the audio processing thread shuts down.
        let mut rec = recorder::Recorder::new(sdl_context.audio().map_err(|e| anyhow!(e))?, send_audio);

        let mut i = 0;
        'mainloop: loop {
            // handle events (if any)
            // TODO could break this out into a separate function, but it needs to interact with everything else...
            if let Some(event) = sdl_context.event_pump().map_err(|e| anyhow!(e))?.poll_event() {
                match event {
                    // <Close window>, Esc, Q, Ctrl+C, Alt+F4: Quit
                    Event::Quit {..} |
                    Event::KeyDown { keycode: Some(Keycode::Escape) | Some(Keycode::Q), .. } |
                    Event::KeyDown { keymod: Mod::LCTRLMOD | Mod::RCTRLMOD, keycode: Some(Keycode::C), .. } |
                    Event::KeyDown { keymod: Mod::LALTMOD | Mod::RALTMOD, keycode: Some(Keycode::F4), .. } => {
                        break 'mainloop;
                    },

                    // R, Space: Rotate
                    Event::KeyDown { keycode: Some(Keycode::R) | Some(Keycode::Space), .. } => {
                        info!("rotate");
                    },

                    // A, D, I: Switch Audio Device Input
                    Event::KeyDown { keycode: Some(Keycode::A) | Some(Keycode::D) | Some(Keycode::I), .. } => {
                        info!("next device");
                    },

                    Event::Window {win_event: WindowEvent::SizeChanged(x, y), ..} | Event::Window {win_event: WindowEvent::Resized(x,y), ..} => {
                        info!("resize: x={}, y={}", x, y);
                    },

                    Event::AudioDeviceAdded {iscapture: true, which: idx, ..} => {
                        rec.device_added(idx)?;
                    },
                    Event::AudioDeviceRemoved {iscapture: true, which: idx, ..} => {
                        info!("capture device removed: idx={}", idx);
                    },
                    _ => {
                        debug!("ignored event: {:?}", event);
                    }
                }
            }

            // render
            while let Ok(audio) = recv_processed.try_recv() {
                debug!("got processed audio: {:?}", audio.len()); // TODO actual rendering...
            }
            i = (i + 1) % 255;
            canvas.set_draw_color(Color::RGB(i, 64, 255 - i));
            canvas.clear();

            canvas.present();
            thread::sleep(Duration::new(0, 1_000_000_000u32 / 60));
        }
    }

    for thread in thread_handles {
        let thread_name = if let Some(name) = &thread.thread().name() {
            name.to_string()
        } else {
            "???".to_string()
        };
        info!("Waiting for thread to exit: {}", thread_name);
        // Would use with_context but the error type is weird
        match thread.join().err() {
            Some(e) => bail!("Failed to wait for thread to exit: {} {:?}", thread_name, e),
            None => {}
        }
    }

    Ok(())
}

M src/recorder.rs => src/recorder.rs +25 -21
@@ 1,67 1,71 @@
use anyhow::{anyhow, Result};
use tracing::info;
use tracing::{info, warn};

use sdl2::audio::{AudioCallback, AudioSpecDesired};
use sdl2::audio::{AudioCallback, AudioDevice, AudioSpecDesired};
use sdl2::AudioSubsystem;

struct Handler {
struct Callback {
    audio_out: crossbeam_channel::Sender<Vec<f32>>
}

impl AudioCallback for Handler {
impl AudioCallback for Callback {
    type Channel = f32;

    fn callback(&mut self, out: &mut [f32]) {
        let mut val: f32 = 0.;
        for x in out.iter() {
            if *x < 0. {
                val += -x;
            } else {
                val += x;
            }
        if let Err(e) = self.audio_out.send(Vec::from(out)) {
            warn!("Failed to send audio: {}", e);
        }
        info!("callback {} {:?}", out.len(), val);
    }
}

pub struct Recorder {
    audio_subsystem: AudioSubsystem,
    audio_out: crossbeam_channel::Sender<Vec<f32>>,
    rec_dev: Option<AudioDevice<Callback>>
}

impl Recorder {
    pub fn new(audio_subsystem: AudioSubsystem) -> Recorder {
    pub fn new(audio_subsystem: AudioSubsystem, audio_out: crossbeam_channel::Sender<Vec<f32>>) -> Recorder {
        Recorder {
            audio_subsystem
            audio_subsystem,
            audio_out,
            rec_dev: Option::None
        }
    }

    pub fn device_added(self: &Recorder, which: u32) -> Result<()> {
    pub fn device_added(self: &mut Recorder, which: u32) -> Result<()> {
        let name = self.audio_subsystem.audio_capture_device_name(which).map_err(|e| anyhow!(e))?;
        info!("capture device added: idx={} name={:?}", which, name);
        if name != "Monitor of Built-in Audio Analog Stereo" {
            // ignore
            info!("added capture device (ignored): idx={} name={:?}", which, name);
            return Ok(());
        }
        info!("setting up capture");
        info!("added capture device (setting up): idx={} name={:?}", which, name);
        self.record(name.as_str())
    }

    fn record(self: &Recorder, device_name: &str) -> Result<()> {
        // TODO events arent logged, this probably needs to be in different thread
    fn record(self: &mut Recorder, device_name: &str) -> Result<()> {
        let desired_spec = AudioSpecDesired {
            freq: Some(44100),
            channels: Some(1),  // mono
            samples: None       // default sample size, power of 2
        };

        let rec_dev = self.audio_subsystem.open_capture(device_name, &desired_spec, |_actual_spec| {
        let audio_out_cpy = self.audio_out.clone();
        let rec_dev = self.audio_subsystem.open_capture(device_name, &desired_spec, |actual_spec| {
            info!("Capturing audio from '{}' with spec: {:?}", device_name, actual_spec);
            // initialize the audio callback
            Handler {
            Callback {
                audio_out: audio_out_cpy
            }
        }).map_err(|e| anyhow!(e))?;

        // Start playback
        rec_dev.resume();

        // Save rec_dev so that it doesn't get closed automatically
        self.rec_dev = Some(rec_dev);

        Ok(())
    }
}

D src/render.rs => src/render.rs +0 -21
@@ 1,21 0,0 @@
use anyhow::Result;

use sdl2::pixels::Color;
use sdl2::render::Canvas;
use sdl2::video::Window;
use std::time::Duration;

pub fn render_loop(mut canvas: Canvas<Window>) -> Result<()> {
    let mut i = 0;

    loop {
        i = (i + 1) % 255;
        canvas.set_draw_color(Color::RGB(i, 64, 255 - i));
        canvas.clear();

        canvas.present();
        ::std::thread::sleep(Duration::new(0, 1_000_000_000u32 / 60));
    }

    Ok(())
}