use crossbeam_channel::{select, unbounded, Sender}; use std::{ fs::File, io::Write, path::{Path, PathBuf}, process::{Child, Command, Stdio}, sync::Arc, thread, time::Duration, }; use tracing::{error, info}; use crate::{ final_ring::FinalRing, pattern::{PatternError, PatternResult}, }; #[derive(Debug)] pub struct Cava { terminate_tx: Sender<()>, // cava_process: Child, // num_bars: u16, final_ring: Arc, } impl Cava { fn spawn_cava>(config_file: P) -> PatternResult { // Start cava let mut cava_process = Command::new("cava") // DISPLAY cannot be set .env_remove("DISPLAY") .arg("-p") .arg(config_file.as_ref()) .stderr(Stdio::piped()) .stdout(Stdio::piped()) .spawn() .map_err(|e| PatternError::CommandNotFound(e.to_string()))?; info!("New cava process spawned"); // Send cava stderr to our stderr // TODO: Can we just remove the stderr(piped) above? let mut stderr_reader = cava_process.stderr.take().unwrap(); let _stderr_thread = thread::spawn(move || { std::io::copy(&mut stderr_reader, &mut std::io::stderr()).unwrap() }); Ok(cava_process) } pub fn new(num_bars: u16) -> PatternResult { info!("Making config file"); let config_file = Self::write_cava_config_file(num_bars as usize * 2)?; info!("Starting cava"); let mut cava_process = Self::spawn_cava(&config_file)?; // TODO: * 2 because it's a u16 let final_ring = Arc::new(FinalRing::new(num_bars as usize * 2 * 2)); let f = final_ring.clone(); // Make a channel to trigger termination of cava let (terminate_tx, terminate_rx) = unbounded(); let _reader_thread = thread::spawn(move || { loop { // Start reading from cava // let f_clone = f.clone(); let reader = cava_process.stdout.take().unwrap(); let (reader_error_tx, reader_error_rx) = unbounded(); let f_clone = f.clone(); let _reader_thread = thread::spawn(move || { // The reader should never end so this will be an error let _ = f_clone.read_loop(reader); reader_error_tx.send(()).unwrap(); }); // Wait for either the reader to fail, cava to crash, or a termination request let should_terminate = select! { recv(terminate_rx) -> _ => { // Stop, because they want us to terminate true } recv(reader_error_rx) -> _ => { error!("Cava reader crashed. Will attempt a restart"); false } }; // We cannot kill a thread. Instead, kill the cava process and expect the threads to die info!("Terminating cava process"); if let Err(e) = cava_process.kill() { error!("Error trying to kill cava process: {e:?}"); } if should_terminate { // We need to stop. Don't attempt to start a new cava break; } else { // Wait a second before continuing std::thread::sleep(Duration::from_secs(1)); // Try spinning up a new cava instance cava_process = match Self::spawn_cava(&config_file) { Ok(c) => c, Err(e) => { error!("Could not spawn a new cava process: {e:?}"); break; } } } } }); Ok(Self { terminate_tx, // cava_process, final_ring, }) } pub fn terminate(self) { self.terminate_tx.send(()).unwrap(); } pub fn get_latest_reading(&self) -> Vec { self.final_ring .get_last_bytes() .array_chunks() .map(|ac| u16::from_le_bytes(*ac)) .collect() } fn write_cava_config_file(num_bars: usize) -> PatternResult { // TODO: Fix config file location let config_file = PathBuf::from("/tmp/config_file.cava"); let mut f = File::create(&config_file)?; f.write_all( format!( r#"## Configuration file for CAVA. Default values are commented out. Use either ';' or '#' for commenting. [general] # Smoothing mode. Can be 'normal', 'scientific' or 'waves'. DEPRECATED as of 0.6.0 ; mode = normal # Accepts only non-negative values. ; framerate = 60 # 'autosens' will attempt to decrease sensitivity if the bars peak. 1 = on, 0 = off # new as of 0.6.0 autosens of low values (dynamic range) # 'overshoot' allows bars to overshoot (in % of terminal height) without initiating autosens. DEPRECATED as of 0.6.0 ; autosens = 1 ; overshoot = 20 # Manual sensitivity in %. If autosens is enabled, this will only be the initial value. # 200 means double height. Accepts only non-negative values. ; sensitivity = 100 # The number of bars (0-200). 0 sets it to auto (fill up console). # Bars' width and space between bars in number of characters. bars = {num_bars} ; bar_width = 2 ; bar_spacing = 1 # bar_height is only used for output in "noritake" format ; bar_height = 32 # For SDL width and space between bars is in pixels, defaults are: ; bar_width = 20 ; bar_spacing = 5 # Lower and higher cutoff frequencies for lowest and highest bars # the bandwidth of the visualizer. # Note: there is a minimum total bandwidth of 43Mhz x number of bars. # Cava will automatically increase the higher cutoff if a too low band is specified. ; lower_cutoff_freq = 50 ; higher_cutoff_freq = 10000 # Seconds with no input before cava goes to sleep mode. Cava will not perform FFT or drawing and # only check for input once per second. Cava will wake up once input is detected. 0 = disable. ; sleep_timer = 0 [input] # Audio capturing method. Possible methods are: 'pulse', 'alsa', 'fifo', 'sndio' or 'shmem' # Defaults to 'pulse', 'alsa' or 'fifo', in that order, dependent on what support cava was built with. # # All input methods uses the same config variable 'source' # to define where it should get the audio. # # For pulseaudio 'source' will be the source. Default: 'auto', which uses the monitor source of the default sink # (all pulseaudio sinks(outputs) have 'monitor' sources(inputs) associated with them). # # For alsa 'source' will be the capture device. # For fifo 'source' will be the path to fifo-file. # For shmem 'source' will be /squeezelite-AA:BB:CC:DD:EE:FF where 'AA:BB:CC:DD:EE:FF' will be squeezelite's MAC address ; method = pulse ; source = auto ; method = alsa ; source = hw:Loopback,1 ; method = fifo ; source = /tmp/mpd.fifo ; sample_rate = 44100 ; sample_bits = 16 ; method = shmem ; source = /squeezelite-AA:BB:CC:DD:EE:FF ; method = portaudio ; source = auto [output] # Output method. Can be 'ncurses', 'noncurses', 'raw', 'noritake' or 'sdl'. # 'noncurses' uses a custom framebuffer technique and prints only changes # from frame to frame in the terminal. 'ncurses' is default if supported. # # 'raw' is an 8 or 16 bit (configurable via the 'bit_format' option) data # stream of the bar heights that can be used to send to other applications. # 'raw' defaults to 200 bars, which can be adjusted in the 'bars' option above. # # 'noritake' outputs a bitmap in the format expected by a Noritake VFD display # in graphic mode. It only support the 3000 series graphical VFDs for now. # # 'sdl' uses the Simple DirectMedia Layer to render in a graphical context. method = raw # Visual channels. Can be 'stereo' or 'mono'. # 'stereo' mirrors both channels with low frequencies in center. # 'mono' outputs left to right lowest to highest frequencies. # 'mono_option' set mono to either take input from 'left', 'right' or 'average'. # set 'reverse' to 1 to display frequencies the other way around. ; channels = stereo ; mono_option = average ; reverse = 0 # Raw output target. A fifo will be created if target does not exist. ; raw_target = /dev/stdout # Raw data format. Can be 'binary' or 'ascii'. ; data_format = binary # Binary bit format, can be '8bit' (0-255) or '16bit' (0-65530). ; bit_format = 16bit # Ascii max value. In 'ascii' mode range will run from 0 to value specified here ; ascii_max_range = 1000 # Ascii delimiters. In ascii format each bar and frame is separated by a delimiters. # Use decimal value in ascii table (i.e. 59 = ';' and 10 = '\n' (line feed)). ; bar_delimiter = 59 ; frame_delimiter = 10 # sdl window size and position. -1,-1 is centered. ; sdl_width = 1000 ; sdl_height = 500 ; sdl_x = -1 ; sdl_y= -1 [color] # Colors can be one of seven predefined: black, blue, cyan, green, magenta, red, white, yellow. # Or defined by hex code '#xxxxxx' (hex code must be within ''). User defined colors requires # ncurses output method and a terminal that can change color definitions such as Gnome-terminal or rxvt. # if supported, ncurses mode will be forced on if user defined colors are used. # default is to keep current terminal color ; background = default ; foreground = default # SDL only support hex code colors, these are the default: ; background = '#111111' ; foreground = '#33cccc' # Gradient mode, only hex defined colors (and thereby ncurses mode) are supported, # background must also be defined in hex or remain commented out. 1 = on, 0 = off. # You can define as many as 8 different colors. They range from bottom to top of screen ; gradient = 0 ; gradient_count = 8 ; gradient_color_1 = '#59cc33' ; gradient_color_2 = '#80cc33' ; gradient_color_3 = '#a6cc33' ; gradient_color_4 = '#cccc33' ; gradient_color_5 = '#cca633' ; gradient_color_6 = '#cc8033' ; gradient_color_7 = '#cc5933' ; gradient_color_8 = '#cc3333' [smoothing] # Percentage value for integral smoothing. Takes values from 0 - 100. # Higher values means smoother, but less precise. 0 to disable. # DEPRECATED as of 0.8.0, use noise_reduction instead ; integral = 77 # Disables or enables the so-called "Monstercat smoothing" with or without "waves". Set to 0 to disable. ; monstercat = 0 ; waves = 0 # Set gravity percentage for "drop off". Higher values means bars will drop faster. # Accepts only non-negative values. 50 means half gravity, 200 means double. Set to 0 to disable "drop off". # DEPRECATED as of 0.8.0, use noise_reduction instead ; gravity = 100 # In bar height, bars that would have been lower that this will not be drawn. # DEPRECATED as of 0.8.0 ; ignore = 0 # Noise reduction, float 0 - 1. default 0.77 # the raw visualization is very noisy, this factor adjusts the integral and gravity filters to keep the signal smooth # 1 will be very slow and smooth, 0 will be fast but noisy. ; noise_reduction = 0.77 [eq] # This one is tricky. You can have as much keys as you want. # Remember to uncomment more then one key! More keys = more precision. # Look at readme.md on github for further explanations and examples. # DEPRECATED as of 0.8.0 can be brought back by popular request, open issue at: # https://github.com/karlstav/cava ; 1 = 1 # bass ; 2 = 1 ; 3 = 1 # midtone ; 4 = 1 ; 5 = 1 # treble"# ) .as_bytes(), )?; f.flush()?; Ok(config_file) } }