From 186747431f0250d2838ab37fb505459d2e490c42 Mon Sep 17 00:00:00 2001 From: George Atkinson Date: Tue, 26 Dec 2023 15:12:10 +0000 Subject: [PATCH] Add basic waveform view and playback --- Cargo.toml | 4 + resources/themes/style.css | 2 + src/app_data.rs | 49 ------ src/engine/audio_data.rs | 66 +++++++ src/engine/audio_stream.rs | 60 +++++++ src/engine/mod.rs | 14 ++ src/engine/sample_player.rs | 196 +++++++++++++++++++++ src/engine/utils.rs | 21 +++ src/engine/waveform.rs | 95 ++++++++++ src/main.rs | 42 ++++- src/panels/browser.rs | 9 +- src/panels/mod.rs | 4 +- src/panels/wave.rs | 64 +++++++ src/panels/waveview.rs | 44 ----- src/state/app_data.rs | 129 ++++++++++++++ src/state/browser.rs | 2 +- src/state/mod.rs | 3 + src/views/mod.rs | 3 + src/views/waveview.rs | 139 +++++++++++++++ test_files/Drum Sounds/Hi-Hats/hat_00.wav | 0 test_files/Drum Sounds/Hi-Hats/hat_01.wav | 0 test_files/Drum Sounds/Hi-Hats/hat_02.wav | 0 test_files/Drum Sounds/Hi-Hats/hat_03.wav | 0 test_files/Drum Sounds/Kicks/kick_00.wav | 0 test_files/Drum Sounds/Kicks/kick_01.wav | 0 test_files/Drum Sounds/Kicks/kick_02.wav | 0 test_files/Drum Sounds/Snares/snare_00.wav | 0 test_files/Drum Sounds/Snares/snare_01.wav | 0 test_files/Drum Sounds/Snares/snare_02.wav | 0 test_files/Drum Sounds/Snares/snare_03.wav | 0 test_files/Drum Sounds/Snares/snare_04.wav | 0 test_files/Drum Sounds/Snares/snare_05.wav | 0 test_files/Drum Sounds/Toms/tom_00.wav | 0 test_files/Drum Sounds/Toms/tom_01.wav | 0 test_files/Drum Sounds/Toms/tom_02.wav | 0 test_files/Drum Sounds/Toms/tom_03.wav | 0 test_files/Drum Sounds/Toms/tom_04.wav | 0 test_files/Drum Sounds/Toms/tom_05.wav | 0 test_files/Drum Sounds/Toms/tom_06.wav | 0 test_files/Drum Sounds/Toms/tom_07.wav | 0 40 files changed, 840 insertions(+), 106 deletions(-) delete mode 100644 src/app_data.rs create mode 100644 src/engine/audio_data.rs create mode 100644 src/engine/audio_stream.rs create mode 100644 src/engine/mod.rs create mode 100644 src/engine/sample_player.rs create mode 100644 src/engine/utils.rs create mode 100644 src/engine/waveform.rs create mode 100644 src/panels/wave.rs delete mode 100644 src/panels/waveview.rs create mode 100644 src/state/app_data.rs create mode 100644 src/views/waveview.rs delete mode 100644 test_files/Drum Sounds/Hi-Hats/hat_00.wav delete mode 100644 test_files/Drum Sounds/Hi-Hats/hat_01.wav delete mode 100644 test_files/Drum Sounds/Hi-Hats/hat_02.wav delete mode 100644 test_files/Drum Sounds/Hi-Hats/hat_03.wav delete mode 100644 test_files/Drum Sounds/Kicks/kick_00.wav delete mode 100644 test_files/Drum Sounds/Kicks/kick_01.wav delete mode 100644 test_files/Drum Sounds/Kicks/kick_02.wav delete mode 100644 test_files/Drum Sounds/Snares/snare_00.wav delete mode 100644 test_files/Drum Sounds/Snares/snare_01.wav delete mode 100644 test_files/Drum Sounds/Snares/snare_02.wav delete mode 100644 test_files/Drum Sounds/Snares/snare_03.wav delete mode 100644 test_files/Drum Sounds/Snares/snare_04.wav delete mode 100644 test_files/Drum Sounds/Snares/snare_05.wav delete mode 100644 test_files/Drum Sounds/Toms/tom_00.wav delete mode 100644 test_files/Drum Sounds/Toms/tom_01.wav delete mode 100644 test_files/Drum Sounds/Toms/tom_02.wav delete mode 100644 test_files/Drum Sounds/Toms/tom_03.wav delete mode 100644 test_files/Drum Sounds/Toms/tom_04.wav delete mode 100644 test_files/Drum Sounds/Toms/tom_05.wav delete mode 100644 test_files/Drum Sounds/Toms/tom_06.wav delete mode 100644 test_files/Drum Sounds/Toms/tom_07.wav diff --git a/Cargo.toml b/Cargo.toml index 67ca151..56f7774 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,10 @@ sha2 = "0.10.7" base64ct = {version = "1.6.0", features = ["alloc"]} itertools = "0.11.0" fuzzy-matcher = "0.3.7" +hound = "3.5.1" +cpal = "0.15.2" +basedrop = { git = "https://github.com/glowcoil/basedrop.git" } +ringbuf = "0.3.3" [profile.dev.package."*"] opt-level = 3 diff --git a/resources/themes/style.css b/resources/themes/style.css index e2d4f62..c5b760d 100644 --- a/resources/themes/style.css +++ b/resources/themes/style.css @@ -341,6 +341,8 @@ wave-panel > .footer { wave-panel .waveform { background-color: #181818; border-radius: 4px; + child-top: 20px; + child-bottom: 20px; } .resize_handle { diff --git a/src/app_data.rs b/src/app_data.rs deleted file mode 100644 index 940b8ad..0000000 --- a/src/app_data.rs +++ /dev/null @@ -1,49 +0,0 @@ -use std::sync::{Arc, Mutex}; - -use vizia::prelude::*; - -use crate::{ - database::prelude::{AudioFile, CollectionID, Database, DatabaseAudioFileHandler}, - state::{ - browser::{BrowserState, Directory}, - TagsState, - }, -}; - -#[derive(Lens)] -pub struct AppData { - pub database: Arc>, - pub browser: BrowserState, - pub tags: TagsState, - pub browser_width: f32, - pub table_height: f32, - pub table_headers: Vec, - pub table_rows: Vec, - pub search_text: String, - pub selected_sample: Option, -} - -pub enum AppEvent { - SetBrowserWidth(f32), - SetTableHeight(f32), - ViewCollection(CollectionID), -} - -impl Model for AppData { - fn event(&mut self, cx: &mut EventContext, event: &mut Event) { - self.browser.event(cx, event); - self.tags.event(cx, event); - - event.map(|app_event, _| match app_event { - AppEvent::SetBrowserWidth(width) => self.browser_width = *width, - AppEvent::SetTableHeight(height) => self.table_height = *height, - AppEvent::ViewCollection(id) => { - if let Ok(db) = self.database.lock() { - if let Ok(audio_files) = db.get_child_audio_files(*id) { - self.table_rows = audio_files; - } - } - } - }); - } -} diff --git a/src/engine/audio_data.rs b/src/engine/audio_data.rs new file mode 100644 index 0000000..036655d --- /dev/null +++ b/src/engine/audio_data.rs @@ -0,0 +1,66 @@ +use super::utils::deinterleave; +use hound::{SampleFormat, WavReader}; + +/// An audio file, loaded into memory. +pub struct AudioData { + /// The sample data. + pub data: Vec, + /// Sample rate of the audio file. + pub sample_rate: f64, + /// number of channels in the audio file. + pub num_channels: usize, + /// number of samples in the audio file. + pub num_samples: usize, +} + +impl AudioData { + /// return a buffer of samples corresponding to a channel in the audio file + #[allow(dead_code)] + pub fn get_channel(&self, idx: usize) -> &'_ [f32] { + debug_assert!(idx < self.num_channels); + let start = self.num_samples * idx; + &self.data[start..(start + self.num_samples)] + } + + /// open a file + pub fn open(path: &str) -> Result { + let mut reader = WavReader::open(path)?; + let spec = reader.spec(); + let mut data = Vec::with_capacity((spec.channels as usize) * (reader.duration() as usize)); + match (spec.bits_per_sample, spec.sample_format) { + (16, SampleFormat::Int) => { + for sample in reader.samples::() { + data.push((sample? as f32) / (0x7fffi32 as f32)); + } + } + (24, SampleFormat::Int) => { + for sample in reader.samples::() { + let val = (sample? as f32) / (0x00ff_ffffi32 as f32); + data.push(val); + } + } + (32, SampleFormat::Int) => { + for sample in reader.samples::() { + data.push((sample? as f32) / (0x7fff_ffffi32 as f32)); + } + } + (32, SampleFormat::Float) => { + for sample in reader.samples::() { + data.push(sample?); + } + } + _ => return Err(hound::Error::Unsupported), + } + + let mut deinterleaved = vec![0.0; data.len()]; + let num_channels = spec.channels as usize; + let num_samples = deinterleaved.len() / num_channels; + deinterleave(&data, &mut deinterleaved, num_channels); + Ok(Self { + data: deinterleaved, + sample_rate: spec.sample_rate as f64, + num_channels, + num_samples, + }) + } +} diff --git a/src/engine/audio_stream.rs b/src/engine/audio_stream.rs new file mode 100644 index 0000000..6ca11a8 --- /dev/null +++ b/src/engine/audio_stream.rs @@ -0,0 +1,60 @@ +use crate::utils::interleave; +use cpal::traits::{DeviceTrait, HostTrait}; +use cpal::Stream; + +/// The playback context is used by the audio callback to map data from the audio +/// file to the playback buffer. +pub struct PlaybackContext<'a> { + pub buffer_size: usize, + pub sample_rate: f64, + pub num_channels: usize, + output_buffer: &'a mut [f32], +} + +impl<'a> PlaybackContext<'a> { + /// Return a buffer of output samples corresponding to a channel index + pub fn get_output(&mut self, idx: usize) -> &'_ mut [f32] { + let offset = idx * self.buffer_size; + &mut self.output_buffer[offset..offset + self.buffer_size] + } +} + +/// Start the audio stream +pub fn audio_stream(mut main_callback: impl FnMut(PlaybackContext) + Send + 'static) -> Stream { + let host = cpal::default_host(); + let output_device = host.default_output_device().expect("no output found"); + let config = output_device.default_output_config().expect("no default output config").config(); + + let sample_rate = config.sample_rate.0 as f64; + let num_channels = config.channels as usize; + let mut output_buffer = vec![]; + let mut input_buffer = vec![]; + + output_buffer.resize_with(1 << 16, || 0.0); + input_buffer.resize_with(1 << 16, || 0.0); + + let callback = move |data: &mut [f32], _: &cpal::OutputCallbackInfo| { + let buffer_size = data.len() / num_channels; + output_buffer.resize(data.len(), 0.0); + for sample in data.iter_mut() { + *sample = 0.0; + } + for sample in &mut output_buffer.iter_mut() { + *sample = 0.0; + } + + let context = PlaybackContext { + buffer_size, + num_channels, + sample_rate, + output_buffer: &mut output_buffer, + }; + + main_callback(context); + interleave(&output_buffer, data, num_channels); + }; + + output_device + .build_output_stream(&config, callback, |err| eprintln!("{}", err), None) + .expect("failed to open stream") +} diff --git a/src/engine/mod.rs b/src/engine/mod.rs new file mode 100644 index 0000000..237b4dd --- /dev/null +++ b/src/engine/mod.rs @@ -0,0 +1,14 @@ +pub mod audio_data; +pub use audio_data::*; + +pub mod utils; +pub use utils::*; + +pub mod audio_stream; +pub use audio_stream::*; + +pub mod sample_player; +pub use sample_player::*; + +pub mod waveform; +pub use waveform::*; diff --git a/src/engine/sample_player.rs b/src/engine/sample_player.rs new file mode 100644 index 0000000..0704388 --- /dev/null +++ b/src/engine/sample_player.rs @@ -0,0 +1,196 @@ +use super::audio_data::AudioData; +use super::audio_stream::PlaybackContext; +use basedrop::{Collector, Handle, Shared}; +use ringbuf::{HeapConsumer, HeapProducer, HeapRb}; +use std::sync::atomic::{AtomicUsize, Ordering}; +use std::sync::Arc; + +enum PlayerState { + Playing, + Stopped, +} + +enum PlayerAction { + Seek(f64), + Scrub(f64), + Play, + Stop, + SetActive(usize, bool), + NewFile(Shared), + Volume(f32), +} + +pub struct SamplePlayer { + file: Option>, + active: [bool; 32], + playhead: Arc, + state: PlayerState, + rx: HeapConsumer, + volume: f32, +} + +pub struct SamplePlayerController { + tx: HeapProducer, + playhead: Arc, + collector: Handle, + sample_rate: Option, + num_channels: Option, + num_samples: Option, + pub file: Option>, +} + +/// create a new sample player and its controller +pub fn sample_player(c: &Collector) -> (SamplePlayer, SamplePlayerController) { + let playhead = Arc::new(AtomicUsize::new(0)); + let (tx, rx) = HeapRb::new(2048).split(); + ( + SamplePlayer { + file: None, + active: [true; 32], + playhead: playhead.clone(), + state: PlayerState::Stopped, + rx, + volume: 1.0, + }, + SamplePlayerController { + tx, + playhead: playhead.clone(), + collector: c.handle(), + sample_rate: None, + num_channels: None, + num_samples: None, + file: None, + }, + ) +} + +impl SamplePlayer { + pub fn playhead(&self) -> usize { + self.playhead.load(Ordering::SeqCst) + } + + #[inline] + pub fn advance(&mut self, context: &mut PlaybackContext) { + while let Some(msg) = self.rx.pop() { + match msg { + PlayerAction::Seek(pos) => { + if let Some(f) = &self.file { + self.playhead.store( + ((f.sample_rate * pos) as usize).min(f.num_samples), + Ordering::SeqCst, + ); + } + } + PlayerAction::NewFile(file) => { + self.file = Some(file); + } + PlayerAction::Scrub(_) => { + //todo... + } + PlayerAction::SetActive(channel, active) => { + self.active[channel] = active; + } + PlayerAction::Play => self.state = PlayerState::Playing, + PlayerAction::Stop => self.state = PlayerState::Stopped, + PlayerAction::Volume(val) => self.volume = val, + } + } + + if let PlayerState::Stopped = self.state { + return; + } + + if let Some(file) = &self.file { + if self.playhead() >= file.num_samples { + self.state = PlayerState::Stopped; + return; + } + + for channel in 0..context.num_channels.max(file.num_channels) { + if !self.active[channel] { + continue; + } + let start = channel * file.num_samples + self.playhead().min(file.num_samples); + let end = channel * file.num_samples + + (self.playhead() + context.buffer_size).min(file.num_samples); + context.get_output(channel)[0..(end - start)] + .copy_from_slice(&file.data[start..end]); + context.get_output(channel)[0..(end - start)] + .iter_mut() + .for_each(|sample| *sample = *sample * self.volume); + } + + self.playhead.fetch_add(context.buffer_size, Ordering::SeqCst); + } + } +} + +#[allow(dead_code)] +impl SamplePlayerController { + pub fn sample_rate(&self) -> Option { + self.sample_rate + } + + pub fn duration_samples(&self) -> Option { + self.num_samples + } + + pub fn num_channels(&self) -> Option { + self.num_channels + } + + fn send_msg(&mut self, playeraction: PlayerAction) { + let mut e = self.tx.push(playeraction); + while let Err(playeraction) = e { + e = self.tx.push(playeraction); + } + } + + pub fn seek(&mut self, seconds: f64) { + self.send_msg(PlayerAction::Seek(seconds)); + } + + pub fn playhead(&self) -> usize { + self.playhead.load(Ordering::SeqCst) + } + + pub fn play(&mut self) { + self.send_msg(PlayerAction::Play); + } + + pub fn stop(&mut self) { + self.send_msg(PlayerAction::Stop); + } + + pub fn scrub(&mut self, seconds: f64) { + self.send_msg(PlayerAction::Scrub(seconds)); + } + + pub fn set_active(&mut self, channel_index: usize, active: bool) { + self.send_msg(PlayerAction::SetActive(channel_index, active)); + } + + pub fn volume(&mut self, val: f32) { + self.send_msg(PlayerAction::Volume(val)); + } + + pub fn load_file(&mut self, s: &str) { + let audio_file = + Shared::new(&self.collector, AudioData::open(s).expect("file does not exist")); + self.num_samples = Some(audio_file.num_samples); + self.num_channels = Some(audio_file.num_channels); + self.sample_rate = Some(audio_file.sample_rate); + self.file = Some(Shared::clone(&audio_file)); + self.send_msg(PlayerAction::NewFile(audio_file)); + } + + pub fn get_magnitude(&self, sample_idx: usize) -> f32 { + if let Some(file) = &self.file { + let ldx = sample_idx; + let rdx = sample_idx + file.num_samples; + (file.data[ldx].abs() + file.data[rdx].abs()) / 2.0 + } else { + 0.0 + } + } +} diff --git a/src/engine/utils.rs b/src/engine/utils.rs new file mode 100644 index 0000000..7447bb1 --- /dev/null +++ b/src/engine/utils.rs @@ -0,0 +1,21 @@ +/// Interleave a buffer of samples into an output buffer. +pub fn interleave(input: &[T], output: &mut [T], num_channels: usize) { + debug_assert_eq!(input.len(), output.len()); + let num_samples = input.len() / num_channels; + for sm in 0..num_samples { + for ch in 0..num_channels { + output[sm * num_channels + ch] = input[ch * num_samples + sm]; + } + } +} + +/// Deinterleave a buffer of samples into an output buffer +pub fn deinterleave(input: &[T], output: &mut [T], num_channels: usize) { + debug_assert_eq!(input.len(), output.len()); + let num_samples = input.len() / num_channels; + for sm in 0..num_samples { + for ch in 0..num_channels { + output[ch * num_samples + sm] = input[sm * num_channels + ch]; + } + } +} diff --git a/src/engine/waveform.rs b/src/engine/waveform.rs new file mode 100644 index 0000000..fd5f6e1 --- /dev/null +++ b/src/engine/waveform.rs @@ -0,0 +1,95 @@ +use std::cmp::Ordering; +use vizia::prelude::Data; + +pub fn to_u8(val: f32) -> u16 { + (((val + 1.0) / 2.0) * std::u16::MAX as f32) as u16 +} + +pub fn to_f32(val: u16) -> f32 { + ((val as f32 / std::u16::MAX as f32) * 2.0) - 1.0 +} + +pub const SAMPLES_PER_PIXEL: [usize; 9] = [4410, 1764, 882, 441, 147, 49, 21, 9, 3]; + +#[derive(Data, Clone, PartialEq)] +pub struct Waveform { + pub index: Vec, + pub data: Vec<(u16, u16, u16)>, +} + +impl Waveform { + pub fn new() -> Self { + Self { index: Vec::new(), data: Vec::new() } + } + + pub fn load(&mut self, audio: &[f32], num_of_pixels: usize) { + self.data.clear(); + self.index.clear(); + for level in 0..SAMPLES_PER_PIXEL.len() + 1 { + self.index.push(self.data.len()); + let samples_per_pixel = if level == SAMPLES_PER_PIXEL.len() { + audio.len() / num_of_pixels + } else { + SAMPLES_PER_PIXEL[level] + }; + + let chunks = audio.chunks(samples_per_pixel); + for chunk in chunks { + let v_min = *chunk + .iter() + .min_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::Equal)) + .unwrap(); + let v_max = *chunk + .iter() + .max_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::Equal)) + .unwrap(); + let v_mean: f32 = + (chunk.iter().map(|s| s * s).sum::() / chunk.len() as f32).sqrt(); + self.data.push((to_u8(v_min), to_u8(v_max), to_u8(v_mean))); + } + } + } + + pub fn set_num_pixels(&mut self, audio: &[f32], num_of_pixels: usize) { + if num_of_pixels > 0 { + if let Some(last) = self.index.last() { + let samples_per_pixel = audio.len() / num_of_pixels; + let chunks = audio.chunks(samples_per_pixel); + for (idx, chunk) in chunks.enumerate() { + let v_min = *chunk + .iter() + .min_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::Equal)) + .unwrap(); + let v_max = *chunk + .iter() + .max_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::Equal)) + .unwrap(); + let v_mean: f32 = + (chunk.iter().map(|s| s * s).sum::() / chunk.len() as f32).sqrt(); + if last + idx < self.data.len() { + self.data[last + idx] = (to_u8(v_min), to_u8(v_max), to_u8(v_mean)) + } else { + self.data.push((to_u8(v_min), to_u8(v_max), to_u8(v_mean))); + } + } + } + } + } + + pub fn get_data(&self, level: usize) -> Option<&[(u16, u16, u16)]> { + if !self.index.is_empty() { + let index = self.index[level]; + let next_index = if level < SAMPLES_PER_PIXEL.len() { + self.index[level + 1] + } else { + self.data.len() + }; + + //println!("level: {} index: {} next: {} {}", level, index, next_index, self.data[index..next_index-1].len()); + + return Some(&self.data[index..next_index - 1]); + } + + None + } +} diff --git a/src/main.rs b/src/main.rs index 277e217..badd502 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,8 @@ #![allow(unused)] // Disable stupid warnings for now use app_data::AppData; +use basedrop::Collector; +use cpal::traits::StreamTrait; use itertools::Itertools; use rusqlite::Connection; use std::{ @@ -27,13 +29,31 @@ use panels::*; mod views; use views::*; -mod app_data; -use app_data::*; +mod engine; +use engine::*; mod popup_menu; fn main() { - Application::new(|cx| { + // Initialize gc + let collector = Collector::new(); + + // Create the sample player and controller + let (mut player, mut controller) = sample_player(&collector); + + // Initialize state and begin the stream + std::thread::spawn(move || { + let stream = audio_stream(move |mut context| { + player.advance(&mut context); + }); + + // TODO - handle error + stream.play(); + + std::thread::park(); + }); + + Application::new(move |cx| { // Add resources cx.add_stylesheet(include_style!("resources/themes/style.css")) .expect("Failed to load stylesheet"); @@ -69,6 +89,7 @@ fn main() { let audio_files = db.get_all_audio_files().unwrap().len(); AppData { + // GUI State browser: BrowserState::new(root), tags: TagsState::default(), browser_width: 300.0, @@ -77,11 +98,24 @@ fn main() { table_rows: Vec::new(), search_text: String::new(), selected_sample: None, - // + + // Database database: Arc::new(Mutex::new(db)), + + // Audio Engine + collector, + controller, + + waveform: Waveform::new(), + zoom_level: 8, + start: 0, } .build(cx); + cx.emit(AppEvent::LoadSample(String::from( + "/Users/gatkinson/Rust/vizia-sample-browser/the-libre-sample-pack/drums/one shot/kicks/couch kick 1 @TeaBoi.wav", + ))); + HStack::new(cx, |cx| { ResizableStack::new( cx, diff --git a/src/panels/browser.rs b/src/panels/browser.rs index 60b8cc2..39bf5b8 100644 --- a/src/panels/browser.rs +++ b/src/panels/browser.rs @@ -33,15 +33,10 @@ impl BrowserPanel { // Panel Icon Icon::new(cx, ICON_FOLDER_OPEN).class("panel-icon"); - // SplitButton::new(cx, |cx| { - // Label::new(cx, "Sorting"); - // Label::new(cx, "Alphabetical Order"); - // Label::new(cx, "Number of Samples"); - // }); - // Search Toggle Button ToggleButton::new(cx, BrowserPanel::search_shown, |cx| Icon::new(cx, ICON_SEARCH)) .on_toggle(|cx| cx.emit(BrowserEvent::ToggleShowSearch)) + .name(Localized::new("toggle-search")) .tooltip(|cx| { Tooltip::new(cx, |cx| { Label::new(cx, Localized::new("toggle-search")); @@ -73,6 +68,7 @@ impl BrowserPanel { .on_toggle(|cx| cx.emit(BrowserEvent::ToggleSearchCaseSensitivity)) .size(Pixels(20.0)) .class("filter-search") + .name(Localized::new("match-case")) .tooltip(|cx| { Tooltip::new(cx, |cx| { Label::new(cx, Localized::new("match-case")); @@ -88,6 +84,7 @@ impl BrowserPanel { .on_toggle(|cx| cx.emit(BrowserEvent::ToggleSearchFilter)) .size(Pixels(20.0)) .class("filter-search") + .name(Localized::new("filter")) .tooltip(|cx| { Tooltip::new(cx, |cx| { Label::new(cx, Localized::new("filter")); diff --git a/src/panels/mod.rs b/src/panels/mod.rs index f0606ce..89882fc 100644 --- a/src/panels/mod.rs +++ b/src/panels/mod.rs @@ -7,5 +7,5 @@ pub use samples::*; pub mod tags; pub use tags::*; -pub mod waveview; -pub use waveview::*; +pub mod wave; +pub use wave::*; diff --git a/src/panels/wave.rs b/src/panels/wave.rs new file mode 100644 index 0000000..715cae7 --- /dev/null +++ b/src/panels/wave.rs @@ -0,0 +1,64 @@ +use vizia::prelude::*; + +use vizia::icons::{ + ICON_CHEVRON_DOWN, ICON_FILTER, ICON_FOLDER, ICON_FOLDER_FILLED, ICON_FOLDER_OPEN, + ICON_LETTER_CASE, ICON_LIST, ICON_LIST_TREE, ICON_PLAYER_PLAY, ICON_PLAYER_SKIP_BACK, + ICON_PLAYER_SKIP_FORWARD, ICON_PLAYER_STOP, ICON_SEARCH, ICON_TAG, ICON_WAVE_SINE, +}; + +use crate::app_data::AppData; +use crate::state::browser::{BrowserEvent, BrowserState}; +use crate::state::AppEvent; +use crate::views::Waveview; + +#[derive(Lens)] +pub struct WavePanel { + // Todo - move this + playing: bool, +} + +impl WavePanel { + pub fn new(cx: &mut Context) -> Handle { + Self { playing: false }.build(cx, |cx| { + // Header + HStack::new(cx, |cx| { + // Panel Icon + Icon::new(cx, ICON_WAVE_SINE).class("panel-icon"); + + Label::new(cx, "Sample Name").right(Stretch(1.0)); + + Chip::new(cx, "24 bit"); + Chip::new(cx, "44100 Hz"); + Chip::new(cx, "2 channels"); + }) + .class("header"); + + // Waveform + // ScrollView::new(cx, 0.0, 0.0, false, true, |cx| {}).class("waveform"); + HStack::new(cx, |cx| { + Waveview::new(cx, AppData::waveform, AppData::zoom_level, AppData::start); + }) + .class("waveform"); + + // Footer + HStack::new(cx, |cx| { + // toolbar here + ButtonGroup::new(cx, |cx| { + Button::new(cx, |cx| Icon::new(cx, ICON_PLAYER_SKIP_BACK)); + Button::new(cx, |cx| Icon::new(cx, ICON_PLAYER_PLAY)) + .on_press(|cx| cx.emit(AppEvent::Play)); + Button::new(cx, |cx| Icon::new(cx, ICON_PLAYER_STOP)) + .on_press(|cx| cx.emit(AppEvent::Stop)); + Button::new(cx, |cx| Icon::new(cx, ICON_PLAYER_SKIP_FORWARD)); + }); + }) + .class("footer"); + }) + } +} + +impl View for WavePanel { + fn element(&self) -> Option<&'static str> { + Some("wave-panel") + } +} diff --git a/src/panels/waveview.rs b/src/panels/waveview.rs deleted file mode 100644 index 837a077..0000000 --- a/src/panels/waveview.rs +++ /dev/null @@ -1,44 +0,0 @@ -use vizia::prelude::*; - -use vizia::icons::{ - ICON_CHEVRON_DOWN, ICON_FILTER, ICON_FOLDER, ICON_FOLDER_FILLED, ICON_FOLDER_OPEN, - ICON_LETTER_CASE, ICON_LIST, ICON_LIST_TREE, ICON_SEARCH, ICON_TAG, ICON_WAVE_SINE, -}; - -use crate::app_data::AppData; -use crate::state::browser::{BrowserEvent, BrowserState}; - -#[derive(Lens)] -pub struct WavePanel {} - -impl WavePanel { - pub fn new(cx: &mut Context) -> Handle { - Self {}.build(cx, |cx| { - // Header - HStack::new(cx, |cx| { - // Panel Icon - Icon::new(cx, ICON_WAVE_SINE).class("panel-icon"); - - Label::new(cx, "Sample Name"); - }) - .class("header"); - - // Waveform - ScrollView::new(cx, 0.0, 0.0, false, true, |cx| {}).class("waveform"); - - // Footer - HStack::new(cx, |cx| { - Label::new(cx, "24 bit"); - Label::new(cx, "44100 Hz"); - Label::new(cx, "2 channels"); - }) - .class("footer"); - }) - } -} - -impl View for WavePanel { - fn element(&self) -> Option<&'static str> { - Some("wave-panel") - } -} diff --git a/src/state/app_data.rs b/src/state/app_data.rs new file mode 100644 index 0000000..d9ca345 --- /dev/null +++ b/src/state/app_data.rs @@ -0,0 +1,129 @@ +use std::sync::{Arc, Mutex}; + +use basedrop::Collector; +use vizia::prelude::*; + +use crate::{ + database::prelude::{AudioFile, CollectionID, Database, DatabaseAudioFileHandler}, + engine::{SamplePlayerController, Waveform}, + state::{ + browser::{BrowserState, Directory}, + TagsState, + }, +}; + +#[derive(Debug, Clone, PartialEq)] +pub enum ChannelMode { + Left, + Right, + Both, +} + +#[derive(Debug, Clone, PartialEq)] +pub enum UnitsMode { + Linear, + Decibel, +} + +/// The state of the playhead. +#[derive(Debug, Clone, PartialEq)] +pub enum PlayState { + Playing, + Paused, + Stopped, +} + +/// Whether the zoom should be focused at the playback cursor or the mouse. +#[derive(Debug, Clone, PartialEq)] +pub enum ZoomMode { + Cursor, + Mouse, +} + +#[derive(Lens)] +pub struct AppData { + // GUI State + pub browser: BrowserState, + pub tags: TagsState, + pub browser_width: f32, + pub table_height: f32, + pub table_headers: Vec, + pub table_rows: Vec, + pub search_text: String, + pub selected_sample: Option, + + // Database + #[lens(ignore)] + pub database: Arc>, + + // Audio Engine + #[lens(ignore)] + pub collector: Collector, + #[lens(ignore)] + pub controller: SamplePlayerController, + + // Audio GUI State + pub waveform: Waveform, + pub zoom_level: usize, + pub start: usize, +} + +pub enum AppEvent { + SetBrowserWidth(f32), + SetTableHeight(f32), + ViewCollection(CollectionID), + + // Audio Control Events + LoadSample(String), + Play, + Pause, + Stop, + // SeekLeft, + // SeekRight, +} + +impl Model for AppData { + fn event(&mut self, cx: &mut EventContext, event: &mut Event) { + self.browser.event(cx, event); + self.tags.event(cx, event); + + event.map(|app_event, _| match app_event { + AppEvent::SetBrowserWidth(width) => self.browser_width = *width, + AppEvent::SetTableHeight(height) => self.table_height = *height, + AppEvent::ViewCollection(id) => { + if let Ok(db) = self.database.lock() { + if let Ok(audio_files) = db.get_child_audio_files(*id) { + self.table_rows = audio_files; + } + } + } + + AppEvent::LoadSample(path) => { + self.controller.load_file(path); + + if let Some(file) = self.controller.file.as_ref() { + // self.num_of_channels = file.num_channels; + // self.sample_rate = file.sample_rate; + // self.num_of_samples = file.num_samples; + // println!("Length: {} ", self.num_of_samples); + + self.waveform.load(&file.data[0..file.num_samples], 800); + } + } + + AppEvent::Play => { + self.controller.seek(0.0); + self.controller.play(); + } + + AppEvent::Pause => { + self.controller.stop(); + } + + AppEvent::Stop => { + self.controller.stop(); + self.controller.seek(0.0); + } + }); + } +} diff --git a/src/state/browser.rs b/src/state/browser.rs index 3abc65c..97938da 100644 --- a/src/state/browser.rs +++ b/src/state/browser.rs @@ -1,6 +1,6 @@ //! GUI state used for the browser panel -use crate::app_data::{AppData, AppEvent}; +use super::app_data::{AppData, AppEvent}; use crate::database::prelude::*; use fuzzy_matcher::skim::SkimMatcherV2; use fuzzy_matcher::FuzzyMatcher; diff --git a/src/state/mod.rs b/src/state/mod.rs index f7d1646..5bed36d 100644 --- a/src/state/mod.rs +++ b/src/state/mod.rs @@ -1,3 +1,6 @@ +pub mod app_data; +pub use app_data::*; + pub mod browser; pub use browser::*; diff --git a/src/views/mod.rs b/src/views/mod.rs index 9a1c0f1..88219ad 100644 --- a/src/views/mod.rs +++ b/src/views/mod.rs @@ -3,3 +3,6 @@ pub use resizable_stack::*; pub mod smart_table; pub use smart_table::*; + +pub mod waveview; +pub use waveview::*; diff --git a/src/views/waveview.rs b/src/views/waveview.rs new file mode 100644 index 0000000..55c961e --- /dev/null +++ b/src/views/waveview.rs @@ -0,0 +1,139 @@ +use vizia::prelude::*; +use vizia::vg; + +use crate::app_data::AppEvent; +use crate::app_data::UnitsMode; +use crate::app_data::ZoomMode; +use crate::waveform::to_f32; +use crate::waveform::Waveform; +use crate::waveform::SAMPLES_PER_PIXEL; + +pub struct Waveview, L2: Lens, L3: Lens> +{ + waveform_lens: L1, + zoom_level_lens: L2, + start_lens: L3, + units_mode: UnitsMode, +} + +impl Waveview +where + L1: Lens, + L2: Lens, + L3: Lens, +{ + pub fn new( + cx: &mut Context, + waveform_lens: L1, + zoom_level_lens: L2, + start_lens: L3, + ) -> Handle { + Self { waveform_lens, zoom_level_lens, start_lens, units_mode: UnitsMode::Decibel } + .build(cx, |cx| {}) + } +} + +impl View for Waveview +where + L1: Lens, + L2: Lens, + L3: Lens, +{ + fn element(&self) -> Option<&'static str> { + Some("waveview") + } + + fn event(&mut self, cx: &mut EventContext, event: &mut Event) { + event.map(|window_event, _| match window_event { + WindowEvent::MouseScroll(x, y) => { + // println!("scroll {} {}", x, y); + // if *y > 0.0 { + // if cx.modifiers().contains(Modifiers::CTRL) { + // // Zoom In + // cx.emit(AppEvent::ZoomIn); + // } else { + // } + // } + // cx.emit(AppEvent::Pan(*x)); + } + + _ => {} + }); + } + + fn draw(&self, cx: &mut DrawContext, canvas: &mut Canvas) { + if let Some(waveform) = self.waveform_lens.get_ref(cx) { + let bounds = cx.bounds(); + + let mut path1 = vg::Path::new(); + let mut path2 = vg::Path::new(); + + path1.move_to(bounds.x, bounds.center().1); + path2.move_to(bounds.x, bounds.center().1); + + let x = bounds.x; + let w = bounds.w; + let y = bounds.y; + let h = bounds.h; + + let zoom_level = self.zoom_level_lens.get(cx); + + let start = self.start_lens.get(cx); + + if let Some(waveform_data) = waveform.get_data(zoom_level) { + for pixel in 0..w as usize { + if start + pixel >= waveform_data.len() { + break; + } + + let v_min = to_f32(waveform_data[start + pixel].0); + let v_max = to_f32(waveform_data[start + pixel].1); + let v_mean = to_f32(waveform_data[start + pixel].2); + + match self.units_mode { + UnitsMode::Decibel => { + let v_min_db = 1.0 + (20.0 * v_min.abs().log10()).max(-60.0) / 60.0; + let v_max_db = 1.0 + (20.0 * v_max.abs().log10()).max(-60.0) / 60.0; + + let v_mean_db = 1.0 + (20.0 * v_mean.abs().log10()).max(-60.0) / 60.0; + + let v_min_db = if v_min < 0.0 { -v_min_db } else { v_min_db }; + + let v_max_db = if v_max < 0.0 { -v_max_db } else { v_max_db }; + + let v_mean_db = if v_mean < 0.0 { -v_mean_db } else { v_mean_db }; + + path1.line_to(x + (pixel as f32), y + h / 2.0 - v_min_db * h / 2.0); + path1.line_to(x + (pixel as f32), y + h / 2.0 - v_max_db * h / 2.0); + + path2.move_to(x + (pixel as f32), y + h / 2.0 + v_mean_db * h / 2.0); + path2.line_to(x + (pixel as f32), y + h / 2.0 - v_mean_db * h / 2.0); + } + + UnitsMode::Linear => { + path1.line_to(x + (pixel as f32), y + h / 2.0 - v_min * h / 2.0); + path1.line_to(x + (pixel as f32), y + h / 2.0 - v_max * h / 2.0); + + path2.move_to(x + (pixel as f32), y + h / 2.0 + v_mean * h / 2.0); + path2.line_to(x + (pixel as f32), y + h / 2.0 - v_mean * h / 2.0); + } + } + } + + // Draw min/max paths + let mut paint = vg::Paint::color(vg::Color::rgba(50, 50, 255, 255)); + paint.set_line_width(1.0); + paint.set_anti_alias(false); + canvas.stroke_path(&mut path1, &paint); + + // Draw rms paths + if zoom_level < 5 { + let mut paint = vg::Paint::color(vg::Color::rgba(80, 80, 255, 255)); + paint.set_line_width(1.0); + paint.set_anti_alias(false); + canvas.stroke_path(&mut path2, &paint); + } + } + } + } +} diff --git a/test_files/Drum Sounds/Hi-Hats/hat_00.wav b/test_files/Drum Sounds/Hi-Hats/hat_00.wav deleted file mode 100644 index e69de29..0000000 diff --git a/test_files/Drum Sounds/Hi-Hats/hat_01.wav b/test_files/Drum Sounds/Hi-Hats/hat_01.wav deleted file mode 100644 index e69de29..0000000 diff --git a/test_files/Drum Sounds/Hi-Hats/hat_02.wav b/test_files/Drum Sounds/Hi-Hats/hat_02.wav deleted file mode 100644 index e69de29..0000000 diff --git a/test_files/Drum Sounds/Hi-Hats/hat_03.wav b/test_files/Drum Sounds/Hi-Hats/hat_03.wav deleted file mode 100644 index e69de29..0000000 diff --git a/test_files/Drum Sounds/Kicks/kick_00.wav b/test_files/Drum Sounds/Kicks/kick_00.wav deleted file mode 100644 index e69de29..0000000 diff --git a/test_files/Drum Sounds/Kicks/kick_01.wav b/test_files/Drum Sounds/Kicks/kick_01.wav deleted file mode 100644 index e69de29..0000000 diff --git a/test_files/Drum Sounds/Kicks/kick_02.wav b/test_files/Drum Sounds/Kicks/kick_02.wav deleted file mode 100644 index e69de29..0000000 diff --git a/test_files/Drum Sounds/Snares/snare_00.wav b/test_files/Drum Sounds/Snares/snare_00.wav deleted file mode 100644 index e69de29..0000000 diff --git a/test_files/Drum Sounds/Snares/snare_01.wav b/test_files/Drum Sounds/Snares/snare_01.wav deleted file mode 100644 index e69de29..0000000 diff --git a/test_files/Drum Sounds/Snares/snare_02.wav b/test_files/Drum Sounds/Snares/snare_02.wav deleted file mode 100644 index e69de29..0000000 diff --git a/test_files/Drum Sounds/Snares/snare_03.wav b/test_files/Drum Sounds/Snares/snare_03.wav deleted file mode 100644 index e69de29..0000000 diff --git a/test_files/Drum Sounds/Snares/snare_04.wav b/test_files/Drum Sounds/Snares/snare_04.wav deleted file mode 100644 index e69de29..0000000 diff --git a/test_files/Drum Sounds/Snares/snare_05.wav b/test_files/Drum Sounds/Snares/snare_05.wav deleted file mode 100644 index e69de29..0000000 diff --git a/test_files/Drum Sounds/Toms/tom_00.wav b/test_files/Drum Sounds/Toms/tom_00.wav deleted file mode 100644 index e69de29..0000000 diff --git a/test_files/Drum Sounds/Toms/tom_01.wav b/test_files/Drum Sounds/Toms/tom_01.wav deleted file mode 100644 index e69de29..0000000 diff --git a/test_files/Drum Sounds/Toms/tom_02.wav b/test_files/Drum Sounds/Toms/tom_02.wav deleted file mode 100644 index e69de29..0000000 diff --git a/test_files/Drum Sounds/Toms/tom_03.wav b/test_files/Drum Sounds/Toms/tom_03.wav deleted file mode 100644 index e69de29..0000000 diff --git a/test_files/Drum Sounds/Toms/tom_04.wav b/test_files/Drum Sounds/Toms/tom_04.wav deleted file mode 100644 index e69de29..0000000 diff --git a/test_files/Drum Sounds/Toms/tom_05.wav b/test_files/Drum Sounds/Toms/tom_05.wav deleted file mode 100644 index e69de29..0000000 diff --git a/test_files/Drum Sounds/Toms/tom_06.wav b/test_files/Drum Sounds/Toms/tom_06.wav deleted file mode 100644 index e69de29..0000000 diff --git a/test_files/Drum Sounds/Toms/tom_07.wav b/test_files/Drum Sounds/Toms/tom_07.wav deleted file mode 100644 index e69de29..0000000