diff --git a/Cargo.lock b/Cargo.lock index 04c16d5..7bfabed 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -48,6 +48,21 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "aksel" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5d8287f81ee47834cfa439551eb25597b8e1304ab6a0ea223a1acee97ae1842" +dependencies = [ + "num-traits", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + [[package]] name = "android-activity" version = "0.6.0" @@ -278,7 +293,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.114", ] [[package]] @@ -313,7 +328,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.114", ] [[package]] @@ -465,7 +480,7 @@ checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.114", ] [[package]] @@ -646,7 +661,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn", + "syn 2.0.114", ] [[package]] @@ -763,6 +778,15 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" +[[package]] +name = "convert_case" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "core-foundation" version = "0.9.4" @@ -964,6 +988,17 @@ dependencies = [ "powerfmt", ] +[[package]] +name = "derivative" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "derive_arbitrary" version = "1.4.2" @@ -972,7 +1007,30 @@ checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.114", +] + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.114", + "unicode-xid", ] [[package]] @@ -1054,7 +1112,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.114", ] [[package]] @@ -1099,6 +1157,16 @@ dependencies = [ "wio", ] +[[package]] +name = "earcutr" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afc28d89ac6cb489f679fcf51aaa62b4667bb0fb73a30f1d4d23949069707c07" +dependencies = [ + "itertools 0.14.0", + "num-traits", +] + [[package]] name = "either" version = "1.15.0" @@ -1138,7 +1206,7 @@ checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.114", ] [[package]] @@ -1348,7 +1416,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.114", ] [[package]] @@ -1453,7 +1521,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.114", ] [[package]] @@ -1640,6 +1708,7 @@ dependencies = [ "directories", "hound", "iced", + "iced_aksel", "iced_fontello", "jack", "ndarray 0.16.1", @@ -1736,6 +1805,8 @@ version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" dependencies = [ + "allocator-api2", + "equivalent", "foldhash 0.2.0", ] @@ -1938,6 +2009,26 @@ dependencies = [ "thiserror 2.0.18", ] +[[package]] +name = "iced_aksel" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "506164a2f6a8d2e2bf0a2998df7449203004ba56f6e3c8e985942327ccaebf35" +dependencies = [ + "aksel", + "derivative", + "derive_more", + "earcutr", + "iced_core", + "iced_graphics", + "iced_renderer", + "indexmap 2.13.0", + "lru", + "lyon", + "lyon_path", + "lyon_tessellation", +] + [[package]] name = "iced_beacon" version = "0.14.0" @@ -2340,6 +2431,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.17" @@ -2556,6 +2656,9 @@ name = "lru" version = "0.16.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1dc47f592c06f33f8e3aea9591776ec7c9f9e4124778ff8a3c3b87159f7e593" +dependencies = [ + "hashbrown 0.16.1", +] [[package]] name = "lyon" @@ -2982,7 +3085,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn", + "syn 2.0.114", ] [[package]] @@ -3379,7 +3482,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.114", ] [[package]] @@ -3540,7 +3643,7 @@ checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.114", ] [[package]] @@ -4216,7 +4319,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.114", ] [[package]] @@ -4240,7 +4343,7 @@ checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.114", ] [[package]] @@ -4561,6 +4664,17 @@ dependencies = [ "zeno", ] +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "syn" version = "2.0.114" @@ -4589,7 +4703,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.114", ] [[package]] @@ -4670,7 +4784,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.114", ] [[package]] @@ -4681,7 +4795,7 @@ checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.114", ] [[package]] @@ -4798,7 +4912,7 @@ checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.114", ] [[package]] @@ -4981,7 +5095,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.114", ] [[package]] @@ -5122,6 +5236,12 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + [[package]] name = "untrusted" version = "0.9.0" @@ -5273,7 +5393,7 @@ dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn", + "syn 2.0.114", "wasm-bindgen-shared", ] @@ -5741,7 +5861,7 @@ checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.114", ] [[package]] @@ -5752,7 +5872,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.114", ] [[package]] @@ -5763,7 +5883,7 @@ checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.114", ] [[package]] @@ -5774,7 +5894,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.114", ] [[package]] @@ -6337,7 +6457,7 @@ checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.114", "synstructure", ] @@ -6385,7 +6505,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn", + "syn 2.0.114", "zbus_names", "zvariant", "zvariant_utils", @@ -6425,7 +6545,7 @@ checksum = "2c7962b26b0a8685668b671ee4b54d007a67d4eaf05fda79ac0ecf41e32270f1" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.114", ] [[package]] @@ -6445,7 +6565,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.114", "synstructure", ] @@ -6466,7 +6586,7 @@ checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.114", ] [[package]] @@ -6499,7 +6619,7 @@ checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.114", ] [[package]] @@ -6601,7 +6721,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn", + "syn 2.0.114", "zvariant_utils", ] @@ -6614,6 +6734,6 @@ dependencies = [ "proc-macro2", "quote", "serde", - "syn", + "syn 2.0.114", "winnow", ] diff --git a/raumklang-gui/Cargo.toml b/raumklang-gui/Cargo.toml index b8c94ad..95c3b87 100644 --- a/raumklang-gui/Cargo.toml +++ b/raumklang-gui/Cargo.toml @@ -31,6 +31,7 @@ ndarray-stats = "0.6.0" chrono = "0.4.42" generic_overlay = { package = "widgets", git = "https://github.com/A-Disruption/widgets.git", version = "0.1.0", default-features=false, features = ["generic_overlay"]} colorous = "1.0.16" +iced_aksel = "0.2.0" [dependencies.iced] version = "0.14.0" diff --git a/raumklang-gui/src/data/project.rs b/raumklang-gui/src/data/project.rs index a85515a..695924c 100644 --- a/raumklang-gui/src/data/project.rs +++ b/raumklang-gui/src/data/project.rs @@ -12,6 +12,7 @@ pub struct Project { pub measurements: Vec, #[serde(default)] pub measurement_operation: Operation, + #[serde(default)] pub export_from_memory: bool, } diff --git a/raumklang-gui/src/data/spectral_decay.rs b/raumklang-gui/src/data/spectral_decay.rs index 4accde4..5f8c737 100644 --- a/raumklang-gui/src/data/spectral_decay.rs +++ b/raumklang-gui/src/data/spectral_decay.rs @@ -1,4 +1,3 @@ -use core::slice; use std::{fmt, sync::Arc, time::Duration}; use raumklang_core::{Window, WindowBuilder}; @@ -12,13 +11,13 @@ use crate::data::{SampleRate, Samples, smooth_fractional_octave}; #[derive(Clone)] pub struct SpectralDecay(Vec); -impl SpectralDecay { - pub fn len(&self) -> usize { - self.0.len() - } +impl IntoIterator for SpectralDecay { + type Item = super::FrequencyResponse; + + type IntoIter = std::vec::IntoIter; - pub fn iter(&self) -> slice::Iter<'_, super::FrequencyResponse> { - self.0.iter() + fn into_iter(self) -> Self::IntoIter { + self.0.into_iter() } } diff --git a/raumklang-gui/src/screen/main.rs b/raumklang-gui/src/screen/main.rs index 4eaa0fb..990357e 100644 --- a/raumklang-gui/src/screen/main.rs +++ b/raumklang-gui/src/screen/main.rs @@ -5,6 +5,10 @@ mod modal; mod recording; mod tab; +use iced::Pixels; +use iced::mouse::ScrollDelta; +use iced_aksel::axis::{MarkerPosition, Position, TickContext, TickLine, TickResult}; +use iced_aksel::scale; use modal::Modal; use tab::Tab; use tokio::fs; @@ -13,6 +17,7 @@ use crate::data::{ self, Project, RecentProjects, SampleRate, Samples, Window, project, spectral_decay, spectrogram, window, }; +use crate::ui::frequency_response::SpectrumLayer; use crate::{ PickAndLoadError, icon, load_project, log, screen::main::{ @@ -25,7 +30,6 @@ use crate::{ ui::{self, Analysis, Loopback, Measurement, measurement}, widget::{processing_overlay, sidebar}, }; -use raumklang_core::dbfs; use impulse_response::ChartOperation; use recording::Recording; @@ -43,7 +47,6 @@ use iced::{ scrollable, stack, text, }, }; -use prism::{Axis, Chart, Labels, axis, line_series}; use rfd::FileHandle; use std::io; @@ -79,8 +82,14 @@ pub struct Main { spectral_decay_config: spectral_decay::Config, spectrogram_config: spectrogram::Config, + fr_state: iced_aksel::State, } +type AxisId = &'static str; + +const FREQ_AXIS_ID: AxisId = "freq"; +const DB_AXIS_ID: AxisId = "db"; + #[allow(clippy::large_enum_variant)] #[derive(Default)] enum State { @@ -137,14 +146,15 @@ pub enum Message { ImpulseResponseComputed(measurement::Id, data::ImpulseResponse), SaveImpulseResponseToFile(measurement::Id, Option>), - FrequencyResponseComputed(measurement::Id, data::FrequencyResponse), ImpulseResponseSaved(measurement::Id, Arc), ImpulseResponseChart(impulse_response::ChartOperation), ImpulseResponse(ui::measurement::Id, ui::impulse_response::Message), + FrequencyResponseComputed(measurement::Id, data::FrequencyResponse), FrequencyResponseToggled(measurement::Id, bool), ChangeSmoothing(frequency_response::Smoothing), FrequencyResponseSmoothed(measurement::Id, Box<[f32]>), + FrequencyResponseChart(frequency_response::Message), ShiftKeyPressed, ShiftKeyReleased, @@ -673,7 +683,10 @@ impl Main { let fr = analysis.frequency_response.result()?; Some(Task::perform( - frequency_response::smooth_frequency_response(fr.clone(), fraction), + frequency_response::smooth_frequency_response( + fr.origin.clone(), + fraction, + ), Message::FrequencyResponseSmoothed.with(*id), )) }); @@ -683,7 +696,7 @@ impl Main { analyses .values_mut() .map(Analysis::frequency_response_mut) - .for_each(|fr| fr.smoothed = None); + .for_each(|fr| fr.reset_smoothing()); cache.clear(); @@ -700,14 +713,64 @@ impl Main { return Task::none(); }; - if let Some(fr) = analyses.get_mut(&id).map(|a| &mut a.frequency_response) { - fr.smoothed = Some(smoothed); + if let Some(data) = analyses + .get_mut(&id) + .map(|a| &mut a.frequency_response) + .and_then(ui::FrequencyResponse::result_mut) + { + data.smoothed = Some(SpectrumLayer::new( + smoothed, + SampleRate::from(data.origin.sample_rate), + )); cache.clear(); } Task::none() } + Message::FrequencyResponseChart(msg) => { + match msg { + frequency_response::Message::OnPlotScroll(cursor_pos, delta) => match delta { + ScrollDelta::Lines { x: _, y } => { + let factor = 1.1f32.powf(y); + + self.fr_state + .axis_mut(&FREQ_AXIS_ID) + .zoom(factor, Some(cursor_pos.x)); + self.fr_state + .axis_mut(&DB_AXIS_ID) + .zoom(factor, Some(cursor_pos.y)); + } + ScrollDelta::Pixels { x: _, y } => { + // For pixel-based scrolling (touchpad) + // Divide by larger number for less sensitive zooming + let factor = 1.0 + y / 500.0; + + self.fr_state + .axis_mut(&FREQ_AXIS_ID) + .zoom(factor, Some(cursor_pos.x)); + self.fr_state + .axis_mut(&DB_AXIS_ID) + .zoom(factor, Some(cursor_pos.y)); + } + }, + frequency_response::Message::OnPlotDrag(delta) => { + // --- Pan X-Axis --- + self.fr_state.axis_mut(&FREQ_AXIS_ID).pan(delta.x); + // self.clamp_x_axis(); + self.fr_state.axis_mut(&DB_AXIS_ID).pan(delta.y); + } + } + // clamp + let x_axis = self.fr_state.axis_mut(&FREQ_AXIS_ID); + let (&min, &max) = x_axis.domain(); + x_axis.set_domain(min.max(MIN_FREQ), max.min(MAX_FREQ)); + let y_axis = self.fr_state.axis_mut(&DB_AXIS_ID); + let (&min, &max) = y_axis.domain(); + y_axis.set_domain(min.max(MIN_DB), max.min(MAX_DB)); + + Task::none() + } Message::SpectralDecayComputed(id, sd) => { let State::Analysing { ref mut analyses, @@ -1247,7 +1310,7 @@ impl Main { fn frequency_responses_tab<'a>( &'a self, - cache: &'a canvas::Cache, + _cache: &'a canvas::Cache, analyses: &'a BTreeMap, ) -> Element<'a, Message> { let sidebar = { @@ -1286,59 +1349,40 @@ impl Main { .any(|fr| fr.result().is_some() && fr.is_shown); let content = if chart_needed { - let series_list = frequency_responses - .filter(|fr| fr.is_shown) - .flat_map(|item| { - let Some(frequency_response) = item.result() else { - return [None, None]; - }; - - let sample_rate = frequency_response.sample_rate; - let len = frequency_response.data.len() * 2 + 1; - let resolution = sample_rate as f32 / len as f32; - - let closure = move |(i, s)| (i as f32 * resolution, dbfs(s)); - - [ - Some( - line_series( - frequency_response - .data - .iter() - .copied() - .enumerate() - .map(closure), - ) - .color(item.color.scale_alpha(0.1)), - ), - item.smoothed.as_ref().map(|smoothed| { - line_series(smoothed.iter().copied().enumerate().map(closure)) - .color(item.color) - }), - ] + let chart = iced_aksel::Chart::new(&self.fr_state) + .style(Box::new(|theme| { + let mut base = iced_aksel::style::default(theme); + let palette = theme.extended_palette(); + + base.axis.label.color = palette.secondary.base.color; + base.axis.tick.color = palette.secondary.base.color; + base.axis.spine.color = palette.secondary.base.color; + base.axis.grid.color = palette.background.weaker.color; + + base + })) + .marker(&FREQ_AXIS_ID, MarkerPosition::Cursor, |ctx| { + Some(ctx.marker(format_frequency_label(ctx.value))) }) - .flatten(); - - let chart: Chart = Chart::new() - .x_axis( - Axis::new(axis::Alignment::Horizontal) - .scale(axis::Scale::Log) - .x_tick_marks( - [20, 50, 100, 1000, 10_000, 20_000] - .into_iter() - .map(|v| v as f32) - .collect(), - ), - ) - .y_labels(Labels::default().format(&|v| format!("{v:.0}"))) - .extend_series(series_list) - .cache(cache); + .marker(&DB_AXIS_ID, MarkerPosition::Cursor, |ctx| { + Some(ctx.marker(format_db_label(ctx.value))) + }) + .on_scroll(frequency_response::Message::OnPlotScroll) + .on_drag(frequency_response::Message::OnPlotDrag); + + let chart = frequency_responses + .filter(|fr| fr.is_shown) + .fold(chart, |chart, fr| { + chart.plot_data(fr, FREQ_AXIS_ID, DB_AXIS_ID) + }); container(chart) } else { container(text("Please select a frequency respone.")).center(Length::Fill) }; + let content = Element::from(content).map(Message::FrequencyResponseChart); + row![ container(sidebar) .width(Length::FillPortion(2)) @@ -1353,7 +1397,7 @@ impl Main { &'a self, selected: Option, analyses: &'a BTreeMap, - cache: &'a canvas::Cache, + _cache: &'a canvas::Cache, ) -> Element<'a, Message> { let sidebar = { let header = { @@ -1401,10 +1445,7 @@ impl Main { sidebar::item(btn, is_active) }; - // FIXME - let analysis = analyses.get(&id); - - let entry = if let Some(analysis) = analysis { + let entry = if let Some(analysis) = analyses.get(&id) { match analysis.spectral_decay.progress() { ui::spectral_decay::Progress::None => entry, ui::spectral_decay::Progress::WaitingForImpulseResponse => { @@ -1434,37 +1475,26 @@ impl Main { .and_then(|id| analyses.get(&id)) .map(|a| &a.spectral_decay); - let content = if let Some(decay) = spectral_decay.and_then(ui::SpectralDecay::result) { - let gradient = colorous::MAGMA; - - let series_list = decay.iter().enumerate().map(|(fr_index, fr)| { - // FIXME refactor bin -> Hz computation - let sample_rate = fr.sample_rate; - let len = fr.data.len() * 2 + 1; - let resolution = sample_rate as f32 / len as f32; - - let closure = move |(i, s)| (i as f32 * resolution, dbfs(s)); - - let color = gradient.eval_rational(fr_index, decay.len()); - line_series(fr.data.iter().copied().enumerate().map(closure)) - .color(iced::Color::from_rgb8(color.r, color.g, color.b)) - }); - - let chart: Chart = Chart::new() - .x_axis( - Axis::new(axis::Alignment::Horizontal) - .scale(axis::Scale::Log) - .x_tick_marks( - [10, 20, 50, 100, 1000] - .into_iter() - .map(|v| v as f32) - .collect(), - ), - ) - // .x_range(20.0..=2000.0) - .y_labels(Labels::default().format(&|v| format!("{v:.0}"))) - .extend_series(series_list) - .cache(cache); + let content = if let Some(decay) = spectral_decay { + let chart = iced_aksel::Chart::new(&self.fr_state) + .style(Box::new(|theme| { + let mut base = iced_aksel::style::default(theme); + let palette = theme.extended_palette(); + + base.axis.label.color = palette.secondary.base.color; + base.axis.tick.color = palette.secondary.base.color; + base.axis.spine.color = palette.secondary.base.color; + base.axis.grid.color = palette.background.weaker.color; + + base + })) + .marker(&FREQ_AXIS_ID, MarkerPosition::Cursor, |ctx| { + Some(ctx.marker(format_frequency_label(ctx.value))) + }) + .marker(&DB_AXIS_ID, MarkerPosition::Cursor, |ctx| { + Some(ctx.marker(format_db_label(ctx.value))) + }) + .plot_data(decay, FREQ_AXIS_ID, DB_AXIS_ID); container(chart) } else { @@ -1802,6 +1832,11 @@ fn compute_spectrogram( impl Default for Main { fn default() -> Self { + let mut fr_state = iced_aksel::State::new(); + + fr_state.set_axis(FREQ_AXIS_ID, create_frequency_axis()); + fr_state.set_axis(DB_AXIS_ID, create_db_axis()); + Self { state: State::default(), modal: Modal::None, @@ -1827,10 +1862,65 @@ impl Default for Main { ir_chart: impulse_response::Chart::default(), spectrogram: Spectrogram::default(), spectrogram_config: spectrogram::Config::default(), + + fr_state, } } } +const MIN_FREQ: f32 = 15.0; +const MAX_FREQ: f32 = 22_000.0; +const MIN_DB: f32 = -90.0; +const MAX_DB: f32 = 12.0; + +fn create_frequency_axis() -> iced_aksel::Axis { + iced_aksel::Axis::new( + scale::Logarithmic::new(10.0, MIN_FREQ, MAX_FREQ), + Position::Bottom, + ) + .with_tick_renderer(frequency_tick_renderer) + .skip_overlapping_labels(8.0) +} + +fn create_db_axis() -> iced_aksel::Axis { + iced_aksel::Axis::new(scale::Linear::new(MIN_DB, MAX_DB), Position::Left) + .with_tick_renderer(db_tick_renderer) + .with_thickness(80.0) + .skip_overlapping_labels(8.0) +} + +fn frequency_tick_renderer(ctx: TickContext) -> TickResult { + let line = TickLine { + length: Pixels(if ctx.tick.level == 0 { 12.0 } else { 6.0 }), + ..ctx.tickline() + }; + let label = format_frequency_label(ctx.tick.value); + TickResult::with_label(ctx.label(label)) + .tick_line(line) + .grid_line(ctx.gridline()) +} + +fn db_tick_renderer(ctx: TickContext) -> TickResult { + let label = format_db_label(ctx.tick.value); + TickResult::with_label(ctx.label(label)) + .tick_line(ctx.tickline()) + .grid_line(ctx.gridline()) +} + +fn format_frequency_label(value: f32) -> String { + if value >= 10_000.0 { + format!("{:.0} kHz", value / 1000.0) + } else if value >= 1000.0 { + format!("{:.1} kHz", value / 1000.0) + } else { + format!("{:.0} Hz", value) + } +} + +fn format_db_label(value: f32) -> String { + format!("{:+.0} dB", value) +} + async fn choose_impulse_response_file_path() -> Option> { rfd::AsyncFileDialog::new() .set_title("Save Impulse Response ...") diff --git a/raumklang-gui/src/screen/main/frequency_response.rs b/raumklang-gui/src/screen/main/frequency_response.rs index 7750ef5..afa9339 100644 --- a/raumklang-gui/src/screen/main/frequency_response.rs +++ b/raumklang-gui/src/screen/main/frequency_response.rs @@ -1,7 +1,16 @@ use std::fmt::{self}; +use iced::mouse::ScrollDelta; +use iced_aksel::plot::DragDelta; + use crate::data; +#[derive(Debug, Clone)] +pub enum Message { + OnPlotDrag(DragDelta), + OnPlotScroll(iced::Point, ScrollDelta), +} + #[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] pub enum Smoothing { #[default] diff --git a/raumklang-gui/src/ui.rs b/raumklang-gui/src/ui.rs index 2a4f9b2..27a9ff6 100644 --- a/raumklang-gui/src/ui.rs +++ b/raumklang-gui/src/ui.rs @@ -9,4 +9,3 @@ pub use analysis::Analysis; pub use frequency_response::FrequencyResponse; pub use impulse_response::ImpulseResponse; pub use measurement::{Loopback, Measurement}; -pub use spectral_decay::SpectralDecay; diff --git a/raumklang-gui/src/ui/frequency_response.rs b/raumklang-gui/src/ui/frequency_response.rs index 3f404c6..6d26c02 100644 --- a/raumklang-gui/src/ui/frequency_response.rs +++ b/raumklang-gui/src/ui/frequency_response.rs @@ -1,3 +1,4 @@ +use crate::data::{SampleRate, smooth_fractional_octave}; use crate::widget::sidebar; use crate::{data, icon}; @@ -9,15 +10,15 @@ use iced::{ widget::{column, container, row, text, toggler}, }; +use iced_aksel::{Measure, Plot, PlotData, PlotPoint, Stroke, shape}; use rand::Rng as _; +use raumklang_core::dbfs; #[derive(Debug, Clone)] pub struct FrequencyResponse { pub color: iced::Color, pub is_shown: bool, - pub smoothed: Option>, - pub state: State, } @@ -26,9 +27,19 @@ pub enum State { None, WaitingForImpulseResponse, Computing, - Computed(data::FrequencyResponse), + Computed(Data), } +#[derive(Debug, Clone)] +pub struct Data { + pub origin: data::FrequencyResponse, + base_smoothed: SpectrumLayer, + pub smoothed: Option, +} + +#[derive(Debug, Clone)] +pub struct SpectrumLayer(pub Vec>); + impl FrequencyResponse { pub fn new() -> Self { let color = random_color(); @@ -36,7 +47,6 @@ impl FrequencyResponse { Self { color, is_shown: true, - smoothed: None, state: State::None, } @@ -90,16 +100,48 @@ impl FrequencyResponse { sidebar::item(content, false) } - pub fn result(&self) -> Option<&data::FrequencyResponse> { - let State::Computed(ref result) = self.state else { + pub fn result(&self) -> Option<&Data> { + let State::Computed(ref data) = self.state else { + return None; + }; + + Some(data) + } + + pub fn result_mut(&mut self) -> Option<&mut Data> { + let State::Computed(data) = &mut self.state else { return None; }; - Some(result) + Some(data) } pub fn set_result(&mut self, fr: data::FrequencyResponse) { - self.state = State::Computed(fr) + let data = smooth_fractional_octave(&fr.data, 48); + + let sample_rate = fr.sample_rate; + let len = fr.data.len() * 2 + 1; + let resolution = sample_rate as f32 / len as f32; + + // FIXME add start and end + let mut curve = Vec::with_capacity(len); + for (i, s) in data.iter().enumerate() { + curve.push(PlotPoint::new(i as f32 * resolution, dbfs(*s))); + } + + self.state = State::Computed(Data { + origin: fr, + base_smoothed: SpectrumLayer(curve), + smoothed: None, + }) + } + + pub fn reset_smoothing(&mut self) { + let State::Computed(data) = &mut self.state else { + return; + }; + + data.smoothed = None; } } @@ -142,3 +184,53 @@ where ]) .into() } + +impl SpectrumLayer { + pub fn new(data: I, sample_rate: SampleRate) -> Self + where + I: IntoIterator, + I::IntoIter: Clone, + { + let data = data.into_iter(); + + let len = data.clone().count() * 2 + 1; + let resolution = f32::from(sample_rate) / len as f32; + + let curve = data + .enumerate() + .map(|(i, s)| PlotPoint::new(i as f32 * resolution, dbfs(s))) + .collect(); + + Self(curve) + } +} + +const MIN_FREQ: f32 = 15.0; +const MAX_FREQ: f32 = 22_000.0; +const MIN_DB: f32 = -90.0; + +impl PlotData for FrequencyResponse { + fn draw(&self, plot: &mut Plot, _theme: &iced::Theme) { + let State::Computed(ref fr) = self.state else { + return; + }; + + if fr.base_smoothed.0.len() < 2 { + return; + } + + let mut fill_points = Vec::with_capacity(fr.base_smoothed.0.len() + 2); + fill_points.push(PlotPoint::new(MIN_FREQ, MIN_DB)); + fill_points.extend(fr.base_smoothed.0.iter().copied()); + fill_points.push(PlotPoint::new(MAX_FREQ, MIN_DB)); + + plot.add_shape(shape::Area::new(fill_points).fill(self.color.scale_alpha(0.1))); + + let line_stroke = Stroke::new(self.color.scale_alpha(0.8), Measure::Screen(1.0)); + if let Some(smoothed) = fr.smoothed.as_ref() { + plot.add_shape(shape::Polyline::new(smoothed.0.clone()).stroke(line_stroke)); + } else { + plot.add_shape(shape::Polyline::new(fr.base_smoothed.0.clone()).stroke(line_stroke)); + } + } +} diff --git a/raumklang-gui/src/ui/spectral_decay.rs b/raumklang-gui/src/ui/spectral_decay.rs index 730f228..e186d99 100644 --- a/raumklang-gui/src/ui/spectral_decay.rs +++ b/raumklang-gui/src/ui/spectral_decay.rs @@ -1,4 +1,9 @@ -use crate::{data, ui::impulse_response}; +use iced_aksel::{Measure, Plot, PlotData, Stroke, shape}; + +use crate::{ + data::{self, SampleRate}, + ui::{frequency_response::SpectrumLayer, impulse_response}, +}; use std::future::Future; @@ -11,11 +16,11 @@ enum State { None, WaitingForImpulseResponse, Computing, - Computed(data::SpectralDecay), + Computed(Vec), } impl SpectralDecay { - pub fn result(&self) -> Option<&data::SpectralDecay> { + pub fn result(&self) -> Option<&Vec> { let State::Computed(result) = &self.0 else { return None; }; @@ -53,6 +58,11 @@ impl SpectralDecay { } pub fn set_result(&mut self, spectral_decay: data::SpectralDecay) { + let spectral_decay = spectral_decay + .into_iter() + .map(|fr| SpectrumLayer::new(fr.data.iter().copied(), SampleRate::from(fr.sample_rate))) + .collect(); + self.0 = State::Computed(spectral_decay); } @@ -68,3 +78,24 @@ pub enum Progress { Computing, Finished, } + +impl PlotData for SpectralDecay { + fn draw(&self, plot: &mut Plot, _theme: &iced::Theme) { + let State::Computed(ref sd) = self.0 else { + return; + }; + + if sd.len() < 2 { + return; + } + + let gradient = colorous::MAGMA; + for (i, fr) in sd.iter().enumerate() { + let color = gradient.eval_rational(i, sd.len()); + let color = iced::Color::from_rgb8(color.r, color.g, color.b); + + let line_stroke = Stroke::new(color.scale_alpha(0.8), Measure::Screen(1.0)); + plot.add_shape(shape::Polyline::new(fr.0.clone()).stroke(line_stroke)); + } + } +}