| 1 | //! Screen 6: spectrum visualizer fed by MPD's fifo output. |
| 2 | |
| 3 | use std::sync::Arc; |
| 4 | |
| 5 | use ratatui::{Frame, layout::Rect, text::Line}; |
| 6 | use tokio::sync::watch; |
| 7 | |
| 8 | use super::{Ctx, Screen}; |
| 9 | use crate::{ |
| 10 | action::Action, |
| 11 | ui, |
| 12 | visualizer::{Bars, Spectrum}, |
| 13 | }; |
| 14 | |
| 15 | const EIGHTHS: [&str; 9] = [" ", "▁", "▂", "▃", "▄", "▅", "▆", "▇", "█"]; |
| 16 | |
| 17 | pub struct VisualizerScreen { |
| 18 | spectrum: watch::Receiver<Arc<Spectrum>>, |
| 19 | bars: Bars, |
| 20 | } |
| 21 | |
| 22 | impl VisualizerScreen { |
| 23 | pub fn new(spectrum: watch::Receiver<Arc<Spectrum>>) -> Self { |
| 24 | Self { |
| 25 | spectrum, |
| 26 | bars: Bars::default(), |
| 27 | } |
| 28 | } |
| 29 | } |
| 30 | |
| 31 | impl Screen for VisualizerScreen { |
| 32 | fn title(&self, _ctx: &Ctx) -> Line<'static> { |
| 33 | Line::from("Music visualizer") |
| 34 | } |
| 35 | |
| 36 | fn draw(&mut self, frame: &mut Frame, area: Rect, ctx: &Ctx) { |
| 37 | let theme = ctx.theme(); |
| 38 | let config = &ctx.config.visualizer; |
| 39 | let inner = area; |
| 40 | let spectrum = Arc::clone(&self.spectrum.borrow_and_update()); |
| 41 | if let Some(error) = &spectrum.error { |
| 42 | let fifo = config.fifo.display().to_string(); |
| 43 | ui::draw_placeholder( |
| 44 | frame, |
| 45 | inner, |
| 46 | &[ |
| 47 | error.as_str(), |
| 48 | "", |
| 49 | "Add a fifo output to mpd.conf and restart MPD:", |
| 50 | "", |
| 51 | "audio_output {", |
| 52 | " type \"fifo\"", |
| 53 | " name \"Visualizer feed\"", |
| 54 | &format!(" path \"{fifo}\""), |
| 55 | " format \"44100:16:2\"", |
| 56 | "}", |
| 57 | ], |
| 58 | theme, |
| 59 | ); |
| 60 | return; |
| 61 | } |
| 62 | if inner.width == 0 || inner.height == 0 { |
| 63 | return; |
| 64 | } |
| 65 | |
| 66 | let stride = config.bar_width + config.bar_gap; |
| 67 | let count = usize::from((inner.width + config.bar_gap) / stride); |
| 68 | let heights = self.bars.update( |
| 69 | &spectrum, |
| 70 | count, |
| 71 | config.min_frequency, |
| 72 | config.max_frequency, |
| 73 | config.falloff, |
| 74 | ); |
| 75 | let colors = &theme.visualizer; |
| 76 | let rows = inner.height; |
| 77 | // Center the bars horizontally. |
| 78 | let used = (count as u16 * stride).saturating_sub(config.bar_gap); |
| 79 | let left = inner.x + inner.width.saturating_sub(used) / 2; |
| 80 | let buffer = frame.buffer_mut(); |
| 81 | for (i, &height) in heights.iter().enumerate() { |
| 82 | let eighths = (height * f32::from(rows) * 8.0).round() as u32; |
| 83 | for row in 0..rows { |
| 84 | let filled = eighths.saturating_sub(u32::from(row) * 8).min(8) as usize; |
| 85 | if filled == 0 { |
| 86 | break; |
| 87 | } |
| 88 | let y = inner.bottom() - 1 - row; |
| 89 | let level = usize::from(row) * colors.len() / usize::from(rows); |
| 90 | let color = colors[level.min(colors.len() - 1)]; |
| 91 | for dx in 0..config.bar_width { |
| 92 | let x = left + i as u16 * stride + dx; |
| 93 | if x < inner.right() { |
| 94 | buffer[(x, y)].set_symbol(EIGHTHS[filled]).set_fg(color); |
| 95 | } |
| 96 | } |
| 97 | } |
| 98 | } |
| 99 | } |
| 100 | |
| 101 | fn handle_action(&mut self, _action: Action, _ctx: &mut Ctx) -> bool { |
| 102 | false |
| 103 | } |
| 104 | } |