Feat: rework audio sample library viewer
This commit is contained in:
@@ -12,6 +12,7 @@ mod spectrum;
|
||||
mod text_input;
|
||||
pub mod theme;
|
||||
mod vu_meter;
|
||||
mod waveform;
|
||||
|
||||
pub use active_patterns::{ActivePatterns, MuteStatus};
|
||||
pub use confirm::ConfirmModal;
|
||||
@@ -26,3 +27,4 @@ pub use sparkles::Sparkles;
|
||||
pub use spectrum::Spectrum;
|
||||
pub use text_input::TextInputModal;
|
||||
pub use vu_meter::VuMeter;
|
||||
pub use waveform::Waveform;
|
||||
|
||||
197
crates/ratatui/src/waveform.rs
Normal file
197
crates/ratatui/src/waveform.rs
Normal file
@@ -0,0 +1,197 @@
|
||||
use crate::scope::Orientation;
|
||||
use crate::theme;
|
||||
use ratatui::buffer::Buffer;
|
||||
use ratatui::layout::Rect;
|
||||
use ratatui::style::Color;
|
||||
use ratatui::widgets::Widget;
|
||||
use std::cell::RefCell;
|
||||
|
||||
thread_local! {
|
||||
static PATTERNS: RefCell<Vec<u8>> = const { RefCell::new(Vec::new()) };
|
||||
}
|
||||
|
||||
pub struct Waveform<'a> {
|
||||
data: &'a [f32],
|
||||
orientation: Orientation,
|
||||
color: Option<Color>,
|
||||
gain: f32,
|
||||
}
|
||||
|
||||
impl<'a> Waveform<'a> {
|
||||
pub fn new(data: &'a [f32]) -> Self {
|
||||
Self {
|
||||
data,
|
||||
orientation: Orientation::Horizontal,
|
||||
color: None,
|
||||
gain: 1.0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn orientation(mut self, o: Orientation) -> Self {
|
||||
self.orientation = o;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn color(mut self, c: Color) -> Self {
|
||||
self.color = Some(c);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn gain(mut self, g: f32) -> Self {
|
||||
self.gain = g;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for Waveform<'_> {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
if area.width == 0 || area.height == 0 || self.data.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let color = self.color.unwrap_or_else(|| theme::get().meter.low);
|
||||
|
||||
match self.orientation {
|
||||
Orientation::Horizontal => {
|
||||
render_horizontal(self.data, area, buf, color, self.gain)
|
||||
}
|
||||
Orientation::Vertical => render_vertical(self.data, area, buf, color, self.gain),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn braille_bit(dot_x: usize, dot_y: usize) -> u8 {
|
||||
match (dot_x, dot_y) {
|
||||
(0, 0) => 0x01,
|
||||
(0, 1) => 0x02,
|
||||
(0, 2) => 0x04,
|
||||
(0, 3) => 0x40,
|
||||
(1, 0) => 0x08,
|
||||
(1, 1) => 0x10,
|
||||
(1, 2) => 0x20,
|
||||
(1, 3) => 0x80,
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn render_horizontal(data: &[f32], area: Rect, buf: &mut Buffer, color: Color, gain: f32) {
|
||||
let width = area.width as usize;
|
||||
let height = area.height as usize;
|
||||
let fine_width = width * 2;
|
||||
let fine_height = height * 4;
|
||||
let len = data.len();
|
||||
|
||||
let peak = data.iter().map(|s| s.abs()).fold(0.0f32, f32::max);
|
||||
let auto_gain = if peak > 0.001 { gain / peak } else { gain };
|
||||
|
||||
PATTERNS.with(|p| {
|
||||
let mut patterns = p.borrow_mut();
|
||||
patterns.clear();
|
||||
patterns.resize(width * height, 0);
|
||||
|
||||
for fine_x in 0..fine_width {
|
||||
let start = fine_x * len / fine_width;
|
||||
let end = ((fine_x + 1) * len / fine_width).max(start + 1).min(len);
|
||||
let slice = &data[start..end];
|
||||
|
||||
let mut min_s = f32::MAX;
|
||||
let mut max_s = f32::MIN;
|
||||
for &s in slice {
|
||||
let s = (s * auto_gain).clamp(-1.0, 1.0);
|
||||
if s < min_s {
|
||||
min_s = s;
|
||||
}
|
||||
if s > max_s {
|
||||
max_s = s;
|
||||
}
|
||||
}
|
||||
|
||||
let fy_top = ((1.0 - max_s) * 0.5 * (fine_height - 1) as f32).round() as usize;
|
||||
let fy_bot = ((1.0 - min_s) * 0.5 * (fine_height - 1) as f32).round() as usize;
|
||||
let fy_top = fy_top.min(fine_height - 1);
|
||||
let fy_bot = fy_bot.min(fine_height - 1);
|
||||
|
||||
let char_x = fine_x / 2;
|
||||
let dot_x = fine_x % 2;
|
||||
|
||||
for fy in fy_top..=fy_bot {
|
||||
let char_y = fy / 4;
|
||||
let dot_y = fy % 4;
|
||||
patterns[char_y * width + char_x] |= braille_bit(dot_x, dot_y);
|
||||
}
|
||||
}
|
||||
|
||||
for cy in 0..height {
|
||||
for cx in 0..width {
|
||||
let pattern = patterns[cy * width + cx];
|
||||
if pattern != 0 {
|
||||
let ch = char::from_u32(0x2800 + pattern as u32).unwrap_or(' ');
|
||||
buf[(area.x + cx as u16, area.y + cy as u16)]
|
||||
.set_char(ch)
|
||||
.set_fg(color);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn render_vertical(data: &[f32], area: Rect, buf: &mut Buffer, color: Color, gain: f32) {
|
||||
let width = area.width as usize;
|
||||
let height = area.height as usize;
|
||||
let fine_width = width * 2;
|
||||
let fine_height = height * 4;
|
||||
let len = data.len();
|
||||
|
||||
let peak = data.iter().map(|s| s.abs()).fold(0.0f32, f32::max);
|
||||
let auto_gain = if peak > 0.001 { gain / peak } else { gain };
|
||||
|
||||
PATTERNS.with(|p| {
|
||||
let mut patterns = p.borrow_mut();
|
||||
patterns.clear();
|
||||
patterns.resize(width * height, 0);
|
||||
|
||||
for fine_y in 0..fine_height {
|
||||
let start = fine_y * len / fine_height;
|
||||
let end = ((fine_y + 1) * len / fine_height).max(start + 1).min(len);
|
||||
let slice = &data[start..end];
|
||||
|
||||
let mut min_s = f32::MAX;
|
||||
let mut max_s = f32::MIN;
|
||||
for &s in slice {
|
||||
let s = (s * auto_gain).clamp(-1.0, 1.0);
|
||||
if s < min_s {
|
||||
min_s = s;
|
||||
}
|
||||
if s > max_s {
|
||||
max_s = s;
|
||||
}
|
||||
}
|
||||
|
||||
let fx_left = ((min_s + 1.0) * 0.5 * (fine_width - 1) as f32).round() as usize;
|
||||
let fx_right = ((max_s + 1.0) * 0.5 * (fine_width - 1) as f32).round() as usize;
|
||||
let fx_left = fx_left.min(fine_width - 1);
|
||||
let fx_right = fx_right.min(fine_width - 1);
|
||||
|
||||
let char_y = fine_y / 4;
|
||||
let dot_y = fine_y % 4;
|
||||
|
||||
for fx in fx_left..=fx_right {
|
||||
let char_x = fx / 2;
|
||||
let dot_x = fx % 2;
|
||||
patterns[char_y * width + char_x] |= braille_bit(dot_x, dot_y);
|
||||
}
|
||||
}
|
||||
|
||||
for cy in 0..height {
|
||||
for cx in 0..width {
|
||||
let pattern = patterns[cy * width + cx];
|
||||
if pattern != 0 {
|
||||
let ch = char::from_u32(0x2800 + pattern as u32).unwrap_or(' ');
|
||||
buf[(area.x + cx as u16, area.y + cy as u16)]
|
||||
.set_char(ch)
|
||||
.set_fg(color);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user