~nickbp/soundview

5ebfe8768a64247b34d8df6ada57edc9c3e34a83 — Nick Parker 4 months ago
Initial code, things still mostly stubbed out
11 files changed, 786 insertions(+), 0 deletions(-)

A .gitignore
A Cargo.lock
A Cargo.toml
A LICENCE.md
A src/events.rs
A src/hsl.rs
A src/lib.rs
A src/logging.rs
A src/main.rs
A src/recorder.rs
A src/render.rs
A  => .gitignore +1 -0
@@ 1,1 @@
/target

A  => Cargo.lock +410 -0
@@ 1,410 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3

[[package]]
name = "ansi_term"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2"
dependencies = [
 "winapi",
]

[[package]]
name = "anyhow"
version = "1.0.51"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b26702f315f53b6071259e15dd9d64528213b44d61de1ec926eca7715d62203"

[[package]]
name = "autocfg"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a"

[[package]]
name = "bitflags"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"

[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"

[[package]]
name = "chrono"
version = "0.4.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73"
dependencies = [
 "libc",
 "num-integer",
 "num-traits",
 "winapi",
]

[[package]]
name = "git-testament"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "080c47ef3c243fb13474429c14dce386021cd64de731c353998a745c2fa2435b"
dependencies = [
 "git-testament-derive",
 "no-std-compat",
]

[[package]]
name = "git-testament-derive"
version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0803898541a48d6f0809fa681bc8d38603f727d191f179631d85ddc3b6a9a2c"
dependencies = [
 "log",
 "proc-macro2",
 "quote",
 "syn",
 "time",
]

[[package]]
name = "itoa"
version = "0.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4"

[[package]]
name = "itoa"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35"

[[package]]
name = "lazy_static"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"

[[package]]
name = "libc"
version = "0.2.112"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b03d17f364a3a042d5e5d46b053bbbf82c92c9430c592dd4c064dc6ee997125"

[[package]]
name = "log"
version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710"
dependencies = [
 "cfg-if",
]

[[package]]
name = "matchers"
version = "0.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f099785f7595cc4b4553a174ce30dd7589ef93391ff414dbb67f62392b9e0ce1"
dependencies = [
 "regex-automata",
]

[[package]]
name = "no-std-compat"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b93853da6d84c2e3c7d730d6473e8817692dd89be387eb01b94d7f108ecb5b8c"

[[package]]
name = "num-integer"
version = "0.1.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db"
dependencies = [
 "autocfg",
 "num-traits",
]

[[package]]
name = "num-traits"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290"
dependencies = [
 "autocfg",
]

[[package]]
name = "once_cell"
version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da32515d9f6e6e489d7bc9d84c71b060db7247dc035bbe44eac88cf87486d8d5"

[[package]]
name = "pin-project-lite"
version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d31d11c69a6b52a174b42bdc0c30e5e11670f90788b2c471c31c1d17d449443"

[[package]]
name = "proc-macro2"
version = "1.0.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f84e92c0f7c9d58328b85a78557813e4bd845130db68d7184635344399423b1"
dependencies = [
 "unicode-xid",
]

[[package]]
name = "quote"
version = "1.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38bc8cc6a5f2e3655e0899c1b848643b2562f853f114bfec7be120678e3ace05"
dependencies = [
 "proc-macro2",
]

[[package]]
name = "regex"
version = "1.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461"
dependencies = [
 "regex-syntax",
]

[[package]]
name = "regex-automata"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
dependencies = [
 "regex-syntax",
]

[[package]]
name = "regex-syntax"
version = "0.6.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b"

[[package]]
name = "ryu"
version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73b4b750c782965c211b42f022f59af1fbceabdd026623714f104152f1ec149f"

[[package]]
name = "sdl2"
version = "0.35.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f035f8e87735fa3a8437292be49fe6056450f7cbb13c230b4bcd1bdd7279421f"
dependencies = [
 "bitflags",
 "lazy_static",
 "libc",
 "sdl2-sys",
]

[[package]]
name = "sdl2-sys"
version = "0.35.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94cb479353c0603785c834e2307440d83d196bf255f204f7f6741358de8d6a2f"
dependencies = [
 "cfg-if",
 "libc",
 "version-compare",
]

[[package]]
name = "serde"
version = "1.0.132"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b9875c23cf305cd1fd7eb77234cbb705f21ea6a72c637a5c6db5fe4b8e7f008"

[[package]]
name = "serde_json"
version = "1.0.73"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bcbd0344bc6533bc7ec56df11d42fb70f1b912351c0825ccb7211b59d8af7cf5"
dependencies = [
 "itoa 1.0.1",
 "ryu",
 "serde",
]

[[package]]
name = "sharded-slab"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31"
dependencies = [
 "lazy_static",
]

[[package]]
name = "smallvec"
version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ecab6c735a6bb4139c0caafd0cc3635748bbb3acf4550e8138122099251f309"

[[package]]
name = "soundview"
version = "0.1.0"
dependencies = [
 "anyhow",
 "git-testament",
 "sdl2",
 "tracing",
 "tracing-subscriber",
]

[[package]]
name = "syn"
version = "1.0.82"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8daf5dd0bb60cbd4137b1b587d2fc0ae729bc07cf01cd70b36a1ed5ade3b9d59"
dependencies = [
 "proc-macro2",
 "quote",
 "unicode-xid",
]

[[package]]
name = "thread_local"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8018d24e04c95ac8790716a5987d0fec4f8b27249ffa0f7d33f1369bdfb88cbd"
dependencies = [
 "once_cell",
]

[[package]]
name = "time"
version = "0.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41effe7cfa8af36f439fac33861b66b049edc6f9a32331e2312660529c1c24ad"
dependencies = [
 "itoa 0.4.8",
 "libc",
 "time-macros",
]

[[package]]
name = "time-macros"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25eb0ca3468fc0acc11828786797f6ef9aa1555e4a211a60d64cc8e4d1be47d6"

[[package]]
name = "tracing"
version = "0.1.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "375a639232caf30edfc78e8d89b2d4c375515393e7af7e16f01cd96917fb2105"
dependencies = [
 "cfg-if",
 "pin-project-lite",
 "tracing-attributes",
 "tracing-core",
]

[[package]]
name = "tracing-attributes"
version = "0.1.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f4f480b8f81512e825f337ad51e94c1eb5d3bbdf2b363dcd01e2b19a9ffe3f8e"
dependencies = [
 "proc-macro2",
 "quote",
 "syn",
]

[[package]]
name = "tracing-core"
version = "0.1.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f4ed65637b8390770814083d20756f87bfa2c21bf2f110babdc5438351746e4"
dependencies = [
 "lazy_static",
]

[[package]]
name = "tracing-log"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6923477a48e41c1951f1999ef8bb5a3023eb723ceadafe78ffb65dc366761e3"
dependencies = [
 "lazy_static",
 "log",
 "tracing-core",
]

[[package]]
name = "tracing-serde"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb65ea441fbb84f9f6748fd496cf7f63ec9af5bca94dd86456978d055e8eb28b"
dependencies = [
 "serde",
 "tracing-core",
]

[[package]]
name = "tracing-subscriber"
version = "0.2.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e0d2eaa99c3c2e41547cfa109e910a68ea03823cccad4a0525dcbc9b01e8c71"
dependencies = [
 "ansi_term",
 "chrono",
 "lazy_static",
 "matchers",
 "regex",
 "serde",
 "serde_json",
 "sharded-slab",
 "smallvec",
 "thread_local",
 "tracing",
 "tracing-core",
 "tracing-log",
 "tracing-serde",
]

[[package]]
name = "unicode-xid"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3"

[[package]]
name = "version-compare"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe88247b92c1df6b6de80ddc290f3976dbdf2f5f5d3fd049a9fb598c6dd5ca73"

[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
dependencies = [
 "winapi-i686-pc-windows-gnu",
 "winapi-x86_64-pc-windows-gnu",
]

[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"

[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"

A  => Cargo.toml +16 -0
@@ 1,16 @@
[package]
name = "soundview"
version = "0.1.0"
edition = "2021"
authors = ["Nick Parker <nick@nickbp.com>"]
license-file = "LICENCE.md"
readme = "README-CRATES.md"
description = "Voiceprint visualization"
repository = "https://sr.ht/~nickbp/soundview/"

[dependencies]
anyhow = "1.0"
git-testament = "0.2"
sdl2 = "0.35"
tracing = "0.1"
tracing-subscriber = "0.2"
\ No newline at end of file

A  => LICENCE.md +52 -0
@@ 1,52 @@
# The Fuck Around and Find Out License, version 0.2

## Purpose

This license gives everyone as much permission to work with
this software as possible, while protecting contributors
from liability, and ensuring this software is used
ethically.

## Acceptance

In order to receive this license, you must agree to its
rules.  The rules of this license are both obligations
under that agreement and conditions to your license.
You must not do anything with this software that triggers
a rule that you cannot or will not follow.

## Copyright

Each contributor licenses you to do everything with this
software that would otherwise infringe that contributor's
copyright in it.

## Ethics

This software must be used for Good, not Evil, as
determined by the primary contributors to the software.

## Excuse

If anyone notifies you in writing that you have not
complied with [Ethics](#ethics), you can keep your
license by taking all practical steps to comply within 30
days after the notice.  If you do not do so, your license
ends immediately.

## Patent

Each contributor licenses you to do everything with this
software that would otherwise infringe any patent claims
they can license or become able to license.

## Reliability

No contributor can revoke this license.

## No Liability

***As far as the law allows, this software comes as is,
without any warranty or condition, and no contributor
will be liable to anyone for any damages related to this
software or this license, under any kind of legal claim.***

A  => src/events.rs +56 -0
@@ 1,56 @@
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/hsl.rs +81 -0
@@ 1,81 @@
use sdl2::pixels::Color;

fn hue_to_rgbval_with_p0(q: f64, mut t: f64) -> f64 {
    if t < 0.0 {
        t += 1.;
    }
    if t < 1./6. {
        q * 6. * t
    } else if t < 1./2. {
        q
    } else if t < 2./3. {
        q * (2. / 3. - t) * 6.
    } else {
        0.
    }
}

fn hue_to_rgbval_with_q1(p: f64, mut t: f64) -> f64 {
    if t < 0. {
        t += 1.;
    }
    if t < 1./6. {
        p + ((1. - p) * 6. * t)
    } else if t < 1./2. {
        1.
    } else if t < 2./3. {
        p + ((1. - p) * (2. / 3. - t) * 6.)
    } else {
        p
    }
}

fn value_to_color(max_lum: f64, lum_exponent: f64, value: f64) -> Color {
    let lum_calc = f64::powf(value, lum_exponent);
    let mut lum = if max_lum > lum_calc { lum_calc } else { max_lum };

    let hue = 1. / 3. * (1. - value);
    lum *= 2.;
    if lum < 1. {
        Color::RGB(
            (hue_to_rgbval_with_p0(lum, hue + 1. / 3.) * 255.) as u8,
            (hue_to_rgbval_with_p0(lum, hue) * 255.) as u8,
            (hue_to_rgbval_with_p0(lum, hue - 1. / 3.) * 255.) as u8
        )
    } else {
        lum -= 1.;
        Color::RGB(
            (hue_to_rgbval_with_q1(lum, hue + 1. / 3.) * 255.) as u8,
            (hue_to_rgbval_with_q1(lum, hue) * 255.) as u8,
            (hue_to_rgbval_with_q1(lum, hue - 1. / 3.) * 255.) as u8
        )
    }
}

pub struct HSL {
    precached_vals: Vec<Color>,
}

impl HSL {
    pub fn new(color_max_lum: usize, color_lum_exaggeration: usize) -> HSL {
        let cache_size = 1024;
        let max_lum = color_max_lum as f64 / 100.;
        let lum_exponent = 1 as f64 - (color_lum_exaggeration as f64 / 100.);
        let mut precached_vals = Vec::new();
        for i in 0..cache_size {
            precached_vals.push(value_to_color(max_lum, lum_exponent, i as f64 / cache_size as f64));
        }
        HSL {
            precached_vals
        }
    }

    pub fn value_to_color(self: &HSL, value: f64) -> Color {
        let max_index: usize = self.precached_vals.len() - 1;
        let mut index: usize = (value * self.precached_vals.len() as f64) as usize;
        if index > max_index {
            index = max_index;
        }
        return self.precached_vals[index];
    }
}

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

A  => src/logging.rs +16 -0
@@ 1,16 @@
use tracing;
use tracing_subscriber::EnvFilter;

pub fn init_logging() {
    let filter_layer = EnvFilter::try_from_env("LOG_LEVEL")
        .or_else(|_| EnvFilter::try_new("info"))
        .expect("Failed to initialize filter layer");

    tracing::subscriber::set_global_default(
        tracing_subscriber::fmt()
            .with_writer(std::io::stderr)
            .with_env_filter(filter_layer)
            .finish()
    )
    .expect("Failed to set default subscriber");
}

A  => src/main.rs +60 -0
@@ 1,60 @@
use anyhow::{anyhow, Result};
use git_testament::{git_testament, render_testament};
use tracing::{debug, info, warn};

use soundview::{events, logging, recorder, render};

git_testament!(TESTAMENT);

fn set_hint(name: &str) {
    let before = sdl2::hint::get(name);
    sdl2::hint::set(name, "1");
    debug!("{}: {:?} => {:?}", name, before, sdl2::hint::get(name));
}

fn main() -> Result<()> {
    logging::init_logging();
    let sdl_version = sdl2::version::version();
    info!("Soundview version {}, SDL {}.{}.{}", render_testament!(TESTAMENT), sdl_version.major, sdl_version.minor, sdl_version.patch);
    if cfg!(unix) {
        // 2.0.16 adds support for SDL_AUDIO_INCLUDE_MONITORS
        if sdl_version.minor == 0 && sdl_version.patch < 16 {
            warn!("SDL 2.0.16 or greater is required to visualize playing audio with PulseAudio");
        }
    }

    // Enables PulseAudio "monitor" devices in the list.
    // These are what allow visualizing audio that's playing on the system.
    // Without this we can only visualize the mic input.
    // This option only works on 2.0.16 or newer.
    set_hint("SDL_AUDIO_INCLUDE_MONITORS");

    let sdl_context = sdl2::init().map_err(|e| anyhow!(e))?;

    let video_subsystem = sdl_context.video().map_err(|e| anyhow!(e))?;
    if video_subsystem.is_screen_saver_enabled() {
        debug!("disabling screensaver");
        video_subsystem.disable_screen_saver();
    }
    let window = video_subsystem.window("soundview", 800, 600)
        .resizable()
        .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))?)
    )?;

    Ok(())
}

A  => src/recorder.rs +67 -0
@@ 1,67 @@
use anyhow::{anyhow, Result};
use tracing::info;

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

struct Handler {
}

impl AudioCallback for Handler {
    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;
            }
        }
        info!("callback {} {:?}", out.len(), val);
    }
}

pub struct Recorder {
    audio_subsystem: AudioSubsystem,
}

impl Recorder {
    pub fn new(audio_subsystem: AudioSubsystem) -> Recorder {
        Recorder {
            audio_subsystem
        }
    }

    pub fn device_added(self: &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
            return Ok(());
        }
        info!("setting up capture");
        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
        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| {
            // initialize the audio callback
            Handler {
            }
        }).map_err(|e| anyhow!(e))?;

        // Start playback
        rec_dev.resume();

        Ok(())
    }
}

A  => src/render.rs +21 -0
@@ 1,21 @@
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(())
}