soundview 0.3.0

Live analyzer/voiceprint visualization of system audio
Documentation
use anyhow::{anyhow, Context, Result};
use crossbeam_channel::Receiver;
use sdl2::event::{Event, WindowEvent};
use sdl2::keyboard::Keycode;
use tracing::{debug, error, warn};

use crate::webgpu;
use crate::{hsl, recorder, renderer, webgpu::Orientation};

/// Endless loop that takes over the main thread.
/// Drives rendering the visualization and handles any keyboard/window events.
pub fn process_event_loop(
    sdl_context: &sdl2::Sdl,
    recv_processed: Receiver<Vec<f32>>,
    orientation: Orientation,
    fullscreen: bool,
    scroll_rate: f32,
    texture_width: usize,
    mut rec: recorder::Recorder,
    fourier_thread: std::thread::JoinHandle<()>,
) -> Result<()> {
    let icon_webp = include_bytes!("soundview.webp");
    let icon_image = image::load_from_memory_with_format(icon_webp, image::ImageFormat::WebP)?;
    let mut icon_bytes = icon_image
        .as_rgba8()
        .context("Unable to get RGBA data for icon")?
        .to_vec();
    let icon = sdl2::surface::Surface::from_data(
        &mut icon_bytes,
        icon_image.width(),
        icon_image.height(),
        sdl2::pixels::PixelFormatEnum::RGBA32.byte_size_of_pixels(icon_image.width() as usize)
            as u32,
        sdl2::pixels::PixelFormatEnum::RGBA32,
    )
    .map_err(|e| anyhow!(e))
    .context("Failed to init application icon")?;

    let mut window_builder =
        sdl_context
            .video()
            .map_err(|e| anyhow!(e))?
            .window("soundview", 1280, 720);
    window_builder.position_centered().resizable().metal_view();
    if fullscreen {
        window_builder.fullscreen_desktop();
        sdl_context.mouse().show_cursor(false);
    }
    let mut window = window_builder.build().map_err(|e| anyhow!(e))?;
    window.set_minimum_size(100, 100).map_err(|e| anyhow!(e))?;
    window.set_icon(icon);

    let fourier_thread_rc = std::cell::RefCell::new(Some(fourier_thread));
    let wgpu_state = pollster::block_on(webgpu::WgpuState::new(&window))?;
    let hsl = hsl::HSL::new(wgpu_state.preferred_format, 50, 40);

    let mut state = pollster::block_on(renderer::State::new(
        wgpu_state,
        hsl,
        recv_processed,
        orientation,
        scroll_rate,
        texture_width,
    ));

    let mut event_pump = sdl_context.event_pump().map_err(|e| anyhow!(e))?;
    loop {
        // Handle any pending events
        for event in event_pump.poll_iter() {
            match event {
                // Close window button: Quit
                Event::Quit { .. } => {
                    shut_down(&mut rec, &fourier_thread_rc);
                    return Ok(());
                }

                Event::Window {
                    window_id,
                    win_event: WindowEvent::SizeChanged(width, height),
                    ..
                } if window_id == window.id() => {
                    state.resize(Some(webgpu::Dimensions {
                        width: width as usize,
                        height: height as usize,
                    }));
                }

                Event::KeyDown {
                    keycode: Some(keycode),
                    ..
                } => match keycode {
                    // Esc or Q: Quit
                    Keycode::Escape | Keycode::Q => {
                        shut_down(&mut rec, &fourier_thread_rc);
                        return Ok(());
                    }
                    // F11 or F: Fullscreen
                    Keycode::F11 | Keycode::F => {
                        if window.fullscreen_state() == sdl2::video::FullscreenType::Off {
                            // toggle on
                            if let Err(e) =
                                window.set_fullscreen(sdl2::video::FullscreenType::Desktop)
                            {
                                warn!("Failed to enable fullscreen: {}", e);
                            }
                            sdl_context.mouse().show_cursor(false);
                        } else {
                            // toggle off
                            if let Err(e) = window.set_fullscreen(sdl2::video::FullscreenType::Off)
                            {
                                warn!("Failed to disable fullscreen: {}", e);
                            }
                            sdl_context.mouse().show_cursor(true);
                        }
                    }
                    // Space or R: Rotate
                    Keycode::Space | Keycode::R => {
                        state.toggle_orientation();
                    }
                    // Left arrow: Prev device
                    Keycode::Left => {
                        // TODO display device name on top corner of display, using font-rs or manual bitmap
                        if let Err(e) = rec.prev_device() {
                            warn!("Failed to switch to previous device: {}", e)
                        }
                    }
                    // Right arrow: Next device
                    Keycode::Right => {
                        // TODO display device name on top corner of display, using font-rs or manual bitmap
                        if let Err(e) = rec.next_device() {
                            warn!("Failed to switch to next device: {}", e)
                        }
                    }
                    // other keys...
                    _ => {}
                },

                // other events...
                _ => {}
            }
        }

        // Render
        match state.surface_texture() {
            Ok(output) => {
                if let Err(e) = state.render(output) {
                    error!("shutting down: render error {}", e);
                    shut_down(&mut rec, &fourier_thread_rc);
                    return Ok(());
                }
            }
            // Reconfigure the surface if lost
            Err(wgpu::SurfaceError::Lost) => state.resize(None),
            // The system is out of memory, we should probably quit
            Err(wgpu::SurfaceError::OutOfMemory) => {
                // Run any teardown operations before exiting the main thread.
                // event_loop.run() has taken over the thread and will never return.
                error!("shutting down: wgpu is out of memory");
                rec.stop();
                match fourier_thread_rc.replace(None) {
                    Some(fourier_thread) => {
                        if let Err(e) = fourier_thread.join() {
                            warn!("failed to wait on fourier thread to exit: {:?}", e);
                        }
                    }
                    None => {} // join already called?
                }
                return Ok(());
            }
            // All other errors (Outdated, Timeout) should be resolved by rendering the next frame.
            // Timeout errors in particular can happen every ~1s when the window is being obscured.
            Err(e) => debug!("render error: {:?}", e),
        }
    }
}

/// Runs any teardown operations before exiting the main thread.
/// event_loop.run() has taken over the thread and will never return.
fn shut_down(
    rec: &mut recorder::Recorder,
    fourier_thread_rc: &std::cell::RefCell<Option<std::thread::JoinHandle<()>>>,
) {
    debug!("shutting down");
    rec.stop();
    match fourier_thread_rc.replace(None) {
        Some(fourier_thread) => {
            if let Err(e) = fourier_thread.join() {
                warn!("failed to wait on fourier thread to exit: {:?}", e);
            }
        }
        None => {} // join already called?
    }
}