├── src ├── render │ ├── widgets │ │ ├── mod.rs │ │ └── image.rs │ ├── ascii.rs │ ├── mod.rs │ ├── renderer.rs │ ├── help.rs │ ├── spectral.rs │ ├── headers.rs │ ├── waveform.rs │ └── metadata.rs ├── dsp │ ├── mod.rs │ ├── normalization.rs │ ├── data.rs │ ├── waveform.rs │ ├── spectrogram.rs │ └── time_window.rs ├── utils │ ├── filled_rectangle.rs │ ├── event.rs │ ├── bindings.rs │ ├── mod.rs │ └── zoom.rs └── main.rs ├── .gitignore ├── .gitattributes ├── tests └── files │ └── rock_1s.wav ├── .github ├── images │ └── audeye_0_2_0.gif └── workflows │ ├── pre-releases.yml │ └── releases.yml ├── .pre-commit-config.yaml ├── LICENSE ├── Cargo.toml └── README.md /src/render/widgets/mod.rs: -------------------------------------------------------------------------------- 1 | mod image; 2 | 3 | pub use image::*; 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | *.wav 3 | *.aif* 4 | venv 5 | .vscode 6 | Cargo.lock 7 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.wav filter=lfs diff=lfs merge=lfs -text 2 | *.gif filter=lfs diff=lfs merge=lfs -text 3 | -------------------------------------------------------------------------------- /tests/files/rock_1s.wav: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:3e81a72cab75094b3e904a88f7e9af298310a84a529995cef61df40c330b98ab 3 | size 268428 4 | -------------------------------------------------------------------------------- /.github/images/audeye_0_2_0.gif: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:939f09af6c347ef6965c3f487cc79cda1792ae20cfb785fa8fc731cf67455482 3 | size 9240649 4 | -------------------------------------------------------------------------------- /src/dsp/mod.rs: -------------------------------------------------------------------------------- 1 | mod data; 2 | mod normalization; 3 | mod spectrogram; 4 | mod time_window; 5 | mod waveform; 6 | 7 | pub use data::{AsyncDspData, AsyncDspDataState, DspData, DspErr}; 8 | pub use spectrogram::{Spectrogram, SpectrogramParameters}; 9 | pub use time_window::{SidePaddingType, WindowType, PADDING_HELP_TEXT}; 10 | pub use waveform::{Waveform, WaveformParameters, WaveformPoint}; 11 | -------------------------------------------------------------------------------- /src/dsp/normalization.rs: -------------------------------------------------------------------------------- 1 | use crate::sndfile::SndFile; 2 | use rayon::prelude::*; 3 | use sndfile::SndFileIO; 4 | 5 | #[inline(always)] 6 | fn clamp(val: &i32) -> i32 { 7 | if *val == i32::MIN { 8 | i32::MIN + 1i32 9 | } else { 10 | *val 11 | } 12 | } 13 | 14 | pub fn compute_norm(sndfile: &mut SndFile) -> f64 { 15 | let data: Vec = sndfile.read_all_to_vec().unwrap(); 16 | 17 | let max = data.par_iter().map(|val| clamp(val).abs()).max().unwrap(); 18 | 19 | if max <= 0i32 { 20 | return f64::EPSILON; 21 | } 22 | max as f64 / i32::MAX as f64 23 | } 24 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: local 3 | hooks: 4 | - id: cargo-check 5 | name: cargo check 6 | description: Check the package for errors. 7 | entry: cargo check 8 | language: system 9 | types: [rust] 10 | pass_filenames: false 11 | - id: clippy 12 | name: clippy 13 | description: Lint rust sources 14 | entry: cargo clippy 15 | language: system 16 | args: ["--", "-D", "warnings"] 17 | types: [rust] 18 | pass_filenames: false 19 | - id: fmt 20 | name: fmt 21 | description: Check files formating with cargo fmt. 22 | entry: cargo fmt 23 | language: system 24 | types: [rust] 25 | args: ["--", "--check"] -------------------------------------------------------------------------------- /src/render/ascii.rs: -------------------------------------------------------------------------------- 1 | pub struct AsciiArtConverter { 2 | ascii_chars: [u8; 10], 3 | } 4 | 5 | impl Default for AsciiArtConverter { 6 | fn default() -> Self { 7 | // let test = "$@B%8&WM#*oahkbdpqwmZO0QLCJUYXzcvunxrjft\\/\\|()1{}[]?-_+~<>i!lI;:,\"^`'. "; 8 | // " .:-=+*#%@" 9 | AsciiArtConverter { 10 | ascii_chars: [ 11 | ' ' as u8, '.' as u8, ':' as u8, '-' as u8, '=' as u8, '+' as u8, '*' as u8, 12 | '#' as u8, '%' as u8, '@' as u8, 13 | ], 14 | } 15 | } 16 | } 17 | 18 | impl AsciiArtConverter { 19 | #[inline(always)] 20 | pub fn convert_u8_to_ascii(&self, value: u8) -> u8 { 21 | let idx = value / (u8::MAX / 10); 22 | return self.ascii_chars[idx as usize]; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/utils/filled_rectangle.rs: -------------------------------------------------------------------------------- 1 | use tui::{ 2 | style::Color, 3 | widgets::canvas::{Painter, Shape}, 4 | }; 5 | 6 | /// Shape to draw a rectangle from a `Rect` with the given color 7 | #[derive(Debug, Clone)] 8 | pub struct FilledRectangle { 9 | pub x: f64, 10 | pub y: f64, 11 | pub width: f64, 12 | pub height: f64, 13 | pub color: Color, 14 | } 15 | 16 | impl Shape for FilledRectangle { 17 | fn draw(&self, painter: &mut Painter) { 18 | let (b_x, b_y) = match painter.get_point(self.x, self.y) { 19 | Some(c) => c, 20 | None => return, 21 | }; 22 | let (e_x, e_y) = match painter.get_point(self.x + self.width, self.y - self.height) { 23 | Some(c) => c, 24 | None => return, 25 | }; 26 | 27 | for x in b_x..e_x { 28 | for y in b_y..e_y { 29 | painter.paint(x, y, self.color); 30 | } 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2022 Maxime COUTANT 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "audeye" 3 | version = "0.2.1" 4 | edition = "2018" 5 | description = "A terminal user interface for audiofile content visualization" 6 | homepage = "https://github.com/maxmarsc/audeye" 7 | documentation = "https://github.com/maxmarsc/audeye" 8 | repository = "https://github.com/maxmarsc/audeye" 9 | keywords = ["audiofile", "tui", "spectrogram", "waveform", "visualization"] 10 | categories = ["command-line-utilities"] 11 | authors = ["Maxime COUTANT "] 12 | license = "MIT" 13 | 14 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 15 | 16 | [dependencies] 17 | structopt = { version = "0.3", default-features = false } 18 | terminal_size = { version = "0.1.17"} 19 | sndfile = "0.1.1" 20 | rayon = "1.5.1" 21 | tui = { version = "0.19", default-features=true, features=["crossterm"]} 22 | crossterm = "0.25" 23 | termion = "1.5" 24 | realfft = "3.0.0" 25 | rustfft = "6.0.1" 26 | rand = "0.8" 27 | argh = "0.1" 28 | fast_image_resize = "0.7.0" 29 | apodize = "1.0.0" 30 | audiotags = "0.4" 31 | num-traits = "0.2.14" 32 | num-integer = "0.1.45" 33 | colorgrad = "0.6.1" 34 | 35 | [dev-dependencies] 36 | assert_cmd = "2.0.4" 37 | predicates = "2.1.1" -------------------------------------------------------------------------------- /src/render/widgets/image.rs: -------------------------------------------------------------------------------- 1 | use tui::buffer::Buffer; 2 | use tui::layout::Rect; 3 | use tui::style::Color; 4 | use tui::widgets::{Block, Widget}; 5 | 6 | pub struct Image<'a> { 7 | block: Option>, 8 | img_buffer: &'a [u8], 9 | } 10 | 11 | impl<'a> Image<'a> { 12 | pub fn new(img_buffer: &'a [u8]) -> Image<'a> { 13 | Image { 14 | block: None, 15 | img_buffer, 16 | } 17 | } 18 | 19 | pub fn block(mut self, block: Block<'a>) -> Image<'a> { 20 | self.block = Some(block); 21 | self 22 | } 23 | } 24 | 25 | impl<'a> Widget for Image<'a> { 26 | fn render(mut self, area: Rect, buf: &mut Buffer) { 27 | let img_area = match self.block.take() { 28 | Some(b) => { 29 | let inner_area = b.inner(area); 30 | b.render(area, buf); 31 | inner_area 32 | } 33 | None => area, 34 | }; 35 | 36 | self.img_buffer 37 | .chunks(6) 38 | .map(|pixels| { 39 | [ 40 | Color::Rgb(pixels[0], pixels[1], pixels[2]), 41 | Color::Rgb(pixels[3], pixels[4], pixels[5]), 42 | ] 43 | }) 44 | .enumerate() 45 | .for_each(|(idx, colors)| { 46 | let x_char = idx as u16 / img_area.height; 47 | let y_char = img_area.height - (idx as u16 % img_area.height) - 1; 48 | 49 | buf.get_mut(x_char + img_area.left(), y_char + img_area.top()) 50 | .set_char('▄') 51 | .set_bg(colors[0]) 52 | .set_fg(colors[1]); 53 | }); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/render/mod.rs: -------------------------------------------------------------------------------- 1 | mod renderer; 2 | mod spectral; 3 | mod waveform; 4 | // mod ascii; 5 | mod headers; 6 | mod help; 7 | mod metadata; 8 | mod widgets; 9 | 10 | pub use headers::ChannelsTabs; 11 | pub use help::HelperPopup; 12 | pub use metadata::MetadataRenderer; 13 | pub use renderer::{Renderer, RenderingInfo}; 14 | pub use spectral::SpectralRenderer; 15 | use tui::{backend::Backend, layout::Rect, Frame}; 16 | pub use waveform::WaveformRenderer; 17 | 18 | use renderer::draw_text_info; 19 | 20 | pub enum RendererType<'a> { 21 | Waveform(WaveformRenderer), 22 | Spectral(SpectralRenderer<'a>), 23 | Metadata(Box), 24 | } 25 | 26 | impl Renderer for RendererType<'_> { 27 | fn draw(&mut self, frame: &mut Frame<'_, B>, info: &RenderingInfo, area: Rect) { 28 | match self { 29 | RendererType::Waveform(renderer) => renderer.draw(frame, info, area), 30 | RendererType::Spectral(renderer) => renderer.draw(frame, info, area), 31 | RendererType::Metadata(renderer) => renderer.draw(frame, info, area), 32 | } 33 | } 34 | 35 | fn needs_redraw(&mut self) -> bool { 36 | match self { 37 | RendererType::Waveform(renderer) => renderer.needs_redraw(), 38 | RendererType::Spectral(renderer) => renderer.needs_redraw(), 39 | RendererType::Metadata(renderer) => renderer.needs_redraw(), 40 | } 41 | } 42 | 43 | fn max_width_resolution(&self) -> usize { 44 | match self { 45 | RendererType::Waveform(renderer) => renderer.max_width_resolution(), 46 | RendererType::Spectral(renderer) => renderer.max_width_resolution(), 47 | RendererType::Metadata(renderer) => renderer.max_width_resolution(), 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/utils/event.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | use std::sync::mpsc; 3 | use std::thread; 4 | use std::time::Duration; 5 | 6 | use termion::event::Key; 7 | use termion::input::TermRead; 8 | 9 | pub enum Event { 10 | Input(I), 11 | Tick, 12 | } 13 | 14 | /// A small event handler that wrap termion input and tick events. Each event 15 | /// type is handled in its own thread and returned to a common `Receiver` 16 | pub struct Events { 17 | rx: mpsc::Receiver>, 18 | } 19 | 20 | #[derive(Debug, Clone, Copy)] 21 | pub struct Config { 22 | pub tick_rate: Duration, 23 | } 24 | 25 | impl Default for Config { 26 | fn default() -> Config { 27 | Config { 28 | tick_rate: Duration::from_millis(250), 29 | } 30 | } 31 | } 32 | 33 | impl Events { 34 | // pub fn new() -> Events { 35 | // Events::with_config(Config::default()) 36 | // } 37 | 38 | pub fn with_config(config: Config) -> Events { 39 | let (tx, rx) = mpsc::channel(); 40 | let _ = { 41 | let tx = tx.clone(); 42 | thread::spawn(move || { 43 | let stdin = io::stdin(); 44 | for key in stdin.keys().flatten() { 45 | if let Err(err) = tx.send(Event::Input(key)) { 46 | eprintln!("{}", err); 47 | return; 48 | } 49 | } 50 | }) 51 | }; 52 | let _ = { 53 | thread::spawn(move || loop { 54 | if let Err(err) = tx.send(Event::Tick) { 55 | eprintln!("{}", err); 56 | break; 57 | } 58 | thread::sleep(config.tick_rate); 59 | }) 60 | }; 61 | Events { 62 | rx, 63 | // input_handle, 64 | // tick_handle, 65 | } 66 | } 67 | 68 | pub fn next(&self) -> Result, mpsc::RecvError> { 69 | self.rx.recv() 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/utils/bindings.rs: -------------------------------------------------------------------------------- 1 | use termion::event::Key; 2 | 3 | pub const QUIT: Key = Key::Char('q'); 4 | pub const PREVIOUS_PANEL: Key = Key::Left; 5 | pub const NEXT_PANEL: Key = Key::Right; 6 | pub const CHANNEL_SELECTOR_1: Key = Key::Char('1'); 7 | pub const CHANNEL_SELECTOR_2: Key = Key::Char('2'); 8 | pub const CHANNEL_SELECTOR_3: Key = Key::Char('3'); 9 | pub const CHANNEL_SELECTOR_4: Key = Key::Char('4'); 10 | pub const CHANNEL_SELECTOR_5: Key = Key::Char('5'); 11 | pub const CHANNEL_SELECTOR_6: Key = Key::Char('6'); 12 | pub const CHANNEL_SELECTOR_7: Key = Key::Char('7'); 13 | pub const CHANNEL_SELECTOR_8: Key = Key::Char('8'); 14 | pub const CHANNEL_SELECTOR_9: Key = Key::Char('9'); 15 | pub const CHANNEL_RESET: Key = Key::Esc; 16 | pub const HELP: Key = Key::Char(' '); 17 | pub const ZOOM_IN: Key = Key::Char('k'); 18 | pub const ZOOM_OUT: Key = Key::Char('j'); 19 | pub const MOVE_LEFT: Key = Key::Char('h'); 20 | pub const MOVE_RIGHT: Key = Key::Char('l'); 21 | 22 | // pub fn binding_iterat 23 | 24 | pub fn key_to_string(key: &Key) -> String { 25 | match key { 26 | Key::Left => String::from("Left arrow"), 27 | Key::Right => String::from("Right arrow"), 28 | Key::Up => String::from("Up arrow"), 29 | Key::Down => String::from("Down arrow"), 30 | Key::Home => String::from("Home"), 31 | Key::F(value) => match value { 32 | 1 => "F1", 33 | 2 => "F2", 34 | 3 => "F3", 35 | 4 => "F4", 36 | 5 => "F5", 37 | 6 => "F6", 38 | 7 => "F7", 39 | 8 => "F8", 40 | 9 => "F9", 41 | 10 => "F10", 42 | 11 => "F11", 43 | 12 => "F12", 44 | _ => panic!(), 45 | } 46 | .to_string(), 47 | Key::Char(value) => match *value { 48 | ' ' => String::from("Space"), 49 | val => format!("{}", val), 50 | }, 51 | Key::Alt(value) => format!("alt-{}", value), 52 | Key::Ctrl(value) => format!("ctl-{}", value), 53 | Key::Esc => String::from("Esc"), 54 | _ => panic!(), 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /.github/workflows/pre-releases.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: "pre-releases" 3 | 4 | on: 5 | push: 6 | tags: 'v[0-9]+.[0-9]+.[0-9]+-rc.[0-9]+' 7 | 8 | env: 9 | CARGO_TERM_COLOR: always 10 | 11 | jobs: 12 | fmt: 13 | name: Rustfmt 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@master 17 | - uses: actions-rs/toolchain@v1 18 | with: 19 | toolchain: stable 20 | profile: minimal 21 | override: true 22 | components: rustfmt 23 | - uses: Swatinem/rust-cache@v2 24 | - uses: actions-rs/cargo@v1 25 | with: 26 | command: fmt 27 | args: --all -- --check 28 | 29 | clippy: 30 | name: Clippy 31 | runs-on: ubuntu-latest 32 | steps: 33 | - uses: actions/checkout@master 34 | - uses: actions-rs/toolchain@v1 35 | with: 36 | toolchain: stable 37 | profile: minimal 38 | override: true 39 | components: clippy 40 | - uses: Swatinem/rust-cache@v2 41 | - uses: actions-rs/cargo@v1 42 | with: 43 | command: clippy 44 | args: -- -D warnings 45 | 46 | publish-rc: 47 | name: "Tagged pre-release" 48 | runs-on: "ubuntu-latest" 49 | needs: 50 | - fmt 51 | - clippy 52 | 53 | steps: 54 | - uses: actions/checkout@v3 55 | with: 56 | lfs: true 57 | - uses: actions-rs/toolchain@v1 58 | with: 59 | toolchain: stable 60 | override: true 61 | - uses: Swatinem/rust-cache@v2 62 | - name: "Prebuild" 63 | run: | 64 | sudo apt-get update 65 | sudo apt-get install -y libsndfile1-dev 66 | 67 | - uses: olegtarasov/get-tag@v2.1 68 | id: tagName 69 | 70 | - name: "Build & tests" 71 | run: | 72 | cargo test --release --verbose 73 | cargo build --release --verbose 74 | cp target/release/audeye "audeye-${GIT_TAG_NAME}-linux-amd64" 75 | 76 | - uses: "marvinpinto/action-automatic-releases@latest" 77 | with: 78 | repo_token: "${{ secrets.GITHUB_TOKEN }}" 79 | prerelease: true 80 | title: "${{ steps.tagName.outputs.tag }}" 81 | files: | 82 | LICENSE 83 | audeye-${{ steps.tagName.outputs.tag }}-linux-amd64 84 | 85 | -------------------------------------------------------------------------------- /src/render/renderer.rs: -------------------------------------------------------------------------------- 1 | use std::convert::TryFrom; 2 | use tui::backend::Backend; 3 | use tui::layout::{Alignment, Constraint, Direction, Layout, Rect}; 4 | use tui::text::Spans; 5 | use tui::widgets::{Block, Borders, Paragraph}; 6 | use tui::Frame; 7 | 8 | use crate::utils::Zoom; 9 | 10 | pub struct RenderingInfo<'a> { 11 | pub activated_channels: Vec<(usize, &'a str)>, 12 | pub zoom: &'a Zoom, 13 | } 14 | 15 | pub trait Renderer { 16 | fn draw(&mut self, frame: &mut Frame<'_, B>, info: &RenderingInfo, area: Rect); 17 | 18 | fn needs_redraw(&mut self) -> bool; 19 | fn max_width_resolution(&self) -> usize { 20 | usize::MAX 21 | } 22 | } 23 | 24 | pub trait ChannelRenderer: Renderer { 25 | fn draw_single_channel( 26 | &mut self, 27 | frame: &mut Frame<'_, B>, 28 | channel: usize, 29 | area: Rect, 30 | block: Block, 31 | zoom: &Zoom, 32 | ); 33 | 34 | fn needs_redraw(&mut self) -> bool; 35 | fn max_width_resolution(&self) -> usize; 36 | } 37 | 38 | impl Renderer for T { 39 | fn draw(&mut self, frame: &mut Frame<'_, B>, info: &RenderingInfo, area: Rect) { 40 | let layout = compute_channels_layout(area, info.activated_channels.len()); 41 | 42 | for (activated_idx, (ch_idx, title)) in info.activated_channels.iter().enumerate() { 43 | let block = Block::default().title(*title).borders(Borders::ALL); 44 | self.draw_single_channel(frame, *ch_idx, layout[activated_idx], block, info.zoom); 45 | } 46 | } 47 | 48 | fn needs_redraw(&mut self) -> bool { 49 | ChannelRenderer::needs_redraw(self) 50 | } 51 | 52 | fn max_width_resolution(&self) -> usize { 53 | ChannelRenderer::max_width_resolution(self) 54 | } 55 | } 56 | 57 | pub fn draw_text_info( 58 | frame: &mut Frame<'_, B>, 59 | area: Rect, 60 | block: Block<'_>, 61 | text: &str, 62 | ) { 63 | let num_lines_to_center: usize = if area.height % 2 == 0 { 64 | usize::from(area.height) / 2 - 1 65 | } else { 66 | usize::from(area.height) / 2 67 | }; 68 | 69 | let mut span_vec = vec![Spans::from(""); num_lines_to_center]; 70 | span_vec[num_lines_to_center - 1] = Spans::from(text); 71 | 72 | let paragraph = Paragraph::new(span_vec) 73 | .block(block) 74 | .alignment(Alignment::Center); 75 | 76 | frame.render_widget(paragraph, area); 77 | } 78 | 79 | fn compute_channels_layout(area: Rect, num_channels: usize) -> Vec { 80 | let constraints = 81 | vec![Constraint::Ratio(1u32, u32::try_from(num_channels).unwrap()); num_channels]; 82 | Layout::default() 83 | .direction(Direction::Vertical) 84 | .constraints::<&[Constraint]>(constraints.as_ref()) 85 | .split(area) 86 | } 87 | -------------------------------------------------------------------------------- /.github/workflows/releases.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: "releases" 3 | 4 | on: 5 | push: 6 | tags: 'v[0-9]+.[0-9]+.[0-9]+' 7 | 8 | env: 9 | CARGO_TERM_COLOR: always 10 | 11 | jobs: 12 | fmt: 13 | name: Rustfmt 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@master 17 | - uses: actions-rs/toolchain@v1 18 | with: 19 | toolchain: stable 20 | profile: minimal 21 | override: true 22 | components: rustfmt 23 | - uses: Swatinem/rust-cache@v2 24 | - uses: actions-rs/cargo@v1 25 | with: 26 | command: fmt 27 | args: --all -- --check 28 | 29 | clippy: 30 | name: Clippy 31 | runs-on: ubuntu-latest 32 | steps: 33 | - uses: actions/checkout@master 34 | - uses: actions-rs/toolchain@v1 35 | with: 36 | toolchain: stable 37 | profile: minimal 38 | override: true 39 | components: clippy 40 | - uses: Swatinem/rust-cache@v2 41 | - uses: actions-rs/cargo@v1 42 | with: 43 | command: clippy 44 | args: -- -D warnings 45 | 46 | publish: 47 | name: "Tagged release" 48 | runs-on: "ubuntu-latest" 49 | needs: 50 | - fmt 51 | - clippy 52 | 53 | steps: 54 | - uses: actions/checkout@v3 55 | with: 56 | lfs: true 57 | - uses: actions-rs/toolchain@v1 58 | with: 59 | toolchain: stable 60 | override: true 61 | - uses: Swatinem/rust-cache@v2 62 | - name: "Prebuild" 63 | run: | 64 | sudo apt-get update 65 | sudo apt-get install -y libsndfile1-dev 66 | 67 | - uses: olegtarasov/get-tag@v2.1 68 | id: tagName 69 | 70 | - name: "Build & tests" 71 | run: | 72 | cargo test --release --verbose 73 | cargo build --release --verbose 74 | cp target/release/audeye "audeye-${GIT_TAG_NAME}-linux-amd64" 75 | 76 | - uses: "marvinpinto/action-automatic-releases@latest" 77 | with: 78 | repo_token: "${{ secrets.GITHUB_TOKEN }}" 79 | prerelease: false 80 | title: "${{ steps.tagName.outputs.tag }}" 81 | files: | 82 | LICENSE 83 | audeye-${{ steps.tagName.outputs.tag }}-linux-amd64 84 | 85 | publish-cargo: 86 | name: "Crates.io release" 87 | needs: publish 88 | runs-on: "ubuntu-latest" 89 | steps: 90 | - uses: actions/checkout@v3 91 | - uses: actions-rs/toolchain@v1 92 | with: 93 | toolchain: stable 94 | override: true 95 | - run: | 96 | sudo apt-get update 97 | sudo apt-get install -y libsndfile1-dev 98 | - uses: actions-rs/cargo@v1 99 | with: 100 | command: publish 101 | args: --token ${{ secrets.CARGO_API_KEY }} --allow-dirty 102 | 103 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Audeye 2 | 3 |

4 | 🔊 💻 👁️Audio content visualization tool 5 |

6 | 7 | ![Demo](.github/images/audeye_0_2_0.gif) 8 | 9 | Audeye is a terminal tool to visualize audio content, written in Rust 10 | 11 | ## Features 12 | - wav / aif / flac / ogg-vorbis and many more (see : [libsndfile format compatibility v1.0.31](https://libsndfile.github.io/libsndfile/formats.html)) 13 | - mono / stereo / 5.1 / 7.1 ... (up to 9 channels) 14 | - Waveform peak & RMS visualizer 15 | - Spectrogram visualizer 16 | - Signal normalization 17 | - Zoom and move inside both visualizers 18 | - Metadata display 19 | 20 | ## Bindings 21 | - `space` : display bindings 22 | - `left arrow` / `right arrow` : navigate through panels 23 | - `j` / `k` : zoom out / in 24 | - `h` / `l` : move left / right 25 | - [`0`-`9`] : activate / deactivate display of the corresponding channel 26 | - `Esc` : reset channel layout 27 | 28 | ## CLI arguments 29 | - `-n` : normalize the audio signal before displaying it (not channel aware) 30 | - `--fft-window-size` 31 | - `--fft-window-type` : `hanning` / `hamming` / `blackman` / `uniform` 32 | - `--fft-overlap` 33 | - `--fft-db-threshold` : minimum energy level to consider (in dB) 34 | - `--fft-padding-type` : `zeros` / `loop` / `ramp` 35 | 36 | ### Paddings types 37 | The padding type determine how to fill the sides of each FFT window when at the 38 | very edges of the audio content 39 | - Zeros : fill with zeros 40 | - Ramp : fill with zeros and a small amplitude ramp to match the last/next sample 41 | - Loop : fill with the end/beginning of the audio file 42 | 43 | # Dependencies 44 | Audeye rely on Rust bindings to [libsndfile](https://github.com/libsndfile/libsndfile) 45 | 46 | ## Debian / Ubuntu 47 | ``` 48 | apt-get install libsndfile1-dev 49 | ``` 50 | 51 | # Installation 52 | ``` 53 | cargo install audeye 54 | ``` 55 | 56 | # Build 57 | 1. [Install Rust](https://www.rust-lang.org/tools/install) 58 | 2. Install [libsndfile](#dependencies) 59 | 2. Then run `cargo run ` 60 | 61 | ## Development 62 | Please consider audeye is still in early development, feedbacks are very welcome 63 | 64 | ### Requirements 65 | - [cargo](https://doc.rust-lang.org/cargo/getting-started/installation.html) 66 | - [libsndfile](#dependencies) 67 | - [precommit](https://pre-commit.com/#install) 68 | - [clippy](https://github.com/rust-lang/rust-clippy) 69 | - [rustfmt](https://github.com/rust-lang/rustfmt) 70 | 71 | ### Contributing 72 | If you wanna contribute, either make a PR (for small changes/adds) or contact me 73 | on twitter / discord if you wanna get involved more deeply 74 | - [Twitter](https://twitter.com/Groumpf_) 75 | - [Discord](https://discordapp.com/users/Groumpf#2353) 76 | 77 | ### Milestone 78 | - [x] Waveform view 79 | - [x] Spectogram view 80 | - [x] Channels view navigation 81 | - [x] Channel naming (stereo, 2.1, 5.1, 7.1 ...) 82 | - [x] Zoom in/out 83 | - [x] Metadata view 84 | - [x] RMS and Peak in waveform view 85 | - [x] Option : normalize 86 | - [x] Option : FFT windows size and overlap 87 | - [x] Option : FFT dB threshold 88 | - [x] Option : FFT window type 89 | - [x] Option : FFT side smoothing 90 | - [x] Unit tests 91 | - [ ] Optionnal labels on graphs 92 | - [ ] Option : FFT logarithmic scale 93 | - [ ] Option : Waveform envelope ? 94 | - [ ] More audio format support 95 | -------------------------------------------------------------------------------- /src/render/help.rs: -------------------------------------------------------------------------------- 1 | use super::{Renderer, RenderingInfo}; 2 | use crate::utils::bindings; 3 | use tui::{ 4 | backend::Backend, 5 | layout::{Alignment, Rect}, 6 | style::{Modifier, Style}, 7 | text::{Span, Spans}, 8 | widgets::{Block, Borders, Paragraph}, 9 | Frame, 10 | }; 11 | 12 | pub struct HelperPopup { 13 | visible: bool, 14 | repaint: bool, 15 | } 16 | 17 | impl Default for HelperPopup { 18 | fn default() -> Self { 19 | Self { 20 | visible: false, 21 | repaint: true, 22 | } 23 | } 24 | } 25 | 26 | impl Renderer for HelperPopup { 27 | fn needs_redraw(&mut self) -> bool { 28 | self.repaint 29 | } 30 | 31 | fn draw(&mut self, frame: &mut Frame<'_, B>, _: &RenderingInfo, area: Rect) { 32 | let name_style = Style::default().add_modifier(Modifier::BOLD); 33 | let value_style = Style::default(); 34 | 35 | let bindings_categories = vec![ 36 | vec![ 37 | ("Quit", bindings::QUIT), 38 | ("Previous panel", bindings::PREVIOUS_PANEL), 39 | ("Next panel", bindings::NEXT_PANEL), 40 | ("Help menu", bindings::HELP), 41 | ], 42 | vec![ 43 | ("Zoom in", bindings::ZOOM_IN), 44 | ("Zoom out", bindings::ZOOM_OUT), 45 | ("Move left", bindings::MOVE_LEFT), 46 | ("Move right", bindings::MOVE_RIGHT), 47 | ], 48 | vec![ 49 | ("Reset channel selection", bindings::CHANNEL_RESET), 50 | ("Enable/disable channel 1", bindings::CHANNEL_SELECTOR_1), 51 | ("Enable/disable channel 2", bindings::CHANNEL_SELECTOR_2), 52 | ("Enable/disable channel 3", bindings::CHANNEL_SELECTOR_3), 53 | ("Enable/disable channel 4", bindings::CHANNEL_SELECTOR_4), 54 | ("Enable/disable channel 5", bindings::CHANNEL_SELECTOR_5), 55 | ("Enable/disable channel 6", bindings::CHANNEL_SELECTOR_6), 56 | ("Enable/disable channel 7", bindings::CHANNEL_SELECTOR_7), 57 | ("Enable/disable channel 8", bindings::CHANNEL_SELECTOR_8), 58 | ("Enable/disable channel 9", bindings::CHANNEL_SELECTOR_9), 59 | ], 60 | ]; 61 | 62 | let spans: Vec = bindings_categories 63 | .iter() 64 | .map(|cat| { 65 | cat.iter() 66 | .map(|(name, value)| { 67 | Spans::from(vec![ 68 | Span::styled(*name, name_style), 69 | Span::raw(" : "), 70 | Span::styled(bindings::key_to_string(value), value_style), 71 | ]) 72 | }) 73 | .collect() 74 | }) 75 | .flat_map(|mut spans: Vec| { 76 | spans.extend(vec![Spans::from("")]); 77 | spans 78 | }) 79 | .collect(); 80 | 81 | let paragraph = Paragraph::new(spans) 82 | .block(Block::default().title("Bindings").borders(Borders::ALL)) 83 | .alignment(Alignment::Left); 84 | 85 | frame.render_widget(paragraph, area); 86 | 87 | self.repaint = false; 88 | } 89 | } 90 | 91 | impl HelperPopup { 92 | pub fn is_visible(&self) -> bool { 93 | self.visible 94 | } 95 | 96 | pub fn set_visible(&mut self, enable: bool) { 97 | if self.visible != enable { 98 | self.repaint = true; 99 | } 100 | self.visible = enable; 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/utils/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod bindings; 2 | pub mod filled_rectangle; 3 | mod zoom; 4 | pub use zoom::*; 5 | pub mod event; 6 | 7 | use num_traits::NumAssign; 8 | // use rand::distributions::{Distribution, Uniform}; 9 | // use rand::rngs::ThreadRng; 10 | 11 | // #[repr(transparent)] 12 | // pub struct Sample(T); 13 | 14 | // #[repr(C)] 15 | // pub struct Frame { 16 | // samples: [Sample; C] 17 | // } 18 | 19 | // fn deinterleaved(src: &[Frame], dst ) 20 | pub fn deinterleave_vec(channels: usize, src: &[T], dst: &mut [Vec]) { 21 | let mut vec_slices: Vec<&mut [T]> = dst.iter_mut().map(|vec| vec.as_mut_slice()).collect(); 22 | deinterleave(channels, src, vec_slices.as_mut_slice()); 23 | } 24 | 25 | pub fn deinterleave(channels: usize, src: &[T], dst: &mut [&mut [T]]) { 26 | src.chunks_exact(channels) 27 | .enumerate() 28 | .for_each(|(frame_idx, samples)| { 29 | for (channel, value) in samples.iter().enumerate() { 30 | dst[channel][frame_idx] = *value; 31 | } 32 | }); 33 | } 34 | 35 | /* === Useful structs that could be helpful when debugging === 36 | Commented to remove clippy warning 37 | */ 38 | 39 | // #[derive(Clone)] 40 | // pub struct RandomSignal { 41 | // distribution: Uniform, 42 | // rng: ThreadRng, 43 | // } 44 | 45 | // impl RandomSignal { 46 | // pub fn new(lower: u64, upper: u64) -> RandomSignal { 47 | // RandomSignal { 48 | // distribution: Uniform::new(lower, upper), 49 | // rng: rand::thread_rng(), 50 | // } 51 | // } 52 | // } 53 | 54 | // impl Iterator for RandomSignal { 55 | // type Item = u64; 56 | // fn next(&mut self) -> Option { 57 | // Some(self.distribution.sample(&mut self.rng)) 58 | // } 59 | // } 60 | 61 | // #[derive(Clone)] 62 | // pub struct SinSignal { 63 | // x: f64, 64 | // interval: f64, 65 | // period: f64, 66 | // scale: f64, 67 | // } 68 | 69 | // impl SinSignal { 70 | // pub fn new(interval: f64, period: f64, scale: f64) -> SinSignal { 71 | // SinSignal { 72 | // x: 0.0, 73 | // interval, 74 | // period, 75 | // scale, 76 | // } 77 | // } 78 | // } 79 | 80 | // impl Iterator for SinSignal { 81 | // type Item = (f64, f64); 82 | // fn next(&mut self) -> Option { 83 | // let point = (self.x, (self.x * 1.0 / self.period).sin() * self.scale); 84 | // self.x += self.interval; 85 | // Some(point) 86 | // } 87 | // } 88 | 89 | pub struct TabsState<'a> { 90 | pub titles: Vec<&'a str>, 91 | pub index: usize, 92 | } 93 | 94 | impl<'a> TabsState<'a> { 95 | pub fn new(titles: Vec<&'a str>) -> TabsState { 96 | TabsState { titles, index: 0 } 97 | } 98 | pub fn next(&mut self) { 99 | self.index = (self.index + 1) % self.titles.len(); 100 | } 101 | 102 | pub fn previous(&mut self) { 103 | if self.index > 0 { 104 | self.index -= 1; 105 | } else { 106 | self.index = self.titles.len() - 1; 107 | } 108 | } 109 | } 110 | 111 | // pub struct StatefulList { 112 | // pub state: ListState, 113 | // pub items: Vec, 114 | // } 115 | 116 | // impl StatefulList { 117 | // pub fn new() -> StatefulList { 118 | // StatefulList { 119 | // state: ListState::default(), 120 | // items: Vec::new(), 121 | // } 122 | // } 123 | 124 | // pub fn with_items(items: Vec) -> StatefulList { 125 | // StatefulList { 126 | // state: ListState::default(), 127 | // items, 128 | // } 129 | // } 130 | 131 | // pub fn next(&mut self) { 132 | // let i = match self.state.selected() { 133 | // Some(i) => { 134 | // if i >= self.items.len() - 1 { 135 | // 0 136 | // } else { 137 | // i + 1 138 | // } 139 | // } 140 | // None => 0, 141 | // }; 142 | // self.state.select(Some(i)); 143 | // } 144 | 145 | // pub fn previous(&mut self) { 146 | // let i = match self.state.selected() { 147 | // Some(i) => { 148 | // if i == 0 { 149 | // self.items.len() - 1 150 | // } else { 151 | // i - 1 152 | // } 153 | // } 154 | // None => 0, 155 | // }; 156 | // self.state.select(Some(i)); 157 | // } 158 | 159 | // pub fn unselect(&mut self) { 160 | // self.state.select(None); 161 | // } 162 | // } 163 | -------------------------------------------------------------------------------- /src/render/spectral.rs: -------------------------------------------------------------------------------- 1 | use super::widgets; 2 | use super::{draw_text_info, renderer::ChannelRenderer}; 3 | use core::panic; 4 | extern crate sndfile; 5 | use crate::utils::Zoom; 6 | use fr::Image; 7 | use std::convert::{TryFrom, TryInto}; 8 | use tui::backend::Backend; 9 | use tui::layout::Rect; 10 | use tui::widgets::Block; 11 | use tui::Frame; 12 | 13 | use crate::dsp::{AsyncDspData, AsyncDspDataState, Spectrogram, SpectrogramParameters}; 14 | 15 | use std::num::NonZeroU32; 16 | 17 | use fast_image_resize as fr; 18 | 19 | pub struct SpectralRenderer<'a> { 20 | channels: usize, 21 | async_renderer: AsyncDspData, 22 | resizer: fr::Resizer, 23 | canva_img: Option>, 24 | max_width_resolution: usize, 25 | } 26 | 27 | impl<'a> SpectralRenderer<'a> { 28 | pub fn new( 29 | path: &std::path::PathBuf, 30 | parameters: SpectrogramParameters, 31 | normalize: bool, 32 | ) -> Self { 33 | let mut snd = sndfile::OpenOptions::ReadOnly(sndfile::ReadOptions::Auto) 34 | .from_path(path) 35 | .expect("Could not open wave file"); 36 | 37 | let channels = snd.get_channels(); 38 | let max_res = snd.len().unwrap() 39 | / (parameters.window_size as f64 * (1f64 - parameters.overlap_rate)) as u64; 40 | 41 | SpectralRenderer { 42 | channels, 43 | async_renderer: AsyncDspData::new(path, parameters, normalize), 44 | // resizer: fr::Resizer::new(fr::ResizeAlg::Nearest), 45 | resizer: fr::Resizer::new(fr::ResizeAlg::Convolution(fr::FilterType::Lanczos3)), 46 | canva_img: None, 47 | max_width_resolution: usize::try_from(max_res).unwrap(), 48 | } 49 | } 50 | } 51 | 52 | impl<'a> ChannelRenderer for SpectralRenderer<'a> { 53 | fn draw_single_channel( 54 | &mut self, 55 | frame: &mut Frame<'_, B>, 56 | channel: usize, 57 | area: Rect, 58 | block: Block, 59 | zoom: &Zoom, 60 | ) { 61 | match self.async_renderer.state() { 62 | AsyncDspDataState::Normalizing => { 63 | draw_text_info(frame, area, block, "Normalizing..."); 64 | return; 65 | } 66 | AsyncDspDataState::Created | AsyncDspDataState::Processing => { 67 | draw_text_info(frame, area, block, "Loading..."); 68 | return; 69 | } 70 | AsyncDspDataState::Failed => { 71 | // Should crash soon 72 | draw_text_info(frame, area, block, "Error"); 73 | return; 74 | } 75 | _ => {} 76 | } 77 | 78 | if channel >= self.channels { 79 | panic!(); 80 | } 81 | 82 | let canva_width = area.width as usize; 83 | let canva_height = area.height as usize; 84 | let data_ref = match self.async_renderer.data() { 85 | Some(data_ref) => data_ref, 86 | None => panic!(), 87 | }; 88 | 89 | // Create source image from spectrogram 90 | let num_bins = data_ref.num_bins(); 91 | 92 | let (data_slice, num_bands) = data_ref.data(channel, zoom); 93 | let src_image = fr::Image::from_slice_u8( 94 | NonZeroU32::new(num_bins.try_into().unwrap()).unwrap(), 95 | NonZeroU32::new(num_bands.try_into().unwrap()).unwrap(), 96 | data_slice, 97 | fr::PixelType::U8x3, 98 | ) 99 | .unwrap(); 100 | 101 | // Compute dst images dimensions 102 | // /!\ The image is transposed (like a matrix) for better memory mapping /!\ 103 | let resize_dst_width = (canva_height - 2) * 2; 104 | let resize_dst_height = canva_width - 2; 105 | 106 | // Store in option to keep it in memory for the renderingspectrogram 107 | self.canva_img = Some(fr::Image::new( 108 | NonZeroU32::new(resize_dst_width.try_into().unwrap()).unwrap(), 109 | NonZeroU32::new(resize_dst_height.try_into().unwrap()).unwrap(), 110 | fr::PixelType::U8x3, 111 | )); 112 | 113 | let canva_img_ref = self.canva_img.as_mut().unwrap(); 114 | let mut dst_view = canva_img_ref.view_mut(); 115 | 116 | // Resize 117 | self.resizer 118 | .resize(&src_image.view(), &mut dst_view) 119 | .unwrap(); 120 | 121 | // Build Image widget 122 | let img_widget = widgets::Image::new(canva_img_ref.buffer()).block(block); 123 | 124 | frame.render_widget(img_widget, area); 125 | } 126 | 127 | fn needs_redraw(&mut self) -> bool { 128 | self.async_renderer.update_status() 129 | } 130 | 131 | fn max_width_resolution(&self) -> usize { 132 | // nasty, should rely on the same variables as the time window generator 133 | self.max_width_resolution 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/dsp/data.rs: -------------------------------------------------------------------------------- 1 | use core::fmt; 2 | use std::marker::PhantomData; 3 | use std::sync::mpsc::{self, Receiver}; 4 | use std::thread::{self, JoinHandle}; 5 | 6 | extern crate sndfile; 7 | use crate::sndfile::SndFile; 8 | 9 | use super::normalization::compute_norm; 10 | 11 | #[derive(Debug)] 12 | pub struct DspErr { 13 | msg: String, 14 | } 15 | 16 | impl DspErr { 17 | pub fn new(msg: &str) -> Self { 18 | Self { 19 | msg: msg.to_string(), 20 | } 21 | } 22 | } 23 | 24 | impl fmt::Display for DspErr { 25 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 26 | write!(f, "{}", self.msg) 27 | } 28 | } 29 | 30 | pub trait DspData

{ 31 | fn new(file: SndFile, parameter: P, normalize: Option) -> Result 32 | where 33 | Self: Sized; 34 | } 35 | 36 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 37 | pub enum AsyncDspDataState { 38 | Created, 39 | Normalizing, 40 | Processing, 41 | Failed, 42 | Finished, 43 | } 44 | 45 | pub struct AsyncDspData + Send + 'static, P: Send + 'static> { 46 | state: AsyncDspDataState, 47 | pub data: Option, 48 | rendered_rx: Receiver, 49 | process_handle: Option>, 50 | phantom: PhantomData

, 51 | } 52 | 53 | impl + Send + 'static, P: Send + 'static> AsyncDspData { 54 | pub fn update_status(&mut self) -> bool { 55 | let mut update_needed = false; 56 | 57 | loop { 58 | match self.rendered_rx.try_recv() { 59 | Ok(AsyncDspDataState::Finished) => { 60 | // Rendered properly 61 | self.load_results(); 62 | self.state = AsyncDspDataState::Finished; 63 | update_needed = true; 64 | break; 65 | } 66 | Ok(AsyncDspDataState::Failed) => { 67 | // Failed to render, try to join to catch error 68 | let opt_handle = self.process_handle.take(); 69 | match opt_handle { 70 | Some(handle) => match handle.join() { 71 | Ok(_) => panic!("Async rendering sent failed signal but succeeded"), 72 | Err(err) => panic!("{:?}", err), 73 | }, 74 | None => panic!("Async rendering handle is None"), 75 | } 76 | } 77 | Ok(new_state) => { 78 | self.state = new_state; 79 | update_needed = true; 80 | } 81 | _ => { 82 | break; 83 | } 84 | } 85 | } 86 | 87 | update_needed 88 | } 89 | 90 | pub fn state(&mut self) -> AsyncDspDataState { 91 | self.state 92 | } 93 | 94 | pub fn data(&mut self) -> Option<&mut T> { 95 | self.data.as_mut() 96 | } 97 | 98 | fn load_results(&mut self) { 99 | let opt_handle = self.process_handle.take(); 100 | match opt_handle { 101 | Some(handle) => { 102 | self.data = Some(handle.join().expect("Async rendering failed")); 103 | } 104 | None => panic!("Async rendering handle is None"), 105 | } 106 | } 107 | 108 | pub fn new(path: &std::path::PathBuf, parameters: P, normalize: bool) -> Self { 109 | let mut snd = sndfile::OpenOptions::ReadOnly(sndfile::ReadOptions::Auto) 110 | .from_path(path) 111 | .expect("Could not open wave file"); 112 | if !snd.is_seekable() { 113 | panic!("Input file is not seekable"); 114 | } 115 | 116 | let (rendered_tx, rendered_rx) = mpsc::channel(); 117 | let join_handle = thread::spawn(move || { 118 | // First, compute the norm if needed 119 | let norm = if normalize { 120 | let _ = rendered_tx.send(AsyncDspDataState::Normalizing); 121 | Some(compute_norm(&mut snd)) 122 | } else { 123 | None 124 | }; 125 | 126 | // Start the processing 127 | let _ = rendered_tx.send(AsyncDspDataState::Processing); 128 | let res = T::new(snd, parameters, norm); 129 | 130 | // Check the processing result 131 | match res { 132 | Ok(data) => { 133 | // Success, we update the state and return the data 134 | let _ = rendered_tx.send(AsyncDspDataState::Finished); 135 | data 136 | } 137 | Err(dsp_err) => { 138 | // Failure, we stop the program and display the error 139 | let _ = rendered_tx.send(AsyncDspDataState::Failed); 140 | panic!("{}", dsp_err); 141 | } 142 | } 143 | }); 144 | 145 | Self { 146 | state: AsyncDspDataState::Created, 147 | data: None, 148 | rendered_rx, 149 | process_handle: Some(join_handle), 150 | phantom: PhantomData, 151 | } 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/render/headers.rs: -------------------------------------------------------------------------------- 1 | use std::collections::BTreeSet; 2 | 3 | use tui::backend::Backend; 4 | use tui::layout::{Alignment, Rect}; 5 | use tui::style::{Color, Modifier, Style}; 6 | use tui::text::{Span, Spans}; 7 | use tui::Frame; 8 | // use tui::widgets::canvas::Canvas; 9 | use tui::widgets::{Block, Borders, Paragraph}; 10 | // use tui::widgets::canvas::Rectangle; 11 | 12 | // use crate::utils::{Zoom}; 13 | 14 | // pub struct ZoomHead<'a> { 15 | // zoom: &'a Zoom 16 | // } 17 | 18 | // impl<'a> ZoomHead<'a> { 19 | // pub fn new(zoom: &'a Zoom) -> Self { 20 | // ZoomHead { zoom } 21 | // } 22 | 23 | // pub fn render(&mut self, frame: &mut Frame<'_, B>, area : Rect) { 24 | // let canva = Canvas::default() 25 | // .background_color(Color::Rgb(10, 10, 10)) 26 | // .paint(|ctx| { 27 | // ctx.draw(&Rectangle{ 28 | // x: self.zoom.start(), 29 | // y: 0f64, 30 | // width: self.zoom.length(), 31 | // height: 1f64, 32 | // color: Color::Red 33 | // })}) 34 | // .x_bounds([0f64, 1f64]) 35 | // .y_bounds([0f64, 1f64]); 36 | 37 | // frame.render_widget(canva, area); 38 | // } 39 | // } 40 | 41 | pub struct ChannelsTabs { 42 | titles: Vec, 43 | activated: BTreeSet, 44 | } 45 | 46 | impl<'a> ChannelsTabs { 47 | pub fn new(count: usize) -> Self { 48 | let mut set = BTreeSet::new(); 49 | 50 | for idx in 0..count { 51 | set.insert(idx); 52 | } 53 | 54 | Self { 55 | titles: Self::get_channels_titles(count), 56 | activated: set, 57 | } 58 | } 59 | 60 | pub fn render(&self, frame: &mut Frame<'_, B>, area: Rect) { 61 | // Styles 62 | let separator = Span::raw(" | "); 63 | let selected_style = Style::default() 64 | .add_modifier(Modifier::BOLD) 65 | .fg(Color::Green); 66 | let not_selected_style = Style::default().fg(Color::Gray); 67 | 68 | // Title block 69 | let block = Block::default() 70 | .borders(Borders::RIGHT | Borders::TOP | Borders::BOTTOM) 71 | .title("Channels") 72 | .title_alignment(Alignment::Right); 73 | 74 | // Create styled channels names 75 | let mut span_vec: Vec = vec![]; 76 | for (activated, name) in self.states() { 77 | let span = if activated { 78 | Span::styled(name, selected_style) 79 | } else { 80 | Span::styled(name, not_selected_style) 81 | }; 82 | 83 | span_vec.push(span); 84 | span_vec.push(separator.clone()); 85 | } 86 | span_vec.pop(); 87 | let spans = vec![Spans::from(span_vec)]; 88 | 89 | // Assign to paragraph object 90 | let paragraph = Paragraph::new(spans) 91 | .block(block) 92 | .alignment(Alignment::Right); 93 | 94 | frame.render_widget(paragraph, area); 95 | } 96 | 97 | pub fn count(&self) -> usize { 98 | self.titles.len() 99 | } 100 | 101 | pub fn activated(&'a self) -> Vec<(usize, &'a str)> { 102 | self.activated 103 | .iter() 104 | .map(|idx| (*idx, self.titles[*idx].as_str())) 105 | .collect() 106 | } 107 | 108 | fn states(&'a self) -> Vec<(bool, &'a str)> { 109 | (0..self.count()) 110 | .map(|idx| { 111 | let title = self.titles[idx].as_str(); 112 | match self.activated.get(&idx) { 113 | Some(_) => (true, title), 114 | None => (false, title), 115 | } 116 | }) 117 | .collect() 118 | } 119 | 120 | pub fn update(&mut self, idx: usize) { 121 | if idx < self.count() { 122 | match self.activated.get(&idx) { 123 | Some(_) => { 124 | if self.activated.len() > 1 { 125 | self.activated.remove(&idx); 126 | } 127 | } 128 | None => { 129 | self.activated.insert(idx); 130 | } 131 | }; 132 | } 133 | } 134 | 135 | pub fn reset(&mut self) { 136 | for idx in 0..self.count() { 137 | self.activated.insert(idx); 138 | } 139 | } 140 | 141 | fn get_channels_titles(count: usize) -> Vec { 142 | match count { 143 | 0usize => panic!(), 144 | 1_usize => vec!["Mono"].into_iter().map(|v| v.to_string()).collect(), // mono 145 | 2_usize => vec!["L", "R"].into_iter().map(|v| v.to_string()).collect(), // stereo 146 | 3_usize => vec!["L", "R", "LFE"] 147 | .into_iter() 148 | .map(|v| v.to_string()) 149 | .collect(), // 2.1 150 | 5_usize => vec!["FL", "FR", "C", "BL", "BR"] 151 | .into_iter() 152 | .map(|v| v.to_string()) 153 | .collect(), // 5.0 154 | 6_usize => vec!["FL", "FR", "C", "LFE", "BL", "BR"] 155 | .into_iter() 156 | .map(|v| v.to_string()) 157 | .collect(), // 5.1 158 | 7_usize => vec!["FL", "FR", "C", "LFE", "SL", "SR", "BC"] 159 | .into_iter() 160 | .map(|v| v.to_string()) 161 | .collect(), // 6.1 162 | 8_usize => vec!["FL", "FR", "C", "LFE", "SL", "SR", "BL", "BR"] 163 | .into_iter() 164 | .map(|v| v.to_string()) 165 | .collect(), // 7.1 166 | _ => (0..count).map(|idx| format!["Channel {:?}", idx]).collect(), 167 | } 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /src/render/waveform.rs: -------------------------------------------------------------------------------- 1 | use super::{draw_text_info, renderer::ChannelRenderer}; 2 | use core::panic; 3 | extern crate sndfile; 4 | use crate::utils::Zoom; 5 | use std::convert::TryFrom; 6 | use tui::backend::Backend; 7 | use tui::layout::Rect; 8 | use tui::symbols::Marker; 9 | use tui::widgets::canvas::{Canvas, Context, Line}; 10 | use tui::{style::Color, widgets::Block, Frame}; 11 | 12 | use crate::dsp::{AsyncDspData, AsyncDspDataState, Waveform, WaveformParameters, WaveformPoint}; 13 | 14 | // fn draw_outlined_shape(ctx: &mut Context, n_int: &Vec, p_int: &Vec) { 15 | // let mut previous_idx = 0usize; 16 | // let (mut prev_n, mut prev_p) = (0f64, 0f64); 17 | // for (idx, (n,p)) in n_int.iter().zip(p_int.iter()).enumerate() { 18 | // if idx != 0 { 19 | // // draw positive line 20 | // ctx.draw(&Line{ 21 | // x1: previous_idx as f64, 22 | // y1: prev_p, 23 | // x2: idx as f64, 24 | // y2: *p as f64, 25 | // color: Color::White 26 | // }); 27 | 28 | // // draw negative line 29 | // ctx.draw(&Line{ 30 | // x1: previous_idx as f64, 31 | // y1: prev_n, 32 | // x2: idx as f64, 33 | // y2: *n as f64, 34 | // color: Color::White 35 | // }); 36 | // } 37 | // previous_idx = idx; 38 | // prev_n = *n as f64; 39 | // prev_p = *p as f64; 40 | // } 41 | // } 42 | 43 | // fn draw_filled_shape(ctx: &mut Context, n_int: &Vec, p_int: &Vec) { 44 | // for (idx, (n,p)) in n_int.iter().zip(p_int.iter()).enumerate() { 45 | // ctx.draw(&Line{ 46 | // x1: idx as f64, 47 | // x2: idx as f64, 48 | // y1: *n as f64, 49 | // y2: *p as f64, 50 | // color: Color::White 51 | // }); 52 | // } 53 | // } 54 | 55 | fn draw_shape(ctx: &mut Context, points: &[WaveformPoint]) { 56 | let mut prev_peak_up = 0f64; 57 | let mut prev_peak_down = 0f64; 58 | 59 | for (idx, points) in points.iter().enumerate() { 60 | // Draw inner RMS shape 61 | ctx.draw(&Line { 62 | x1: idx as f64, 63 | x2: idx as f64, 64 | y1: -(points.rms as f64), 65 | y2: points.rms as f64, 66 | color: Color::White, 67 | }); 68 | 69 | if idx != 0usize { 70 | // Draw top and low peaks 71 | ctx.draw(&Line { 72 | x1: idx as f64 - 1f64, 73 | x2: idx as f64, 74 | y1: prev_peak_up, 75 | y2: points.peak_max as f64, 76 | color: Color::White, 77 | }); 78 | 79 | ctx.draw(&Line { 80 | x1: idx as f64 - 1f64, 81 | x2: idx as f64, 82 | y1: prev_peak_down, 83 | y2: points.peak_min as f64, 84 | color: Color::White, 85 | }); 86 | } 87 | 88 | prev_peak_down = points.peak_min as f64; 89 | prev_peak_up = points.peak_max as f64; 90 | } 91 | } 92 | 93 | pub struct WaveformRenderer { 94 | channels: usize, 95 | async_renderer: AsyncDspData, 96 | max_width_res: usize, 97 | } 98 | 99 | impl WaveformRenderer { 100 | pub fn new(path: &std::path::PathBuf, normalize: bool) -> WaveformRenderer { 101 | let mut snd = sndfile::OpenOptions::ReadOnly(sndfile::ReadOptions::Auto) 102 | .from_path(path) 103 | .expect("Could not open wave file"); 104 | 105 | let channels = snd.get_channels(); 106 | let max_res = usize::try_from(snd.len().unwrap()).unwrap(); 107 | 108 | WaveformRenderer { 109 | channels, 110 | async_renderer: AsyncDspData::new(path, WaveformParameters, normalize), 111 | max_width_res: max_res, 112 | } 113 | } 114 | } 115 | 116 | impl ChannelRenderer for WaveformRenderer { 117 | fn draw_single_channel( 118 | &mut self, 119 | frame: &mut Frame<'_, B>, 120 | channel: usize, 121 | area: Rect, 122 | block: Block, 123 | zoom: &Zoom, 124 | ) { 125 | match self.async_renderer.state() { 126 | AsyncDspDataState::Normalizing => { 127 | draw_text_info(frame, area, block, "Normalizing..."); 128 | return; 129 | } 130 | AsyncDspDataState::Created | AsyncDspDataState::Processing => { 131 | draw_text_info(frame, area, block, "Loading..."); 132 | return; 133 | } 134 | AsyncDspDataState::Failed => { 135 | // Should crash soon 136 | draw_text_info(frame, area, block, "Error"); 137 | return; 138 | } 139 | _ => {} 140 | } 141 | 142 | if channel >= self.channels { 143 | panic!(); 144 | } 145 | 146 | // Prepare 147 | let data_ref = self.async_renderer.data().unwrap(); 148 | let canva_width_int = area.width as usize - 2; 149 | let estimated_witdh_res = canva_width_int * 2; // Braille res is 2 per char 150 | 151 | // Compute local min & max for each block 152 | let points = data_ref.compute_points(channel, estimated_witdh_res, zoom); 153 | 154 | // Draw the canva 155 | let canva = Canvas::default() 156 | .block(block) 157 | .paint(|ctx| draw_shape(ctx, &points)) 158 | .marker(Marker::Braille) 159 | .x_bounds([-1., estimated_witdh_res as f64 + 1f64]) 160 | .y_bounds([i32::MIN as f64, i32::MAX as f64]); 161 | 162 | frame.render_widget(canva, area) 163 | } 164 | 165 | fn needs_redraw(&mut self) -> bool { 166 | self.async_renderer.update_status() 167 | } 168 | 169 | fn max_width_resolution(&self) -> usize { 170 | self.max_width_res 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /src/utils/zoom.rs: -------------------------------------------------------------------------------- 1 | const ZOOM_FACTOR: f64 = 0.9; 2 | 3 | #[derive(Debug, Clone)] 4 | pub struct ZoomError; 5 | pub struct Zoom { 6 | start: f64, 7 | length: f64, 8 | min: f64, 9 | } 10 | 11 | impl Zoom { 12 | /// Builds a new zoom tracker. Default to no zoom at all 13 | /// # Arguments 14 | /// 15 | /// * 'max_zoom' - The maximum zoom value allowed for the current 16 | /// file. 1 is no zoom, 0 is infinite zoom, 0.5 is half the content 17 | /// showed at screen 18 | /// >1 zoom is allowed and will be casted as 1.0 19 | pub fn new(max_zoom: f64) -> Result { 20 | let mut min = max_zoom; 21 | if max_zoom <= 0f64 { 22 | return Err(ZoomError); 23 | } else if min > 1f64 { 24 | min = 1f64; 25 | } 26 | Ok(Zoom { 27 | start: 0f64, 28 | length: 1f64, 29 | min, 30 | }) 31 | } 32 | 33 | /// Get the relative starting point of the current zoom window btw 0 and 1 34 | pub fn start(&self) -> f64 { 35 | self.start 36 | } 37 | 38 | /// Get the relative length of the current zoom window btw 0 and 1 39 | pub fn length(&self) -> f64 { 40 | self.length 41 | } 42 | 43 | /// Set a new limit for the zoom value 44 | pub fn update_zoom_max(&mut self, max: f64) { 45 | self.min = if max <= 1f64 { max } else { 1f64 }; 46 | 47 | // Already above the limit, nothing to change 48 | if self.length >= self.min { 49 | return; 50 | } 51 | 52 | // Under the new limit, need to update the current state 53 | let mut center = self.start + self.length / 2f64; 54 | self.length = self.min; 55 | 56 | // Compute if the new center is appropriate or not anymore 57 | if center + (self.length / 2f64) > 1f64 { 58 | center = 1f64 - (self.length / 2f64); 59 | } else if center - (self.length / 2f64) < 0f64 { 60 | center = self.length / 2f64; 61 | } 62 | 63 | self.start = center - self.length / 2f64; 64 | } 65 | 66 | pub fn zoom_in(&mut self) { 67 | let center = self.start + self.length / 2f64; 68 | 69 | self.length = if self.length * ZOOM_FACTOR <= self.min { 70 | self.min 71 | } else { 72 | self.length * ZOOM_FACTOR 73 | }; 74 | 75 | self.start = center - self.length / 2f64; 76 | } 77 | 78 | pub fn zoom_out(&mut self) { 79 | let mut center = self.start + self.length / 2f64; 80 | 81 | self.length = if self.length / ZOOM_FACTOR > 1f64 { 82 | 1f64 83 | } else { 84 | self.length / ZOOM_FACTOR 85 | }; 86 | 87 | // Compute if the new center is appropriate or not anymore 88 | if center + (self.length / 2f64) > 1f64 { 89 | center = 1f64 - (self.length / 2f64); 90 | } else if center - (self.length / 2f64) < 0f64 { 91 | center = self.length / 2f64; 92 | } 93 | 94 | self.start = center - self.length / 2f64; 95 | } 96 | 97 | pub fn move_left(&mut self) { 98 | let offset = self.length / 10f64; 99 | 100 | self.start = if self.start - offset < 0f64 { 101 | 0f64 102 | } else { 103 | self.start - offset 104 | } 105 | } 106 | 107 | pub fn move_right(&mut self) { 108 | let offset = self.length / 10f64; 109 | 110 | self.start = if self.start + self.length + offset > 1f64 { 111 | 1f64 - self.length 112 | } else { 113 | self.start + offset 114 | } 115 | } 116 | } 117 | 118 | #[cfg(test)] 119 | mod tests { 120 | use super::*; 121 | use rand::Rng; 122 | 123 | const VALID_MAX_VALUES: &[f64] = &[0.5f64, 1.0f64, 3.0f64, 0.000000001f64, 0.99999999f64]; 124 | const INVALID_MAX_VALUES: &[f64] = &[0f64, -1.0f64, -3.0f64, -0.000000001f64, -0.99999999f64]; 125 | 126 | const MAX_TOP_VALUE: f64 = f64::MAX; 127 | const MAX_BTM_VALUE: f64 = f64::EPSILON; 128 | 129 | fn valid_values(rand_count: usize) -> Vec { 130 | let mut rng = rand::thread_rng(); 131 | 132 | let mut vec = vec![(rng.gen::() + MAX_BTM_VALUE) * MAX_TOP_VALUE; rand_count]; 133 | vec.extend_from_slice(VALID_MAX_VALUES); 134 | vec 135 | } 136 | 137 | #[test] 138 | fn inits() { 139 | for max in valid_values(1000) { 140 | Zoom::new(max).unwrap(); 141 | } 142 | } 143 | 144 | #[test] 145 | #[should_panic] 146 | fn bad_init() { 147 | for max in INVALID_MAX_VALUES { 148 | Zoom::new(*max).unwrap(); 149 | } 150 | } 151 | 152 | #[test] 153 | fn check_init() { 154 | for max in valid_values(1000) { 155 | let z = Zoom::new(max).unwrap(); 156 | assert_eq!(z.length(), 1f64); 157 | assert_eq!(z.start(), 0f64) 158 | } 159 | } 160 | 161 | #[test] 162 | fn check_update_zoom_max() { 163 | for max in valid_values(1000) { 164 | let mut z = Zoom::new(max).unwrap(); 165 | 166 | for new_max in valid_values(50) { 167 | z.update_zoom_max(new_max); 168 | assert!(z.start() >= 0f64); 169 | assert!(z.start() < 1f64); 170 | assert!(z.length() <= 1f64); 171 | assert!(z.length() > 0f64); 172 | } 173 | } 174 | } 175 | 176 | #[test] 177 | fn check_zoom_in() { 178 | for max in valid_values(1000) { 179 | let mut z = Zoom::new(max).unwrap(); 180 | 181 | for _ in 0..1000 { 182 | z.zoom_in(); 183 | assert!(z.start() >= 0f64); 184 | assert!(z.start() < 1f64); 185 | assert!(z.length() <= 1f64); 186 | assert!(z.length() > 0f64); 187 | } 188 | } 189 | } 190 | 191 | #[test] 192 | fn check_zoom_out() { 193 | for max in valid_values(1000) { 194 | let mut z = Zoom::new(max).unwrap(); 195 | 196 | for _ in 0..1000 { 197 | z.zoom_out(); 198 | assert!(z.start() >= 0f64); 199 | assert!(z.start() < 1f64); 200 | assert!(z.length() <= 1f64); 201 | assert!(z.length() > 0f64); 202 | } 203 | } 204 | } 205 | 206 | #[test] 207 | fn check_fuzz_zoom_io() { 208 | let mut rng = rand::thread_rng(); 209 | 210 | for max in valid_values(100) { 211 | let mut z = Zoom::new(max).unwrap(); 212 | 213 | for _ in 0..1000 { 214 | match rng.gen::() % 2 { 215 | 0 => z.zoom_in(), 216 | 1 => z.zoom_out(), 217 | _ => unreachable!(), 218 | } 219 | } 220 | 221 | assert!(z.start() >= 0f64); 222 | assert!(z.start() < 1f64); 223 | assert!(z.length() <= 1f64); 224 | assert!(z.length() > 0f64); 225 | } 226 | } 227 | 228 | #[test] 229 | fn check_move_left() { 230 | for max in valid_values(1000) { 231 | let mut z = Zoom::new(max).unwrap(); 232 | 233 | for _ in 0..1000 { 234 | z.move_left(); 235 | assert!(z.start() >= 0f64); 236 | assert!(z.start() < 1f64); 237 | assert!(z.length() <= 1f64); 238 | assert!(z.length() > 0f64); 239 | } 240 | } 241 | } 242 | 243 | #[test] 244 | fn check_move_right() { 245 | for max in valid_values(1000) { 246 | let mut z = Zoom::new(max).unwrap(); 247 | 248 | for _ in 0..1000 { 249 | z.move_right(); 250 | assert!(z.start() >= 0f64); 251 | assert!(z.start() < 1f64); 252 | assert!(z.length() <= 1f64); 253 | assert!(z.length() > 0f64); 254 | } 255 | } 256 | } 257 | 258 | #[test] 259 | fn check_fuzz_move() { 260 | let mut rng = rand::thread_rng(); 261 | 262 | for max in valid_values(100) { 263 | let mut z = Zoom::new(max).unwrap(); 264 | 265 | for _ in 0..1000 { 266 | match rng.gen::() % 2 { 267 | 0 => z.move_left(), 268 | 1 => z.move_right(), 269 | _ => unreachable!(), 270 | } 271 | } 272 | 273 | assert!(z.start() >= 0f64); 274 | assert!(z.start() < 1f64); 275 | assert!(z.length() <= 1f64); 276 | assert!(z.length() > 0f64); 277 | } 278 | } 279 | 280 | #[test] 281 | fn check_fuzz_all() { 282 | let mut rng = rand::thread_rng(); 283 | 284 | for max in valid_values(100) { 285 | let mut z = Zoom::new(max).unwrap(); 286 | 287 | for _ in 0..1000 { 288 | match rng.gen::() % 4 { 289 | 0 => z.move_left(), 290 | 1 => z.move_right(), 291 | 2 => z.zoom_in(), 292 | 3 => z.zoom_out(), 293 | _ => unreachable!(), 294 | } 295 | } 296 | 297 | assert!(z.start() >= 0f64); 298 | assert!(z.start() < 1f64); 299 | assert!(z.length() <= 1f64); 300 | assert!(z.length() > 0f64); 301 | } 302 | } 303 | } 304 | -------------------------------------------------------------------------------- /src/dsp/waveform.rs: -------------------------------------------------------------------------------- 1 | extern crate sndfile; 2 | use sndfile::SndFileIO; 3 | 4 | use crate::sndfile::SndFile; 5 | use std::io::SeekFrom; 6 | 7 | use std::convert::TryFrom; 8 | 9 | use rayon::prelude::*; 10 | 11 | use super::{DspData, DspErr}; 12 | use crate::utils::Zoom; 13 | 14 | fn compute_point(frames: &[i32]) -> WaveformPoint { 15 | let mut min = 0i32; 16 | let mut max = 0i32; 17 | let mut sum = 0f64; 18 | 19 | for elm in frames { 20 | sum += (*elm as f64) * (*elm as f64); 21 | if *elm > max { 22 | max = *elm; 23 | } else if *elm < min { 24 | min = *elm; 25 | } 26 | } 27 | 28 | WaveformPoint { 29 | rms: (sum / frames.len() as f64).sqrt() as i32, 30 | peak_min: min, 31 | peak_max: max, 32 | } 33 | } 34 | 35 | pub struct Waveform { 36 | frames: Vec>, 37 | } 38 | 39 | #[derive(Default, Debug, Copy, Clone)] 40 | pub struct WaveformPoint { 41 | pub rms: T, 42 | pub peak_min: T, 43 | pub peak_max: T, 44 | } 45 | 46 | #[derive(Default)] 47 | pub struct WaveformParameters; 48 | 49 | impl DspData for Waveform { 50 | fn new( 51 | mut sndfile: SndFile, 52 | _: WaveformParameters, 53 | norm: Option, 54 | ) -> Result { 55 | // Compute block size 56 | let frames = sndfile.len().expect("Unable to retrieve number of frames"); 57 | sndfile.seek(SeekFrom::Start(0)).expect("Failed to seek 0"); 58 | let block_size = 4096usize; 59 | let block_count = if frames % block_size as u64 == 0 { 60 | usize::try_from(frames / block_size as u64).unwrap() 61 | } else { 62 | usize::try_from(frames / block_size as u64 + 1).unwrap() 63 | }; 64 | let channels = sndfile.get_channels(); 65 | 66 | // Create data vectors 67 | let mut data = Waveform { 68 | frames: vec![vec![0i32; usize::try_from(frames).unwrap()]; channels], 69 | }; 70 | let mut block_data: Vec = vec![0; block_size * channels]; 71 | 72 | // Find min and max for each block 73 | for block_idx in 0..block_count { 74 | // Read block from file 75 | // let mut nb_frames: usize = 0; 76 | let read = sndfile.read_to_slice(block_data.as_mut_slice()); 77 | let nb_frames = match read { 78 | Ok(frames) => { 79 | if frames == 0 { 80 | panic!("0 frames read") 81 | } 82 | frames 83 | } 84 | Err(err) => panic!("{:?}", err), 85 | }; 86 | 87 | // Load into frames vector 88 | let frame_offset = block_idx * block_size; 89 | { 90 | let interleaved_slice = &block_data[..nb_frames * channels]; 91 | // let mono_slices = data.frames.iter_mut().map(|channel| { 92 | // channel.as_mut_slice()[frame_idx..frame_idx + interleaved_slice.len()] 93 | // }).collect(); 94 | 95 | // We could use dynamic dispatch to automatically switch btw 96 | // the different evaluation method (norm / no-norm) but it would 97 | // surely slow it down. 98 | // TODO: benchmark ? 99 | match norm { 100 | Some(fnorm) => { 101 | let fnorm_inv = 1f64 / fnorm; 102 | 103 | interleaved_slice 104 | .chunks_exact(channels) 105 | .enumerate() 106 | .for_each(|(frame_idx, samples)| { 107 | for (channel, value) in samples.iter().enumerate() { 108 | data.frames[channel][frame_offset + frame_idx] = 109 | (*value as f64 * fnorm_inv) as i32; 110 | } 111 | }); 112 | } 113 | None => { 114 | interleaved_slice 115 | .chunks_exact(channels) 116 | .enumerate() 117 | .for_each(|(frame_idx, samples)| { 118 | for (channel, value) in samples.iter().enumerate() { 119 | data.frames[channel][frame_offset + frame_idx] = *value; 120 | } 121 | }); 122 | } 123 | } 124 | } 125 | } 126 | 127 | Ok(data) 128 | } 129 | } 130 | 131 | impl Waveform { 132 | pub fn compute_points( 133 | &self, 134 | channel: usize, 135 | block_count: usize, 136 | zoom: &Zoom, 137 | ) -> Vec> { 138 | // Alloc vectors 139 | let mut points = vec![WaveformPoint::default(); block_count]; 140 | 141 | // Compute block size and count 142 | let total_frames = self.frames[0].len(); 143 | let start = (total_frames as f64 * zoom.start()) as usize; 144 | let end = (total_frames as f64 * (zoom.start() + zoom.length())) as usize; 145 | let rendered_frames = end - start; 146 | let block_size = if rendered_frames % block_count == 0 { 147 | rendered_frames / block_count 148 | } else { 149 | rendered_frames / block_count + 1 150 | }; 151 | 152 | let samples_chunks = self.frames[channel][start..end].par_chunks_exact(block_size); 153 | let remains = samples_chunks.remainder(); 154 | 155 | points[..block_count] 156 | .par_iter_mut() 157 | .zip(samples_chunks.into_par_iter()) 158 | .for_each(|(point, chunk)| { 159 | *point = compute_point(chunk); 160 | }); 161 | 162 | if !remains.is_empty() { 163 | // Consume the end 164 | points[block_count - 1] = compute_point(remains); 165 | } 166 | 167 | points 168 | } 169 | } 170 | 171 | #[cfg(test)] 172 | mod tests { 173 | use crate::dsp::{AsyncDspData, AsyncDspDataState, DspData, Waveform, WaveformParameters}; 174 | use crate::Zoom; 175 | use sndfile; 176 | use std::path::{Path, PathBuf}; 177 | use std::thread::sleep; 178 | use std::time::Duration; 179 | 180 | fn get_test_files_location() -> PathBuf { 181 | return Path::new(&env!("CARGO_MANIFEST_DIR").to_string()) 182 | .join("tests") 183 | .join("files"); 184 | } 185 | 186 | #[test] 187 | fn build() { 188 | for norm in [None, Some(1.1f64)] { 189 | let snd = sndfile::OpenOptions::ReadOnly(sndfile::ReadOptions::Auto) 190 | .from_path(get_test_files_location().join("rock_1s.wav")) 191 | .unwrap(); 192 | 193 | Waveform::new(snd, WaveformParameters::default(), norm).unwrap(); 194 | } 195 | } 196 | 197 | #[test] 198 | fn async_build() { 199 | let sleep_interval = Duration::new(1, 0); 200 | let path = get_test_files_location().join("rock_1s.wav"); 201 | 202 | let mut async_data: AsyncDspData = 203 | AsyncDspData::new(&path, WaveformParameters::default(), false); 204 | let mut attempts = 0; 205 | 206 | loop { 207 | sleep(sleep_interval); 208 | async_data.update_status(); 209 | let state = async_data.state(); 210 | 211 | assert_ne!(state, AsyncDspDataState::Failed); 212 | assert!(attempts < 30); 213 | 214 | if state == AsyncDspDataState::Finished { 215 | break; 216 | } 217 | attempts += 1; 218 | } 219 | } 220 | 221 | #[test] 222 | fn async_build_normalize() { 223 | let sleep_interval = Duration::new(1, 0); 224 | let path = get_test_files_location().join("rock_1s.wav"); 225 | 226 | let mut async_data: AsyncDspData = 227 | AsyncDspData::new(&path, WaveformParameters::default(), true); 228 | let mut attempts = 0; 229 | 230 | loop { 231 | sleep(sleep_interval); 232 | async_data.update_status(); 233 | let state = async_data.state(); 234 | 235 | assert_ne!(state, AsyncDspDataState::Failed); 236 | assert!(attempts < 30); 237 | 238 | if state == AsyncDspDataState::Finished { 239 | break; 240 | } 241 | attempts += 1; 242 | } 243 | } 244 | 245 | #[test] 246 | fn compute_points() { 247 | let snd = sndfile::OpenOptions::ReadOnly(sndfile::ReadOptions::Auto) 248 | .from_path(get_test_files_location().join("rock_1s.wav")) 249 | .unwrap(); 250 | let channels = snd.get_channels(); 251 | let mut zoom = Zoom::new(0.5f64).unwrap(); 252 | let blocks_counts = [50usize, 100, 128, 150, 1024]; 253 | 254 | let waveform = Waveform::new(snd, WaveformParameters::default(), None).unwrap(); 255 | 256 | // No zoom 257 | for ch_idx in 0..channels { 258 | for block_count in blocks_counts { 259 | let points = waveform.compute_points(ch_idx, block_count, &zoom); 260 | 261 | assert_eq!(block_count, points.len()); 262 | } 263 | } 264 | 265 | // Zoom in and move 266 | for _ in 0..10 { 267 | zoom.zoom_in(); 268 | zoom.move_right(); 269 | 270 | for ch_idx in 0..channels { 271 | for block_count in blocks_counts { 272 | let points = waveform.compute_points(ch_idx, block_count, &zoom); 273 | 274 | assert_eq!(block_count, points.len()); 275 | } 276 | } 277 | } 278 | } 279 | } 280 | -------------------------------------------------------------------------------- /src/render/metadata.rs: -------------------------------------------------------------------------------- 1 | use sndfile::{Endian, MajorFormat, SubtypeFormat}; 2 | use tui::style::{Modifier, Style}; 3 | use tui::text::Span; 4 | // use sndfile::TagType; 5 | use std::convert::TryFrom; 6 | use tui::layout::{Alignment, Constraint, Direction, Layout, Rect}; 7 | use tui::widgets::{Block, Borders, Paragraph}; 8 | use tui::Frame; 9 | use tui::{backend::Backend, text::Spans}; 10 | 11 | extern crate sndfile; 12 | use super::{Renderer, RenderingInfo}; 13 | use crate::sndfile::TagType; 14 | 15 | fn format_to_string(fmt: MajorFormat) -> String { 16 | match fmt { 17 | MajorFormat::WAV => "wav", 18 | MajorFormat::AIFF => "aiff", 19 | MajorFormat::AU => "au", 20 | MajorFormat::RAW => "raw", 21 | MajorFormat::PAF => "paf", 22 | MajorFormat::SVX => "svx", 23 | MajorFormat::NIST => "nist", 24 | MajorFormat::VOC => "voc", 25 | MajorFormat::IRCAM => "ircam", 26 | MajorFormat::W64 => "w64", 27 | MajorFormat::MAT4 => "mat4", 28 | MajorFormat::MAT5 => "mat5", 29 | MajorFormat::PVF => "pvf", 30 | MajorFormat::XI => "xi", 31 | MajorFormat::HTK => "htk", 32 | MajorFormat::SDS => "sds", 33 | MajorFormat::AVR => "avr", 34 | MajorFormat::WAVEX => "wavex", 35 | MajorFormat::SD2 => "sd2", 36 | MajorFormat::FLAC => "flac", 37 | MajorFormat::CAF => "caf", 38 | MajorFormat::WVE => "wve", 39 | MajorFormat::OGG => "ogg", 40 | MajorFormat::MPC2K => "mpc2k", 41 | MajorFormat::RF64 => "rf64", 42 | } 43 | .to_string() 44 | } 45 | 46 | fn subtype_to_string(fmt: SubtypeFormat) -> String { 47 | match fmt { 48 | SubtypeFormat::PCM_S8 => "PCM 8-bit signed", 49 | SubtypeFormat::PCM_16 => "PCM 16-bit signed", 50 | SubtypeFormat::PCM_24 => "PCM 24-bit signed", 51 | SubtypeFormat::PCM_32 => "PCM 32-bit signed", 52 | SubtypeFormat::PCM_U8 => "PCM 8-bit unsigned", 53 | SubtypeFormat::FLOAT => "Single precision floating point (f32)", 54 | SubtypeFormat::DOUBLE => "Double precision floating point (f64)", 55 | SubtypeFormat::ULAW => "u-law", 56 | SubtypeFormat::ALAW => "A-law", 57 | SubtypeFormat::IMA_ADPCM => "IMA/DVI ADPCM", 58 | SubtypeFormat::MS_ADPCM => "Microsoft ADPCM", 59 | SubtypeFormat::GSM610 => "GSM 06.10", 60 | SubtypeFormat::VOX_ADPCM => "ADPCM", 61 | SubtypeFormat::G721_32 => "CCITT G.721 (ADPCM 32kbits/s)", 62 | SubtypeFormat::G723_24 => "CCITT G.723 (ADPCM 24kbits/s)", 63 | SubtypeFormat::G723_40 => "CCITT G.723 (ADPCM 40kbits/s)", 64 | SubtypeFormat::DWVW_12 => "DWVW 12-bit", 65 | SubtypeFormat::DWVW_16 => "DWVW 16-bit", 66 | SubtypeFormat::DWVW_24 => "DWVW 24-bit", 67 | SubtypeFormat::DWVW_N => "DWVW N-bit", 68 | SubtypeFormat::DPCM_8 => "DPCM 8-bit", 69 | SubtypeFormat::DPCM_16 => "DPCM 16-bit", 70 | SubtypeFormat::VORBIS => "Vorbis", 71 | SubtypeFormat::ALAC_16 => "ALAC 16-bit", 72 | SubtypeFormat::ALAC_20 => "ALAC 20-bit", 73 | SubtypeFormat::ALAC_24 => "ALAC 24-bit", 74 | SubtypeFormat::ALAC_32 => "ALAC 32-bit", 75 | } 76 | .to_string() 77 | } 78 | 79 | fn channel_layout_to_string(channels: usize) -> String { 80 | match channels { 81 | 1 => "mono", 82 | 2 => "stereo", 83 | 3 => "ambinosic 2.1", 84 | 4 => "quadrophonic", 85 | 5 => "ambisonic 5", 86 | 6 => "ambisonic 5.1", 87 | 7 => "ambisonic 7", 88 | 8 => "ambisonic 7.1", 89 | _ => panic!("Unsupported channel layout"), 90 | } 91 | .to_string() 92 | } 93 | 94 | fn endianess_to_string(endianess: Endian) -> String { 95 | match endianess { 96 | Endian::Little => "Forced little endian", 97 | Endian::Big => "Forced big endian", 98 | Endian::File => "default", 99 | _ => panic!("Unsupported endianess"), 100 | } 101 | .to_string() 102 | } 103 | 104 | fn compute_duration_string(samplerate: f64, frames: f64) -> String { 105 | let duration = frames / samplerate; 106 | 107 | let duration_h = f64::floor(duration / 3600f64); 108 | let duration_m = f64::floor(duration / 60f64); 109 | let duration_s = duration % 60f64; 110 | 111 | format!( 112 | "{:02.0}:{:02.0}:{:02.2}", 113 | duration_h, duration_m, duration_s 114 | ) 115 | } 116 | 117 | struct Metadata { 118 | // Format data 119 | samplerate: String, 120 | channel_layout: String, 121 | format: String, 122 | subtype: String, 123 | endianess: String, 124 | frames: String, 125 | duration: String, 126 | // Tags 127 | title: String, 128 | copyright: String, 129 | software: String, 130 | artist: String, 131 | comment: String, 132 | date: String, 133 | album: String, 134 | license: String, 135 | track_number: String, 136 | genre: String, 137 | } 138 | 139 | impl Metadata { 140 | fn new(path: &std::path::PathBuf) -> Self { 141 | let mut snd = sndfile::OpenOptions::ReadOnly(sndfile::ReadOptions::Auto) 142 | .from_path(path) 143 | .unwrap(); 144 | 145 | let default_msg = "N/A"; 146 | 147 | Metadata { 148 | samplerate: snd.get_samplerate().to_string(), 149 | channel_layout: channel_layout_to_string(snd.get_channels()), 150 | format: format_to_string(snd.get_major_format()), 151 | subtype: subtype_to_string(snd.get_subtype_format()), 152 | endianess: endianess_to_string(snd.get_endian()), 153 | frames: snd.len().unwrap().to_string(), 154 | duration: compute_duration_string( 155 | snd.get_samplerate() as f64, 156 | snd.len().unwrap() as f64, 157 | ), 158 | title: snd 159 | .get_tag(TagType::Title) 160 | .unwrap_or_else(|| default_msg.to_string()), 161 | copyright: snd 162 | .get_tag(TagType::Copyright) 163 | .unwrap_or_else(|| default_msg.to_string()), 164 | artist: snd 165 | .get_tag(TagType::Artist) 166 | .unwrap_or_else(|| default_msg.to_string()), 167 | software: snd 168 | .get_tag(TagType::Software) 169 | .unwrap_or_else(|| default_msg.to_string()), 170 | comment: snd 171 | .get_tag(TagType::Comment) 172 | .unwrap_or_else(|| default_msg.to_string()), 173 | date: snd 174 | .get_tag(TagType::Date) 175 | .unwrap_or_else(|| default_msg.to_string()), 176 | album: snd 177 | .get_tag(TagType::Album) 178 | .unwrap_or_else(|| default_msg.to_string()), 179 | license: snd 180 | .get_tag(TagType::License) 181 | .unwrap_or_else(|| default_msg.to_string()), 182 | track_number: snd 183 | .get_tag(TagType::Tracknumber) 184 | .unwrap_or_else(|| default_msg.to_string()), 185 | genre: snd 186 | .get_tag(TagType::Genre) 187 | .unwrap_or_else(|| default_msg.to_string()), 188 | } 189 | } 190 | } 191 | 192 | pub struct MetadataRenderer { 193 | metadata: Metadata, 194 | redraw: bool, 195 | } 196 | 197 | impl MetadataRenderer { 198 | pub fn new(path: &std::path::PathBuf) -> Self { 199 | MetadataRenderer { 200 | metadata: Metadata::new(path), 201 | redraw: true, 202 | } 203 | } 204 | } 205 | 206 | impl Renderer for MetadataRenderer { 207 | fn draw(&mut self, frame: &mut Frame<'_, B>, _: &RenderingInfo, area: Rect) { 208 | let name_style = Style::default().add_modifier(Modifier::BOLD); 209 | let value_style = Style::default(); 210 | 211 | // properties 212 | let properties = [ 213 | ("Format", &self.metadata.format), 214 | ("Format subtype", &self.metadata.subtype), 215 | ("Endianess", &self.metadata.endianess), 216 | ("Samplerate", &self.metadata.samplerate), 217 | ("Channel layout", &self.metadata.channel_layout), 218 | ("Frames", &self.metadata.frames), 219 | ("Duration", &self.metadata.duration), 220 | ]; 221 | 222 | // tags 223 | let tags = vec![ 224 | ("Title", &self.metadata.title), 225 | ("Copyright", &self.metadata.copyright), 226 | ("Encoder", &self.metadata.software), 227 | ("Artist", &self.metadata.artist), 228 | ("Comment", &self.metadata.comment), 229 | ("Date", &self.metadata.date), 230 | ("Album", &self.metadata.album), 231 | ("License", &self.metadata.license), 232 | ("Track number", &self.metadata.track_number), 233 | ("Genre", &self.metadata.genre), 234 | ("Date", &self.metadata.date), 235 | ]; 236 | 237 | // Layouts 238 | let constraints = vec![ 239 | Constraint::Length(u16::try_from(properties.len()).unwrap() + 2u16), 240 | Constraint::Min(u16::try_from(tags.len()).unwrap() + 2u16), 241 | ]; 242 | let layout = Layout::default() 243 | .direction(Direction::Vertical) 244 | .constraints::<&[Constraint]>(constraints.as_ref()) 245 | .split(area); 246 | 247 | // Build the spans 248 | let properties_spans: Vec = properties 249 | .iter() 250 | .map(|(name, value)| { 251 | Spans::from(vec![ 252 | Span::styled(*name, name_style), 253 | Span::raw(" : "), 254 | Span::styled(*value, value_style), 255 | ]) 256 | }) 257 | .collect(); 258 | let tags_spans: Vec = tags 259 | .iter() 260 | .map(|(name, value)| { 261 | Spans::from(vec![ 262 | Span::styled(*name, name_style), 263 | Span::raw(" : "), 264 | Span::styled(*value, value_style), 265 | ]) 266 | }) 267 | .collect(); 268 | 269 | // build paragraph and render 270 | let properties_paragraph = Paragraph::new(properties_spans) 271 | .block(Block::default().title("Properties").borders(Borders::ALL)) 272 | .alignment(Alignment::Left); 273 | frame.render_widget(properties_paragraph, layout[0]); 274 | 275 | let tags_paragraph = Paragraph::new(tags_spans) 276 | .block(Block::default().title("Tags").borders(Borders::ALL)) 277 | .alignment(Alignment::Left); 278 | frame.render_widget(tags_paragraph, layout[1]); 279 | 280 | self.redraw = false 281 | } 282 | 283 | fn needs_redraw(&mut self) -> bool { 284 | self.redraw 285 | } 286 | } 287 | -------------------------------------------------------------------------------- /src/dsp/spectrogram.rs: -------------------------------------------------------------------------------- 1 | extern crate sndfile; 2 | use crate::sndfile::SndFile; 3 | use realfft::RealFftPlanner; 4 | 5 | use super::time_window::{SidePaddingType, TimeWindowBatcher, WindowType}; 6 | use super::{DspData, DspErr}; 7 | use crate::utils::Zoom; 8 | 9 | use colorgrad::{inferno, Gradient}; 10 | 11 | #[inline(always)] 12 | fn db_to_u8x4(db: f64, threshold: f64, gradient: &Gradient) -> [u8; 4] { 13 | let grad_pos = if db > 0f64 { 14 | 1f64 15 | } else if db < threshold { 16 | 0f64 17 | } else { 18 | (db - threshold) / -threshold 19 | }; 20 | 21 | gradient.at(grad_pos).to_rgba8() 22 | } 23 | 24 | /// Ordered vertically and by channel. Each channel vector contains contiguous 25 | /// frequency bins 26 | pub struct Spectrogram { 27 | num_bands: usize, 28 | num_bins: usize, 29 | // Ordered by [channel] 30 | color_frames: Vec>, 31 | } 32 | 33 | pub struct SpectrogramParameters { 34 | pub window_size: usize, 35 | pub overlap_rate: f64, 36 | pub db_threshold: f64, 37 | pub window_type: WindowType, 38 | pub side_padding_type: SidePaddingType, 39 | } 40 | 41 | impl DspData for Spectrogram { 42 | fn new( 43 | sndfile: SndFile, 44 | parameters: SpectrogramParameters, 45 | norm: Option, 46 | ) -> Result { 47 | let channels = sndfile.get_channels(); 48 | let mut window_batcher = match TimeWindowBatcher::new( 49 | sndfile, 50 | parameters.window_size, 51 | parameters.overlap_rate, 52 | parameters.window_type, 53 | parameters.side_padding_type, 54 | ) { 55 | Ok(batcher) => batcher, 56 | Err(err) => return Err(err), 57 | }; 58 | if parameters.db_threshold > 0f64 { 59 | return Err(DspErr::new("dB threshold should be a negative value")); 60 | } 61 | let num_bins = parameters.window_size / 2; 62 | let num_bands = window_batcher.get_num_bands(); 63 | 64 | // Allocate the memory for the u8 spectrograms 65 | let mut spectrograms_u8x4 = vec![vec![0u8; num_bands * num_bins * 3]; channels]; 66 | let gradient = inferno(); 67 | 68 | // Plan the fft 69 | let mut planner = RealFftPlanner::::new(); 70 | let r2c = planner.plan_fft_forward(parameters.window_size); 71 | let mut spectrum = r2c.make_output_vec(); 72 | let mut scratch = r2c.make_scratch_vec(); 73 | 74 | // Compute the Spectrogram 75 | let mut batch_idx = 0usize; 76 | let fft_len = parameters.window_size as f64 / 2f64; 77 | let correction_factor = parameters.window_type.correction_factor(); 78 | 79 | while let Some(mut batchs) = window_batcher.get_next_batch() { 80 | // Iterate over each channel 81 | for (ch_idx, mono_batch) in batchs.iter_mut().enumerate() { 82 | // Process the FFT 83 | r2c.process_with_scratch(mono_batch, &mut spectrum, &mut scratch) 84 | .unwrap(); 85 | 86 | let u8x3_spectrogram_slice = &mut spectrograms_u8x4[ch_idx] 87 | [batch_idx * (num_bins) * 3..(batch_idx + 1) * (num_bins) * 3]; 88 | 89 | // Compute the magnitude and reduce it to u8 90 | match norm { 91 | Some(fnorm) => { 92 | let fnorm_inv = 1f64 / fnorm; 93 | spectrum[1..num_bins + 1] 94 | .iter() 95 | .enumerate() 96 | .for_each(|(fidx, value)| { 97 | let bin_amp = 98 | (value * correction_factor * fnorm_inv / fft_len).norm_sqr(); 99 | let db_bin_amp = 10f64 * f64::log10(bin_amp + f64::EPSILON); 100 | let color = 101 | db_to_u8x4(db_bin_amp, parameters.db_threshold, &gradient); 102 | u8x3_spectrogram_slice[fidx * 3] = color[0]; 103 | u8x3_spectrogram_slice[fidx * 3 + 1] = color[1]; 104 | u8x3_spectrogram_slice[fidx * 3 + 2] = color[2]; 105 | }); 106 | } 107 | None => { 108 | spectrum[1..num_bins + 1] 109 | .iter() 110 | .enumerate() 111 | .for_each(|(fidx, value)| { 112 | let bin_amp = (value * correction_factor / fft_len).norm_sqr(); 113 | let db_bin_amp = 10f64 * f64::log10(bin_amp + f64::EPSILON); 114 | let color = 115 | db_to_u8x4(db_bin_amp, parameters.db_threshold, &gradient); 116 | u8x3_spectrogram_slice[fidx * 3] = color[0]; 117 | u8x3_spectrogram_slice[fidx * 3 + 1] = color[1]; 118 | u8x3_spectrogram_slice[fidx * 3 + 2] = color[2]; 119 | }); 120 | } 121 | } 122 | } 123 | 124 | batch_idx += 1; 125 | } 126 | 127 | Ok(Spectrogram { 128 | num_bands, 129 | num_bins, 130 | // frames: spectrograms_u8, 131 | color_frames: spectrograms_u8x4, 132 | }) 133 | } 134 | } 135 | 136 | impl Spectrogram { 137 | pub fn data(&mut self, channel: usize, zoom: &Zoom) -> (&mut [u8], usize) { 138 | let start = (self.num_bands as f64 * zoom.start()) as usize; 139 | let end = (self.num_bands as f64 * (zoom.start() + zoom.length())) as usize; 140 | 141 | ( 142 | &mut self.color_frames[channel][start * self.num_bins * 3..end * self.num_bins * 3], 143 | end - start, 144 | ) 145 | } 146 | 147 | pub fn num_bins(&self) -> usize { 148 | self.num_bins 149 | } 150 | } 151 | 152 | #[cfg(test)] 153 | mod tests { 154 | use crate::dsp::{ 155 | AsyncDspData, AsyncDspDataState, DspData, SidePaddingType, Spectrogram, 156 | SpectrogramParameters, WindowType, 157 | }; 158 | use crate::Zoom; 159 | use sndfile; 160 | use std::path::{Path, PathBuf}; 161 | use std::thread::sleep; 162 | use std::time::Duration; 163 | 164 | fn get_test_files_location() -> PathBuf { 165 | return Path::new(&env!("CARGO_MANIFEST_DIR").to_string()) 166 | .join("tests") 167 | .join("files"); 168 | } 169 | 170 | #[test] 171 | fn build() { 172 | let overlaps = [0.25f64, 0.5f64, 0.75f64]; 173 | let windows = [512usize, 1024, 2048, 4096]; 174 | let window_types = [ 175 | WindowType::Hamming, 176 | WindowType::Blackman, 177 | WindowType::Hanning, 178 | WindowType::Uniform, 179 | ]; 180 | let db_thresholds = [-130f64, -80f64, -30f64]; 181 | let side_paddings = [ 182 | SidePaddingType::Loop, 183 | SidePaddingType::SmoothRamp, 184 | SidePaddingType::Zeros, 185 | ]; 186 | 187 | for overlap in overlaps { 188 | for window_size in windows { 189 | for wtype in window_types { 190 | for db_th in db_thresholds { 191 | for padding_type in side_paddings { 192 | for norm in [None, Some(1.1f64)] { 193 | let parameters = SpectrogramParameters { 194 | window_size, 195 | overlap_rate: overlap, 196 | window_type: wtype, 197 | db_threshold: db_th, 198 | side_padding_type: padding_type, 199 | }; 200 | 201 | let snd = 202 | sndfile::OpenOptions::ReadOnly(sndfile::ReadOptions::Auto) 203 | .from_path(get_test_files_location().join("rock_1s.wav")) 204 | .unwrap(); 205 | Spectrogram::new(snd, parameters, norm).unwrap(); 206 | } 207 | } 208 | } 209 | } 210 | } 211 | } 212 | } 213 | 214 | #[test] 215 | fn async_build() { 216 | const OVERLAP: f64 = 0.25f64; 217 | const WINDOW_SIZE: usize = 4096; 218 | const DB_THRESHOLD: f64 = -130f64; 219 | let sleep_interval = Duration::new(1, 0); 220 | 221 | let parameters = SpectrogramParameters { 222 | window_size: WINDOW_SIZE, 223 | overlap_rate: OVERLAP, 224 | window_type: WindowType::Hanning, 225 | db_threshold: DB_THRESHOLD, 226 | side_padding_type: SidePaddingType::Zeros, 227 | }; 228 | let path = get_test_files_location().join("rock_1s.wav"); 229 | 230 | let mut async_data: AsyncDspData = 231 | AsyncDspData::new(&path, parameters, false); 232 | let mut attempts = 0; 233 | 234 | loop { 235 | sleep(sleep_interval); 236 | async_data.update_status(); 237 | let state = async_data.state(); 238 | 239 | assert_ne!(state, AsyncDspDataState::Failed); 240 | assert!(attempts < 90); 241 | 242 | if state == AsyncDspDataState::Finished { 243 | break; 244 | } 245 | attempts += 1; 246 | } 247 | } 248 | 249 | #[test] 250 | fn async_build_normalize() { 251 | const OVERLAP: f64 = 0.25f64; 252 | const WINDOW_SIZE: usize = 4096; 253 | const DB_THRESHOLD: f64 = -130f64; 254 | let sleep_interval = Duration::new(1, 0); 255 | 256 | let parameters = SpectrogramParameters { 257 | window_size: WINDOW_SIZE, 258 | overlap_rate: OVERLAP, 259 | window_type: WindowType::Hanning, 260 | db_threshold: DB_THRESHOLD, 261 | side_padding_type: SidePaddingType::Zeros, 262 | }; 263 | let path = get_test_files_location().join("rock_1s.wav"); 264 | 265 | let mut async_data: AsyncDspData = 266 | AsyncDspData::new(&path, parameters, true); 267 | let mut attempts = 0; 268 | 269 | loop { 270 | sleep(sleep_interval); 271 | async_data.update_status(); 272 | let state = async_data.state(); 273 | 274 | assert_ne!(state, AsyncDspDataState::Failed); 275 | assert!(attempts < 90); 276 | 277 | if state == AsyncDspDataState::Finished { 278 | break; 279 | } 280 | attempts += 1; 281 | } 282 | } 283 | 284 | #[test] 285 | fn get_data() { 286 | const OVERLAP: f64 = 0.25f64; 287 | const WINDOW_SIZE: usize = 4096; 288 | const DB_THRESHOLD: f64 = -130f64; 289 | 290 | let parameters = SpectrogramParameters { 291 | window_size: WINDOW_SIZE, 292 | overlap_rate: OVERLAP, 293 | window_type: WindowType::Hanning, 294 | db_threshold: DB_THRESHOLD, 295 | side_padding_type: SidePaddingType::Zeros, 296 | }; 297 | 298 | let snd = sndfile::OpenOptions::ReadOnly(sndfile::ReadOptions::Auto) 299 | .from_path(get_test_files_location().join("rock_1s.wav")) 300 | .unwrap(); 301 | let channels = snd.get_channels(); 302 | let mut spectro = Spectrogram::new(snd, parameters, None).unwrap(); 303 | let num_bins = spectro.num_bins(); 304 | 305 | let mut zoom = Zoom::new(0.5f64).unwrap(); 306 | 307 | // No zoom 308 | for ch_idx in 0..channels { 309 | let (no_zoom_data, num_bands) = spectro.data(ch_idx, &mut zoom); 310 | 311 | assert_ne!(no_zoom_data.len(), 0usize); 312 | assert_eq!(num_bins * num_bands * 3, no_zoom_data.len()); 313 | } 314 | 315 | // Zoom in and move 316 | for _ in 0..10 { 317 | zoom.zoom_in(); 318 | zoom.move_right(); 319 | 320 | for ch_idx in 0..channels { 321 | let (no_zoom_data, num_bands) = spectro.data(ch_idx, &mut zoom); 322 | 323 | assert_ne!(no_zoom_data.len(), 0usize); 324 | assert_eq!(num_bins * num_bands * 3, no_zoom_data.len()); 325 | } 326 | } 327 | } 328 | } 329 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use sndfile::SndFileError; 2 | use std::convert::From; 3 | use std::time::Duration; 4 | use structopt::StructOpt; 5 | use tui::backend::Backend; 6 | use tui::style::Modifier; 7 | use tui::widgets::Clear; 8 | use tui::Frame; 9 | extern crate crossterm; 10 | extern crate num_integer; 11 | extern crate num_traits; 12 | extern crate sndfile; 13 | 14 | use crate::dsp::SpectrogramParameters; 15 | use crate::render::MetadataRenderer; 16 | use std::io::{Error, ErrorKind}; 17 | 18 | mod utils; 19 | use utils::bindings; 20 | use utils::event::{Config, Event, Events}; 21 | use utils::TabsState; 22 | use utils::Zoom; 23 | 24 | mod render; 25 | use render::ChannelsTabs; 26 | use render::HelperPopup; 27 | use render::Renderer; 28 | use render::RendererType; 29 | use render::RenderingInfo; 30 | use render::SpectralRenderer; 31 | use render::WaveformRenderer; 32 | 33 | mod dsp; 34 | use dsp::{SidePaddingType, WindowType, PADDING_HELP_TEXT}; 35 | 36 | use std::io; 37 | use termion::{input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen}; 38 | use tui::{ 39 | backend::CrosstermBackend, 40 | layout::{Constraint, Direction, Layout, Rect}, 41 | style::Color, 42 | style::Style, 43 | text::{Span, Spans}, 44 | widgets::{ 45 | canvas::{Canvas, Rectangle}, 46 | Block, Borders, Tabs, 47 | }, 48 | Terminal, 49 | }; 50 | 51 | const WAVEFORM_TAB_IDX: usize = 0; 52 | const SPECTRAL_TAB_IDX: usize = 1; 53 | const METADATA_TAB_IDX: usize = 2; 54 | 55 | struct App<'a> { 56 | tabs: TabsState<'a>, 57 | channels: ChannelsTabs, 58 | previous_frame: Rect, 59 | repaint: bool, 60 | should_stop: bool, 61 | zoom: Zoom, 62 | helper: HelperPopup, 63 | } 64 | 65 | #[derive(StructOpt)] 66 | struct CliArgs { 67 | // The file to read 68 | #[structopt(parse(from_os_str), help = "The path of the file to analyze")] 69 | path: std::path::PathBuf, 70 | 71 | // FFT options 72 | #[structopt(long = "fft-window-size", default_value = "4096")] 73 | fft_window_size: usize, 74 | #[structopt(long = "fft-overlap", default_value = "0.75")] 75 | fft_overlap: f64, 76 | #[structopt(long = "fft-db-threshold", default_value = "-130")] 77 | fft_db_threshold: f64, 78 | #[structopt(long = "fft-window-type", 79 | parse(try_from_str = WindowType::parse), 80 | default_value=WindowType::default(), 81 | possible_values=WindowType::possible_values(),)] 82 | fft_window_type: WindowType, 83 | #[structopt(long = "fft-padding-type", 84 | parse(try_from_str = SidePaddingType::parse), 85 | default_value=SidePaddingType::default(), 86 | possible_values=SidePaddingType::possible_values(), 87 | help=PADDING_HELP_TEXT)] 88 | fft_padding_type: SidePaddingType, 89 | 90 | // Normalize option 91 | #[structopt(short = "n", long = "normalize")] 92 | normalize: bool, 93 | } 94 | 95 | fn draw_tabs(frame: &mut Frame<'_, B>, area: Rect, app: &App) { 96 | // Tabs drawing 97 | let tab_titles: Vec = app 98 | .tabs 99 | .titles 100 | .iter() 101 | .map(|t| { 102 | let (first, rest) = t.split_at(1); 103 | Spans::from(vec![ 104 | Span::styled(first, Style::default().fg(Color::Yellow)), 105 | Span::styled(rest, Style::default().fg(Color::Green)), 106 | ]) 107 | }) 108 | .collect(); 109 | let tabs = Tabs::new(tab_titles) 110 | .block( 111 | Block::default() 112 | .borders(Borders::LEFT | Borders::TOP | Borders::BOTTOM) 113 | .title("Tabs"), 114 | ) 115 | .select(app.tabs.index) 116 | .highlight_style( 117 | Style::default() 118 | .add_modifier(Modifier::BOLD) 119 | .bg(Color::DarkGray), 120 | ); 121 | 122 | frame.render_widget(tabs, area); 123 | } 124 | 125 | fn draw_zoom_head( 126 | frame: &mut Frame<'_, B>, 127 | area: Rect, 128 | zoom_start: f64, 129 | zoom_len: f64, 130 | ) { 131 | let canva = Canvas::default() 132 | .background_color(Color::Rgb(16, 16, 16)) 133 | .block(Block::default().borders(Borders::TOP | Borders::BOTTOM)) 134 | .paint(|ctx| { 135 | ctx.draw(&Rectangle { 136 | x: zoom_start, 137 | y: 0f64, 138 | width: zoom_len, 139 | height: 1f64, 140 | color: Color::White, 141 | }) 142 | }) 143 | .x_bounds([0f64, 1f64]) 144 | .y_bounds([0f64, 1f64]); 145 | 146 | frame.render_widget(canva, area); 147 | } 148 | 149 | fn helper_layout(area: Rect) -> Rect { 150 | let x_offset = area.width / 4; 151 | let y_offset = area.height / 4; 152 | 153 | Rect { 154 | x: area.x + x_offset, 155 | y: area.y + y_offset, 156 | width: area.width / 2, 157 | height: area.height / 2, 158 | } 159 | } 160 | 161 | fn main() -> Result<(), io::Error> { 162 | // Get cli args 163 | let args = CliArgs::from_args(); 164 | 165 | let stdout = io::stdout().into_raw_mode()?; 166 | let stdout = MouseTerminal::from(stdout); 167 | let stdout = AlternateScreen::from(stdout); 168 | let backend = CrosstermBackend::new(stdout); 169 | let mut terminal = Terminal::new(backend)?; 170 | const TAB_SIZE: u16 = 3; 171 | 172 | let events = Events::with_config(Config { 173 | tick_rate: Duration::from_millis(100), 174 | }); 175 | 176 | // Check file 177 | let snd_res = sndfile::OpenOptions::ReadOnly(sndfile::ReadOptions::Auto).from_path(&args.path); 178 | if let Err(err) = snd_res { 179 | return Err(match err { 180 | SndFileError::UnrecognisedFormat(msg) => Error::new(ErrorKind::InvalidData, msg), 181 | SndFileError::SystemError(msg) => Error::new(ErrorKind::InvalidData, msg), 182 | SndFileError::MalformedFile(msg) => Error::new(ErrorKind::InvalidData, msg), 183 | SndFileError::UnsupportedEncoding(msg) => Error::new(ErrorKind::InvalidData, msg), 184 | SndFileError::InvalidParameter(msg) => Error::new(ErrorKind::InvalidData, msg), 185 | SndFileError::InternalError(msg) => Error::new(ErrorKind::InvalidData, msg), 186 | SndFileError::IOError(io_err) => io_err, 187 | }); 188 | } 189 | 190 | let snd = snd_res.unwrap(); 191 | let channels = snd.get_channels(); 192 | if channels > 9usize { 193 | let err = Error::new( 194 | ErrorKind::InvalidInput, 195 | "Audeye does not support configuration with more than 9 channels", 196 | ); 197 | return Err(err); 198 | } 199 | 200 | // Create the renderers 201 | let mut waveform = RendererType::Waveform(WaveformRenderer::new(&args.path, args.normalize)); 202 | let mut spectral = RendererType::Spectral(SpectralRenderer::new( 203 | &args.path, 204 | SpectrogramParameters { 205 | window_size: args.fft_window_size, 206 | overlap_rate: args.fft_overlap, 207 | db_threshold: args.fft_db_threshold, 208 | window_type: args.fft_window_type, 209 | side_padding_type: args.fft_padding_type, 210 | }, 211 | args.normalize, 212 | )); 213 | let mut metadata_render = RendererType::Metadata(Box::new(MetadataRenderer::new(&args.path))); 214 | 215 | // Build the app 216 | // Compute the max zoom allowed 217 | let res_max = usize::min( 218 | waveform.max_width_resolution(), 219 | spectral.max_width_resolution(), 220 | ) as f64; 221 | 222 | let mut app = App { 223 | tabs: TabsState::new(vec!["Waveform", "Spectral", "Metadata"]), 224 | channels: ChannelsTabs::new(channels), 225 | previous_frame: Rect::default(), 226 | repaint: true, 227 | should_stop: false, 228 | zoom: Zoom::new(terminal.size()?.width as f64 / res_max).unwrap(), 229 | helper: HelperPopup::default(), 230 | }; 231 | 232 | // let mut zoom_head = ZoomHead::new(&mut app.zoom); 233 | 234 | loop { 235 | // Get current size 236 | let tsize = terminal.size()?; 237 | 238 | let renderer = match app.tabs.index { 239 | WAVEFORM_TAB_IDX => &mut waveform, 240 | SPECTRAL_TAB_IDX => &mut spectral, 241 | METADATA_TAB_IDX => &mut metadata_render, 242 | _ => unreachable!(), 243 | }; 244 | 245 | if tsize != app.previous_frame { 246 | app.repaint = true; 247 | let new_max_zoom = terminal.size()?.width as f64 / res_max; 248 | app.zoom.update_zoom_max(new_max_zoom); 249 | } 250 | 251 | if app.repaint || renderer.needs_redraw() { 252 | terminal.draw(|f| { 253 | // Chunks settings 254 | let size = f.size(); 255 | 256 | // Build rendering info structure for the renderers 257 | let rendering_info = RenderingInfo { 258 | activated_channels: app.channels.activated(), 259 | zoom: &app.zoom, 260 | }; 261 | 262 | // Setup headers and view layout 263 | let chunks = Layout::default() 264 | .direction(Direction::Vertical) 265 | .constraints([Constraint::Length(TAB_SIZE), Constraint::Min(3)]) 266 | .split(size); 267 | 268 | let header_chunks = Layout::default() 269 | .direction(Direction::Horizontal) 270 | .constraints([ 271 | Constraint::Percentage(33), 272 | Constraint::Percentage(33), 273 | Constraint::Percentage(33), 274 | ]) 275 | .split(chunks[0]); 276 | 277 | // View tabs 278 | draw_tabs(f, header_chunks[0], &app); 279 | 280 | // Channel tabs 281 | app.channels.render(f, header_chunks[2]); 282 | 283 | // Zoom head 284 | draw_zoom_head(f, header_chunks[1], app.zoom.start(), app.zoom.length()); 285 | 286 | // Renderer view drawing 287 | renderer.draw(f, &rendering_info, chunks[1]); 288 | 289 | // Helper menu 290 | if app.helper.is_visible() { 291 | let helper_rect = helper_layout(chunks[1]); 292 | f.render_widget(Clear, helper_rect); 293 | app.helper.draw(f, &rendering_info, helper_rect); 294 | } 295 | })?; 296 | } 297 | 298 | // Reset state 299 | app.previous_frame = tsize; 300 | app.repaint = false; 301 | 302 | loop { 303 | let event = events.next().unwrap(); 304 | 305 | match event { 306 | Event::Input(input) => match input { 307 | bindings::QUIT => { 308 | app.should_stop = true; 309 | } 310 | bindings::NEXT_PANEL => { 311 | app.tabs.next(); 312 | app.repaint = true; 313 | } 314 | bindings::PREVIOUS_PANEL => { 315 | app.tabs.previous(); 316 | app.repaint = true; 317 | } 318 | bindings::CHANNEL_SELECTOR_1 => { 319 | app.channels.update(0); 320 | app.repaint = true; 321 | } 322 | bindings::CHANNEL_SELECTOR_2 => { 323 | app.channels.update(1); 324 | app.repaint = true; 325 | } 326 | bindings::CHANNEL_SELECTOR_3 => { 327 | app.channels.update(2); 328 | app.repaint = true; 329 | } 330 | bindings::CHANNEL_SELECTOR_4 => { 331 | app.channels.update(3); 332 | app.repaint = true; 333 | } 334 | bindings::CHANNEL_SELECTOR_5 => { 335 | app.channels.update(4); 336 | app.repaint = true; 337 | } 338 | bindings::CHANNEL_SELECTOR_6 => { 339 | app.channels.update(5); 340 | app.repaint = true; 341 | } 342 | bindings::CHANNEL_SELECTOR_7 => { 343 | app.channels.update(6); 344 | app.repaint = true; 345 | } 346 | bindings::CHANNEL_SELECTOR_8 => { 347 | app.channels.update(7); 348 | app.repaint = true; 349 | } 350 | bindings::CHANNEL_SELECTOR_9 => { 351 | app.channels.update(8); 352 | app.repaint = true; 353 | } 354 | bindings::CHANNEL_RESET => { 355 | app.channels.reset(); 356 | app.repaint = true; 357 | } 358 | bindings::MOVE_LEFT => { 359 | app.zoom.move_left(); 360 | app.repaint = true; 361 | } 362 | bindings::MOVE_RIGHT => { 363 | app.zoom.move_right(); 364 | app.repaint = true; 365 | } 366 | bindings::ZOOM_OUT => { 367 | app.zoom.zoom_out(); 368 | app.repaint = true; 369 | } 370 | bindings::ZOOM_IN => { 371 | app.zoom.zoom_in(); 372 | app.repaint = true; 373 | } 374 | bindings::HELP => { 375 | app.helper.set_visible(!app.helper.is_visible()); 376 | app.repaint = true; 377 | } 378 | _ => {} 379 | }, 380 | Event::Tick => { 381 | break; 382 | } 383 | } 384 | } 385 | 386 | if app.should_stop { 387 | break; 388 | } 389 | } 390 | 391 | Ok(()) 392 | } 393 | -------------------------------------------------------------------------------- /src/dsp/time_window.rs: -------------------------------------------------------------------------------- 1 | extern crate sndfile; 2 | use crate::sndfile::SndFile; 3 | use apodize::{blackman_iter, hamming_iter, hanning_iter}; 4 | use sndfile::SndFileIO; 5 | use std::{cmp::min, convert::TryFrom, fmt::Display, io::SeekFrom}; 6 | 7 | use super::DspErr; 8 | use crate::utils::deinterleave_vec; 9 | 10 | #[derive(Debug, Clone, Copy)] 11 | pub enum WindowType { 12 | Blackman, 13 | Hanning, 14 | Hamming, 15 | Uniform, 16 | } 17 | 18 | #[derive(Debug, Clone, Copy)] 19 | pub struct WindowTypeParseError; 20 | 21 | impl Display for WindowTypeParseError { 22 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 23 | write!(f, "Invalid window type") 24 | } 25 | } 26 | 27 | const HANNING: &str = "hanning"; 28 | const HAMMING: &str = "hamming"; 29 | const BLACKMAN: &str = "blackman"; 30 | const UNIFORM: &str = "uniform"; 31 | 32 | impl WindowType { 33 | fn build_window(&self, size: usize) -> Vec { 34 | match self { 35 | Self::Blackman => blackman_iter(size).collect(), 36 | Self::Hamming => hamming_iter(size).collect(), 37 | Self::Hanning => hanning_iter(size).collect(), 38 | Self::Uniform => vec![1f64; size], 39 | } 40 | } 41 | 42 | pub fn correction_factor(&self) -> f64 { 43 | match self { 44 | Self::Blackman => 2.80f64, 45 | Self::Hamming => 1.85f64, 46 | Self::Hanning => 2f64, 47 | Self::Uniform => 1f64, 48 | } 49 | } 50 | 51 | pub fn parse(name: &str) -> Result { 52 | if name == HANNING { 53 | Ok(Self::Hanning) 54 | } else if name == HAMMING { 55 | Ok(Self::Hamming) 56 | } else if name == BLACKMAN { 57 | Ok(Self::Blackman) 58 | } else if name == UNIFORM { 59 | Ok(Self::Uniform) 60 | } else { 61 | Err(WindowTypeParseError) 62 | } 63 | } 64 | 65 | pub fn possible_values() -> &'static [&'static str] { 66 | &[HAMMING, HANNING, BLACKMAN, UNIFORM] 67 | } 68 | 69 | pub fn default() -> &'static str { 70 | HANNING 71 | } 72 | } 73 | 74 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 75 | pub enum SidePaddingType { 76 | Zeros, 77 | SmoothRamp, 78 | Loop, 79 | } 80 | 81 | struct SidePadding { 82 | padding_type: SidePaddingType, 83 | padding_left: Vec>, 84 | padding_right: Vec>, 85 | max_padding_right: usize, 86 | max_padding_left: usize, 87 | } 88 | 89 | impl SidePadding { 90 | fn new( 91 | padding_type: SidePaddingType, 92 | sndfile: &mut SndFile, 93 | max_padding_left: usize, 94 | max_padding_right: usize, 95 | ) -> Self { 96 | let channels = sndfile.get_channels(); 97 | 98 | let mut padding_left = vec![vec![0f64; max_padding_left]; channels]; 99 | let mut padding_right = vec![vec![0f64; max_padding_right]; channels]; 100 | 101 | if padding_type == SidePaddingType::Loop { 102 | let frames = sndfile.len().unwrap(); 103 | let mut interleaved_data = vec![0f64; channels * max_padding_right]; 104 | 105 | // Read the beginning of the file 106 | sndfile.seek(SeekFrom::Start(0)).expect("Failed to seek 0"); 107 | sndfile 108 | .read_to_slice(interleaved_data.as_mut_slice()) 109 | .unwrap(); 110 | deinterleave_vec( 111 | channels, 112 | interleaved_data.as_slice(), 113 | padding_right.as_mut_slice(), 114 | ); 115 | 116 | // Read the end of the file 117 | let idx_offset = frames - max_padding_left as u64; 118 | sndfile.seek(SeekFrom::Start(idx_offset)).unwrap(); 119 | sndfile 120 | .read_to_slice(interleaved_data.as_mut_slice()) 121 | .unwrap(); 122 | deinterleave_vec( 123 | channels, 124 | &interleaved_data[..max_padding_left * channels], 125 | padding_left.as_mut_slice(), 126 | ); 127 | } 128 | 129 | Self { 130 | padding_type, 131 | padding_left, 132 | padding_right, 133 | max_padding_right, 134 | max_padding_left, 135 | } 136 | } 137 | 138 | pub fn pad_left(&mut self, content: &mut [f64], next_sample: f64, channel: usize) { 139 | if content.len() > self.max_padding_left { 140 | panic!(); 141 | } 142 | let pad_slice = match self.padding_type { 143 | SidePaddingType::SmoothRamp => { 144 | let ramp_size = min(content.len(), self.max_padding_left / 64); 145 | let mut crt_val = 0f64; 146 | let step = next_sample / ramp_size as f64; 147 | 148 | let start_idx = content.len() - ramp_size; 149 | 150 | // Fill the start with zeros 151 | self.padding_left[channel][..start_idx] 152 | .iter_mut() 153 | .for_each(|val| *val = 0f64); 154 | 155 | // Fill the rest 156 | self.padding_left[channel][start_idx..content.len()] 157 | .iter_mut() 158 | .for_each(|pad_sample| { 159 | *pad_sample = crt_val; 160 | crt_val += step; 161 | }); 162 | 163 | &self.padding_left[channel][..content.len()] 164 | } 165 | _ => { 166 | let start = self.max_padding_left - content.len(); 167 | &self.padding_left[channel][start..] 168 | } 169 | }; 170 | 171 | pad_slice 172 | .iter() 173 | .zip(content.iter_mut()) 174 | .for_each(|(pad_sample, content_sample)| { 175 | *content_sample = *pad_sample; 176 | }); 177 | } 178 | 179 | pub fn pad_right(&mut self, content: &mut [f64], prev_sample: f64, channel: usize) { 180 | if content.len() > self.max_padding_right { 181 | panic!(); 182 | } 183 | let pad_slice = match self.padding_type { 184 | SidePaddingType::SmoothRamp => { 185 | let ramp_size = min(content.len(), self.max_padding_left / 64); 186 | let mut crt_val = 0f64; 187 | let step = prev_sample / ramp_size as f64; 188 | 189 | // Fill the start with the ramp 190 | self.padding_right[channel][..ramp_size] 191 | .iter_mut() 192 | .for_each(|pad_sample| { 193 | *pad_sample = crt_val; 194 | crt_val += step; 195 | }); 196 | 197 | // Fill the rest with zeros 198 | self.padding_right[channel][ramp_size..] 199 | .iter_mut() 200 | .for_each(|val| *val = 0f64); 201 | 202 | &self.padding_right[channel][..content.len()] 203 | } 204 | _ => { 205 | // let start = self.left_side_offset - content.len(); 206 | &self.padding_right[channel][..content.len()] 207 | } 208 | }; 209 | 210 | pad_slice 211 | .iter() 212 | .zip(content.iter_mut()) 213 | .for_each(|(pad_sample, content_sample)| { 214 | *content_sample = *pad_sample; 215 | }); 216 | } 217 | } 218 | 219 | #[derive(Debug, Clone, Copy)] 220 | pub struct SidePaddingTypeParseError; 221 | 222 | impl Display for SidePaddingTypeParseError { 223 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 224 | write!(f, "Invalid side padding type") 225 | } 226 | } 227 | 228 | const ZEROS: &str = "zeros"; 229 | const RAMP: &str = "ramp"; 230 | const LOOP: &str = "loop"; 231 | pub const PADDING_HELP_TEXT: &str = 232 | "How to fill the missing samples for the firsts and lasts sample windows 233 | \tzeros : fill with zeros samples 234 | \tramp : small linear ramp to match the last/next sample 235 | \tloop : loop the end to the beginning and vice-versa\n"; 236 | 237 | impl SidePaddingType { 238 | pub fn parse(name: &str) -> Result { 239 | if name == ZEROS { 240 | Ok(Self::Zeros) 241 | } else if name == RAMP { 242 | Ok(Self::SmoothRamp) 243 | } else if name == LOOP { 244 | Ok(Self::Loop) 245 | } else { 246 | Err(SidePaddingTypeParseError) 247 | } 248 | } 249 | 250 | pub fn possible_values() -> &'static [&'static str] { 251 | &[ZEROS, RAMP, LOOP] 252 | } 253 | 254 | pub fn default() -> &'static str { 255 | ZEROS 256 | } 257 | } 258 | 259 | pub struct TimeWindowBatcher { 260 | sndfile: SndFile, 261 | frames: u64, 262 | tband_size: usize, 263 | window_size: usize, 264 | crt_band_idx: usize, 265 | num_bands: usize, 266 | batch: Vec>, 267 | window: Vec, 268 | tmp_interleaved_block: Vec, 269 | side_padding: SidePadding, 270 | } 271 | 272 | impl TimeWindowBatcher { 273 | pub fn new( 274 | mut sndfile: SndFile, 275 | window_size: usize, 276 | overlap: f64, 277 | window_type: WindowType, 278 | side_padding: SidePaddingType, 279 | ) -> Result { 280 | if 0f64 >= overlap || overlap >= 1f64 { 281 | return Err(DspErr::new( 282 | "Overlap values should be contained within ]0:1[", 283 | )); 284 | } 285 | 286 | let frames = sndfile.len().unwrap(); 287 | let channels = sndfile.get_channels(); 288 | let tband_size = usize::try_from((window_size as f64 * (1. - overlap)) as i32).unwrap(); 289 | sndfile.seek(SeekFrom::Start(0)).expect("Failed to seek 0"); 290 | let num_bands = if frames % tband_size as u64 == 0 { 291 | usize::try_from(frames / tband_size as u64).unwrap() 292 | } else { 293 | usize::try_from(frames / tband_size as u64 + 1).unwrap() 294 | }; 295 | 296 | let max_padding_left = (window_size - tband_size) / 2; 297 | let side_padding = 298 | SidePadding::new(side_padding, &mut sndfile, max_padding_left, window_size); 299 | 300 | Ok(TimeWindowBatcher { 301 | sndfile, 302 | frames, 303 | tband_size, 304 | window_size, 305 | crt_band_idx: 0, 306 | num_bands, 307 | batch: vec![vec![0f64; window_size]; channels], 308 | window: window_type.build_window(window_size), 309 | tmp_interleaved_block: vec![0f64; window_size * channels], 310 | side_padding, 311 | }) 312 | } 313 | 314 | pub fn get_num_bands(&self) -> usize { 315 | self.num_bands 316 | } 317 | 318 | pub fn get_next_batch(&mut self) -> Option> { 319 | // We reached the end 320 | if self.crt_band_idx >= self.num_bands { 321 | return None; 322 | } 323 | 324 | // Compute the first sample to seek 325 | let new_seek_idx = self.crt_band_idx as u64 * self.tband_size as u64; 326 | self.sndfile 327 | .seek(SeekFrom::Start(new_seek_idx)) 328 | .unwrap_or_else(|_| panic!("Failed to seek frame {}", new_seek_idx)); 329 | 330 | // The offset left and right of the window lobe 331 | let side_offset = (self.window_size - self.tband_size) / 2; 332 | 333 | let left_padding_idx = if new_seek_idx < side_offset as u64 { 334 | // Beginning of the file, need a zero offset at the start 335 | usize::try_from(side_offset as u64 - new_seek_idx).unwrap() 336 | } else { 337 | 0_usize 338 | }; 339 | 340 | let right_padding_idx = if new_seek_idx + self.window_size as u64 > self.frames { 341 | // End of the file, need a zero offset at the end 342 | usize::try_from(self.frames - new_seek_idx).unwrap() 343 | } else { 344 | self.window_size 345 | }; 346 | 347 | let channels = self.batch.len(); 348 | 349 | // Read interleaved data 350 | let interleaved_write_slice = &mut self.tmp_interleaved_block 351 | [left_padding_idx * channels..right_padding_idx * channels]; 352 | match self.sndfile.read_to_slice(interleaved_write_slice) { 353 | Ok(frames) => { 354 | if frames != right_padding_idx - left_padding_idx { 355 | panic!( 356 | "Only read {} frames over {}", 357 | frames, 358 | right_padding_idx - left_padding_idx 359 | ); 360 | } 361 | } 362 | Err(_) => { 363 | panic!("Failed to read"); 364 | } 365 | } 366 | 367 | // Write the padding zeros - TODO: vectorize ? 368 | for (channel, ch_vec) in self.batch.iter_mut().enumerate() { 369 | // Left padding 370 | let next_sample = ch_vec[left_padding_idx]; 371 | self.side_padding 372 | .pad_left(&mut ch_vec[..left_padding_idx], next_sample, channel); 373 | 374 | // Right padding 375 | let prev_sample = ch_vec[right_padding_idx - 1]; 376 | self.side_padding 377 | .pad_right(&mut ch_vec[right_padding_idx..], prev_sample, channel); 378 | } 379 | 380 | { 381 | // Write deinterleaved data into batch vector 382 | let batch_mut_slice = self.batch.as_mut_slice(); 383 | interleaved_write_slice 384 | .chunks(channels) 385 | .enumerate() 386 | .for_each(|(frame_idx, samples)| { 387 | for (channel, value) in samples.iter().enumerate() { 388 | batch_mut_slice[channel][left_padding_idx + frame_idx] = *value; 389 | } 390 | }); 391 | } 392 | 393 | // Apply window to the batch 394 | for ch_vec in &mut self.batch { 395 | // Faster ? see : https://www.nickwilcox.com/blog/autovec/ 396 | let window_slice = self.window.as_slice(); 397 | let ch_vec_slice = &mut ch_vec[0..window_slice.len()]; 398 | 399 | for i in 0..ch_vec_slice.len() { 400 | ch_vec_slice[i] *= window_slice[i]; 401 | } 402 | } 403 | 404 | // build return type 405 | let ret: Vec<&mut [f64]> = self.batch.iter_mut().map(|v| v.as_mut_slice()).collect(); 406 | 407 | // Update index 408 | self.crt_band_idx += 1; 409 | 410 | Some(ret) 411 | } 412 | } 413 | 414 | #[cfg(test)] 415 | mod tests { 416 | use std::path::{Path, PathBuf}; 417 | 418 | const WINDOW_SIZES: &[usize] = &[256, 512, 1024, 2048, 4096, 8192, 333, 10000, 984]; 419 | 420 | fn get_test_files_location() -> PathBuf { 421 | return Path::new(&env!("CARGO_MANIFEST_DIR").to_string()) 422 | .join("tests") 423 | .join("files"); 424 | } 425 | 426 | mod window_type { 427 | use crate::dsp::WindowType; 428 | 429 | use super::WINDOW_SIZES; 430 | 431 | #[test] 432 | fn parse_default() { 433 | let _ = WindowType::parse(WindowType::default()).unwrap(); 434 | } 435 | 436 | #[test] 437 | fn parse_all() { 438 | for value in WindowType::possible_values() { 439 | WindowType::parse(value).unwrap(); 440 | } 441 | } 442 | 443 | #[test] 444 | fn build_window() { 445 | for value in WindowType::possible_values() { 446 | let wtype = WindowType::parse(value).unwrap(); 447 | 448 | for wsize in WINDOW_SIZES { 449 | let window = wtype.build_window(*wsize); 450 | assert_eq!(window.len(), *wsize); 451 | 452 | for sample in window { 453 | assert!(sample <= 1f64); 454 | assert!(sample >= 0f64); 455 | } 456 | } 457 | } 458 | } 459 | } 460 | 461 | mod side_padding { 462 | use crate::dsp::time_window::SidePadding; 463 | use crate::dsp::SidePaddingType; 464 | 465 | use super::get_test_files_location; 466 | 467 | #[test] 468 | fn default() { 469 | const PADDING_LEFT: usize = 1024; 470 | const WINDOW_SIZE: usize = 4096; 471 | 472 | let mut snd = sndfile::OpenOptions::ReadOnly(sndfile::ReadOptions::Auto) 473 | .from_path(get_test_files_location().join("rock_1s.wav")) 474 | .unwrap(); 475 | 476 | let mut data = vec![0f64; WINDOW_SIZE]; 477 | 478 | let default_type = SidePaddingType::parse(SidePaddingType::default()).unwrap(); 479 | let mut padder = SidePadding::new(default_type, &mut snd, PADDING_LEFT, WINDOW_SIZE); 480 | 481 | for i in 0..snd.get_channels() { 482 | padder.pad_left(&mut data[..PADDING_LEFT / 2], 1f64, i); 483 | padder.pad_right(&mut data[..WINDOW_SIZE / 2], 1f64, i); 484 | } 485 | } 486 | 487 | #[test] 488 | fn possible_values() { 489 | const PADDING_LEFT: usize = 1024; 490 | const WINDOW_SIZE: usize = 4096; 491 | 492 | let mut snd = sndfile::OpenOptions::ReadOnly(sndfile::ReadOptions::Auto) 493 | .from_path(get_test_files_location().join("rock_1s.wav")) 494 | .unwrap(); 495 | let mut data = vec![0f64; WINDOW_SIZE]; 496 | 497 | for padding_type_str in SidePaddingType::possible_values() { 498 | let padding_type = SidePaddingType::parse(padding_type_str).unwrap(); 499 | let mut padder = 500 | SidePadding::new(padding_type, &mut snd, PADDING_LEFT, WINDOW_SIZE); 501 | 502 | for i in 0..snd.get_channels() { 503 | padder.pad_left(&mut data[..PADDING_LEFT / 2], 1f64, i); 504 | padder.pad_right(&mut data[..WINDOW_SIZE / 2], 1f64, i); 505 | } 506 | } 507 | } 508 | 509 | #[test] 510 | #[should_panic] 511 | fn padding_left_oversize_zeros() { 512 | const PADDING_LEFT: usize = 1024; 513 | const PADDING_RIGHT: usize = 4096; 514 | 515 | let mut snd = sndfile::OpenOptions::ReadOnly(sndfile::ReadOptions::Auto) 516 | .from_path(get_test_files_location().join("rock_1s.wav")) 517 | .unwrap(); 518 | let mut data = vec![0f64; PADDING_LEFT * 2]; 519 | 520 | let mut padder = SidePadding::new( 521 | SidePaddingType::Zeros, 522 | &mut snd, 523 | PADDING_LEFT, 524 | PADDING_RIGHT, 525 | ); 526 | padder.pad_left(&mut data[..PADDING_LEFT * 2], 1f64, 0); 527 | } 528 | 529 | #[test] 530 | #[should_panic] 531 | fn padding_left_oversize_loop() { 532 | const PADDING_LEFT: usize = 1024; 533 | const PADDING_RIGHT: usize = 4096; 534 | 535 | let mut snd = sndfile::OpenOptions::ReadOnly(sndfile::ReadOptions::Auto) 536 | .from_path(get_test_files_location().join("rock_1s.wav")) 537 | .unwrap(); 538 | let mut data = vec![0f64; PADDING_LEFT * 2]; 539 | 540 | let mut padder = 541 | SidePadding::new(SidePaddingType::Loop, &mut snd, PADDING_LEFT, PADDING_RIGHT); 542 | padder.pad_left(&mut data[..PADDING_LEFT * 2], 1f64, 0); 543 | } 544 | 545 | #[test] 546 | #[should_panic] 547 | fn padding_left_oversize_ramp() { 548 | const PADDING_LEFT: usize = 1024; 549 | const PADDING_RIGHT: usize = 4096; 550 | 551 | let mut snd = sndfile::OpenOptions::ReadOnly(sndfile::ReadOptions::Auto) 552 | .from_path(get_test_files_location().join("rock_1s.wav")) 553 | .unwrap(); 554 | let mut data = vec![0f64; PADDING_LEFT * 2]; 555 | 556 | let mut padder = SidePadding::new( 557 | SidePaddingType::SmoothRamp, 558 | &mut snd, 559 | PADDING_LEFT, 560 | PADDING_RIGHT, 561 | ); 562 | padder.pad_left(&mut data[..PADDING_LEFT * 2], 1f64, 0); 563 | } 564 | 565 | #[test] 566 | #[should_panic] 567 | fn padding_right_oversize_zeros() { 568 | const PADDING_LEFT: usize = 4096; 569 | const PADDING_RIGHT: usize = 4096; 570 | 571 | let mut snd = sndfile::OpenOptions::ReadOnly(sndfile::ReadOptions::Auto) 572 | .from_path(get_test_files_location().join("rock_1s.wav")) 573 | .unwrap(); 574 | let mut data = vec![0f64; PADDING_RIGHT * 2]; 575 | 576 | let mut padder = SidePadding::new( 577 | SidePaddingType::Zeros, 578 | &mut snd, 579 | PADDING_LEFT, 580 | PADDING_RIGHT, 581 | ); 582 | padder.pad_right(&mut data[..PADDING_RIGHT * 2], 1f64, 0); 583 | } 584 | 585 | #[test] 586 | #[should_panic] 587 | fn padding_right_oversize_loop() { 588 | const PADDING_LEFT: usize = 4096; 589 | const PADDING_RIGHT: usize = 4096; 590 | 591 | let mut snd = sndfile::OpenOptions::ReadOnly(sndfile::ReadOptions::Auto) 592 | .from_path(get_test_files_location().join("rock_1s.wav")) 593 | .unwrap(); 594 | let mut data = vec![0f64; PADDING_RIGHT * 2]; 595 | 596 | let mut padder = 597 | SidePadding::new(SidePaddingType::Loop, &mut snd, PADDING_LEFT, PADDING_RIGHT); 598 | padder.pad_right(&mut data[..PADDING_RIGHT * 2], 1f64, 0); 599 | } 600 | 601 | #[test] 602 | #[should_panic] 603 | fn padding_right_oversize_ramp() { 604 | const PADDING_LEFT: usize = 4096; 605 | const PADDING_RIGHT: usize = 4096; 606 | 607 | let mut snd = sndfile::OpenOptions::ReadOnly(sndfile::ReadOptions::Auto) 608 | .from_path(get_test_files_location().join("rock_1s.wav")) 609 | .unwrap(); 610 | let mut data = vec![0f64; PADDING_RIGHT * 2]; 611 | 612 | let mut padder = SidePadding::new( 613 | SidePaddingType::SmoothRamp, 614 | &mut snd, 615 | PADDING_LEFT, 616 | PADDING_RIGHT, 617 | ); 618 | padder.pad_right(&mut data[..PADDING_RIGHT * 2], 1f64, 0); 619 | } 620 | } 621 | 622 | mod time_window { 623 | use super::get_test_files_location; 624 | use crate::dsp::{time_window::TimeWindowBatcher, SidePaddingType, WindowType}; 625 | use std::convert::TryFrom; 626 | 627 | #[test] 628 | fn build() { 629 | let valid_window_size = &[128usize, 256, 512, 1024, 2048, 4096, 8192, 666, 1333]; 630 | let valid_overlaps = &[0.1f64, 0.25f64, 0.5f64, 0.75f64, 0.95f64]; 631 | 632 | for window_size in valid_window_size { 633 | for overlap in valid_overlaps { 634 | let snd = sndfile::OpenOptions::ReadOnly(sndfile::ReadOptions::Auto) 635 | .from_path(get_test_files_location().join("rock_1s.wav")) 636 | .unwrap(); 637 | TimeWindowBatcher::new( 638 | snd, 639 | *window_size, 640 | *overlap, 641 | WindowType::Hanning, 642 | SidePaddingType::Zeros, 643 | ) 644 | .unwrap(); 645 | } 646 | } 647 | } 648 | 649 | #[test] 650 | #[should_panic] 651 | fn negative_overlap() { 652 | const WINDOW_SIZE: usize = 2048; 653 | const OVERLAP: f64 = -1f64; 654 | let snd = sndfile::OpenOptions::ReadOnly(sndfile::ReadOptions::Auto) 655 | .from_path(get_test_files_location().join("rock_1s.wav")) 656 | .unwrap(); 657 | 658 | TimeWindowBatcher::new( 659 | snd, 660 | WINDOW_SIZE, 661 | OVERLAP, 662 | WindowType::Hanning, 663 | SidePaddingType::Zeros, 664 | ) 665 | .unwrap(); 666 | } 667 | 668 | #[test] 669 | #[should_panic] 670 | fn overlap_greater_than_one() { 671 | const WINDOW_SIZE: usize = 2048; 672 | const OVERLAP: f64 = 1.5f64; 673 | let snd = sndfile::OpenOptions::ReadOnly(sndfile::ReadOptions::Auto) 674 | .from_path(get_test_files_location().join("rock_1s.wav")) 675 | .unwrap(); 676 | 677 | TimeWindowBatcher::new( 678 | snd, 679 | WINDOW_SIZE, 680 | OVERLAP, 681 | WindowType::Hanning, 682 | SidePaddingType::Zeros, 683 | ) 684 | .unwrap(); 685 | } 686 | 687 | #[test] 688 | fn check_content() { 689 | let valid_window_size = &[128usize, 256, 512, 1024, 2048, 4096, 8192, 666, 1333]; 690 | let valid_overlaps = &[0.1f64, 0.25f64, 0.5f64, 0.75f64, 0.95f64]; 691 | 692 | for window_size in valid_window_size { 693 | for overlap in valid_overlaps { 694 | let mut snd = sndfile::OpenOptions::ReadOnly(sndfile::ReadOptions::Auto) 695 | .from_path(get_test_files_location().join("rock_1s.wav")) 696 | .unwrap(); 697 | let frames = snd.len().unwrap(); 698 | let channels = snd.get_channels(); 699 | 700 | let band_size = (*window_size as f64 * (1f64 - *overlap)) as i32; 701 | let expected_num_batch = usize::try_from(frames / band_size as u64).unwrap(); 702 | 703 | let mut batcher = TimeWindowBatcher::new( 704 | snd, 705 | *window_size, 706 | *overlap, 707 | WindowType::Hanning, 708 | SidePaddingType::Zeros, 709 | ) 710 | .unwrap(); 711 | let num_batch = batcher.get_num_bands(); 712 | 713 | assert!(expected_num_batch == num_batch || expected_num_batch + 1 == num_batch); 714 | 715 | let mut count = 0usize; 716 | 717 | loop { 718 | let batch_opt = batcher.get_next_batch(); 719 | if batch_opt.is_none() { 720 | break; 721 | } 722 | let batch = batch_opt.unwrap(); 723 | 724 | assert_eq!(batch.len(), channels); 725 | 726 | for chan in batch { 727 | assert_eq!(chan.len(), *window_size); 728 | } 729 | 730 | count += 1; 731 | } 732 | 733 | assert_eq!(count, num_batch); 734 | } 735 | } 736 | } 737 | } 738 | } 739 | --------------------------------------------------------------------------------