From e58686edd25f6fba30b221bbde3e451df2aa375d Mon Sep 17 00:00:00 2001 From: Hendrik Kunert Date: Thu, 22 Jan 2026 12:03:41 +0100 Subject: [PATCH 01/30] Rename `selected_impulse_response` --- raumklang-gui/src/screen/main.rs | 41 ++++++++++++++++---------------- 1 file changed, 20 insertions(+), 21 deletions(-) diff --git a/raumklang-gui/src/screen/main.rs b/raumklang-gui/src/screen/main.rs index fd84b9d..0f861bd 100644 --- a/raumklang-gui/src/screen/main.rs +++ b/raumklang-gui/src/screen/main.rs @@ -19,11 +19,11 @@ use crate::{ widget::sidebar, PickAndLoadError, }; +use raumklang_core::dbfs; + use impulse_response::{ChartOperation, WindowSettings}; use recording::Recording; -use raumklang_core::dbfs; - use chrono::{DateTime, Utc}; use generic_overlay::generic_overlay::{dropdown_menu, dropdown_root}; @@ -70,7 +70,7 @@ enum State { loopback: ui::Loopback, active_tab: Tab, window: Window, - selected_impulse_response: Option, + selected: Option, charts: Charts, }, } @@ -195,7 +195,7 @@ impl Main { measurement::Message::Loaded, ) }) - .unwrap_or(Task::none()); + .unwrap_or_else(Task::none); let load_measurements = project.measurements.into_iter().map(|measurement| { Task::perform( @@ -223,7 +223,7 @@ impl Main { let State::Analysing { ref mut active_tab, ref window, - ref mut selected_impulse_response, + ref mut selected, ref loopback, .. } = self.state @@ -265,7 +265,7 @@ impl Main { }; *active_tab = tab; - *selected_impulse_response = None; + *selected = None; tasks } @@ -329,7 +329,7 @@ impl Main { .map_or(44_100, |l| l.as_ref().sample_rate()), )) .into(), - selected_impulse_response: None, + selected: None, charts: Charts::default(), loopback, }, @@ -346,7 +346,7 @@ impl Main { log::debug!("Impulse response selected: {id}"); let State::Analysing { - ref mut selected_impulse_response, + selected: ref mut selected_analysis, ref mut charts, ref active_tab, ref loopback, @@ -356,7 +356,7 @@ impl Main { return Task::none(); }; - *selected_impulse_response = Some(id); + *selected_analysis = Some(id); charts.impulse_responses.data_cache.clear(); let Some(measurement) = self.measurements.get_mut(id) else { @@ -379,7 +379,7 @@ impl Main { let State::Analysing { window, active_tab, - selected_impulse_response, + selected: selected_analysis, charts, loopback, .. @@ -400,7 +400,7 @@ impl Main { analysis.impulse_response.computed(impulse_response.clone()); - if selected_impulse_response.is_some_and(|selected| selected == id) { + if selected_analysis.is_some_and(|selected| selected == id) { charts .impulse_responses .x_range @@ -896,7 +896,7 @@ impl Main { }; let State::Analysing { - selected_impulse_response, + selected: selected_analysis, ref loopback, .. } = self.state @@ -918,7 +918,7 @@ impl Main { analysis.spectral_decay = ui::spectral_decay::State::default() }); - selected_impulse_response + selected_analysis .and_then(|id| self.measurements.iter_mut().find(|m| m.id() == id)) .map_or(Task::none(), |measurement| { compute_spectral_decay( @@ -946,7 +946,7 @@ impl Main { }; let State::Analysing { - selected_impulse_response, + selected: selected_analysis, ref loopback, .. } = self.state @@ -972,7 +972,7 @@ impl Main { analysis.spectrogram = ui::spectrogram::State::default() }); - selected_impulse_response + selected_analysis .and_then(|id| self.measurements.get_mut(id)) .map_or(Task::none(), |measurement| { compute_spectrogram(loopback, measurement, &self.spectrogram_config) @@ -1045,7 +1045,7 @@ impl Main { } State::Analysing { active_tab, - selected_impulse_response, + selected: selected_analysis, charts, loopback, .. @@ -1060,18 +1060,17 @@ impl Main { Tab::ImpulseResponses { window_settings, .. } => self.impulse_responses_tab( - *selected_impulse_response, + *selected_analysis, &charts.impulse_responses, window_settings, ), Tab::FrequencyResponses => { self.frequency_responses_tab(&charts.frequency_responses) } - Tab::SpectralDecay => self - .spectral_decay_tab(*selected_impulse_response, &charts.spectral_decay_cache), - Tab::Spectrogram => { - self.spectrogram_tab(*selected_impulse_response, &charts.spectrogram) + Tab::SpectralDecay => { + self.spectral_decay_tab(*selected_analysis, &charts.spectral_decay_cache) } + Tab::Spectrogram => self.spectrogram_tab(*selected_analysis, &charts.spectrogram), }, }; From 08b96c6358a32773dce4f6ea50d6a3f6669d600b Mon Sep 17 00:00:00 2001 From: Hendrik Kunert Date: Tue, 27 Jan 2026 20:30:23 +0100 Subject: [PATCH 02/30] Improve project loading --- raumklang-gui/src/main.rs | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/raumklang-gui/src/main.rs b/raumklang-gui/src/main.rs index 9b7703b..07963ac 100644 --- a/raumklang-gui/src/main.rs +++ b/raumklang-gui/src/main.rs @@ -15,7 +15,7 @@ use screen::{ use data::{project, RecentProjects}; -use iced::{futures::FutureExt, Element, Font, Subscription, Task, Theme}; +use iced::{Element, Font, Subscription, Task, Theme}; use std::{ path::{Path, PathBuf}, @@ -97,13 +97,9 @@ impl Raumklang { Task::none() } - landing::Message::Load => Task::perform( - pick_project_file().then(async |res| { - let path = res?; - load_project(path).await - }), - Message::ProjectLoaded, - ), + landing::Message::Load => Task::future(pick_project_file()) + .and_then(|path| Task::future(load_project(path))) + .map(Message::ProjectLoaded), landing::Message::Recent(id) => match self.recent_projects.get(id) { Some(path) => Task::perform(load_project(path.clone()), Message::ProjectLoaded), None => Task::none(), From d9972df5c45379899ed126ae55f7e962c1d1e454 Mon Sep 17 00:00:00 2001 From: Hendrik Kunert Date: Wed, 28 Jan 2026 14:22:39 +0100 Subject: [PATCH 03/30] Start re-structuring of main screen --- raumklang-gui/src/screen/main.rs | 2014 +++++++++++++++------------ raumklang-gui/src/ui/measurement.rs | 18 +- 2 files changed, 1149 insertions(+), 883 deletions(-) diff --git a/raumklang-gui/src/screen/main.rs b/raumklang-gui/src/screen/main.rs index 0f861bd..9ea1132 100644 --- a/raumklang-gui/src/screen/main.rs +++ b/raumklang-gui/src/screen/main.rs @@ -12,10 +12,10 @@ use crate::{ }, icon, load_project, log, screen::main::{ - chart::waveform, impulse_response::processing_overlay, + chart::waveform, impulse_response::processing_overlay, measurement::Selected, spectral_decay_config::SpectralDecayConfig, spectrogram_config::SpectrogramConfig, }, - ui, + ui::{self, Loopback}, widget::sidebar, PickAndLoadError, }; @@ -35,7 +35,7 @@ use iced::{ button, canvas, center, column, container, opaque, pick_list, right, row, rule, scrollable, space, stack, text, text::Wrapping, Button, }, - Alignment::{self}, + Alignment::{self, Center}, Color, Element, Function, Length, Subscription, Task, Theme, }; use prism::{axis, line_series, Axis, Chart, Labels}; @@ -47,40 +47,36 @@ use std::{ }; pub struct Main { - state: State, + tab: Tab, selected: Option, + state: State, + + loopback: Option, + signal_cache: canvas::Cache, smoothing: frequency_response::Smoothing, measurements: ui::measurement::List, - modal: Modal, zoom: chart::Zoom, offset: chart::Offset, - project_path: Option, - spectral_decay_config: data::spectral_decay::Config, - spectrogram_config: spectrogram::Preferences, + // modal: Modal, + // project_path: Option, + // spectral_decay_config: data::spectral_decay::Config, + // spectrogram_config: spectrogram::Preferences, + window: Option>, + charts: Charts, } #[allow(clippy::large_enum_variant)] enum State { - CollectingMeasuremnts { - recording: Option, - loopback: Option, - }, + Collecting, Analysing { - loopback: ui::Loopback, - active_tab: Tab, - window: Window, selected: Option, - charts: Charts, }, } impl Default for State { fn default() -> Self { - State::CollectingMeasuremnts { - recording: None, - loopback: None, - } + State::Collecting } } @@ -141,6 +137,8 @@ pub enum Message { ProjectSaved(Result), TabSelected(TabId), + OpenMeasurements, + OpenImpulseResponses, Measurements(measurement::Message), MeasurementChart(waveform::Interaction), @@ -187,88 +185,36 @@ pub enum TabId { impl Main { pub fn from_project(path: PathBuf, project: data::Project) -> (Self, Task) { - let load_loopback = project - .loopback - .map(|loopback| { - Task::perform( - measurement::load_measurement(loopback.0.path, measurement::Kind::Loopback), - measurement::Message::Loaded, - ) - }) - .unwrap_or_else(Task::none); - - let load_measurements = project.measurements.into_iter().map(|measurement| { - Task::perform( - measurement::load_measurement(measurement.path, measurement::Kind::Normal), - measurement::Message::Loaded, - ) - }); + // let load_loopback = project + // .loopback + // .map(|loopback| { + // Task::perform( + // measurement::load_measurement(loopback.0.path, measurement::Kind::Loopback), + // measurement::Message::Loaded, + // ) + // }) + // .unwrap_or_else(Task::none); + + // let load_measurements = project.measurements.into_iter().map(|measurement| { + // Task::perform( + // measurement::load_measurement(measurement.path, measurement::Kind::Normal), + // measurement::Message::Loaded, + // ) + // }); ( - Self { - project_path: Some(path), - ..Default::default() - }, - Task::batch([load_loopback, Task::batch(load_measurements)]).map(Message::Measurements), + Self::default(), + // Self { + // project_path: Some(path), + // ..Default::default() + // }, + // Task::batch([load_loopback, Task::batch(load_measurements)]).map(Message::Measurements), + Task::none(), ) } - pub fn update( - &mut self, - recent_projects: &mut RecentProjects, - message: Message, - ) -> Task { - match message { - Message::TabSelected(id) => { - let State::Analysing { - ref mut active_tab, - ref window, - ref mut selected, - ref loopback, - .. - } = self.state - else { - return Task::none(); - }; - - let (tab, tasks) = match (&active_tab, id) { - (Tab::Measurements { .. }, TabId::Measurements) - | (Tab::ImpulseResponses { .. }, TabId::ImpulseResponses) - | (Tab::FrequencyResponses, TabId::FrequencyResponses) => return Task::none(), - ( - Tab::ImpulseResponses { - ref window_settings, - .. - }, - tab_id, - ) if window_settings.window != *window => { - self.modal = Modal::PendingWindow { goto_tab: tab_id }; - return Task::none(); - } - (_, TabId::Measurements) => { - (Tab::Measurements { recording: None }, Task::none()) - } - (_, TabId::ImpulseResponses) => ( - Tab::ImpulseResponses { - window_settings: WindowSettings::new(window.clone()), - }, - Task::none(), - ), - (_, TabId::FrequencyResponses) => { - let tasks = self.measurements.loaded_mut().map(|measurement| { - compute_frequency_response(loopback, measurement, window) - }); - (Tab::FrequencyResponses, Task::batch(tasks)) - } - (_, TabId::SpectralDecay) => (Tab::SpectralDecay, Task::none()), - (_, TabId::Spectrogram) => (Tab::Spectrogram, Task::none()), - }; - - *active_tab = tab; - *selected = None; - - tasks - } + pub fn update(&mut self, recent_projects: &mut RecentProjects, msg: Message) -> Task { + match msg { Message::Measurements(message) => { match message { measurement::Message::Load(kind) => { @@ -281,11 +227,24 @@ impl Main { .map(Message::Measurements); } measurement::Message::Loaded(Ok(result)) => match Arc::into_inner(result) { - Some(measurement::LoadedKind::Loopback(new)) => match &mut self.state { - State::CollectingMeasuremnts { loopback, .. } => *loopback = Some(new), - State::Analysing { loopback, .. } => *loopback = new, - }, + Some(measurement::LoadedKind::Loopback(loopback)) => { + self.window = loopback + .loaded() + .map(raumklang_core::Loopback::sample_rate) + .map(SampleRate::from) + .map(Window::new) + .map(Into::into); + + self.loopback = Some(loopback); + + if !self.measurements.is_empty() { + self.state = State::Analysing { selected: None } + } + } Some(measurement::LoadedKind::Normal(measurement)) => { + if self.measurements.is_empty() { + self.state = State::Analysing { selected: None } + } self.measurements.push(measurement) } None => {} @@ -295,6 +254,10 @@ impl Main { } measurement::Message::Remove(id) => { self.measurements.remove(id); + + if self.measurements.is_empty() { + self.state = State::Collecting + } } measurement::Message::Select(selected) => { self.selected = Some(selected); @@ -302,94 +265,39 @@ impl Main { } } - let is_analysing_possible = self.analysing_possible(); - let state = std::mem::take(&mut self.state); - self.state = match (state, is_analysing_possible) { - ( - State::CollectingMeasuremnts { - recording, - loopback, - }, - false, - ) => State::CollectingMeasuremnts { - recording, - loopback, - }, - ( - State::CollectingMeasuremnts { - recording, - loopback: Some(loopback), - }, - true, - ) => State::Analysing { - active_tab: Tab::Measurements { recording }, - window: Window::new(SampleRate::from( - loopback - .loaded() - .map_or(44_100, |l| l.as_ref().sample_rate()), - )) - .into(), - selected: None, - charts: Charts::default(), - loopback, - }, - (old_state, true) => old_state, - (State::Analysing { loopback, .. }, false) => State::CollectingMeasuremnts { - recording: None, - loopback: Some(loopback), - }, - }; - Task::none() } Message::ImpulseResponseSelected(id) => { - log::debug!("Impulse response selected: {id}"); - - let State::Analysing { - selected: ref mut selected_analysis, - ref mut charts, - ref active_tab, - ref loopback, - .. - } = self.state - else { + let State::Analysing { selected } = &mut self.state else { return Task::none(); }; - *selected_analysis = Some(id); - charts.impulse_responses.data_cache.clear(); - - let Some(measurement) = self.measurements.get_mut(id) else { - return Task::none(); - }; + *selected = Some(id); - match active_tab { + match &self.tab { Tab::Measurements { .. } => Task::none(), - Tab::ImpulseResponses { .. } => compute_impulse_response(loopback, measurement), - Tab::FrequencyResponses => Task::none(), - Tab::SpectralDecay => { - compute_spectral_decay(loopback, measurement, self.spectral_decay_config) - } - Tab::Spectrogram => { - compute_spectrogram(loopback, measurement, &self.spectrogram_config) - } + Tab::ImpulseResponses { .. } => self.compute_impulse_response(id), + Tab::FrequencyResponses => todo!(), + Tab::SpectralDecay => todo!(), + Tab::Spectrogram => todo!(), } } Message::ImpulseResponseComputed(id, impulse_response) => { - let State::Analysing { - window, - active_tab, - selected: selected_analysis, - charts, - loopback, - .. - } = &mut self.state - else { + // let State::Analysing { + // window, + // active_tab, + // selected: selected_analysis, + // charts, + // loopback, + // .. + // } = &mut self.state + // else { + // return Task::none(); + // }; + let State::Analysing { selected } = self.state else { return Task::none(); }; - let impulse_response = ui::ImpulseResponse::from_data(impulse_response); - let Some(measurement) = self.measurements.get_mut(id) else { return Task::none(); }; @@ -398,590 +306,816 @@ impl Main { return Task::none(); }; + let impulse_response = ui::ImpulseResponse::from_data(impulse_response); analysis.impulse_response.computed(impulse_response.clone()); - if selected_analysis.is_some_and(|selected| selected == id) { - charts + if selected.is_some_and(|selected| selected == id) { + self.charts .impulse_responses .x_range .get_or_insert(0.0..=impulse_response.data.len() as f32); - charts.impulse_responses.data_cache.clear(); - } - - if let Tab::FrequencyResponses = active_tab { - compute_frequency_response(loopback, measurement, window) - } else if let Tab::SpectralDecay = active_tab { - compute_spectral_decay(loopback, measurement, self.spectral_decay_config) - } else if let Tab::Spectrogram = active_tab { - compute_spectrogram(loopback, measurement, &self.spectrogram_config) - } else { - Task::none() - } - } - Message::ImpulseResponses(impulse_response::Message::Chart(operation)) => { - let State::Analysing { - active_tab: - Tab::ImpulseResponses { - ref mut window_settings, - .. - }, - ref mut charts, - .. - } = self.state - else { - return Task::none(); - }; - - if let ChartOperation::Interaction(ref interaction) = operation { - match interaction { - chart::Interaction::HandleMoved(index, new_pos) => { - let mut handles: window::Handles = Into::into(&window_settings.window); - handles.update(*index, *new_pos); - window_settings.window.update(handles); - } - chart::Interaction::ZoomChanged(zoom) => { - charts.impulse_responses.zoom = *zoom; - } - chart::Interaction::OffsetChanged(offset) => { - charts.impulse_responses.offset = *offset; - } - } - } - - charts.impulse_responses.update(operation); - - Task::none() - } - Message::FrequencyResponseComputed(id, frequency_response) => { - log::debug!("Frequency response computed: {id}"); - - let State::Analysing { ref mut charts, .. } = self.state else { - return Task::none(); - }; - - let Some(measurement) = self.measurements.get_mut(id) else { - return Task::none(); - }; - - let Some(analysis) = measurement.analysis_mut() else { - return Task::none(); - }; - - analysis - .frequency_response - .computed(frequency_response.clone()); - - charts.frequency_responses.cache.clear(); - - if let Some(fraction) = self.smoothing.fraction() { - Task::perform( - frequency_response::smooth_frequency_response( - id, - frequency_response, - fraction, - ), - Message::FrequencyResponseSmoothed, - ) - } else { - Task::none() - } - } - Message::FrequencyResponseToggled(id, state) => { - let State::Analysing { ref mut charts, .. } = self.state else { - return Task::none(); - }; - - let Some(frequency_response) = self - .measurements - .get_mut(id) - .and_then(ui::Measurement::analysis_mut) - .map(|analysis| &mut analysis.frequency_response) - else { - return Task::none(); - }; - - frequency_response.is_shown = state; - - charts.frequency_responses.cache.clear(); - - Task::none() - } - Message::SmoothingChanged(smoothing) => { - let State::Analysing { ref mut charts, .. } = self.state else { - return Task::none(); - }; - - self.smoothing = smoothing; - - if let Some(fraction) = smoothing.fraction() { - let tasks = self.measurements.iter().flat_map(|measurement| { - let fr = measurement - .analysis() - .as_ref() - .and_then(|a| a.frequency_response.data.clone())?; - - Some(Task::perform( - frequency_response::smooth_frequency_response( - measurement.id(), - fr, - fraction, - ), - Message::FrequencyResponseSmoothed, - )) - }); - - Task::batch(tasks) - } else { - self.measurements - .iter_mut() - .flat_map(ui::Measurement::analysis_mut) - .map(|analysis| &mut analysis.frequency_response) - .for_each(|fr| fr.smoothed = None); - - charts.frequency_responses.cache.clear(); - - Task::none() - } - } - Message::FrequencyResponseSmoothed((id, smoothed_data)) => { - let State::Analysing { ref mut charts, .. } = self.state else { - return Task::none(); - }; - - let Some(frequency_response) = self - .measurements - .get_mut(id) - .and_then(ui::Measurement::analysis_mut) - .map(|analysis| &mut analysis.frequency_response) - else { - return Task::none(); - }; - - frequency_response.smoothed = Some(smoothed_data); - charts.frequency_responses.cache.clear(); - - Task::none() - } - Message::ShiftKeyPressed => { - let State::Analysing { ref mut charts, .. } = self.state else { - return Task::none(); - }; - - charts.impulse_responses.shift_key_pressed(); - - Task::none() - } - Message::ShiftKeyReleased => { - let State::Analysing { ref mut charts, .. } = self.state else { - return Task::none(); - }; - - charts.impulse_responses.shift_key_released(); - - Task::none() - } - Message::Modal(action) => { - let Modal::PendingWindow { goto_tab } = std::mem::take(&mut self.modal) else { - return Task::none(); - }; - - let State::Analysing { - ref mut active_tab, - ref mut window, - .. - } = self.state - else { - return Task::none(); - }; - - let Tab::ImpulseResponses { - ref mut window_settings, - .. - } = active_tab - else { - return Task::none(); - }; - - match action { - ModalAction::Discard => { - *window_settings = WindowSettings::new(window.clone()); - } - ModalAction::Apply => { - self.measurements - .iter_mut() - .flat_map(ui::Measurement::analysis_mut) - .for_each(|analysis| { - analysis.frequency_response = ui::FrequencyResponse::default(); - analysis.spectral_decay = ui::spectral_decay::State::default(); - analysis.spectrogram = ui::spectrogram::State::default(); - }); - - *window = window_settings.window.clone(); - } - } - - Task::done(Message::TabSelected(goto_tab)) - } - Message::Recording(message) => { - let (State::CollectingMeasuremnts { - ref mut recording, - loopback: Some(ref mut loopback), - } - | State::Analysing { - active_tab: Tab::Measurements { ref mut recording }, - ref mut loopback, - .. - }) = self.state - else { - return Task::none(); - }; - - if let Some(view) = recording { - match view.update(message) { - recording::Action::None => Task::none(), - recording::Action::Cancel => { - *recording = None; - Task::none() - } - recording::Action::Finished(result) => { - match result { - recording::Result::Loopback(signal) => { - *loopback = ui::Loopback::new("Loopback".to_string(), signal) - } - recording::Result::Measurement(signal) => { - self.measurements.push(ui::Measurement::new( - "Measurement".to_string(), - None, - Some(signal), - )) - } - } - *recording = None; - Task::none() - } - recording::Action::Task(task) => task.map(Message::Recording), - } - } else { - Task::none() - } - } - Message::StartRecording(kind) => match &mut self.state { - State::CollectingMeasuremnts { recording, .. } - | State::Analysing { - active_tab: Tab::Measurements { recording }, - .. - } => { - *recording = Some(Recording::new(kind)); - Task::none() - } - _ => Task::none(), - }, - Message::MeasurementChart(interaction) => { - match interaction { - waveform::Interaction::ZoomChanged(zoom) => self.zoom = zoom, - waveform::Interaction::OffsetChanged(offset) => self.offset = offset, - } - - self.signal_cache.clear(); - - Task::none() - } - Message::SaveImpulseResponseFileDialog(id) => { - Task::future(choose_impulse_response_file_path()) - .and_then(Task::done) - .map(Message::SaveImpulseResponse.with(id)) - } - Message::SaveImpulseResponse(id, path) => { - let State::Analysing { - active_tab: Tab::ImpulseResponses { .. }, - .. - } = &self.state - else { - return Task::none(); - }; - - if let Some(impulse_response) = self - .measurements - .get(id) - .and_then(ui::Measurement::analysis) - .and_then(|analysis| analysis.impulse_response.result()) - .cloned() - { - Task::perform( - save_impulse_response(path.clone(), impulse_response), - |_| Message::ImpulseResponsesSaved(path), - ) - } else { - self.compute_impulse_response(id) - .chain(Task::done(Message::SaveImpulseResponse(id, path))) - } - } - Message::ImpulseResponsesSaved(path) => { - log::debug!("Impulse response saved to: {}", path.display()); - - Task::none() - } - Message::NewProject => { - *self = Self::default(); - - Task::none() - } - Message::LoadProject => Task::perform( - crate::pick_project_file().then(async |res| { - let path = res?; - load_project(path).await - }), - Message::ProjectLoaded, - ), - Message::ProjectLoaded(Ok((project, path))) => match Arc::into_inner(project) { - Some(project) => { - recent_projects.insert(path.clone()); - - let (screen, tasks) = Self::from_project(path, project); - *self = screen; - - Task::batch([ - tasks, - Task::future(recent_projects.clone().save()).discard(), - ]) - } - None => Task::none(), - }, - Message::ProjectLoaded(Err(err)) => { - log::debug!("Loading project failed: {err}"); - - Task::none() - } - Message::RecentProject(id) => match recent_projects.get(id) { - Some(path) => Task::perform(load_project(path.clone()), Message::ProjectLoaded), - None => Task::none(), - }, - Message::SaveProject => { - let loopback = if let State::CollectingMeasuremnts { ref loopback, .. } = self.state - { - loopback.as_ref() - } else if let State::Analysing { ref loopback, .. } = self.state { - Some(loopback) - } else { - None - }; - - let loopback = loopback - .as_ref() - .and_then(|l| l.path.clone()) - .map(|path| project::Loopback(project::Measurement { path })); - - let measurements = self - .measurements - .iter() - .flat_map(|m| m.path.clone()) - .map(|path| project::Measurement { path }) - .collect(); - - let project = Project { - loopback, - measurements, - }; - - if let Some(path) = self.project_path.clone() { - Task::perform( - project - .save(path.clone()) - .map_ok(move |_| path) - .map_err(PickAndSaveError::File), - Message::ProjectSaved, - ) - } else { - Task::perform( - pick_project_file().then(async |res| { - let path = res?; - project.save(path.clone()).await?; - - Ok(path) - }), - Message::ProjectSaved, - ) - } - } - Message::ProjectSaved(Ok(path)) => { - log::debug!("Project saved."); - - self.project_path = Some(path); - - Task::none() - } - Message::ProjectSaved(Err(err)) => { - log::debug!("Saving project failed: {err}"); - Task::none() - } - Message::SpectralDecayComputed(id, decay) => { - log::debug!( - "Spectral decay for measurement (ID: {}) with: {} slices, computed.", - id, - decay.len() - ); - - let Some(spectral_decay) = self - .measurements - .get_mut(id) - .and_then(ui::Measurement::analysis_mut) - .map(|analysis| &mut analysis.spectral_decay) - else { - return Task::none(); - }; - - spectral_decay.computed(decay); - - if let State::Analysing { charts, .. } = &mut self.state { - charts.spectral_decay_cache.clear(); - }; - - Task::none() - } - Message::Spectrogram(interaction) => { - log::debug!("Spectrogram chart: {interaction:?}."); - - let State::Analysing { charts, .. } = &mut self.state else { - return Task::none(); - }; - - match interaction { - chart::spectrogram::Interaction::ZoomChanged(zoom) => { - charts.spectrogram.zoom = zoom - } - chart::spectrogram::Interaction::OffsetChanged(offset) => { - charts.spectrogram.offset = offset - } + self.charts.impulse_responses.data_cache.clear(); } - charts.spectrogram.cache.clear(); - + // if let Tab::FrequencyResponses = active_tab { + // compute_frequency_response(loopback, measurement, window) + // } else if let Tab::SpectralDecay = active_tab { + // compute_spectral_decay(loopback, measurement, self.spectral_decay_config) + // } else if let Tab::Spectrogram = active_tab { + // compute_spectrogram(loopback, measurement, &self.spectrogram_config) + // } else { + // Task::none() + // } Task::none() } - Message::SpectrogramComputed(id, data) => { - log::debug!( - "Spectrogram for measurement (ID: {}) with: {} slices, computed.", - id, - data.len() - ); - - let Some(spectrogram) = self - .measurements - .get_mut(id) - .and_then(ui::Measurement::analysis_mut) - .map(|analysis| &mut analysis.spectrogram) - else { - return Task::none(); - }; - - spectrogram.computed(data); - - if let State::Analysing { charts, .. } = &mut self.state { - charts.spectrogram.cache.clear(); - }; - + Message::OpenMeasurements => { + self.tab = Tab::Measurements { recording: None }; Task::none() } - Message::OpenSpectralDecayConfig => { - self.modal = Modal::SpectralDecayConfig(SpectralDecayConfig::new( - self.spectral_decay_config, - )); - - Task::none() - } - Message::SpectralDecayConfig(message) => { - let Modal::SpectralDecayConfig(config) = &mut self.modal else { + Message::OpenImpulseResponses => { + let Some(window) = &self.window else { return Task::none(); }; - let State::Analysing { - selected: selected_analysis, - ref loopback, - .. - } = self.state - else { - return Task::none(); + self.tab = Tab::ImpulseResponses { + window_settings: WindowSettings::new(window.clone()), }; - match config.update(message) { - Some(action) => { - self.modal = Modal::None; - - if let spectral_decay_config::Action::Apply(config) = action { - self.spectral_decay_config = config; - - self.measurements - .iter_mut() - .flat_map(ui::Measurement::analysis_mut) - .for_each(|analysis| { - analysis.spectral_decay = ui::spectral_decay::State::default() - }); - - selected_analysis - .and_then(|id| self.measurements.iter_mut().find(|m| m.id() == id)) - .map_or(Task::none(), |measurement| { - compute_spectral_decay( - loopback, - measurement, - self.spectral_decay_config, - ) - }) - } else { - Task::none() - } - } - None => Task::none(), - } - } - Message::OpenSpectrogramConfig => { - self.modal = - Modal::SpectrogramConfig(SpectrogramConfig::new(self.spectrogram_config)); - - Task::none() - } - Message::SpectrogramConfig(message) => { - let Modal::SpectrogramConfig(config) = &mut self.modal else { - return Task::none(); - }; - - let State::Analysing { - selected: selected_analysis, - ref loopback, - .. - } = self.state - else { - return Task::none(); - }; - - match config.update(message) { - spectrogram_config::Action::None => Task::none(), - spectrogram_config::Action::Close => { - self.modal = Modal::None; - - Task::none() - } - spectrogram_config::Action::ConfigChanged(preferences) => { - self.modal = Modal::None; - self.spectrogram_config = preferences; - - self.measurements - .iter_mut() - .flat_map(ui::Measurement::analysis_mut) - .for_each(|analysis| { - analysis.spectrogram = ui::spectrogram::State::default() - }); - - selected_analysis - .and_then(|id| self.measurements.get_mut(id)) - .map_or(Task::none(), |measurement| { - compute_spectrogram(loopback, measurement, &self.spectrogram_config) - }) - } - } + return Task::none(); } + _ => Task::none(), } } + // pub fn update( + // &mut self, + // recent_projects: &mut RecentProjects, + // message: Message, + // ) -> Task { + // match message { + // Message::TabSelected(id) => { + // let State::Analysing { + // ref mut active_tab, + // ref window, + // ref mut selected, + // ref loopback, + // .. + // } = self.state + // else { + // return Task::none(); + // }; + + // let (tab, tasks) = match (&active_tab, id) { + // (Tab::Measurements { .. }, TabId::Measurements) + // | (Tab::ImpulseResponses { .. }, TabId::ImpulseResponses) + // | (Tab::FrequencyResponses, TabId::FrequencyResponses) => return Task::none(), + // ( + // Tab::ImpulseResponses { + // ref window_settings, + // .. + // }, + // tab_id, + // ) if window_settings.window != *window => { + // self.modal = Modal::PendingWindow { goto_tab: tab_id }; + // return Task::none(); + // } + // (_, TabId::Measurements) => { + // (Tab::Measurements { recording: None }, Task::none()) + // } + // (_, TabId::ImpulseResponses) => ( + // Tab::ImpulseResponses { + // window_settings: WindowSettings::new(window.clone()), + // }, + // Task::none(), + // ), + // (_, TabId::FrequencyResponses) => { + // let tasks = self.measurements.loaded_mut().map(|measurement| { + // compute_frequency_response(loopback, measurement, window) + // }); + // (Tab::FrequencyResponses, Task::batch(tasks)) + // } + // (_, TabId::SpectralDecay) => (Tab::SpectralDecay, Task::none()), + // (_, TabId::Spectrogram) => (Tab::Spectrogram, Task::none()), + // }; + + // *active_tab = tab; + // *selected = None; + + // tasks + // } + // Message::Measurements(message) => { + // match message { + // measurement::Message::Load(kind) => { + // let dialog_caption = kind.to_string(); + + // return Task::perform( + // measurement::pick_file_and_load_signal(dialog_caption, kind), + // measurement::Message::Loaded, + // ) + // .map(Message::Measurements); + // } + // measurement::Message::Loaded(Ok(result)) => match Arc::into_inner(result) { + // Some(measurement::LoadedKind::Loopback(new)) => match &mut self.state { + // State::CollectingMeasuremnts { loopback, .. } => *loopback = Some(new), + // State::Analysing { loopback, .. } => *loopback = new, + // }, + // Some(measurement::LoadedKind::Normal(measurement)) => { + // self.measurements.push(measurement) + // } + // None => {} + // }, + // measurement::Message::Loaded(Err(err)) => { + // log::error!("{err}"); + // } + // measurement::Message::Remove(id) => { + // self.measurements.remove(id); + // } + // measurement::Message::Select(selected) => { + // self.selected = Some(selected); + // self.signal_cache.clear(); + // } + // } + + // let is_analysing_possible = self.analysing_possible(); + // let state = std::mem::take(&mut self.state); + // self.state = match (state, is_analysing_possible) { + // ( + // State::CollectingMeasuremnts { + // recording, + // loopback, + // }, + // false, + // ) => State::CollectingMeasuremnts { + // recording, + // loopback, + // }, + // ( + // State::CollectingMeasuremnts { + // recording, + // loopback: Some(loopback), + // }, + // true, + // ) => State::Analysing { + // active_tab: Tab::Measurements { recording }, + // window: Window::new(SampleRate::from( + // loopback + // .loaded() + // .map_or(44_100, |l| l.as_ref().sample_rate()), + // )) + // .into(), + // selected: None, + // charts: Charts::default(), + // loopback, + // }, + // (old_state, true) => old_state, + // (State::Analysing { loopback, .. }, false) => State::CollectingMeasuremnts { + // recording: None, + // loopback: Some(loopback), + // }, + // }; + + // Task::none() + // } + // Message::ImpulseResponseSelected(id) => { + // log::debug!("Impulse response selected: {id}"); + + // let State::Analysing { + // selected: ref mut selected_analysis, + // ref mut charts, + // ref active_tab, + // ref loopback, + // .. + // } = self.state + // else { + // return Task::none(); + // }; + + // *selected_analysis = Some(id); + // charts.impulse_responses.data_cache.clear(); + + // let Some(measurement) = self.measurements.get_mut(id) else { + // return Task::none(); + // }; + + // match active_tab { + // Tab::Measurements { .. } => Task::none(), + // Tab::ImpulseResponses { .. } => compute_impulse_response(loopback, measurement), + // Tab::FrequencyResponses => Task::none(), + // Tab::SpectralDecay => { + // compute_spectral_decay(loopback, measurement, self.spectral_decay_config) + // } + // Tab::Spectrogram => { + // compute_spectrogram(loopback, measurement, &self.spectrogram_config) + // } + // } + // } + // Message::ImpulseResponseComputed(id, impulse_response) => { + // let State::Analysing { + // window, + // active_tab, + // selected: selected_analysis, + // charts, + // loopback, + // .. + // } = &mut self.state + // else { + // return Task::none(); + // }; + + // let impulse_response = ui::ImpulseResponse::from_data(impulse_response); + + // let Some(measurement) = self.measurements.get_mut(id) else { + // return Task::none(); + // }; + + // let Some(analysis) = measurement.analysis_mut() else { + // return Task::none(); + // }; + + // analysis.impulse_response.computed(impulse_response.clone()); + + // if selected_analysis.is_some_and(|selected| selected == id) { + // charts + // .impulse_responses + // .x_range + // .get_or_insert(0.0..=impulse_response.data.len() as f32); + + // charts.impulse_responses.data_cache.clear(); + // } + + // if let Tab::FrequencyResponses = active_tab { + // compute_frequency_response(loopback, measurement, window) + // } else if let Tab::SpectralDecay = active_tab { + // compute_spectral_decay(loopback, measurement, self.spectral_decay_config) + // } else if let Tab::Spectrogram = active_tab { + // compute_spectrogram(loopback, measurement, &self.spectrogram_config) + // } else { + // Task::none() + // } + // } + // Message::ImpulseResponses(impulse_response::Message::Chart(operation)) => { + // let State::Analysing { + // active_tab: + // Tab::ImpulseResponses { + // ref mut window_settings, + // .. + // }, + // ref mut charts, + // .. + // } = self.state + // else { + // return Task::none(); + // }; + + // if let ChartOperation::Interaction(ref interaction) = operation { + // match interaction { + // chart::Interaction::HandleMoved(index, new_pos) => { + // let mut handles: window::Handles = Into::into(&window_settings.window); + // handles.update(*index, *new_pos); + // window_settings.window.update(handles); + // } + // chart::Interaction::ZoomChanged(zoom) => { + // charts.impulse_responses.zoom = *zoom; + // } + // chart::Interaction::OffsetChanged(offset) => { + // charts.impulse_responses.offset = *offset; + // } + // } + // } + + // charts.impulse_responses.update(operation); + + // Task::none() + // } + // Message::FrequencyResponseComputed(id, frequency_response) => { + // log::debug!("Frequency response computed: {id}"); + + // let State::Analysing { ref mut charts, .. } = self.state else { + // return Task::none(); + // }; + + // let Some(measurement) = self.measurements.get_mut(id) else { + // return Task::none(); + // }; + + // let Some(analysis) = measurement.analysis_mut() else { + // return Task::none(); + // }; + + // analysis + // .frequency_response + // .computed(frequency_response.clone()); + + // charts.frequency_responses.cache.clear(); + + // if let Some(fraction) = self.smoothing.fraction() { + // Task::perform( + // frequency_response::smooth_frequency_response( + // id, + // frequency_response, + // fraction, + // ), + // Message::FrequencyResponseSmoothed, + // ) + // } else { + // Task::none() + // } + // } + // Message::FrequencyResponseToggled(id, state) => { + // let State::Analysing { ref mut charts, .. } = self.state else { + // return Task::none(); + // }; + + // let Some(frequency_response) = self + // .measurements + // .get_mut(id) + // .and_then(ui::Measurement::analysis_mut) + // .map(|analysis| &mut analysis.frequency_response) + // else { + // return Task::none(); + // }; + + // frequency_response.is_shown = state; + + // charts.frequency_responses.cache.clear(); + + // Task::none() + // } + // Message::SmoothingChanged(smoothing) => { + // let State::Analysing { ref mut charts, .. } = self.state else { + // return Task::none(); + // }; + + // self.smoothing = smoothing; + + // if let Some(fraction) = smoothing.fraction() { + // let tasks = self.measurements.iter().flat_map(|measurement| { + // let fr = measurement + // .analysis() + // .as_ref() + // .and_then(|a| a.frequency_response.data.clone())?; + + // Some(Task::perform( + // frequency_response::smooth_frequency_response( + // measurement.id(), + // fr, + // fraction, + // ), + // Message::FrequencyResponseSmoothed, + // )) + // }); + + // Task::batch(tasks) + // } else { + // self.measurements + // .iter_mut() + // .flat_map(ui::Measurement::analysis_mut) + // .map(|analysis| &mut analysis.frequency_response) + // .for_each(|fr| fr.smoothed = None); + + // charts.frequency_responses.cache.clear(); + + // Task::none() + // } + // } + // Message::FrequencyResponseSmoothed((id, smoothed_data)) => { + // let State::Analysing { ref mut charts, .. } = self.state else { + // return Task::none(); + // }; + + // let Some(frequency_response) = self + // .measurements + // .get_mut(id) + // .and_then(ui::Measurement::analysis_mut) + // .map(|analysis| &mut analysis.frequency_response) + // else { + // return Task::none(); + // }; + + // frequency_response.smoothed = Some(smoothed_data); + // charts.frequency_responses.cache.clear(); + + // Task::none() + // } + // Message::ShiftKeyPressed => { + // let State::Analysing { ref mut charts, .. } = self.state else { + // return Task::none(); + // }; + + // charts.impulse_responses.shift_key_pressed(); + + // Task::none() + // } + // Message::ShiftKeyReleased => { + // let State::Analysing { ref mut charts, .. } = self.state else { + // return Task::none(); + // }; + + // charts.impulse_responses.shift_key_released(); + + // Task::none() + // } + // Message::Modal(action) => { + // let Modal::PendingWindow { goto_tab } = std::mem::take(&mut self.modal) else { + // return Task::none(); + // }; + + // let State::Analysing { + // ref mut active_tab, + // ref mut window, + // .. + // } = self.state + // else { + // return Task::none(); + // }; + + // let Tab::ImpulseResponses { + // ref mut window_settings, + // .. + // } = active_tab + // else { + // return Task::none(); + // }; + + // match action { + // ModalAction::Discard => { + // *window_settings = WindowSettings::new(window.clone()); + // } + // ModalAction::Apply => { + // self.measurements + // .iter_mut() + // .flat_map(ui::Measurement::analysis_mut) + // .for_each(|analysis| { + // analysis.frequency_response = ui::FrequencyResponse::default(); + // analysis.spectral_decay = ui::spectral_decay::State::default(); + // analysis.spectrogram = ui::spectrogram::State::default(); + // }); + + // *window = window_settings.window.clone(); + // } + // } + + // Task::done(Message::TabSelected(goto_tab)) + // } + // Message::Recording(message) => { + // let (State::CollectingMeasuremnts { + // ref mut recording, + // loopback: Some(ref mut loopback), + // } + // | State::Analysing { + // active_tab: Tab::Measurements { ref mut recording }, + // ref mut loopback, + // .. + // }) = self.state + // else { + // return Task::none(); + // }; + + // if let Some(view) = recording { + // match view.update(message) { + // recording::Action::None => Task::none(), + // recording::Action::Cancel => { + // *recording = None; + // Task::none() + // } + // recording::Action::Finished(result) => { + // match result { + // recording::Result::Loopback(signal) => { + // *loopback = ui::Loopback::new("Loopback".to_string(), signal) + // } + // recording::Result::Measurement(signal) => { + // self.measurements.push(ui::Measurement::new( + // "Measurement".to_string(), + // None, + // Some(signal), + // )) + // } + // } + // *recording = None; + // Task::none() + // } + // recording::Action::Task(task) => task.map(Message::Recording), + // } + // } else { + // Task::none() + // } + // } + // Message::StartRecording(kind) => match &mut self.state { + // State::CollectingMeasuremnts { recording, .. } + // | State::Analysing { + // active_tab: Tab::Measurements { recording }, + // .. + // } => { + // *recording = Some(Recording::new(kind)); + // Task::none() + // } + // _ => Task::none(), + // }, + // Message::MeasurementChart(interaction) => { + // match interaction { + // waveform::Interaction::ZoomChanged(zoom) => self.zoom = zoom, + // waveform::Interaction::OffsetChanged(offset) => self.offset = offset, + // } + + // self.signal_cache.clear(); + + // Task::none() + // } + // Message::SaveImpulseResponseFileDialog(id) => { + // Task::future(choose_impulse_response_file_path()) + // .and_then(Task::done) + // .map(Message::SaveImpulseResponse.with(id)) + // } + // Message::SaveImpulseResponse(id, path) => { + // let State::Analysing { + // active_tab: Tab::ImpulseResponses { .. }, + // .. + // } = &self.state + // else { + // return Task::none(); + // }; + + // if let Some(impulse_response) = self + // .measurements + // .get(id) + // .and_then(ui::Measurement::analysis) + // .and_then(|analysis| analysis.impulse_response.result()) + // .cloned() + // { + // Task::perform( + // save_impulse_response(path.clone(), impulse_response), + // |_| Message::ImpulseResponsesSaved(path), + // ) + // } else { + // self.compute_impulse_response(id) + // .chain(Task::done(Message::SaveImpulseResponse(id, path))) + // } + // } + // Message::ImpulseResponsesSaved(path) => { + // log::debug!("Impulse response saved to: {}", path.display()); + + // Task::none() + // } + // Message::NewProject => { + // *self = Self::default(); + + // Task::none() + // } + // Message::LoadProject => Task::perform( + // crate::pick_project_file().then(async |res| { + // let path = res?; + // load_project(path).await + // }), + // Message::ProjectLoaded, + // ), + // Message::ProjectLoaded(Ok((project, path))) => match Arc::into_inner(project) { + // Some(project) => { + // recent_projects.insert(path.clone()); + + // let (screen, tasks) = Self::from_project(path, project); + // *self = screen; + + // Task::batch([ + // tasks, + // Task::future(recent_projects.clone().save()).discard(), + // ]) + // } + // None => Task::none(), + // }, + // Message::ProjectLoaded(Err(err)) => { + // log::debug!("Loading project failed: {err}"); + + // Task::none() + // } + // Message::RecentProject(id) => match recent_projects.get(id) { + // Some(path) => Task::perform(load_project(path.clone()), Message::ProjectLoaded), + // None => Task::none(), + // }, + // Message::SaveProject => { + // let loopback = if let State::CollectingMeasuremnts { ref loopback, .. } = self.state + // { + // loopback.as_ref() + // } else if let State::Analysing { ref loopback, .. } = self.state { + // Some(loopback) + // } else { + // None + // }; + + // let loopback = loopback + // .as_ref() + // .and_then(|l| l.path.clone()) + // .map(|path| project::Loopback(project::Measurement { path })); + + // let measurements = self + // .measurements + // .iter() + // .flat_map(|m| m.path.clone()) + // .map(|path| project::Measurement { path }) + // .collect(); + + // let project = Project { + // loopback, + // measurements, + // }; + + // if let Some(path) = self.project_path.clone() { + // Task::perform( + // project + // .save(path.clone()) + // .map_ok(move |_| path) + // .map_err(PickAndSaveError::File), + // Message::ProjectSaved, + // ) + // } else { + // Task::perform( + // pick_project_file().then(async |res| { + // let path = res?; + // project.save(path.clone()).await?; + + // Ok(path) + // }), + // Message::ProjectSaved, + // ) + // } + // } + // Message::ProjectSaved(Ok(path)) => { + // log::debug!("Project saved."); + + // self.project_path = Some(path); + + // Task::none() + // } + // Message::ProjectSaved(Err(err)) => { + // log::debug!("Saving project failed: {err}"); + // Task::none() + // } + // Message::SpectralDecayComputed(id, decay) => { + // log::debug!( + // "Spectral decay for measurement (ID: {}) with: {} slices, computed.", + // id, + // decay.len() + // ); + + // let Some(spectral_decay) = self + // .measurements + // .get_mut(id) + // .and_then(ui::Measurement::analysis_mut) + // .map(|analysis| &mut analysis.spectral_decay) + // else { + // return Task::none(); + // }; + + // spectral_decay.computed(decay); + + // if let State::Analysing { charts, .. } = &mut self.state { + // charts.spectral_decay_cache.clear(); + // }; + + // Task::none() + // } + // Message::Spectrogram(interaction) => { + // log::debug!("Spectrogram chart: {interaction:?}."); + + // let State::Analysing { charts, .. } = &mut self.state else { + // return Task::none(); + // }; + + // match interaction { + // chart::spectrogram::Interaction::ZoomChanged(zoom) => { + // charts.spectrogram.zoom = zoom + // } + // chart::spectrogram::Interaction::OffsetChanged(offset) => { + // charts.spectrogram.offset = offset + // } + // } + + // charts.spectrogram.cache.clear(); + + // Task::none() + // } + // Message::SpectrogramComputed(id, data) => { + // log::debug!( + // "Spectrogram for measurement (ID: {}) with: {} slices, computed.", + // id, + // data.len() + // ); + + // let Some(spectrogram) = self + // .measurements + // .get_mut(id) + // .and_then(ui::Measurement::analysis_mut) + // .map(|analysis| &mut analysis.spectrogram) + // else { + // return Task::none(); + // }; + + // spectrogram.computed(data); + + // if let State::Analysing { charts, .. } = &mut self.state { + // charts.spectrogram.cache.clear(); + // }; + + // Task::none() + // } + // Message::OpenSpectralDecayConfig => { + // self.modal = Modal::SpectralDecayConfig(SpectralDecayConfig::new( + // self.spectral_decay_config, + // )); + + // Task::none() + // } + // Message::SpectralDecayConfig(message) => { + // let Modal::SpectralDecayConfig(config) = &mut self.modal else { + // return Task::none(); + // }; + + // let State::Analysing { + // selected: selected_analysis, + // ref loopback, + // .. + // } = self.state + // else { + // return Task::none(); + // }; + + // match config.update(message) { + // Some(action) => { + // self.modal = Modal::None; + + // if let spectral_decay_config::Action::Apply(config) = action { + // self.spectral_decay_config = config; + + // self.measurements + // .iter_mut() + // .flat_map(ui::Measurement::analysis_mut) + // .for_each(|analysis| { + // analysis.spectral_decay = ui::spectral_decay::State::default() + // }); + + // selected_analysis + // .and_then(|id| self.measurements.iter_mut().find(|m| m.id() == id)) + // .map_or(Task::none(), |measurement| { + // compute_spectral_decay( + // loopback, + // measurement, + // self.spectral_decay_config, + // ) + // }) + // } else { + // Task::none() + // } + // } + // None => Task::none(), + // } + // } + // Message::OpenSpectrogramConfig => { + // self.modal = + // Modal::SpectrogramConfig(SpectrogramConfig::new(self.spectrogram_config)); + + // Task::none() + // } + // Message::SpectrogramConfig(message) => { + // let Modal::SpectrogramConfig(config) = &mut self.modal else { + // return Task::none(); + // }; + + // let State::Analysing { + // selected: selected_analysis, + // ref loopback, + // .. + // } = self.state + // else { + // return Task::none(); + // }; + + // match config.update(message) { + // spectrogram_config::Action::None => Task::none(), + // spectrogram_config::Action::Close => { + // self.modal = Modal::None; + + // Task::none() + // } + // spectrogram_config::Action::ConfigChanged(preferences) => { + // self.modal = Modal::None; + // self.spectrogram_config = preferences; + + // self.measurements + // .iter_mut() + // .flat_map(ui::Measurement::analysis_mut) + // .for_each(|analysis| { + // analysis.spectrogram = ui::spectrogram::State::default() + // }); + + // selected_analysis + // .and_then(|id| self.measurements.get_mut(id)) + // .map_or(Task::none(), |measurement| { + // compute_spectrogram(loopback, measurement, &self.spectrogram_config) + // }) + // } + // } + // } + // } + // } pub fn view<'a>(&'a self, recent_projects: &'a RecentProjects) -> Element<'a, Message> { let header = { @@ -1021,102 +1155,224 @@ impl Main { .width(Length::Fill) }; + let tab = |s, is_active, message| { + button(text(s).size(20)) + .padding(10) + .style(move |theme: &Theme, status| { + let palette = theme.extended_palette(); + + let base = button::text(theme, status); + + if is_active { + button::Style { + text_color: palette.background.neutral.text, + background: Some(palette.background.base.color.into()), + ..base + } + } else { + base + } + }) + .on_press_maybe(message) + }; + + let is_analysing = matches!(self.state, State::Analysing { .. }); + + let tabs = row![ + tab( + "Measurements", + matches!(self.tab, Tab::Measurements { .. }), + Some(Message::OpenMeasurements) + ), + tab( + "Impulse Responses", + matches!(self.tab, Tab::ImpulseResponses { .. }), + is_analysing.then_some(Message::OpenImpulseResponses) // Message::OpenImpulseResponses + ), + tab( + "Frequency Responses", + matches!(self.tab, Tab::FrequencyResponses), + None // Message::OpenFrequencyResponses + ), + tab( + "Spectral Decays", + matches!(self.tab, Tab::SpectralDecay), + None // Message::OpenFrequencyResponses + ), + tab( + "Spectrogram", + matches!(self.tab, Tab::Spectrogram), + None // Message::OpenFrequencyResponses + ), + ] + .spacing(5) + .align_y(Center); + container(column![ dropdown_root("Project", project_menu).style(button::secondary), - match &self.state { - State::CollectingMeasuremnts { .. } => TabId::Measurements.view(false), - State::Analysing { active_tab, .. } => TabId::from(active_tab).view(true), - } + tabs, ]) .width(Length::Fill) .style(container::dark) }; - let content = match &self.state { - State::CollectingMeasuremnts { - recording, - loopback, - } => { - if let Some(recording) = recording { - recording.view().map(Message::Recording) - } else { - self.measurements_tab(loopback.as_ref()) - } - } - State::Analysing { - active_tab, - selected: selected_analysis, - charts, - loopback, - .. - } => match active_tab { - Tab::Measurements { recording } => { - if let Some(recording) = recording { - recording.view().map(Message::Recording) + let content = { + match &self.tab { + Tab::Measurements { recording } => self.measurements_tab(), + Tab::ImpulseResponses { window_settings } => { + let selected = if let State::Analysing { selected } = self.state { + selected } else { - self.measurements_tab(Some(loopback)) - } - } - Tab::ImpulseResponses { - window_settings, .. - } => self.impulse_responses_tab( - *selected_analysis, - &charts.impulse_responses, - window_settings, - ), - Tab::FrequencyResponses => { - self.frequency_responses_tab(&charts.frequency_responses) - } - Tab::SpectralDecay => { - self.spectral_decay_tab(*selected_analysis, &charts.spectral_decay_cache) + None + }; + self.impulse_responses_tab( + selected, + &self.charts.impulse_responses, + window_settings, + ) } - Tab::Spectrogram => self.spectrogram_tab(*selected_analysis, &charts.spectrogram), - }, + Tab::FrequencyResponses => todo!(), + Tab::SpectralDecay => todo!(), + Tab::Spectrogram => todo!(), + } }; - let content = container(column![header, container(content).padding(10)]); - - match self.modal { - Modal::None => content.into(), - Modal::PendingWindow { .. } => { - let pending_window = { - container( - column![ - text("Window pending!").size(18), - column![ - text("You have modified the window used for frequency response computations."), - text("You need to discard or apply your changes before proceeding."), - ].spacing(5), - row![ - space::horizontal(), - button("Discard") - .style(button::danger) - .on_press(Message::Modal(ModalAction::Discard)), - button("Apply") - .style(button::success) - .on_press(Message::Modal(ModalAction::Apply)) - ] - .spacing(5) - ] - .spacing(10)) - .padding(20) - .width(400) - .style(container::bordered_box) - }; - - modal(content, pending_window) - } - Modal::SpectralDecayConfig(ref config) => { - modal(content, config.view().map(Message::SpectralDecayConfig)) - } - Modal::SpectrogramConfig(ref spectrogram_config) => modal( - content, - spectrogram_config.view().map(Message::SpectrogramConfig), - ), - } + container(column![header, container(content).padding(10)]).into() } - - fn measurements_tab<'a>(&'a self, loopback: Option<&'a ui::Loopback>) -> Element<'a, Message> { + // pub fn view<'a>(&'a self, recent_projects: &'a RecentProjects) -> Element<'a, Message> { + // let header = { + // let project_menu = { + // let recent_project_entries = column( + // recent_projects + // .iter() + // .enumerate() + // .filter_map(|(i, p)| p.file_name().map(|f| (i, f))) + // .filter_map(|(i, p)| p.to_str().map(|f| (i, f))) + // .map(|(i, s)| { + // button(s) + // .on_press(Message::RecentProject(i)) + // .style(button::subtle) + // .width(Length::Fill) + // .into() + // }), + // ) + // .width(Length::Fill); + // column![ + // button("New") + // .on_press(Message::NewProject) + // .style(button::subtle) + // .width(Length::Fill), + // button("Save") + // .on_press(Message::SaveProject) + // .style(button::subtle) + // .width(Length::Fill), + // button("Open ...") + // .on_press(Message::LoadProject) + // .style(button::subtle) + // .width(Length::Fill), + // dropdown_menu("Open recent ...", recent_project_entries) + // .style(button::subtle) + // .width(Length::Fill), + // ] + // .width(Length::Fill) + // }; + + // container(column![ + // dropdown_root("Project", project_menu).style(button::secondary), + // match &self.state { + // State::CollectingMeasuremnts { .. } => TabId::Measurements.view(false), + // State::Analysing { active_tab, .. } => TabId::from(active_tab).view(true), + // } + // ]) + // .width(Length::Fill) + // .style(container::dark) + // }; + + // let content = match &self.state { + // State::CollectingMeasuremnts { + // recording, + // loopback, + // } => { + // if let Some(recording) = recording { + // recording.view().map(Message::Recording) + // } else { + // self.measurements_tab(loopback.as_ref()) + // } + // } + // State::Analysing { + // active_tab, + // selected: selected_analysis, + // charts, + // loopback, + // .. + // } => match active_tab { + // Tab::Measurements { recording } => { + // if let Some(recording) = recording { + // recording.view().map(Message::Recording) + // } else { + // self.measurements_tab(Some(loopback)) + // } + // } + // Tab::ImpulseResponses { + // window_settings, .. + // } => self.impulse_responses_tab( + // *selected_analysis, + // &charts.impulse_responses, + // window_settings, + // ), + // Tab::FrequencyResponses => { + // self.frequency_responses_tab(&charts.frequency_responses) + // } + // Tab::SpectralDecay => { + // self.spectral_decay_tab(*selected_analysis, &charts.spectral_decay_cache) + // } + // Tab::Spectrogram => self.spectrogram_tab(*selected_analysis, &charts.spectrogram), + // }, + // }; + + // let content = container(column![header, container(content).padding(10)]); + + // match self.modal { + // Modal::None => content.into(), + // Modal::PendingWindow { .. } => { + // let pending_window = { + // container( + // column![ + // text("Window pending!").size(18), + // column![ + // text("You have modified the window used for frequency response computations."), + // text("You need to discard or apply your changes before proceeding."), + // ].spacing(5), + // row![ + // space::horizontal(), + // button("Discard") + // .style(button::danger) + // .on_press(Message::Modal(ModalAction::Discard)), + // button("Apply") + // .style(button::success) + // .on_press(Message::Modal(ModalAction::Apply)) + // ] + // .spacing(5) + // ] + // .spacing(10)) + // .padding(20) + // .width(400) + // .style(container::bordered_box) + // }; + + // modal(content, pending_window) + // } + // Modal::SpectralDecayConfig(ref config) => { + // modal(content, config.view().map(Message::SpectralDecayConfig)) + // } + // Modal::SpectrogramConfig(ref spectrogram_config) => modal( + // content, + // spectrogram_config.view().map(Message::SpectrogramConfig), + // ), + // } + // } + + fn measurements_tab<'a>(&'a self) -> Element<'a, Message> { let sidebar = { let loopback = Category::new("Loopback") .push_button( @@ -1131,7 +1387,7 @@ impl Main { .on_press(Message::StartRecording(recording::Kind::Loopback)) .style(button::secondary), ) - .push_entry_maybe(loopback.map(|loopback| { + .push_entry_maybe(self.loopback.as_ref().map(|loopback| { measurement::loopback_entry(self.selected, loopback).map(Message::Measurements) })); @@ -1182,17 +1438,18 @@ impl Main { .into() }; - let loopback = if let State::CollectingMeasuremnts { ref loopback, .. } = self.state { - loopback.as_ref() - } else if let State::Analysing { ref loopback, .. } = self.state { - Some(loopback) - } else { - None - }; + // let loopback = if let State::CollectingMeasuremnts { ref loopback, .. } = self.state { + // loopback.as_ref() + // } else if let State::Analysing { ref loopback, .. } = self.state { + // Some(loopback) + // } else { + // None + // }; let content = if let Some(measurement) = self.selected.and_then(|selected| match selected { - measurement::Selected::Loopback => loopback + measurement::Selected::Loopback => self + .loopback .as_ref() .and_then(|l| l.loaded()) .map(AsRef::as_ref), @@ -1623,48 +1880,42 @@ impl Main { _ => None, }); - let recording = match &self.state { - State::CollectingMeasuremnts { - recording: Some(recording), - .. - } - | State::Analysing { - active_tab: - Tab::Measurements { - recording: Some(recording), - }, - .. - } => recording.subscription().map(Message::Recording), - _ => Subscription::none(), - }; - - Subscription::batch([hotkeys, recording]) + // let recording = match &self.state { + // State::CollectingMeasuremnts { + // recording: Some(recording), + // .. + // } + // | State::Analysing { + // active_tab: + // Tab::Measurements { + // recording: Some(recording), + // }, + // .. + // } => recording.subscription().map(Message::Recording), + // _ => Subscription::none(), + // }; + + // Subscription::batch([hotkeys, recording]) + Subscription::batch([hotkeys]) } - fn analysing_possible(&self) -> bool { - let is_loopback_loaded = if let State::CollectingMeasuremnts { - loopback: Some(ref loopback), - .. - } - | State::Analysing { ref loopback, .. } = self.state - { - loopback.is_loaded() - } else { - return false; - }; - - is_loopback_loaded && self.measurements.iter().any(ui::Measurement::is_loaded) - } + // fn analysing_possible(&self) -> bool { + // let is_loopback_loaded = if let State::CollectingMeasuremnts { + // loopback: Some(ref loopback), + // .. + // } + // | State::Analysing { ref loopback, .. } = self.state + // { + // loopback.is_loaded() + // } else { + // return false; + // }; + + // is_loopback_loaded && self.measurements.iter().any(ui::Measurement::is_loaded) + // } fn compute_impulse_response(&mut self, id: ui::measurement::Id) -> Task { - let loopback = if let State::CollectingMeasuremnts { - loopback: Some(ref loopback), - .. - } - | State::Analysing { ref loopback, .. } = self.state - { - loopback - } else { + let Some(loopback) = self.loopback.as_ref() else { return Task::none(); }; @@ -1757,20 +2008,27 @@ fn compute_impulse_response( impl Default for Main { fn default() -> Self { Self { - state: State::CollectingMeasuremnts { - recording: None, - loopback: None, - }, + tab: Tab::Measurements { recording: None }, + // state: State::CollectingMeasuremnts { + // recording: None, + // loopback: None, + // }, + state: State::default(), + + loopback: None, measurements: ui::measurement::List::default(), - project_path: None, + // project_path: None, selected: None, smoothing: frequency_response::Smoothing::default(), - modal: Modal::None, + // modal: Modal::None, signal_cache: canvas::Cache::default(), zoom: chart::Zoom::default(), offset: chart::Offset::default(), - spectral_decay_config: data::spectral_decay::Config::default(), - spectrogram_config: spectrogram::Preferences::default(), + // spectral_decay_config: data::spectral_decay::Config::default(), + // spectrogram_config: spectrogram::Preferences::default(), + window: None, + + charts: Charts::default(), } } } diff --git a/raumklang-gui/src/ui/measurement.rs b/raumklang-gui/src/ui/measurement.rs index d2cd3af..921e4ef 100644 --- a/raumklang-gui/src/ui/measurement.rs +++ b/raumklang-gui/src/ui/measurement.rs @@ -18,19 +18,23 @@ impl List { self.0.iter() } - pub(crate) fn iter_mut(&mut self) -> impl Iterator { + pub fn iter_mut(&mut self) -> impl Iterator { self.0.iter_mut() } + pub fn loaded(&self) -> impl Iterator { + self.0.iter().filter(|m| m.is_loaded()) + } + pub fn loaded_mut(&mut self) -> impl Iterator { self.0.iter_mut().filter(|m| m.is_loaded()) } - pub(crate) fn push(&mut self, measurement: Measurement) { + pub fn push(&mut self, measurement: Measurement) { self.0.push(measurement); } - pub(crate) fn remove(&mut self, id: Id) -> Option { + pub fn remove(&mut self, id: Id) -> Option { let index = self .0 .iter() @@ -41,13 +45,17 @@ impl List { Some(self.0.remove(index)) } - pub(crate) fn get(&self, id: Id) -> Option<&Measurement> { + pub fn get(&self, id: Id) -> Option<&Measurement> { self.0.iter().find(|m| m.id == id) } - pub(crate) fn get_mut(&mut self, id: Id) -> Option<&mut Measurement> { + pub fn get_mut(&mut self, id: Id) -> Option<&mut Measurement> { self.0.iter_mut().find(|m| m.id == id) } + + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } } #[derive(Debug, Clone)] From 7f286738d5efedda51cd671a16bd4c318043c0ee Mon Sep 17 00:00:00 2001 From: Hendrik Kunert Date: Wed, 28 Jan 2026 15:20:12 +0100 Subject: [PATCH 04/30] Rework analses state and storing --- raumklang-gui/src/screen/main.rs | 836 +++++++++++++++------------- raumklang-gui/src/ui/measurement.rs | 34 +- 2 files changed, 454 insertions(+), 416 deletions(-) diff --git a/raumklang-gui/src/screen/main.rs b/raumklang-gui/src/screen/main.rs index 9ea1132..7548866 100644 --- a/raumklang-gui/src/screen/main.rs +++ b/raumklang-gui/src/screen/main.rs @@ -15,7 +15,7 @@ use crate::{ chart::waveform, impulse_response::processing_overlay, measurement::Selected, spectral_decay_config::SpectralDecayConfig, spectrogram_config::SpectrogramConfig, }, - ui::{self, Loopback}, + ui::{self, measurement::Analysis, Loopback}, widget::sidebar, PickAndLoadError, }; @@ -41,6 +41,7 @@ use iced::{ use prism::{axis, line_series, Axis, Chart, Labels}; use std::{ + collections::{BTreeMap, HashMap}, fmt::Display, path::{Path, PathBuf}, sync::Arc, @@ -71,9 +72,19 @@ enum State { Collecting, Analysing { selected: Option, + analyses: BTreeMap, }, } +impl State { + pub fn analysis() -> Self { + Self::Analysing { + selected: None, + analyses: BTreeMap::new(), + } + } +} + impl Default for State { fn default() -> Self { State::Collecting @@ -238,12 +249,12 @@ impl Main { self.loopback = Some(loopback); if !self.measurements.is_empty() { - self.state = State::Analysing { selected: None } + self.state = State::analysis() } } Some(measurement::LoadedKind::Normal(measurement)) => { if self.measurements.is_empty() { - self.state = State::Analysing { selected: None } + self.state = State::analysis() } self.measurements.push(measurement) } @@ -268,7 +279,7 @@ impl Main { Task::none() } Message::ImpulseResponseSelected(id) => { - let State::Analysing { selected } = &mut self.state else { + let State::Analysing { selected, analyses } = &mut self.state else { return Task::none(); }; @@ -294,15 +305,23 @@ impl Main { // else { // return Task::none(); // }; - let State::Analysing { selected } = self.state else { - return Task::none(); - }; - let Some(measurement) = self.measurements.get_mut(id) else { + // let Some(measurement) = self.measurements.get_mut(id) else { + // return Task::none(); + // }; + + // let Some(analysis) = measurement.analysis_mut() else { + // return Task::none(); + // }; + let State::Analysing { + selected, + ref mut analyses, + } = self.state + else { return Task::none(); }; - let Some(analysis) = measurement.analysis_mut() else { + let Some(analysis) = analyses.get_mut(&id) else { return Task::none(); }; @@ -1220,7 +1239,7 @@ impl Main { match &self.tab { Tab::Measurements { recording } => self.measurements_tab(), Tab::ImpulseResponses { window_settings } => { - let selected = if let State::Analysing { selected } = self.state { + let selected = if let State::Analysing { selected, .. } = self.state { selected } else { None @@ -1487,7 +1506,11 @@ impl Main { let entries = self.measurements.iter().flat_map(|measurement| { let signal = measurement.signal()?; - let analysis = measurement.analysis()?; + let analysis = if let State::Analysing { analyses, .. } = &self.state { + analyses.get(&measurement.id()) + } else { + None + }; Some(impulse_response_item( selected, @@ -1509,9 +1532,13 @@ impl Main { let content = { let placeholder = center(text("Impulse response not computed, yet.")).into(); - selected - .and_then(|id| self.measurements.get(id)) - .and_then(ui::Measurement::analysis) + let analysis = if let State::Analysing { analyses, .. } = &self.state { + selected.and_then(|id| analyses.get(&id)) + } else { + None + }; + + analysis .and_then(|analysis| analysis.impulse_response.result()) .map_or(placeholder, |impulse_response| { chart @@ -1530,333 +1557,333 @@ impl Main { .into() } - fn frequency_responses_tab<'a>( - &'a self, - chart_settings: &'a frequency_response::ChartData, - ) -> Element<'a, Message> { - let sidebar = { - let header = sidebar::header("Frequency Responses"); - - let entries = self.measurements.iter().flat_map(|measurement| { - let analysis = &measurement.analysis()?; + // fn frequency_responses_tab<'a>( + // &'a self, + // chart_settings: &'a frequency_response::ChartData, + // ) -> Element<'a, Message> { + // let sidebar = { + // let header = sidebar::header("Frequency Responses"); - let content = analysis.frequency_response.view( - &measurement.name, - analysis.impulse_response.progress(), - Message::FrequencyResponseToggled.with(measurement.id()), - ); + // let entries = self.measurements.iter().flat_map(|measurement| { + // let analysis = &measurement.analysis()?; - Some(sidebar::item(content, false)) - }); - - container(column![header, scrollable(column(entries).spacing(6))].spacing(6)) - .padding(6) - .style(|theme| { - container::rounded_box(theme) - .background(theme.extended_palette().background.weakest.color) - }) - }; - - let header = { - row![pick_list( - frequency_response::Smoothing::ALL, - Some(&self.smoothing), - Message::SmoothingChanged, - )] - }; - - let frequency_responses = self - .measurements - .iter() - .flat_map(ui::Measurement::analysis) - .map(|analysis| &analysis.frequency_response); - - let content = if frequency_responses - .clone() - .any(|fr| fr.is_shown && fr.result().is_some()) - { - let series_list = frequency_responses - .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) - }), - ] - }) - .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(), - ), - ) - .x_range(chart_settings.x_range.clone().unwrap_or(20.0..=22_500.0)) - .y_labels(Labels::default().format(&|v| format!("{v:.0}"))) - .extend_series(series_list) - .cache(&chart_settings.cache); + // let content = analysis.frequency_response.view( + // &measurement.name, + // analysis.impulse_response.progress(), + // Message::FrequencyResponseToggled.with(measurement.id()), + // ); - container(chart) - } else { - container(text("Please select a frequency respone.")).center(Length::Fill) - }; + // Some(sidebar::item(content, false)) + // }); - row![ - container(sidebar) - .width(Length::FillPortion(2)) - .style(container::bordered_box), - column![header, container(content).width(Length::FillPortion(5))].spacing(12) - ] - .spacing(10) - .into() - } + // container(column![header, scrollable(column(entries).spacing(6))].spacing(6)) + // .padding(6) + // .style(|theme| { + // container::rounded_box(theme) + // .background(theme.extended_palette().background.weakest.color) + // }) + // }; - pub fn spectral_decay_tab<'a>( - &'a self, - selected: Option, - cache: &'a canvas::Cache, - ) -> Element<'a, Message> { - let sidebar = { - let header = { - let config_btn = button(icon::settings().center()) - .style(button::subtle) - .on_press(Message::OpenSpectralDecayConfig); - Category::new("Spectral Decays").push_button(config_btn) - }; + // let header = { + // row![pick_list( + // frequency_response::Smoothing::ALL, + // Some(&self.smoothing), + // Message::SmoothingChanged, + // )] + // }; - let entries = self.measurements.iter().flat_map(|measurement| { - let id = measurement.id(); - let is_active = selected.is_some_and(|s| s == id); + // let frequency_responses = self + // .measurements + // .iter() + // .flat_map(ui::Measurement::analysis) + // .map(|analysis| &analysis.frequency_response); - let signal = measurement.signal()?; - let entry = { - // TODO: refactor, basically the same btn as IR and Spectrogram - let dt: DateTime = signal.modified.into(); - let btn = button( - column![ - text(&measurement.name) - .size(16) - .wrapping(Wrapping::WordOrGlyph), - text!("{}", dt.format("%x %X")).size(10) - ] - .clip(true) - .spacing(6), - ) - .on_press_with(move || Message::ImpulseResponseSelected(id)) - .width(Length::Fill) - .style(move |theme: &Theme, status| { - let base = button::subtle(theme, status); - let background = theme.extended_palette().background; + // let content = if frequency_responses + // .clone() + // .any(|fr| fr.is_shown && fr.result().is_some()) + // { + // let series_list = frequency_responses + // .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) + // }), + // ] + // }) + // .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(), + // ), + // ) + // .x_range(chart_settings.x_range.clone().unwrap_or(20.0..=22_500.0)) + // .y_labels(Labels::default().format(&|v| format!("{v:.0}"))) + // .extend_series(series_list) + // .cache(&chart_settings.cache); - if is_active { - base.with_background(background.weak.color) - } else { - base - } - }); + // container(chart) + // } else { + // container(text("Please select a frequency respone.")).center(Length::Fill) + // }; - sidebar::item(btn, is_active) - }; + // row![ + // container(sidebar) + // .width(Length::FillPortion(2)) + // .style(container::bordered_box), + // column![header, container(content).width(Length::FillPortion(5))].spacing(12) + // ] + // .spacing(10) + // .into() + // } - let analysis = measurement.analysis()?; - let entry = match analysis.spectral_decay_progress() { - ui::spectral_decay::Progress::None => entry, - ui::spectral_decay::Progress::ComputingImpulseResponse => { - processing_overlay("Impulse Response", entry) - } - ui::spectral_decay::Progress::Computing => { - processing_overlay("Spectral Decay", entry) - } - ui::spectral_decay::Progress::Finished => entry, - }; + // pub fn spectral_decay_tab<'a>( + // &'a self, + // selected: Option, + // cache: &'a canvas::Cache, + // ) -> Element<'a, Message> { + // let sidebar = { + // let header = { + // let config_btn = button(icon::settings().center()) + // .style(button::subtle) + // .on_press(Message::OpenSpectralDecayConfig); + // Category::new("Spectral Decays").push_button(config_btn) + // }; - Some(entry) - }); + // let entries = self.measurements.iter().flat_map(|measurement| { + // let id = measurement.id(); + // let is_active = selected.is_some_and(|s| s == id); + + // let signal = measurement.signal()?; + // let entry = { + // // TODO: refactor, basically the same btn as IR and Spectrogram + // let dt: DateTime = signal.modified.into(); + // let btn = button( + // column![ + // text(&measurement.name) + // .size(16) + // .wrapping(Wrapping::WordOrGlyph), + // text!("{}", dt.format("%x %X")).size(10) + // ] + // .clip(true) + // .spacing(6), + // ) + // .on_press_with(move || Message::ImpulseResponseSelected(id)) + // .width(Length::Fill) + // .style(move |theme: &Theme, status| { + // let base = button::subtle(theme, status); + // let background = theme.extended_palette().background; + + // if is_active { + // base.with_background(background.weak.color) + // } else { + // base + // } + // }); - container(column![header, scrollable(column(entries))].spacing(6)) - .padding(6) - .style(|theme| { - container::rounded_box(theme) - .background(theme.extended_palette().background.weakest.color) - }) - }; + // sidebar::item(btn, is_active) + // }; - let content = if let Some(decay) = self - .measurements - .iter() - .find(|m| Some(m.id()) == selected) - .and_then(ui::Measurement::analysis) - .and_then(|analysis| analysis.spectral_decay.result()) - { - let gradient = colorous::MAGMA; - - let series_list = decay.iter().enumerate().map(|(fr_index, fr)| { - 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 analysis = measurement.analysis()?; + // let entry = match analysis.spectral_decay_progress() { + // ui::spectral_decay::Progress::None => entry, + // ui::spectral_decay::Progress::ComputingImpulseResponse => { + // processing_overlay("Impulse Response", entry) + // } + // ui::spectral_decay::Progress::Computing => { + // processing_overlay("Spectral Decay", entry) + // } + // ui::spectral_decay::Progress::Finished => entry, + // }; - 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); + // Some(entry) + // }); - container(chart) - } else { - container(text("Please select a frequency respone.")) - }; + // container(column![header, scrollable(column(entries))].spacing(6)) + // .padding(6) + // .style(|theme| { + // container::rounded_box(theme) + // .background(theme.extended_palette().background.weakest.color) + // }) + // }; - row![ - container(sidebar) - .width(Length::FillPortion(2)) - .style(container::bordered_box), - container(content).width(Length::FillPortion(5)) - ] - .spacing(10) - .into() - } + // let content = if let Some(decay) = self + // .measurements + // .iter() + // .find(|m| Some(m.id()) == selected) + // .and_then(ui::Measurement::analysis) + // .and_then(|analysis| analysis.spectral_decay.result()) + // { + // let gradient = colorous::MAGMA; + + // let series_list = decay.iter().enumerate().map(|(fr_index, fr)| { + // 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); - fn spectrogram_tab<'a>( - &'a self, - selected: Option, - spectrogram: &'a Spectrogram, - ) -> Element<'a, Message> { - let sidebar = { - let header = { - let config_btn = button(icon::settings().center()) - .style(button::subtle) - .on_press(Message::OpenSpectrogramConfig); - Category::new("Spectrograms").push_button(config_btn) - }; + // container(chart) + // } else { + // container(text("Please select a frequency respone.")) + // }; - let entries = self.measurements.iter().flat_map(|measurement| { - let id = measurement.id(); - let is_active = selected.is_some_and(|selected| selected == id); + // row![ + // container(sidebar) + // .width(Length::FillPortion(2)) + // .style(container::bordered_box), + // container(content).width(Length::FillPortion(5)) + // ] + // .spacing(10) + // .into() + // } - let signal = measurement.signal()?; - let entry = { - let dt: DateTime = signal.modified.into(); - let btn = button( - column![ - text(&measurement.name) - .size(16) - .wrapping(Wrapping::WordOrGlyph), - text!("{}", dt.format("%x %X")).size(10) - ] - .clip(true) - .spacing(6), - ) - .on_press_with(move || Message::ImpulseResponseSelected(id)) - .width(Length::Fill) - .style(move |theme: &Theme, status| { - let base = button::subtle(theme, status); - let background = theme.extended_palette().background; + // fn spectrogram_tab<'a>( + // &'a self, + // selected: Option, + // spectrogram: &'a Spectrogram, + // ) -> Element<'a, Message> { + // let sidebar = { + // let header = { + // let config_btn = button(icon::settings().center()) + // .style(button::subtle) + // .on_press(Message::OpenSpectrogramConfig); + // Category::new("Spectrograms").push_button(config_btn) + // }; - if is_active { - base.with_background(background.weak.color) - } else { - base - } - }); + // let entries = self.measurements.iter().flat_map(|measurement| { + // let id = measurement.id(); + // let is_active = selected.is_some_and(|selected| selected == id); + + // let signal = measurement.signal()?; + // let entry = { + // let dt: DateTime = signal.modified.into(); + // let btn = button( + // column![ + // text(&measurement.name) + // .size(16) + // .wrapping(Wrapping::WordOrGlyph), + // text!("{}", dt.format("%x %X")).size(10) + // ] + // .clip(true) + // .spacing(6), + // ) + // .on_press_with(move || Message::ImpulseResponseSelected(id)) + // .width(Length::Fill) + // .style(move |theme: &Theme, status| { + // let base = button::subtle(theme, status); + // let background = theme.extended_palette().background; + + // if is_active { + // base.with_background(background.weak.color) + // } else { + // base + // } + // }); - sidebar::item(btn, is_active) - }; + // sidebar::item(btn, is_active) + // }; - let analysis = measurement.analysis()?; - let entry = match analysis.spectrogram_progress() { - ui::spectrogram::Progress::None => entry, - ui::spectrogram::Progress::ComputingImpulseResponse => { - processing_overlay("Impulse Response", entry) - } - ui::spectrogram::Progress::Computing => { - processing_overlay("Spectral Decay", entry) - } - ui::spectrogram::Progress::Finished => entry, - }; + // let analysis = measurement.analysis()?; + // let entry = match analysis.spectrogram_progress() { + // ui::spectrogram::Progress::None => entry, + // ui::spectrogram::Progress::ComputingImpulseResponse => { + // processing_overlay("Impulse Response", entry) + // } + // ui::spectrogram::Progress::Computing => { + // processing_overlay("Spectral Decay", entry) + // } + // ui::spectrogram::Progress::Finished => entry, + // }; - Some(entry) - }); + // Some(entry) + // }); - container(column![header, scrollable(column(entries))].spacing(6)) - .padding(6) - .style(|theme| { - container::rounded_box(theme) - .background(theme.extended_palette().background.weakest.color) - }) - }; + // container(column![header, scrollable(column(entries))].spacing(6)) + // .padding(6) + // .style(|theme| { + // container::rounded_box(theme) + // .background(theme.extended_palette().background.weakest.color) + // }) + // }; - let spectrogram_data = selected - .and_then(|id| self.measurements.get(id)) - .and_then(ui::Measurement::analysis) - .and_then(|analysis| analysis.spectrogram.result()); - - let content = if let Some(data) = spectrogram_data { - let chart = chart::spectrogram( - data, - &spectrogram.cache, - spectrogram.zoom, - spectrogram.offset, - ) - .map(Message::Spectrogram); - - container(chart) - } else { - container(text("Please select a frequency respone.")) - }; + // let spectrogram_data = selected + // .and_then(|id| self.measurements.get(id)) + // .and_then(ui::Measurement::analysis) + // .and_then(|analysis| analysis.spectrogram.result()); + + // let content = if let Some(data) = spectrogram_data { + // let chart = chart::spectrogram( + // data, + // &spectrogram.cache, + // spectrogram.zoom, + // spectrogram.offset, + // ) + // .map(Message::Spectrogram); + + // container(chart) + // } else { + // container(text("Please select a frequency respone.")) + // }; - row![ - container(sidebar) - .width(Length::FillPortion(2)) - .style(container::bordered_box), - container(content).width(Length::FillPortion(5)) - ] - .spacing(10) - .into() - } + // row![ + // container(sidebar) + // .width(Length::FillPortion(2)) + // .style(container::bordered_box), + // container(content).width(Length::FillPortion(5)) + // ] + // .spacing(10) + // .into() + // } pub fn subscription(&self) -> Subscription { use keyboard::key; @@ -1915,13 +1942,24 @@ impl Main { // } fn compute_impulse_response(&mut self, id: ui::measurement::Id) -> Task { + let State::Analysing { + ref mut analyses, .. + } = self.state + else { + return Task::none(); + }; + + let analysis = analyses + .entry(id) + .or_insert(ui::measurement::Analysis::default()); + let Some(loopback) = self.loopback.as_ref() else { return Task::none(); }; let measurement = self.measurements.get_mut(id).unwrap(); - compute_impulse_response(loopback, measurement) + compute_impulse_response(loopback, measurement, analysis) } } @@ -1930,7 +1968,7 @@ fn impulse_response_item<'a>( id: ui::measurement::Id, name: &'a str, signal: &'a raumklang_core::Measurement, - analysis: &'a ui::measurement::Analysis, + analysis: Option<&'a ui::measurement::Analysis>, ) -> Element<'a, Message> { let is_active = selected.is_some_and(|selected| selected == id); @@ -1970,30 +2008,30 @@ fn impulse_response_item<'a>( sidebar::item(content, is_active) }; - match analysis.impulse_response.progress() { - ui::impulse_response::Progress::None => entry, - ui::impulse_response::Progress::Computing => { + match analysis.map(|a| a.impulse_response.progress()) { + Some(ui::impulse_response::Progress::Computing) => { impulse_response::processing_overlay("Impulse Response", entry) } - ui::impulse_response::Progress::Finished => entry, + _ => entry, } } fn compute_impulse_response( loopback: &ui::Loopback, measurement: &mut ui::Measurement, + analysis: &mut ui::measurement::Analysis, ) -> Task { let Some(loopback) = loopback.loaded() else { return Task::none(); }; - if let Some(analysis) = &mut measurement.analysis_mut() { - if analysis.impulse_response.result().is_some() { - return Task::none(); - } + // if let Some(analysis) = &mut measurement.analysis_mut() { + if analysis.impulse_response.result().is_some() { + return Task::none(); + } - analysis.impulse_response = ui::impulse_response::State::Computing; - }; + analysis.impulse_response = ui::impulse_response::State::Computing; + // }; let Some(signal) = measurement.signal() else { return Task::none(); @@ -2063,78 +2101,78 @@ async fn save_impulse_response(path: Arc, impulse_response: ui::ImpulseRes .unwrap(); } -fn compute_frequency_response( - loopback: &ui::Loopback, - measurement: &mut ui::Measurement, - window: &Window, -) -> Task { - let id = measurement.id(); - - let Some(analysis) = measurement.analysis_mut() else { - return Task::none(); - }; - - if let Some(impulse_response) = analysis.impulse_response.result() { - analysis.frequency_response.progress = ui::frequency_response::Progress::Computing; - - Task::perform( - data::frequency_response::compute(impulse_response.origin.clone(), window.clone()), - Message::FrequencyResponseComputed.with(id), - ) - } else { - compute_impulse_response(loopback, measurement) - } -} - -fn compute_spectral_decay( - loopback: &ui::Loopback, - measurement: &mut ui::Measurement, - config: data::spectral_decay::Config, -) -> Task { - let Some(analysis) = measurement.analysis_mut() else { - return Task::none(); - }; - - if analysis.spectral_decay.result().is_some() { - return Task::none(); - } - - if let Some(impulse_response) = analysis.impulse_response.result() { - analysis.spectral_decay = ui::spectral_decay::State::Computing; - - Task::perform( - data::spectral_decay::compute(impulse_response.origin.clone(), config), - Message::SpectralDecayComputed.with(measurement.id()), - ) - } else { - compute_impulse_response(loopback, measurement) - } -} - -fn compute_spectrogram( - loopback: &ui::Loopback, - measurement: &mut ui::Measurement, - config: &spectrogram::Preferences, -) -> Task { - let Some(analysis) = measurement.analysis_mut() else { - return Task::none(); - }; - - if analysis.spectrogram.result().is_some() { - return Task::none(); - } - - if let Some(impulse_response) = analysis.impulse_response.result() { - analysis.spectrogram = ui::spectrogram::State::Computing; - - Task::perform( - data::spectrogram::compute(impulse_response.origin.clone(), *config), - Message::SpectrogramComputed.with(measurement.id()), - ) - } else { - compute_impulse_response(loopback, measurement) - } -} +// fn compute_frequency_response( +// loopback: &ui::Loopback, +// measurement: &mut ui::Measurement, +// window: &Window, +// ) -> Task { +// let id = measurement.id(); + +// let Some(analysis) = measurement.analysis_mut() else { +// return Task::none(); +// }; + +// if let Some(impulse_response) = analysis.impulse_response.result() { +// analysis.frequency_response.progress = ui::frequency_response::Progress::Computing; + +// Task::perform( +// data::frequency_response::compute(impulse_response.origin.clone(), window.clone()), +// Message::FrequencyResponseComputed.with(id), +// ) +// } else { +// compute_impulse_response(loopback, measurement) +// } +// } + +// fn compute_spectral_decay( +// loopback: &ui::Loopback, +// measurement: &mut ui::Measurement, +// config: data::spectral_decay::Config, +// ) -> Task { +// let Some(analysis) = measurement.analysis_mut() else { +// return Task::none(); +// }; + +// if analysis.spectral_decay.result().is_some() { +// return Task::none(); +// } + +// if let Some(impulse_response) = analysis.impulse_response.result() { +// analysis.spectral_decay = ui::spectral_decay::State::Computing; + +// Task::perform( +// data::spectral_decay::compute(impulse_response.origin.clone(), config), +// Message::SpectralDecayComputed.with(measurement.id()), +// ) +// } else { +// compute_impulse_response(loopback, measurement) +// } +// } + +// fn compute_spectrogram( +// loopback: &ui::Loopback, +// measurement: &mut ui::Measurement, +// config: &spectrogram::Preferences, +// ) -> Task { +// let Some(analysis) = measurement.analysis_mut() else { +// return Task::none(); +// }; + +// if analysis.spectrogram.result().is_some() { +// return Task::none(); +// } + +// if let Some(impulse_response) = analysis.impulse_response.result() { +// analysis.spectrogram = ui::spectrogram::State::Computing; + +// Task::perform( +// data::spectrogram::compute(impulse_response.origin.clone(), *config), +// Message::SpectrogramComputed.with(measurement.id()), +// ) +// } else { +// compute_impulse_response(loopback, measurement) +// } +// } impl TabId { pub fn iter() -> impl Iterator { diff --git a/raumklang-gui/src/ui/measurement.rs b/raumklang-gui/src/ui/measurement.rs index 921e4ef..7a79e0a 100644 --- a/raumklang-gui/src/ui/measurement.rs +++ b/raumklang-gui/src/ui/measurement.rs @@ -75,7 +75,7 @@ enum State { NotLoaded, Loaded { signal: raumklang_core::Measurement, - analysis: Analysis, + // analysis: Analysis, }, } @@ -91,7 +91,7 @@ impl Measurement { let state = match signal { Some(signal) => State::Loaded { signal, - analysis: Analysis::default(), + // analysis: Analysis::default(), }, None => State::NotLoaded, }; @@ -136,21 +136,21 @@ impl Measurement { self.id } - pub fn analysis(&self) -> Option<&Analysis> { - match self.state { - State::NotLoaded => None, - State::Loaded { ref analysis, .. } => Some(analysis), - } - } - - pub fn analysis_mut(&mut self) -> Option<&mut Analysis> { - match self.state { - State::NotLoaded => None, - State::Loaded { - ref mut analysis, .. - } => Some(analysis), - } - } + // pub fn analysis(&self) -> Option<&Analysis> { + // match self.state { + // State::NotLoaded => None, + // State::Loaded { ref analysis, .. } => Some(analysis), + // } + // } + + // pub fn analysis_mut(&mut self) -> Option<&mut Analysis> { + // match self.state { + // State::NotLoaded => None, + // State::Loaded { + // ref mut analysis, .. + // } => Some(analysis), + // } + // } } impl Display for Id { From 00a38214498c657b7978742b0f628272fc02a9ee Mon Sep 17 00:00:00 2001 From: Hendrik Kunert Date: Wed, 28 Jan 2026 17:59:19 +0100 Subject: [PATCH 05/30] Add frequency response tab back --- raumklang-gui/src/screen/main.rs | 442 ++++++++++++--------- raumklang-gui/src/ui/frequency_response.rs | 8 +- 2 files changed, 257 insertions(+), 193 deletions(-) diff --git a/raumklang-gui/src/screen/main.rs b/raumklang-gui/src/screen/main.rs index 7548866..f9f5171 100644 --- a/raumklang-gui/src/screen/main.rs +++ b/raumklang-gui/src/screen/main.rs @@ -72,15 +72,22 @@ enum State { Collecting, Analysing { selected: Option, - analyses: BTreeMap, + impulse_responses: BTreeMap, + frequency_responses: BTreeMap, }, } impl State { - pub fn analysis() -> Self { + pub fn analysis<'a>(measurements: impl IntoIterator) -> Self { + let mut frequency_responses = BTreeMap::new(); + measurements.into_iter().for_each(|m| { + frequency_responses.insert(m.id(), ui::FrequencyResponse::new()); + }); + Self::Analysing { selected: None, - analyses: BTreeMap::new(), + impulse_responses: BTreeMap::new(), + frequency_responses, } } } @@ -150,6 +157,7 @@ pub enum Message { TabSelected(TabId), OpenMeasurements, OpenImpulseResponses, + OpenFrequencyResponses, Measurements(measurement::Message), MeasurementChart(waveform::Interaction), @@ -249,14 +257,23 @@ impl Main { self.loopback = Some(loopback); if !self.measurements.is_empty() { - self.state = State::analysis() + self.state = State::analysis(self.measurements.iter()) } } Some(measurement::LoadedKind::Normal(measurement)) => { if self.measurements.is_empty() { - self.state = State::analysis() + self.state = State::analysis([&measurement]); + } + if let State::Analysing { + ref mut frequency_responses, + .. + } = self.state + { + frequency_responses + .insert(measurement.id(), ui::FrequencyResponse::new()); } - self.measurements.push(measurement) + + self.measurements.push(measurement); } None => {} }, @@ -279,7 +296,7 @@ impl Main { Task::none() } Message::ImpulseResponseSelected(id) => { - let State::Analysing { selected, analyses } = &mut self.state else { + let State::Analysing { selected, .. } = &mut self.state else { return Task::none(); }; @@ -293,60 +310,86 @@ impl Main { Tab::Spectrogram => todo!(), } } - Message::ImpulseResponseComputed(id, impulse_response) => { - // let State::Analysing { - // window, - // active_tab, - // selected: selected_analysis, - // charts, - // loopback, - // .. - // } = &mut self.state - // else { - // return Task::none(); - // }; - - // let Some(measurement) = self.measurements.get_mut(id) else { - // return Task::none(); - // }; - - // let Some(analysis) = measurement.analysis_mut() else { - // return Task::none(); - // }; + Message::ImpulseResponseComputed(id, new_ir) => { let State::Analysing { selected, - ref mut analyses, + ref mut impulse_responses, + ref mut frequency_responses, + .. } = self.state else { return Task::none(); }; - let Some(analysis) = analyses.get_mut(&id) else { - return Task::none(); - }; + let impulse_response = impulse_responses + .entry(id) + .or_insert(ui::impulse_response::State::default()); - let impulse_response = ui::ImpulseResponse::from_data(impulse_response); - analysis.impulse_response.computed(impulse_response.clone()); + let new_ir = ui::ImpulseResponse::from_data(new_ir); + impulse_response.computed(new_ir.clone()); if selected.is_some_and(|selected| selected == id) { self.charts .impulse_responses .x_range - .get_or_insert(0.0..=impulse_response.data.len() as f32); + .get_or_insert(0.0..=new_ir.data.len() as f32); self.charts.impulse_responses.data_cache.clear(); } - // if let Tab::FrequencyResponses = active_tab { - // compute_frequency_response(loopback, measurement, window) - // } else if let Tab::SpectralDecay = active_tab { + let Some(loopback) = self.loopback.as_ref() else { + return Task::none(); + }; + + let Some(measurement) = self.measurements.get_mut(id) else { + return Task::none(); + }; + + let Some(window) = self.window.as_ref() else { + return Task::none(); + }; + + if let Tab::FrequencyResponses = self.tab { + let Some(fr) = frequency_responses.get_mut(&id) else { + return Task::none(); + }; + + compute_frequency_response(loopback, measurement, window, impulse_response, fr) + } else { + Task::none() + } + // else if let Tab::SpectralDecay = active_tab { // compute_spectral_decay(loopback, measurement, self.spectral_decay_config) // } else if let Tab::Spectrogram = active_tab { // compute_spectrogram(loopback, measurement, &self.spectrogram_config) // } else { // Task::none() // } - Task::none() + } + Message::FrequencyResponseComputed(id, new_fr) => { + log::debug!("Frequency response computed: {id}"); + + let State::Analysing { + ref mut frequency_responses, + .. + } = self.state + else { + return Task::none(); + }; + + let frequency_response = frequency_responses.entry(id).or_default(); + frequency_response.computed(new_fr.clone()); + + self.charts.frequency_responses.cache.clear(); + + if let Some(fraction) = self.smoothing.fraction() { + Task::perform( + frequency_response::smooth_frequency_response(id, new_fr, fraction), + Message::FrequencyResponseSmoothed, + ) + } else { + Task::none() + } } Message::OpenMeasurements => { self.tab = Tab::Measurements { recording: None }; @@ -363,6 +406,18 @@ impl Main { return Task::none(); } + + Message::OpenFrequencyResponses => { + self.tab = Tab::FrequencyResponses; + + let ids: Vec<_> = self + .measurements + .loaded() + .map(ui::Measurement::id) + .collect(); + + Task::batch(ids.into_iter().map(|id| self.compute_impulse_response(id))) + } _ => Task::none(), } } @@ -1211,7 +1266,7 @@ impl Main { tab( "Frequency Responses", matches!(self.tab, Tab::FrequencyResponses), - None // Message::OpenFrequencyResponses + is_analysing.then_some(Message::OpenFrequencyResponses) // Message::OpenImpulseResponses ), tab( "Spectral Decays", @@ -1250,7 +1305,9 @@ impl Main { window_settings, ) } - Tab::FrequencyResponses => todo!(), + Tab::FrequencyResponses => { + self.frequency_responses_tab(&self.charts.frequency_responses) + } Tab::SpectralDecay => todo!(), Tab::Spectrogram => todo!(), } @@ -1457,14 +1514,6 @@ impl Main { .into() }; - // let loopback = if let State::CollectingMeasuremnts { ref loopback, .. } = self.state { - // loopback.as_ref() - // } else if let State::Analysing { ref loopback, .. } = self.state { - // Some(loopback) - // } else { - // None - // }; - let content = if let Some(measurement) = self.selected.and_then(|selected| match selected { measurement::Selected::Loopback => self @@ -1506,8 +1555,11 @@ impl Main { let entries = self.measurements.iter().flat_map(|measurement| { let signal = measurement.signal()?; - let analysis = if let State::Analysing { analyses, .. } = &self.state { - analyses.get(&measurement.id()) + let impulse_response = if let State::Analysing { + impulse_responses, .. + } = &self.state + { + impulse_responses.get(&measurement.id()) } else { None }; @@ -1517,7 +1569,7 @@ impl Main { measurement.id(), &measurement.name, signal, - analysis, + impulse_response, )) }); @@ -1532,14 +1584,17 @@ impl Main { let content = { let placeholder = center(text("Impulse response not computed, yet.")).into(); - let analysis = if let State::Analysing { analyses, .. } = &self.state { - selected.and_then(|id| analyses.get(&id)) + let impulse_response = if let State::Analysing { + impulse_responses, .. + } = &self.state + { + selected.and_then(|id| impulse_responses.get(&id)) } else { None }; - analysis - .and_then(|analysis| analysis.impulse_response.result()) + impulse_response + .and_then(ui::impulse_response::State::result) .map_or(placeholder, |impulse_response| { chart .view(impulse_response, window_settings) @@ -1557,113 +1612,128 @@ impl Main { .into() } - // fn frequency_responses_tab<'a>( - // &'a self, - // chart_settings: &'a frequency_response::ChartData, - // ) -> Element<'a, Message> { - // let sidebar = { - // let header = sidebar::header("Frequency Responses"); + fn frequency_responses_tab<'a>( + &'a self, + chart_settings: &'a frequency_response::ChartData, + ) -> Element<'a, Message> { + let sidebar = { + let header = sidebar::header("Frequency Responses"); - // let entries = self.measurements.iter().flat_map(|measurement| { - // let analysis = &measurement.analysis()?; + let entries = self.measurements.iter().flat_map(|measurement| { + let State::Analysing { + // selected, + impulse_responses, + frequency_responses, + .. + } = &self.state + else { + return None; + }; - // let content = analysis.frequency_response.view( - // &measurement.name, - // analysis.impulse_response.progress(), - // Message::FrequencyResponseToggled.with(measurement.id()), - // ); + let frequency_response = frequency_responses.get(&measurement.id())?; + let impulse_response_progress = impulse_responses + .get(&measurement.id()) + .map(ui::impulse_response::State::progress); - // Some(sidebar::item(content, false)) - // }); + let content = frequency_response.view( + &measurement.name, + impulse_response_progress, + Message::FrequencyResponseToggled.with(measurement.id()), + ); - // container(column![header, scrollable(column(entries).spacing(6))].spacing(6)) - // .padding(6) - // .style(|theme| { - // container::rounded_box(theme) - // .background(theme.extended_palette().background.weakest.color) - // }) - // }; + Some(sidebar::item(content, false)) + }); - // let header = { - // row![pick_list( - // frequency_response::Smoothing::ALL, - // Some(&self.smoothing), - // Message::SmoothingChanged, - // )] - // }; + container(column![header, scrollable(column(entries).spacing(6))].spacing(6)) + .padding(6) + .style(|theme| { + container::rounded_box(theme) + .background(theme.extended_palette().background.weakest.color) + }) + }; - // let frequency_responses = self - // .measurements - // .iter() - // .flat_map(ui::Measurement::analysis) - // .map(|analysis| &analysis.frequency_response); + let header = { + row![pick_list( + frequency_response::Smoothing::ALL, + Some(&self.smoothing), + Message::SmoothingChanged, + )] + }; - // let content = if frequency_responses - // .clone() - // .any(|fr| fr.is_shown && fr.result().is_some()) - // { - // let series_list = frequency_responses - // .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) - // }), - // ] - // }) - // .flatten(); + let frequency_responses = if let State::Analysing { + frequency_responses, + .. + } = &self.state + { + Some(frequency_responses) + } else { + None + }; - // 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(), - // ), - // ) - // .x_range(chart_settings.x_range.clone().unwrap_or(20.0..=22_500.0)) - // .y_labels(Labels::default().format(&|v| format!("{v:.0}"))) - // .extend_series(series_list) - // .cache(&chart_settings.cache); + let content = if let Some(frequency_responses) = frequency_responses { + let series_list = frequency_responses + .values() + .flat_map(|item| { + let Some(frequency_response) = item.result() else { + return [None, None]; + }; - // container(chart) - // } else { - // container(text("Please select a frequency respone.")).center(Length::Fill) - // }; + 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) + }), + ] + }) + .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(), + ), + ) + .x_range(chart_settings.x_range.clone().unwrap_or(20.0..=22_500.0)) + .y_labels(Labels::default().format(&|v| format!("{v:.0}"))) + .extend_series(series_list) + .cache(&chart_settings.cache); - // row![ - // container(sidebar) - // .width(Length::FillPortion(2)) - // .style(container::bordered_box), - // column![header, container(content).width(Length::FillPortion(5))].spacing(12) - // ] - // .spacing(10) - // .into() - // } + container(chart) + } else { + container(text("Please select a frequency respone.")).center(Length::Fill) + }; + + row![ + container(sidebar) + .width(Length::FillPortion(2)) + .style(container::bordered_box), + column![header, container(content).width(Length::FillPortion(5))].spacing(12) + ] + .spacing(10) + .into() + } // pub fn spectral_decay_tab<'a>( // &'a self, @@ -1943,15 +2013,17 @@ impl Main { fn compute_impulse_response(&mut self, id: ui::measurement::Id) -> Task { let State::Analysing { - ref mut analyses, .. + ref mut impulse_responses, + .. } = self.state else { return Task::none(); }; - let analysis = analyses - .entry(id) - .or_insert(ui::measurement::Analysis::default()); + let impulse_response = impulse_responses.entry(id).or_default(); + if impulse_response.result().is_some() { + return Task::none(); + } let Some(loopback) = self.loopback.as_ref() else { return Task::none(); @@ -1959,7 +2031,7 @@ impl Main { let measurement = self.measurements.get_mut(id).unwrap(); - compute_impulse_response(loopback, measurement, analysis) + compute_impulse_response(loopback, measurement, impulse_response) } } @@ -1968,7 +2040,7 @@ fn impulse_response_item<'a>( id: ui::measurement::Id, name: &'a str, signal: &'a raumklang_core::Measurement, - analysis: Option<&'a ui::measurement::Analysis>, + impulse_response: Option<&'a ui::impulse_response::State>, ) -> Element<'a, Message> { let is_active = selected.is_some_and(|selected| selected == id); @@ -2008,8 +2080,8 @@ fn impulse_response_item<'a>( sidebar::item(content, is_active) }; - match analysis.map(|a| a.impulse_response.progress()) { - Some(ui::impulse_response::Progress::Computing) => { + match impulse_response { + Some(ui::impulse_response::State::Computing) => { impulse_response::processing_overlay("Impulse Response", entry) } _ => entry, @@ -2019,24 +2091,18 @@ fn impulse_response_item<'a>( fn compute_impulse_response( loopback: &ui::Loopback, measurement: &mut ui::Measurement, - analysis: &mut ui::measurement::Analysis, + impulse_response: &mut ui::impulse_response::State, ) -> Task { let Some(loopback) = loopback.loaded() else { return Task::none(); }; - // if let Some(analysis) = &mut measurement.analysis_mut() { - if analysis.impulse_response.result().is_some() { - return Task::none(); - } - - analysis.impulse_response = ui::impulse_response::State::Computing; - // }; - let Some(signal) = measurement.signal() else { return Task::none(); }; + *impulse_response = ui::impulse_response::State::Computing; + Task::perform( data::impulse_response::compute(loopback.clone(), signal.clone()), Message::ImpulseResponseComputed.with(measurement.id()), @@ -2101,28 +2167,26 @@ async fn save_impulse_response(path: Arc, impulse_response: ui::ImpulseRes .unwrap(); } -// fn compute_frequency_response( -// loopback: &ui::Loopback, -// measurement: &mut ui::Measurement, -// window: &Window, -// ) -> Task { -// let id = measurement.id(); - -// let Some(analysis) = measurement.analysis_mut() else { -// return Task::none(); -// }; +fn compute_frequency_response( + loopback: &ui::Loopback, + measurement: &mut ui::Measurement, + window: &Window, + impulse_response: &mut ui::impulse_response::State, + frequency_response: &mut ui::FrequencyResponse, +) -> Task { + let id = measurement.id(); -// if let Some(impulse_response) = analysis.impulse_response.result() { -// analysis.frequency_response.progress = ui::frequency_response::Progress::Computing; + if let Some(impulse_response) = impulse_response.result() { + frequency_response.progress = ui::frequency_response::Progress::Computing; -// Task::perform( -// data::frequency_response::compute(impulse_response.origin.clone(), window.clone()), -// Message::FrequencyResponseComputed.with(id), -// ) -// } else { -// compute_impulse_response(loopback, measurement) -// } -// } + Task::perform( + data::frequency_response::compute(impulse_response.origin.clone(), window.clone()), + Message::FrequencyResponseComputed.with(id), + ) + } else { + compute_impulse_response(loopback, measurement, impulse_response) + } +} // fn compute_spectral_decay( // loopback: &ui::Loopback, diff --git a/raumklang-gui/src/ui/frequency_response.rs b/raumklang-gui/src/ui/frequency_response.rs index 80e10b8..207df18 100644 --- a/raumklang-gui/src/ui/frequency_response.rs +++ b/raumklang-gui/src/ui/frequency_response.rs @@ -42,7 +42,7 @@ impl FrequencyResponse { pub fn view<'a, Message>( &'a self, measurement_name: &'a str, - impulse_response_progess: impulse_response::Progress, + impulse_response_progess: Option, on_toggle: impl Fn(bool) -> Message + 'a, ) -> Element<'a, Message> where @@ -77,13 +77,13 @@ impl FrequencyResponse { item } else { match impulse_response_progess { - impulse_response::Progress::None => item, - impulse_response::Progress::Computing => { + Some(impulse_response::Progress::Computing) => { processing_overlay("Impulse Response", item) } - impulse_response::Progress::Finished => { + Some(impulse_response::Progress::Finished) => { processing_overlay(self.progress.to_string(), item) } + _ => item, } } } From 5560cd4c8b04c10ba6e60e57324d803fe2962d85 Mon Sep 17 00:00:00 2001 From: Hendrik Kunert Date: Wed, 28 Jan 2026 19:17:10 +0100 Subject: [PATCH 06/30] Improve FR computation by unsing a `Sipper` --- raumklang-gui/src/screen/main.rs | 100 ++++++++++++++----------------- 1 file changed, 44 insertions(+), 56 deletions(-) diff --git a/raumklang-gui/src/screen/main.rs b/raumklang-gui/src/screen/main.rs index f9f5171..b883771 100644 --- a/raumklang-gui/src/screen/main.rs +++ b/raumklang-gui/src/screen/main.rs @@ -31,6 +31,7 @@ use iced::{ alignment::{Horizontal, Vertical}, futures::{FutureExt, TryFutureExt}, keyboard, padding, + task::sipper, widget::{ button, canvas, center, column, container, opaque, pick_list, right, row, rule, scrollable, space, stack, text, text::Wrapping, Button, @@ -78,16 +79,11 @@ enum State { } impl State { - pub fn analysis<'a>(measurements: impl IntoIterator) -> Self { - let mut frequency_responses = BTreeMap::new(); - measurements.into_iter().for_each(|m| { - frequency_responses.insert(m.id(), ui::FrequencyResponse::new()); - }); - + pub fn analysis() -> Self { Self::Analysing { selected: None, impulse_responses: BTreeMap::new(), - frequency_responses, + frequency_responses: BTreeMap::new(), } } } @@ -257,20 +253,12 @@ impl Main { self.loopback = Some(loopback); if !self.measurements.is_empty() { - self.state = State::analysis(self.measurements.iter()) + self.state = State::analysis() } } Some(measurement::LoadedKind::Normal(measurement)) => { if self.measurements.is_empty() { - self.state = State::analysis([&measurement]); - } - if let State::Analysing { - ref mut frequency_responses, - .. - } = self.state - { - frequency_responses - .insert(measurement.id(), ui::FrequencyResponse::new()); + self.state = State::analysis(); } self.measurements.push(measurement); @@ -305,7 +293,7 @@ impl Main { match &self.tab { Tab::Measurements { .. } => Task::none(), Tab::ImpulseResponses { .. } => self.compute_impulse_response(id), - Tab::FrequencyResponses => todo!(), + Tab::FrequencyResponses => Task::none(), Tab::SpectralDecay => todo!(), Tab::Spectrogram => todo!(), } @@ -314,16 +302,13 @@ impl Main { let State::Analysing { selected, ref mut impulse_responses, - ref mut frequency_responses, .. } = self.state else { return Task::none(); }; - let impulse_response = impulse_responses - .entry(id) - .or_insert(ui::impulse_response::State::default()); + let impulse_response = impulse_responses.entry(id).or_default(); let new_ir = ui::ImpulseResponse::from_data(new_ir); impulse_response.computed(new_ir.clone()); @@ -337,34 +322,7 @@ impl Main { self.charts.impulse_responses.data_cache.clear(); } - let Some(loopback) = self.loopback.as_ref() else { - return Task::none(); - }; - - let Some(measurement) = self.measurements.get_mut(id) else { - return Task::none(); - }; - - let Some(window) = self.window.as_ref() else { - return Task::none(); - }; - - if let Tab::FrequencyResponses = self.tab { - let Some(fr) = frequency_responses.get_mut(&id) else { - return Task::none(); - }; - - compute_frequency_response(loopback, measurement, window, impulse_response, fr) - } else { - Task::none() - } - // else if let Tab::SpectralDecay = active_tab { - // compute_spectral_decay(loopback, measurement, self.spectral_decay_config) - // } else if let Tab::Spectrogram = active_tab { - // compute_spectrogram(loopback, measurement, &self.spectrogram_config) - // } else { - // Task::none() - // } + Task::none() } Message::FrequencyResponseComputed(id, new_fr) => { log::debug!("Frequency response computed: {id}"); @@ -408,15 +366,45 @@ impl Main { } Message::OpenFrequencyResponses => { + let State::Analysing { + ref mut impulse_responses, + ref mut frequency_responses, + .. + } = self.state + else { + return Task::none(); + }; + self.tab = Tab::FrequencyResponses; - let ids: Vec<_> = self - .measurements - .loaded() - .map(ui::Measurement::id) - .collect(); + let tasks = self.measurements.loaded_mut().flat_map(|measurement| { + let id = measurement.id(); + let window = self.window.clone()?; + let loopback = self.loopback.as_ref().and_then(Loopback::loaded).cloned()?; + let measurement = measurement.signal().cloned()?; + + impulse_responses.insert(id, ui::impulse_response::State::Computing); + frequency_responses + .entry(id) + .or_insert_with(ui::FrequencyResponse::new); + + let sipper = sipper(async move |mut progress| { + let impulse_response = + data::impulse_response::compute(loopback, measurement).await; + + progress.send(impulse_response.clone()).await; + + data::frequency_response::compute(impulse_response.origin, window).await + }); + + Some(Task::sip( + sipper, + Message::ImpulseResponseComputed.with(id), + Message::FrequencyResponseComputed.with(id), + )) + }); - Task::batch(ids.into_iter().map(|id| self.compute_impulse_response(id))) + Task::batch(tasks) } _ => Task::none(), } From b18658bd5b140a2b009a3f4f2b6777ea0d99787f Mon Sep 17 00:00:00 2001 From: Hendrik Kunert Date: Thu, 29 Jan 2026 11:24:17 +0100 Subject: [PATCH 07/30] Cleanup analyses computations --- raumklang-gui/src/screen/main.rs | 291 +++++++++------------ raumklang-gui/src/ui.rs | 2 + raumklang-gui/src/ui/analysis.rs | 114 ++++++++ raumklang-gui/src/ui/frequency_response.rs | 6 +- raumklang-gui/src/ui/measurement.rs | 74 +++--- 5 files changed, 286 insertions(+), 201 deletions(-) create mode 100644 raumklang-gui/src/ui/analysis.rs diff --git a/raumklang-gui/src/screen/main.rs b/raumklang-gui/src/screen/main.rs index b883771..0d9d656 100644 --- a/raumklang-gui/src/screen/main.rs +++ b/raumklang-gui/src/screen/main.rs @@ -15,7 +15,7 @@ use crate::{ chart::waveform, impulse_response::processing_overlay, measurement::Selected, spectral_decay_config::SpectralDecayConfig, spectrogram_config::SpectrogramConfig, }, - ui::{self, measurement::Analysis, Loopback}, + ui::{self, analysis, Analysis, Loopback}, widget::sidebar, PickAndLoadError, }; @@ -73,8 +73,7 @@ enum State { Collecting, Analysing { selected: Option, - impulse_responses: BTreeMap, - frequency_responses: BTreeMap, + analyses: BTreeMap, }, } @@ -82,8 +81,7 @@ impl State { pub fn analysis() -> Self { Self::Analysing { selected: None, - impulse_responses: BTreeMap::new(), - frequency_responses: BTreeMap::new(), + analyses: BTreeMap::new(), } } } @@ -274,6 +272,13 @@ impl Main { if self.measurements.is_empty() { self.state = State::Collecting } + + if let State::Analysing { + ref mut analyses, .. + } = self.state + { + analyses.remove(&id); + } } measurement::Message::Select(selected) => { self.selected = Some(selected); @@ -284,11 +289,12 @@ impl Main { Task::none() } Message::ImpulseResponseSelected(id) => { - let State::Analysing { selected, .. } = &mut self.state else { + let State::Analysing { selected, analyses } = &mut self.state else { return Task::none(); }; *selected = Some(id); + analyses.entry(id).or_default(); match &self.tab { Tab::Measurements { .. } => Task::none(), @@ -301,17 +307,17 @@ impl Main { Message::ImpulseResponseComputed(id, new_ir) => { let State::Analysing { selected, - ref mut impulse_responses, + ref mut analyses, .. } = self.state else { return Task::none(); }; - let impulse_response = impulse_responses.entry(id).or_default(); - let new_ir = ui::ImpulseResponse::from_data(new_ir); - impulse_response.computed(new_ir.clone()); + + let analysis = analyses.entry(id).or_default(); + analysis.apply(analysis::Event::ImpulseResponseComputed(new_ir.clone())); if selected.is_some_and(|selected| selected == id) { self.charts @@ -322,21 +328,26 @@ impl Main { self.charts.impulse_responses.data_cache.clear(); } - Task::none() + match &self.tab { + Tab::Measurements { .. } => Task::none(), + Tab::ImpulseResponses { .. } => Task::none(), + Tab::FrequencyResponses => self.compute_frequency_response(id), + Tab::SpectralDecay => todo!(), + Tab::Spectrogram => todo!(), + } } Message::FrequencyResponseComputed(id, new_fr) => { log::debug!("Frequency response computed: {id}"); let State::Analysing { - ref mut frequency_responses, - .. + ref mut analyses, .. } = self.state else { return Task::none(); }; - let frequency_response = frequency_responses.entry(id).or_default(); - frequency_response.computed(new_fr.clone()); + let analysis = analyses.entry(id).or_default(); + analysis.apply(analysis::Event::FrequencyResponseComputed(new_fr.clone())); self.charts.frequency_responses.cache.clear(); @@ -366,50 +377,72 @@ impl Main { } Message::OpenFrequencyResponses => { - let State::Analysing { - ref mut impulse_responses, - ref mut frequency_responses, - .. - } = self.state - else { + let State::Analysing { .. } = self.state else { return Task::none(); }; self.tab = Tab::FrequencyResponses; - let tasks = self.measurements.loaded_mut().flat_map(|measurement| { - let id = measurement.id(); - let window = self.window.clone()?; - let loopback = self.loopback.as_ref().and_then(Loopback::loaded).cloned()?; - let measurement = measurement.signal().cloned()?; - - impulse_responses.insert(id, ui::impulse_response::State::Computing); - frequency_responses - .entry(id) - .or_insert_with(ui::FrequencyResponse::new); - - let sipper = sipper(async move |mut progress| { - let impulse_response = - data::impulse_response::compute(loopback, measurement).await; - - progress.send(impulse_response.clone()).await; - - data::frequency_response::compute(impulse_response.origin, window).await - }); + // FIXME get rid of vec alloc + let ids: Vec<_> = self + .measurements + .loaded() + .map(ui::Measurement::id) + .collect(); - Some(Task::sip( - sipper, - Message::ImpulseResponseComputed.with(id), - Message::FrequencyResponseComputed.with(id), - )) - }); + let tasks = ids + .into_iter() + .map(|id| self.compute_frequency_response(id)); Task::batch(tasks) } _ => Task::none(), } } + + pub fn compute_impulse_response(&mut self, id: ui::measurement::Id) -> Task { + let State::Analysing { + ref mut analyses, .. + } = self.state + else { + return Task::none(); + }; + + let analysis = analyses.entry(id).or_default(); + + let Some(loopback) = self.loopback.as_ref().and_then(Loopback::loaded) else { + return Task::none(); + }; + + let measurement = self + .measurements + .get(id) + .and_then(ui::Measurement::signal) + .unwrap(); + + analysis + .compute_impulse_response(loopback.clone(), measurement.clone()) + .map(|f| Task::perform(f, Message::ImpulseResponseComputed.with(id))) + .unwrap_or_default() + } + + pub fn compute_frequency_response(&mut self, id: ui::measurement::Id) -> Task { + let State::Analysing { + ref mut analyses, .. + } = self.state + else { + return Task::none(); + }; + + let analysis = analyses.entry(id).or_default(); + + analysis + .compute_frequency_response(self.window.as_ref().cloned().unwrap()) + .map(|f| Task::perform(f, Message::FrequencyResponseComputed.with(id))) + .unwrap_or_else(|| self.compute_impulse_response(id)) + } // pub fn update( + // // &mut self, // recent_projects: &mut RecentProjects, // message: Message, @@ -1282,19 +1315,27 @@ impl Main { match &self.tab { Tab::Measurements { recording } => self.measurements_tab(), Tab::ImpulseResponses { window_settings } => { - let selected = if let State::Analysing { selected, .. } = self.state { - selected - } else { - None + let State::Analysing { + selected, + ref analyses, + } = self.state + else { + panic!("invalid state"); }; + self.impulse_responses_tab( selected, &self.charts.impulse_responses, window_settings, + analyses, ) } Tab::FrequencyResponses => { - self.frequency_responses_tab(&self.charts.frequency_responses) + let State::Analysing { ref analyses, .. } = self.state else { + panic!("invalid state"); + }; + + self.frequency_responses_tab(&self.charts.frequency_responses, analyses) } Tab::SpectralDecay => todo!(), Tab::Spectrogram => todo!(), @@ -1537,20 +1578,16 @@ impl Main { selected: Option, chart: &'a impulse_response::Chart, window_settings: &'a WindowSettings, + analyses: &'a BTreeMap, ) -> Element<'a, Message> { let sidebar = { let header = sidebar::header("Impulse Responses"); let entries = self.measurements.iter().flat_map(|measurement| { let signal = measurement.signal()?; - let impulse_response = if let State::Analysing { - impulse_responses, .. - } = &self.state - { - impulse_responses.get(&measurement.id()) - } else { - None - }; + let impulse_response = analyses + .get(&measurement.id()) + .map(Analysis::impulse_response_progress); Some(impulse_response_item( selected, @@ -1572,17 +1609,10 @@ impl Main { let content = { let placeholder = center(text("Impulse response not computed, yet.")).into(); - let impulse_response = if let State::Analysing { - impulse_responses, .. - } = &self.state - { - selected.and_then(|id| impulse_responses.get(&id)) - } else { - None - }; - - impulse_response - .and_then(ui::impulse_response::State::result) + selected + .as_ref() + .and_then(|id| analyses.get(id)) + .and_then(Analysis::impulse_response) .map_or(placeholder, |impulse_response| { chart .view(impulse_response, window_settings) @@ -1603,29 +1633,18 @@ impl Main { fn frequency_responses_tab<'a>( &'a self, chart_settings: &'a frequency_response::ChartData, + analyses: &'a BTreeMap, ) -> Element<'a, Message> { let sidebar = { let header = sidebar::header("Frequency Responses"); let entries = self.measurements.iter().flat_map(|measurement| { - let State::Analysing { - // selected, - impulse_responses, - frequency_responses, - .. - } = &self.state - else { - return None; - }; - - let frequency_response = frequency_responses.get(&measurement.id())?; - let impulse_response_progress = impulse_responses - .get(&measurement.id()) - .map(ui::impulse_response::State::progress); + let analysis = analyses.get(&measurement.id())?; + let frequency_response = analysis.frequency_response()?; let content = frequency_response.view( &measurement.name, - impulse_response_progress, + analysis.impulse_response_progress(), Message::FrequencyResponseToggled.with(measurement.id()), ); @@ -1648,19 +1667,11 @@ impl Main { )] }; - let frequency_responses = if let State::Analysing { - frequency_responses, - .. - } = &self.state - { - Some(frequency_responses) - } else { - None - }; + let frequency_responses = analyses.values().map(Analysis::frequency_response); - let content = if let Some(frequency_responses) = frequency_responses { + let content = if frequency_responses.clone().any(|o| o.is_some()) { let series_list = frequency_responses - .values() + .flatten() .flat_map(|item| { let Some(frequency_response) = item.result() else { return [None, None]; @@ -1998,29 +2009,6 @@ impl Main { // is_loopback_loaded && self.measurements.iter().any(ui::Measurement::is_loaded) // } - - fn compute_impulse_response(&mut self, id: ui::measurement::Id) -> Task { - let State::Analysing { - ref mut impulse_responses, - .. - } = self.state - else { - return Task::none(); - }; - - let impulse_response = impulse_responses.entry(id).or_default(); - if impulse_response.result().is_some() { - return Task::none(); - } - - let Some(loopback) = self.loopback.as_ref() else { - return Task::none(); - }; - - let measurement = self.measurements.get_mut(id).unwrap(); - - compute_impulse_response(loopback, measurement, impulse_response) - } } fn impulse_response_item<'a>( @@ -2028,7 +2016,7 @@ fn impulse_response_item<'a>( id: ui::measurement::Id, name: &'a str, signal: &'a raumklang_core::Measurement, - impulse_response: Option<&'a ui::impulse_response::State>, + impulse_response: Option, ) -> Element<'a, Message> { let is_active = selected.is_some_and(|selected| selected == id); @@ -2069,34 +2057,13 @@ fn impulse_response_item<'a>( }; match impulse_response { - Some(ui::impulse_response::State::Computing) => { + Some(ui::impulse_response::Progress::Computing) => { impulse_response::processing_overlay("Impulse Response", entry) } _ => entry, } } -fn compute_impulse_response( - loopback: &ui::Loopback, - measurement: &mut ui::Measurement, - impulse_response: &mut ui::impulse_response::State, -) -> Task { - let Some(loopback) = loopback.loaded() else { - return Task::none(); - }; - - let Some(signal) = measurement.signal() else { - return Task::none(); - }; - - *impulse_response = ui::impulse_response::State::Computing; - - Task::perform( - data::impulse_response::compute(loopback.clone(), signal.clone()), - Message::ImpulseResponseComputed.with(measurement.id()), - ) -} - impl Default for Main { fn default() -> Self { Self { @@ -2155,26 +2122,26 @@ async fn save_impulse_response(path: Arc, impulse_response: ui::ImpulseRes .unwrap(); } -fn compute_frequency_response( - loopback: &ui::Loopback, - measurement: &mut ui::Measurement, - window: &Window, - impulse_response: &mut ui::impulse_response::State, - frequency_response: &mut ui::FrequencyResponse, -) -> Task { - let id = measurement.id(); - - if let Some(impulse_response) = impulse_response.result() { - frequency_response.progress = ui::frequency_response::Progress::Computing; - - Task::perform( - data::frequency_response::compute(impulse_response.origin.clone(), window.clone()), - Message::FrequencyResponseComputed.with(id), - ) - } else { - compute_impulse_response(loopback, measurement, impulse_response) - } -} +// fn compute_frequency_response( +// loopback: &ui::Loopback, +// measurement: &mut ui::Measurement, +// window: &Window, +// impulse_response: &mut ui::impulse_response::State, +// frequency_response: &mut ui::FrequencyResponse, +// ) -> Task { +// let id = measurement.id(); + +// if let Some(impulse_response) = impulse_response.result() { +// frequency_response.progress = ui::frequency_response::Progress::Computing; + +// Task::perform( +// data::frequency_response::compute(impulse_response.origin.clone(), window.clone()), +// Message::FrequencyResponseComputed.with(id), +// ) +// } else { +// compute_impulse_response(loopback, measurement, impulse_response) +// } +// } // fn compute_spectral_decay( // loopback: &ui::Loopback, diff --git a/raumklang-gui/src/ui.rs b/raumklang-gui/src/ui.rs index 28002b1..27a9ff6 100644 --- a/raumklang-gui/src/ui.rs +++ b/raumklang-gui/src/ui.rs @@ -1,9 +1,11 @@ +pub mod analysis; pub mod frequency_response; pub mod impulse_response; pub mod measurement; pub mod spectral_decay; pub mod spectrogram; +pub use analysis::Analysis; pub use frequency_response::FrequencyResponse; pub use impulse_response::ImpulseResponse; pub use measurement::{Loopback, Measurement}; diff --git a/raumklang-gui/src/ui/analysis.rs b/raumklang-gui/src/ui/analysis.rs new file mode 100644 index 0000000..4b28542 --- /dev/null +++ b/raumklang-gui/src/ui/analysis.rs @@ -0,0 +1,114 @@ +use std::{future::Future, mem}; + +use raumklang_core::{Loopback, Measurement}; + +use crate::{ + data, + ui::{impulse_response, FrequencyResponse, ImpulseResponse}, +}; + +#[derive(Debug, Clone, Default)] +pub struct Analysis(State); + +pub enum Event { + ImpulseResponseComputed(ImpulseResponse), + FrequencyResponseComputed(data::FrequencyResponse), +} + +impl Analysis { + pub(crate) fn apply(&mut self, event: Event) { + match event { + Event::ImpulseResponseComputed(impulse_response) => { + let State::ImpulseResponseComputing(frequency_response) = mem::take(&mut self.0) + else { + return; + }; + + self.0 = State::ImpulseResponse { + impulse_response: impulse_response, + frequency_response, + } + } + Event::FrequencyResponseComputed(fr) => { + let State::ImpulseResponse { + ref mut frequency_response, + .. + } = self.0 + else { + return; + }; + + frequency_response.computed(fr); + } + } + } + + pub(crate) fn impulse_response(&self) -> Option<&ImpulseResponse> { + let State::ImpulseResponse { + ref impulse_response, + .. + } = self.0 + else { + return None; + }; + + Some(impulse_response) + } + + pub(crate) fn frequency_response(&self) -> Option<&FrequencyResponse> { + match &self.0 { + State::None => None, + State::ImpulseResponseComputing(frequency_response) + | State::ImpulseResponse { + frequency_response, .. + } => Some(frequency_response), + } + } + + pub(crate) fn impulse_response_progress(&self) -> impulse_response::Progress { + match self.0 { + State::None => impulse_response::Progress::None, + State::ImpulseResponseComputing(_) => impulse_response::Progress::Computing, + State::ImpulseResponse { .. } => impulse_response::Progress::Finished, + } + } + + pub(crate) fn compute_impulse_response( + &mut self, + loopback: Loopback, + measurement: Measurement, + ) -> Option> { + match self.0 { + State::ImpulseResponse { .. } => None, + State::ImpulseResponseComputing(_) => None, + State::None => { + self.0 = State::ImpulseResponseComputing(FrequencyResponse::new()); + + Some(data::impulse_response::compute(loopback, measurement)) + } + } + } + + pub(crate) fn compute_frequency_response( + &mut self, + window: data::Window, + ) -> Option> { + let impulse_response = self.impulse_response().cloned()?; + + Some(data::frequency_response::compute( + impulse_response.origin, + window, + )) + } +} + +#[derive(Debug, Clone, Default)] +enum State { + #[default] + None, + ImpulseResponseComputing(FrequencyResponse), + ImpulseResponse { + impulse_response: ImpulseResponse, + frequency_response: FrequencyResponse, + }, +} diff --git a/raumklang-gui/src/ui/frequency_response.rs b/raumklang-gui/src/ui/frequency_response.rs index 207df18..ab8e701 100644 --- a/raumklang-gui/src/ui/frequency_response.rs +++ b/raumklang-gui/src/ui/frequency_response.rs @@ -42,7 +42,7 @@ impl FrequencyResponse { pub fn view<'a, Message>( &'a self, measurement_name: &'a str, - impulse_response_progess: Option, + impulse_response_progess: impulse_response::Progress, on_toggle: impl Fn(bool) -> Message + 'a, ) -> Element<'a, Message> where @@ -77,10 +77,10 @@ impl FrequencyResponse { item } else { match impulse_response_progess { - Some(impulse_response::Progress::Computing) => { + impulse_response::Progress::Computing => { processing_overlay("Impulse Response", item) } - Some(impulse_response::Progress::Finished) => { + impulse_response::Progress::Finished => { processing_overlay(self.progress.to_string(), item) } _ => item, diff --git a/raumklang-gui/src/ui/measurement.rs b/raumklang-gui/src/ui/measurement.rs index 7a79e0a..cba1576 100644 --- a/raumklang-gui/src/ui/measurement.rs +++ b/raumklang-gui/src/ui/measurement.rs @@ -8,7 +8,9 @@ use std::{ sync::atomic::{self, AtomicUsize}, }; -use crate::ui::{impulse_response, spectral_decay, spectrogram, FrequencyResponse}; +use crate::ui::{ + impulse_response, spectral_decay, spectrogram, FrequencyResponse, ImpulseResponse, +}; #[derive(Debug, Default, Clone)] pub struct List(Vec); @@ -159,38 +161,38 @@ impl Display for Id { } } -#[derive(Debug, Clone, Default)] -pub struct Analysis { - pub impulse_response: impulse_response::State, - pub frequency_response: FrequencyResponse, - pub spectral_decay: spectral_decay::State, - pub spectrogram: spectrogram::State, -} - -impl Analysis { - pub(crate) fn spectral_decay_progress(&self) -> spectral_decay::Progress { - match self.impulse_response { - impulse_response::State::None => spectral_decay::Progress::None, - impulse_response::State::Computing => { - spectral_decay::Progress::ComputingImpulseResponse - } - impulse_response::State::Computed(_) => match self.spectral_decay { - spectral_decay::State::None => spectral_decay::Progress::None, - spectral_decay::State::Computing => spectral_decay::Progress::Computing, - spectral_decay::State::Computed(_) => spectral_decay::Progress::Finished, - }, - } - } - - pub(crate) fn spectrogram_progress(&self) -> spectrogram::Progress { - match self.impulse_response { - impulse_response::State::None => spectrogram::Progress::None, - impulse_response::State::Computing => spectrogram::Progress::ComputingImpulseResponse, - impulse_response::State::Computed(_) => match self.spectrogram { - spectrogram::State::None => spectrogram::Progress::None, - spectrogram::State::Computing => spectrogram::Progress::Computing, - spectrogram::State::Computed(_) => spectrogram::Progress::Finished, - }, - } - } -} +// #[derive(Debug, Clone, Default)] +// pub struct Analysis { +// pub impulse_response: impulse_response::State, +// pub frequency_response: FrequencyResponse, +// pub spectral_decay: spectral_decay::State, +// pub spectrogram: spectrogram::State, +// } + +// impl Analysis { +// pub(crate) fn spectral_decay_progress(&self) -> spectral_decay::Progress { +// match self.impulse_response { +// impulse_response::State::None => spectral_decay::Progress::None, +// impulse_response::State::Computing => { +// spectral_decay::Progress::ComputingImpulseResponse +// } +// impulse_response::State::Computed(_) => match self.spectral_decay { +// spectral_decay::State::None => spectral_decay::Progress::None, +// spectral_decay::State::Computing => spectral_decay::Progress::Computing, +// spectral_decay::State::Computed(_) => spectral_decay::Progress::Finished, +// }, +// } +// } + +// pub(crate) fn spectrogram_progress(&self) -> spectrogram::Progress { +// match self.impulse_response { +// impulse_response::State::None => spectrogram::Progress::None, +// impulse_response::State::Computing => spectrogram::Progress::ComputingImpulseResponse, +// impulse_response::State::Computed(_) => match self.spectrogram { +// spectrogram::State::None => spectrogram::Progress::None, +// spectrogram::State::Computing => spectrogram::Progress::Computing, +// spectrogram::State::Computed(_) => spectrogram::Progress::Finished, +// }, +// } +// } +// } From 7d94aad1c7f62160e35d90efb2b48a4c3f74d081 Mon Sep 17 00:00:00 2001 From: Hendrik Kunert Date: Thu, 29 Jan 2026 11:56:32 +0100 Subject: [PATCH 08/30] Make available tabs depend on state --- raumklang-gui/src/screen/main.rs | 142 ++++++++++++++++++++++--------- raumklang-gui/src/ui/analysis.rs | 8 ++ 2 files changed, 109 insertions(+), 41 deletions(-) diff --git a/raumklang-gui/src/screen/main.rs b/raumklang-gui/src/screen/main.rs index 0d9d656..a1a4048 100644 --- a/raumklang-gui/src/screen/main.rs +++ b/raumklang-gui/src/screen/main.rs @@ -49,7 +49,7 @@ use std::{ }; pub struct Main { - tab: Tab, + // tab: Tab, selected: Option, state: State, @@ -72,6 +72,7 @@ pub struct Main { enum State { Collecting, Analysing { + active_tab: Tab, selected: Option, analyses: BTreeMap, }, @@ -80,10 +81,18 @@ enum State { impl State { pub fn analysis() -> Self { Self::Analysing { + active_tab: Tab::default(), selected: None, analyses: BTreeMap::new(), } } + + fn active_tab(&self) -> Option<&Tab> { + match &self { + State::Collecting => None, + State::Analysing { active_tab, .. } => Some(active_tab), + } + } } impl Default for State { @@ -289,14 +298,20 @@ impl Main { Task::none() } Message::ImpulseResponseSelected(id) => { - let State::Analysing { selected, analyses } = &mut self.state else { + let State::Analysing { + active_tab: tab, + selected, + analyses, + .. + } = &mut self.state + else { return Task::none(); }; *selected = Some(id); analyses.entry(id).or_default(); - match &self.tab { + match tab { Tab::Measurements { .. } => Task::none(), Tab::ImpulseResponses { .. } => self.compute_impulse_response(id), Tab::FrequencyResponses => Task::none(), @@ -307,6 +322,7 @@ impl Main { Message::ImpulseResponseComputed(id, new_ir) => { let State::Analysing { selected, + ref active_tab, ref mut analyses, .. } = self.state @@ -328,7 +344,7 @@ impl Main { self.charts.impulse_responses.data_cache.clear(); } - match &self.tab { + match active_tab { Tab::Measurements { .. } => Task::none(), Tab::ImpulseResponses { .. } => Task::none(), Tab::FrequencyResponses => self.compute_frequency_response(id), @@ -361,15 +377,32 @@ impl Main { } } Message::OpenMeasurements => { - self.tab = Tab::Measurements { recording: None }; + let State::Analysing { + active_tab: ref mut tab, + .. + } = self.state + else { + return Task::none(); + }; + + *tab = Tab::Measurements { recording: None }; + Task::none() } Message::OpenImpulseResponses => { + let State::Analysing { + active_tab: ref mut tab, + .. + } = self.state + else { + return Task::none(); + }; + let Some(window) = &self.window else { return Task::none(); }; - self.tab = Tab::ImpulseResponses { + *tab = Tab::ImpulseResponses { window_settings: WindowSettings::new(window.clone()), }; @@ -377,11 +410,15 @@ impl Main { } Message::OpenFrequencyResponses => { - let State::Analysing { .. } = self.state else { + let State::Analysing { + active_tab: ref mut tab, + .. + } = self.state + else { return Task::none(); }; - self.tab = Tab::FrequencyResponses; + *tab = Tab::FrequencyResponses; // FIXME get rid of vec alloc let ids: Vec<_> = self @@ -1271,32 +1308,35 @@ impl Main { .on_press_maybe(message) }; - let is_analysing = matches!(self.state, State::Analysing { .. }); - + let active_tab = self.state.active_tab(); let tabs = row![ tab( "Measurements", - matches!(self.tab, Tab::Measurements { .. }), + active_tab.is_none(), Some(Message::OpenMeasurements) ), tab( "Impulse Responses", - matches!(self.tab, Tab::ImpulseResponses { .. }), - is_analysing.then_some(Message::OpenImpulseResponses) // Message::OpenImpulseResponses + matches!(active_tab, Some(Tab::ImpulseResponses { .. })), + active_tab + .is_some() + .then_some(Message::OpenImpulseResponses) ), tab( "Frequency Responses", - matches!(self.tab, Tab::FrequencyResponses), - is_analysing.then_some(Message::OpenFrequencyResponses) // Message::OpenImpulseResponses + matches!(active_tab, Some(Tab::FrequencyResponses)), + active_tab + .is_some() + .then_some(Message::OpenFrequencyResponses) ), tab( "Spectral Decays", - matches!(self.tab, Tab::SpectralDecay), + matches!(active_tab, Some(Tab::SpectralDecay)), None // Message::OpenFrequencyResponses ), tab( "Spectrogram", - matches!(self.tab, Tab::Spectrogram), + matches!(active_tab, Some(Tab::Spectrogram)), None // Message::OpenFrequencyResponses ), ] @@ -1312,34 +1352,55 @@ impl Main { }; let content = { - match &self.tab { - Tab::Measurements { recording } => self.measurements_tab(), - Tab::ImpulseResponses { window_settings } => { - let State::Analysing { - selected, - ref analyses, - } = self.state - else { - panic!("invalid state"); - }; - - self.impulse_responses_tab( + match self.state { + State::Collecting => self.measurements_tab(), + State::Analysing { + ref active_tab, + selected, + ref analyses, + } => match active_tab { + Tab::Measurements { recording } => self.measurements_tab(), + Tab::ImpulseResponses { window_settings } => self.impulse_responses_tab( selected, &self.charts.impulse_responses, window_settings, analyses, - ) - } - Tab::FrequencyResponses => { - let State::Analysing { ref analyses, .. } = self.state else { - panic!("invalid state"); - }; - - self.frequency_responses_tab(&self.charts.frequency_responses, analyses) - } - Tab::SpectralDecay => todo!(), - Tab::Spectrogram => todo!(), + ), + Tab::FrequencyResponses => { + self.frequency_responses_tab(&self.charts.frequency_responses, analyses) + } + Tab::SpectralDecay => todo!(), + Tab::Spectrogram => todo!(), + }, } + // match &self.tab { + // Tab::Measurements { recording } => self.measurements_tab(), + // Tab::ImpulseResponses { window_settings } => { + // let State::Analysing { + // selected, + // ref analyses, + // } = self.state + // else { + // panic!("invalid state"); + // }; + + // self.impulse_responses_tab( + // selected, + // &self.charts.impulse_responses, + // window_settings, + // analyses, + // ) + // } + // Tab::FrequencyResponses => { + // let State::Analysing { ref analyses, .. } = self.state else { + // panic!("invalid state"); + // }; + + // self.frequency_responses_tab(&self.charts.frequency_responses, analyses) + // } + // Tab::SpectralDecay => todo!(), + // Tab::Spectrogram => todo!(), + // } }; container(column![header, container(content).padding(10)]).into() @@ -2067,7 +2128,6 @@ fn impulse_response_item<'a>( impl Default for Main { fn default() -> Self { Self { - tab: Tab::Measurements { recording: None }, // state: State::CollectingMeasuremnts { // recording: None, // loopback: None, diff --git a/raumklang-gui/src/ui/analysis.rs b/raumklang-gui/src/ui/analysis.rs index 4b28542..d944ac5 100644 --- a/raumklang-gui/src/ui/analysis.rs +++ b/raumklang-gui/src/ui/analysis.rs @@ -95,6 +95,14 @@ impl Analysis { ) -> Option> { let impulse_response = self.impulse_response().cloned()?; + if self + .frequency_response() + .and_then(FrequencyResponse::result) + .is_some() + { + return None; + } + Some(data::frequency_response::compute( impulse_response.origin, window, From 04065df4fb533fec9046bab9930cf04754e841bd Mon Sep 17 00:00:00 2001 From: Hendrik Kunert Date: Thu, 29 Jan 2026 13:02:50 +0100 Subject: [PATCH 09/30] Refactor `Measurement` view --- raumklang-gui/src/screen/main.rs | 184 ++++++++-------- raumklang-gui/src/screen/main/measurement.rs | 62 ------ raumklang-gui/src/ui/measurement.rs | 210 ++++++++++--------- 3 files changed, 190 insertions(+), 266 deletions(-) diff --git a/raumklang-gui/src/screen/main.rs b/raumklang-gui/src/screen/main.rs index a1a4048..caf48db 100644 --- a/raumklang-gui/src/screen/main.rs +++ b/raumklang-gui/src/screen/main.rs @@ -49,9 +49,8 @@ use std::{ }; pub struct Main { - // tab: Tab, - selected: Option, state: State, + selected: Option, loopback: Option, @@ -194,6 +193,7 @@ pub enum Message { SpectralDecayConfig(spectral_decay_config::Message), OpenSpectrogramConfig, SpectrogramConfig(spectrogram_config::Message), + Measurement(ui::measurement::Message), } #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -275,20 +275,6 @@ impl Main { measurement::Message::Loaded(Err(err)) => { log::error!("{err}"); } - measurement::Message::Remove(id) => { - self.measurements.remove(id); - - if self.measurements.is_empty() { - self.state = State::Collecting - } - - if let State::Analysing { - ref mut analyses, .. - } = self.state - { - analyses.remove(&id); - } - } measurement::Message::Select(selected) => { self.selected = Some(selected); self.signal_cache.clear(); @@ -376,6 +362,30 @@ impl Main { Task::none() } } + Message::Measurement(msg) => { + match msg { + ui::measurement::Message::Select(id) => { + self.selected = Some(Selected::Measurement(id)); + self.signal_cache.clear(); + } + ui::measurement::Message::Remove(id) => { + self.measurements.remove(id); + + if self.measurements.is_empty() { + self.state = State::Collecting + } + + if let State::Analysing { + ref mut analyses, .. + } = self.state + { + analyses.remove(&id); + } + } + }; + + Task::none() + } Message::OpenMeasurements => { let State::Analysing { active_tab: ref mut tab, @@ -437,47 +447,6 @@ impl Main { } } - pub fn compute_impulse_response(&mut self, id: ui::measurement::Id) -> Task { - let State::Analysing { - ref mut analyses, .. - } = self.state - else { - return Task::none(); - }; - - let analysis = analyses.entry(id).or_default(); - - let Some(loopback) = self.loopback.as_ref().and_then(Loopback::loaded) else { - return Task::none(); - }; - - let measurement = self - .measurements - .get(id) - .and_then(ui::Measurement::signal) - .unwrap(); - - analysis - .compute_impulse_response(loopback.clone(), measurement.clone()) - .map(|f| Task::perform(f, Message::ImpulseResponseComputed.with(id))) - .unwrap_or_default() - } - - pub fn compute_frequency_response(&mut self, id: ui::measurement::Id) -> Task { - let State::Analysing { - ref mut analyses, .. - } = self.state - else { - return Task::none(); - }; - - let analysis = analyses.entry(id).or_default(); - - analysis - .compute_frequency_response(self.window.as_ref().cloned().unwrap()) - .map(|f| Task::perform(f, Message::FrequencyResponseComputed.with(id))) - .unwrap_or_else(|| self.compute_impulse_response(id)) - } // pub fn update( // // &mut self, @@ -1373,34 +1342,6 @@ impl Main { Tab::Spectrogram => todo!(), }, } - // match &self.tab { - // Tab::Measurements { recording } => self.measurements_tab(), - // Tab::ImpulseResponses { window_settings } => { - // let State::Analysing { - // selected, - // ref analyses, - // } = self.state - // else { - // panic!("invalid state"); - // }; - - // self.impulse_responses_tab( - // selected, - // &self.charts.impulse_responses, - // window_settings, - // analyses, - // ) - // } - // Tab::FrequencyResponses => { - // let State::Analysing { ref analyses, .. } = self.state else { - // panic!("invalid state"); - // }; - - // self.frequency_responses_tab(&self.charts.frequency_responses, analyses) - // } - // Tab::SpectralDecay => todo!(), - // Tab::Spectrogram => todo!(), - // } }; container(column![header, container(content).padding(10)]).into() @@ -1557,22 +1498,24 @@ impl Main { measurement::loopback_entry(self.selected, loopback).map(Message::Measurements) })); - let measurements = Category::new("Measurements") - .push_button( - button("+") - .style(button::secondary) - .on_press(Message::Measurements(measurement::Message::Load( + let measurements = + Category::new("Measurements") + .push_button(button("+").style(button::secondary).on_press( + Message::Measurements(measurement::Message::Load( measurement::Kind::Normal, - ))), - ) - .push_button( - button(icon::record()) - .on_press(Message::StartRecording(recording::Kind::Measurement)) - .style(button::secondary), - ) - .extend_entries(self.measurements.iter().map(|measurement| { - measurement::list_entry(self.selected, measurement).map(Message::Measurements) - })); + )), + )) + .push_button( + button(icon::record()) + .on_press(Message::StartRecording(recording::Kind::Measurement)) + .style(button::secondary), + ) + .extend_entries(self.measurements.iter().map(|measurement| { + let active = self + .selected + .is_some_and(|id| id == Selected::Measurement(measurement.id())); + measurement.view(active).map(Message::Measurement) + })); container(scrollable( column![loopback, measurements].spacing(20).padding(10), @@ -2070,6 +2013,47 @@ impl Main { // is_loopback_loaded && self.measurements.iter().any(ui::Measurement::is_loaded) // } + fn compute_impulse_response(&mut self, id: ui::measurement::Id) -> Task { + let State::Analysing { + ref mut analyses, .. + } = self.state + else { + return Task::none(); + }; + + let analysis = analyses.entry(id).or_default(); + + let Some(loopback) = self.loopback.as_ref().and_then(Loopback::loaded) else { + return Task::none(); + }; + + let measurement = self + .measurements + .get(id) + .and_then(ui::Measurement::signal) + .unwrap(); + + analysis + .compute_impulse_response(loopback.clone(), measurement.clone()) + .map(|f| Task::perform(f, Message::ImpulseResponseComputed.with(id))) + .unwrap_or_default() + } + + fn compute_frequency_response(&mut self, id: ui::measurement::Id) -> Task { + let State::Analysing { + ref mut analyses, .. + } = self.state + else { + return Task::none(); + }; + + let analysis = analyses.entry(id).or_default(); + + analysis + .compute_frequency_response(self.window.as_ref().cloned().unwrap()) + .map(|f| Task::perform(f, Message::FrequencyResponseComputed.with(id))) + .unwrap_or_else(|| self.compute_impulse_response(id)) + } } fn impulse_response_item<'a>( diff --git a/raumklang-gui/src/screen/main/measurement.rs b/raumklang-gui/src/screen/main/measurement.rs index f087a04..f98e883 100644 --- a/raumklang-gui/src/screen/main/measurement.rs +++ b/raumklang-gui/src/screen/main/measurement.rs @@ -7,7 +7,6 @@ use crate::{ use chrono::{DateTime, Utc}; use iced::{ widget::{button, column, right, row, rule, text, tooltip}, - Alignment::Center, Element, Length::{self, Fill}, }; @@ -21,7 +20,6 @@ pub enum Message { Select(Selected), Load(Kind), Loaded(Result, Arc>), - Remove(measurement::Id), } #[derive(Debug, Clone, Copy, PartialEq, PartialOrd)] @@ -110,66 +108,6 @@ pub fn loopback_entry<'a>( sidebar::item(content, is_active) } -pub fn list_entry<'a>( - selected: Option, - measurement: &'a ui::Measurement, -) -> Element<'a, Message> { - let id = measurement.id(); - - let is_active = selected.is_some_and(|s| s == Selected::Measurement(id)); - - let info: Element<_> = match &measurement.signal() { - Some(signal) => { - let dt: DateTime = signal.modified.into(); - column![ - text("Last modified:").size(10), - text!("{}", dt.format("%x %X")).size(10) - ] - .into() - } - None => text("Offline").style(text::danger).into(), - }; - - let measurement_btn = button( - column![ - text(&measurement.name).wrapping(text::Wrapping::WordOrGlyph), - info - ] - .spacing(5), - ) - .on_press_maybe(if measurement.is_loaded() { - Some(Message::Select(Selected::Measurement(id))) - } else { - None - }) - .style(move |theme, status| { - let background = theme.extended_palette().background; - let base = button::subtle(theme, status); - - if is_active { - base.with_background(background.weak.color) - } else { - base - } - }) - .width(Fill) - .clip(true); - - let delete_btn = button(icon::delete().align_x(Center).align_y(Center)) - .on_press_with(move || Message::Remove(id)) - .width(30) - .height(30) - .style(button::danger); - - let content = row![ - measurement_btn, - rule::vertical(1.0), - right(delete_btn).width(Length::Shrink).padding([0, 6]) - ]; - - sidebar::item(content, is_active) -} - pub async fn load_measurement( path: impl AsRef, kind: Kind, diff --git a/raumklang-gui/src/ui/measurement.rs b/raumklang-gui/src/ui/measurement.rs index cba1576..3dc1f3f 100644 --- a/raumklang-gui/src/ui/measurement.rs +++ b/raumklang-gui/src/ui/measurement.rs @@ -2,62 +2,26 @@ pub mod loopback; pub use loopback::Loopback; +use chrono::{DateTime, Utc}; +use iced::{ + widget::{button, column, right, row, rule, text}, + Alignment::Center, + Element, + Length::{Fill, Shrink}, +}; + use std::{ fmt::Display, path::{Path, PathBuf}, sync::atomic::{self, AtomicUsize}, }; -use crate::ui::{ - impulse_response, spectral_decay, spectrogram, FrequencyResponse, ImpulseResponse, -}; - -#[derive(Debug, Default, Clone)] -pub struct List(Vec); - -impl List { - pub fn iter(&self) -> impl Iterator + Clone { - self.0.iter() - } - - pub fn iter_mut(&mut self) -> impl Iterator { - self.0.iter_mut() - } - - pub fn loaded(&self) -> impl Iterator { - self.0.iter().filter(|m| m.is_loaded()) - } +use crate::{icon, widget::sidebar}; - pub fn loaded_mut(&mut self) -> impl Iterator { - self.0.iter_mut().filter(|m| m.is_loaded()) - } - - pub fn push(&mut self, measurement: Measurement) { - self.0.push(measurement); - } - - pub fn remove(&mut self, id: Id) -> Option { - let index = self - .0 - .iter() - .enumerate() - .find(|(_, m)| m.id == id) - .map(|(i, _)| i)?; - - Some(self.0.remove(index)) - } - - pub fn get(&self, id: Id) -> Option<&Measurement> { - self.0.iter().find(|m| m.id == id) - } - - pub fn get_mut(&mut self, id: Id) -> Option<&mut Measurement> { - self.0.iter_mut().find(|m| m.id == id) - } - - pub fn is_empty(&self) -> bool { - self.0.is_empty() - } +#[derive(Debug, Clone)] +pub enum Message { + Select(Id), + Remove(Id), } #[derive(Debug, Clone)] @@ -91,10 +55,7 @@ impl Measurement { let id = Id(ID.fetch_add(1, atomic::Ordering::Relaxed)); let state = match signal { - Some(signal) => State::Loaded { - signal, - // analysis: Analysis::default(), - }, + Some(signal) => State::Loaded { signal }, None => State::NotLoaded, }; @@ -120,6 +81,51 @@ impl Measurement { Self::new(name, path, signal) } + pub fn view(&self, active: bool) -> Element<'_, Message> { + let info: Element<_> = match &self.signal() { + Some(signal) => { + let dt: DateTime = signal.modified.into(); + column![ + text("Last modified:").size(10), + text!("{}", dt.format("%x %X")).size(10) + ] + .into() + } + None => text("Offline").style(text::danger).into(), + }; + + let measurement_btn = button( + column![text(&self.name).wrapping(text::Wrapping::WordOrGlyph), info].spacing(5), + ) + .on_press_maybe(self.is_loaded().then_some(Message::Select(self.id))) + .style(move |theme, status| { + let background = theme.extended_palette().background; + let base = button::subtle(theme, status); + + if active { + base.with_background(background.weak.color) + } else { + base + } + }) + .width(Fill) + .clip(true); + + let delete_btn = button(icon::delete().align_x(Center).align_y(Center)) + .on_press_with(move || Message::Remove(self.id)) + .width(30) + .height(30) + .style(button::danger); + + let content = row![ + measurement_btn, + rule::vertical(1.0), + right(delete_btn).width(Shrink).padding([0, 6]) + ]; + + sidebar::item(content, active) + } + pub fn is_loaded(&self) -> bool { match &self.state { State::NotLoaded => false, @@ -137,22 +143,54 @@ impl Measurement { pub(crate) fn id(&self) -> Id { self.id } +} + +#[derive(Debug, Default, Clone)] +pub struct List(Vec); + +impl List { + pub fn iter(&self) -> impl Iterator + Clone { + self.0.iter() + } + + pub fn iter_mut(&mut self) -> impl Iterator { + self.0.iter_mut() + } + + pub fn loaded(&self) -> impl Iterator { + self.0.iter().filter(|m| m.is_loaded()) + } - // pub fn analysis(&self) -> Option<&Analysis> { - // match self.state { - // State::NotLoaded => None, - // State::Loaded { ref analysis, .. } => Some(analysis), - // } - // } - - // pub fn analysis_mut(&mut self) -> Option<&mut Analysis> { - // match self.state { - // State::NotLoaded => None, - // State::Loaded { - // ref mut analysis, .. - // } => Some(analysis), - // } - // } + pub fn loaded_mut(&mut self) -> impl Iterator { + self.0.iter_mut().filter(|m| m.is_loaded()) + } + + pub fn push(&mut self, measurement: Measurement) { + self.0.push(measurement); + } + + pub fn remove(&mut self, id: Id) -> Option { + let index = self + .0 + .iter() + .enumerate() + .find(|(_, m)| m.id == id) + .map(|(i, _)| i)?; + + Some(self.0.remove(index)) + } + + pub fn get(&self, id: Id) -> Option<&Measurement> { + self.0.iter().find(|m| m.id == id) + } + + pub fn get_mut(&mut self, id: Id) -> Option<&mut Measurement> { + self.0.iter_mut().find(|m| m.id == id) + } + + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } } impl Display for Id { @@ -160,39 +198,3 @@ impl Display for Id { write!(f, "{}", self.0) } } - -// #[derive(Debug, Clone, Default)] -// pub struct Analysis { -// pub impulse_response: impulse_response::State, -// pub frequency_response: FrequencyResponse, -// pub spectral_decay: spectral_decay::State, -// pub spectrogram: spectrogram::State, -// } - -// impl Analysis { -// pub(crate) fn spectral_decay_progress(&self) -> spectral_decay::Progress { -// match self.impulse_response { -// impulse_response::State::None => spectral_decay::Progress::None, -// impulse_response::State::Computing => { -// spectral_decay::Progress::ComputingImpulseResponse -// } -// impulse_response::State::Computed(_) => match self.spectral_decay { -// spectral_decay::State::None => spectral_decay::Progress::None, -// spectral_decay::State::Computing => spectral_decay::Progress::Computing, -// spectral_decay::State::Computed(_) => spectral_decay::Progress::Finished, -// }, -// } -// } - -// pub(crate) fn spectrogram_progress(&self) -> spectrogram::Progress { -// match self.impulse_response { -// impulse_response::State::None => spectrogram::Progress::None, -// impulse_response::State::Computing => spectrogram::Progress::ComputingImpulseResponse, -// impulse_response::State::Computed(_) => match self.spectrogram { -// spectrogram::State::None => spectrogram::Progress::None, -// spectrogram::State::Computing => spectrogram::Progress::Computing, -// spectrogram::State::Computed(_) => spectrogram::Progress::Finished, -// }, -// } -// } -// } From 85eb5a7d5c8852d69a2b07a4538d7fef75df3833 Mon Sep 17 00:00:00 2001 From: Hendrik Kunert Date: Thu, 29 Jan 2026 13:30:53 +0100 Subject: [PATCH 10/30] Refactor `Loopback` view --- raumklang-gui/src/screen/main.rs | 37 +++++----- raumklang-gui/src/screen/main/measurement.rs | 75 +------------------- raumklang-gui/src/ui/measurement.rs | 14 +++- raumklang-gui/src/ui/measurement/loopback.rs | 63 ++++++++++++++++ 4 files changed, 92 insertions(+), 97 deletions(-) diff --git a/raumklang-gui/src/screen/main.rs b/raumklang-gui/src/screen/main.rs index caf48db..52efef7 100644 --- a/raumklang-gui/src/screen/main.rs +++ b/raumklang-gui/src/screen/main.rs @@ -12,7 +12,7 @@ use crate::{ }, icon, load_project, log, screen::main::{ - chart::waveform, impulse_response::processing_overlay, measurement::Selected, + chart::waveform, impulse_response::processing_overlay, spectral_decay_config::SpectralDecayConfig, spectrogram_config::SpectrogramConfig, }, ui::{self, analysis, Analysis, Loopback}, @@ -50,15 +50,16 @@ use std::{ pub struct Main { state: State, - selected: Option, + selected: Option, loopback: Option, + measurements: ui::measurement::List, signal_cache: canvas::Cache, - smoothing: frequency_response::Smoothing, - measurements: ui::measurement::List, zoom: chart::Zoom, offset: chart::Offset, + + smoothing: frequency_response::Smoothing, // modal: Modal, // project_path: Option, // spectral_decay_config: data::spectral_decay::Config, @@ -275,10 +276,6 @@ impl Main { measurement::Message::Loaded(Err(err)) => { log::error!("{err}"); } - measurement::Message::Select(selected) => { - self.selected = Some(selected); - self.signal_cache.clear(); - } } Task::none() @@ -364,8 +361,8 @@ impl Main { } Message::Measurement(msg) => { match msg { - ui::measurement::Message::Select(id) => { - self.selected = Some(Selected::Measurement(id)); + ui::measurement::Message::Select(selected) => { + self.selected = Some(selected); self.signal_cache.clear(); } ui::measurement::Message::Remove(id) => { @@ -1495,7 +1492,8 @@ impl Main { .style(button::secondary), ) .push_entry_maybe(self.loopback.as_ref().map(|loopback| { - measurement::loopback_entry(self.selected, loopback).map(Message::Measurements) + let active = self.selected == Some(ui::measurement::Selected::Loopback); + loopback.view(active).map(Message::Measurement) })); let measurements = @@ -1511,9 +1509,8 @@ impl Main { .style(button::secondary), ) .extend_entries(self.measurements.iter().map(|measurement| { - let active = self - .selected - .is_some_and(|id| id == Selected::Measurement(measurement.id())); + let active = self.selected + == Some(ui::measurement::Selected::Measurement(measurement.id())); measurement.view(active).map(Message::Measurement) })); @@ -1549,16 +1546,14 @@ impl Main { let content = if let Some(measurement) = self.selected.and_then(|selected| match selected { - measurement::Selected::Loopback => self + ui::measurement::Selected::Loopback => self .loopback .as_ref() - .and_then(|l| l.loaded()) + .and_then(Loopback::loaded) .map(AsRef::as_ref), - measurement::Selected::Measurement(id) => self - .measurements - .iter() - .find(|m| m.id() == id) - .and_then(ui::Measurement::signal), + ui::measurement::Selected::Measurement(id) => { + self.measurements.get(id).and_then(ui::Measurement::signal) + } }) { chart::waveform(measurement, &self.signal_cache, self.zoom, self.offset) .map(Message::MeasurementChart) diff --git a/raumklang-gui/src/screen/main/measurement.rs b/raumklang-gui/src/screen/main/measurement.rs index f98e883..79646b7 100644 --- a/raumklang-gui/src/screen/main/measurement.rs +++ b/raumklang-gui/src/screen/main/measurement.rs @@ -1,15 +1,5 @@ -use crate::{ - icon, - ui::{self, measurement, Loopback}, - widget::sidebar, -}; +use crate::ui::{self}; -use chrono::{DateTime, Utc}; -use iced::{ - widget::{button, column, right, row, rule, text, tooltip}, - Element, - Length::{self, Fill}, -}; use raumklang_core::WavLoadError; use rfd::FileHandle; @@ -17,17 +7,10 @@ use std::{fmt::Display, path::Path, sync::Arc}; #[derive(Debug, Clone)] pub enum Message { - Select(Selected), Load(Kind), Loaded(Result, Arc>), } -#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)] -pub enum Selected { - Loopback, - Measurement(measurement::Id), -} - #[derive(Debug, Clone, Copy)] pub enum Kind { Loopback, @@ -52,62 +35,6 @@ pub enum LoadedKind { Normal(ui::Measurement), } -pub fn loopback_entry<'a>( - selected: Option, - loopback: &'a Loopback, -) -> Element<'a, Message> { - let is_active = selected.is_some_and(|s| matches!(s, Selected::Loopback)); - - let info: Element<_> = match &loopback.state { - measurement::loopback::State::Loaded(loopback) => { - let dt: DateTime = loopback.as_ref().modified.into(); - column![ - text("Last modified:").size(10), - text!("{}", dt.format("%x %X")).size(10) - ] - .into() - } - measurement::loopback::State::NotLoaded(err) => tooltip( - text("Offline").style(text::danger), - text!("{err}").style(text::danger), - tooltip::Position::default(), - ) - .into(), - }; - - let measurement_btn = button(column![text(&loopback.name).size(16)].push(info).spacing(5)) - .on_press_maybe( - loopback - .loaded() - .map(|_| Message::Select(Selected::Loopback)), - ) - .style(move |theme, status| { - let background = theme.extended_palette().background; - let base = button::subtle(theme, status); - - if is_active { - base.with_background(background.weak.color) - } else { - base - } - }) - .width(Fill); - - let delete_btn = button(icon::delete()) - // .on_press(Message::RemoveLoopback) - .width(30) - .height(30) - .style(button::danger); - - let content = row![ - measurement_btn, - rule::vertical(1.0), - right(delete_btn).width(Length::Shrink).padding([0, 6]) - ]; - - sidebar::item(content, is_active) -} - pub async fn load_measurement( path: impl AsRef, kind: Kind, diff --git a/raumklang-gui/src/ui/measurement.rs b/raumklang-gui/src/ui/measurement.rs index 3dc1f3f..3723149 100644 --- a/raumklang-gui/src/ui/measurement.rs +++ b/raumklang-gui/src/ui/measurement.rs @@ -20,10 +20,16 @@ use crate::{icon, widget::sidebar}; #[derive(Debug, Clone)] pub enum Message { - Select(Id), + Select(Selected), Remove(Id), } +#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)] +pub enum Selected { + Loopback, + Measurement(Id), +} + #[derive(Debug, Clone)] pub struct Measurement { id: Id, @@ -97,7 +103,11 @@ impl Measurement { let measurement_btn = button( column![text(&self.name).wrapping(text::Wrapping::WordOrGlyph), info].spacing(5), ) - .on_press_maybe(self.is_loaded().then_some(Message::Select(self.id))) + .on_press_maybe( + self.is_loaded() + .then_some(Selected::Measurement(self.id)) + .map(Message::Select), + ) .style(move |theme, status| { let background = theme.extended_palette().background; let base = button::subtle(theme, status); diff --git a/raumklang-gui/src/ui/measurement/loopback.rs b/raumklang-gui/src/ui/measurement/loopback.rs index 2333dba..9d7f4c2 100644 --- a/raumklang-gui/src/ui/measurement/loopback.rs +++ b/raumklang-gui/src/ui/measurement/loopback.rs @@ -1,3 +1,15 @@ +use super::{Message, Selected}; + +use crate::{icon, widget::sidebar}; + +use iced::{ + widget::{button, column, right, row, rule, text, tooltip}, + Element, + Length::{Fill, Shrink}, +}; + +use chrono::{DateTime, Utc}; + use std::{ path::{Path, PathBuf}, sync::Arc, @@ -49,6 +61,57 @@ impl Loopback { } } + pub fn view(&self, active: bool) -> Element<'_, super::Message> { + let info: Element<_> = match &self.state { + State::Loaded(loopback) => { + let dt: DateTime = loopback.as_ref().modified.into(); + column![ + text("Last modified:").size(10), + text!("{}", dt.format("%x %X")).size(10) + ] + .into() + } + State::NotLoaded(err) => tooltip( + text("Offline").style(text::danger), + text!("{err}").style(text::danger), + tooltip::Position::default(), + ) + .into(), + }; + + let measurement_btn = button(column![text(&self.name).size(16)].push(info).spacing(5)) + .on_press_maybe( + self.loaded() + .is_some() + .then_some(Message::Select(Selected::Loopback)), + ) + .style(move |theme, status| { + let background = theme.extended_palette().background; + let base = button::subtle(theme, status); + + if active { + base.with_background(background.weak.color) + } else { + base + } + }) + .width(Fill); + + let delete_btn = button(icon::delete()) + // .on_press(Message::RemoveLoopback) + .width(30) + .height(30) + .style(button::danger); + + let content = row![ + measurement_btn, + rule::vertical(1.0), + right(delete_btn).width(Shrink).padding([0, 6]) + ]; + + sidebar::item(content, active) + } + pub(crate) fn loaded(&self) -> Option<&raumklang_core::Loopback> { match &self.state { State::Loaded(loopback) => Some(loopback), From 1c212c8c4849ec66c53fdde7e911e48c54344778 Mon Sep 17 00:00:00 2001 From: Hendrik Kunert Date: Thu, 29 Jan 2026 14:43:58 +0100 Subject: [PATCH 11/30] Refactor `ui::FrequencyResponse` --- raumklang-gui/src/ui/analysis.rs | 20 ++++--- raumklang-gui/src/ui/frequency_response.rs | 57 +++++++++++--------- raumklang-gui/src/ui/measurement/loopback.rs | 4 +- 3 files changed, 46 insertions(+), 35 deletions(-) diff --git a/raumklang-gui/src/ui/analysis.rs b/raumklang-gui/src/ui/analysis.rs index d944ac5..5b824cf 100644 --- a/raumklang-gui/src/ui/analysis.rs +++ b/raumklang-gui/src/ui/analysis.rs @@ -4,7 +4,7 @@ use raumklang_core::{Loopback, Measurement}; use crate::{ data, - ui::{impulse_response, FrequencyResponse, ImpulseResponse}, + ui::{frequency_response, impulse_response, FrequencyResponse, ImpulseResponse}, }; #[derive(Debug, Clone, Default)] @@ -93,18 +93,22 @@ impl Analysis { &mut self, window: data::Window, ) -> Option> { - let impulse_response = self.impulse_response().cloned()?; + let State::ImpulseResponse { + ref impulse_response, + ref mut frequency_response, + } = self.0 + else { + return None; + }; - if self - .frequency_response() - .and_then(FrequencyResponse::result) - .is_some() - { + if let frequency_response::Progress::Finished = frequency_response.progress { return None; } + frequency_response.progress = frequency_response::Progress::Computing; + Some(data::frequency_response::compute( - impulse_response.origin, + impulse_response.origin.clone(), window, )) } diff --git a/raumklang-gui/src/ui/frequency_response.rs b/raumklang-gui/src/ui/frequency_response.rs index ab8e701..52399f4 100644 --- a/raumklang-gui/src/ui/frequency_response.rs +++ b/raumklang-gui/src/ui/frequency_response.rs @@ -18,6 +18,7 @@ pub struct FrequencyResponse { pub color: iced::Color, pub is_shown: bool, pub progress: Progress, + pub data: Option, pub smoothed: Option>, } @@ -37,6 +38,7 @@ impl FrequencyResponse { pub fn computed(&mut self, data: data::FrequencyResponse) { self.data = Some(data); + self.progress = Progress::Finished; } pub fn view<'a, Message>( @@ -48,43 +50,46 @@ impl FrequencyResponse { where Message: 'a, { - let item = row![ - icon::record().color(self.color).align_y(Alignment::Center), - container( + let item = { + let color_dot = icon::record().color(self.color).align_y(Alignment::Center); + + let content = container( text(measurement_name) .size(16) .style(|theme| { let mut base = text::default(theme); - let text_color = theme.extended_palette().background.weakest.text; - base.color = Some(text_color); + let palette = theme.extended_palette(); + base.color = Some(palette.background.weakest.text); base }) - .align_y(Alignment::Center) - .wrapping(text::Wrapping::Glyph), + .wrapping(text::Wrapping::Glyph) + .align_y(Alignment::Center), ) .width(Length::Fill) - .clip(true), - container(toggler(self.is_shown).on_toggle(on_toggle)).align_right(Length::Shrink) - ] - .align_y(Alignment::Center) - .spacing(10) - .padding(6) - .into(); - - if self.data.is_some() { - item - } else { - match impulse_response_progess { - impulse_response::Progress::Computing => { - processing_overlay("Impulse Response", item) - } - impulse_response::Progress::Finished => { + .clip(true); + + let switch = + container(toggler(self.is_shown).on_toggle(on_toggle)).align_right(Length::Shrink); + + row![color_dot, content, switch] + .align_y(Alignment::Center) + .spacing(10) + .padding(20) + .into() + }; + + match impulse_response_progess { + impulse_response::Progress::None | impulse_response::Progress::Computing => { + processing_overlay("Impulse Response", item) + } + impulse_response::Progress::Finished => match self.progress { + Progress::None | Progress::Computing => { processing_overlay(self.progress.to_string(), item) } - _ => item, - } + Progress::Finished => item, + }, } } @@ -104,6 +109,7 @@ pub enum Progress { #[default] None, Computing, + Finished, } impl Display for Progress { @@ -111,6 +117,7 @@ impl Display for Progress { let text = match self { Self::None => "Not Started", Self::Computing => "Impulse Response", + Self::Finished => "Finished", }; write!(f, "{}", text) diff --git a/raumklang-gui/src/ui/measurement/loopback.rs b/raumklang-gui/src/ui/measurement/loopback.rs index 9d7f4c2..c61ee32 100644 --- a/raumklang-gui/src/ui/measurement/loopback.rs +++ b/raumklang-gui/src/ui/measurement/loopback.rs @@ -19,11 +19,11 @@ use std::{ pub struct Loopback { pub name: String, pub path: Option, - pub state: State, + state: State, } #[derive(Debug, Clone)] -pub enum State { +enum State { Loaded(raumklang_core::Loopback), NotLoaded(Arc), } From 2af830aa816497fef2be6115be17753550bf90fe Mon Sep 17 00:00:00 2001 From: Hendrik Kunert Date: Thu, 29 Jan 2026 17:00:03 +0100 Subject: [PATCH 12/30] Simplify loopback and measurement loading --- raumklang-gui/src/screen/main.rs | 231 ++++++++++--------- raumklang-gui/src/screen/main/measurement.rs | 77 ------- 2 files changed, 118 insertions(+), 190 deletions(-) delete mode 100644 raumklang-gui/src/screen/main/measurement.rs diff --git a/raumklang-gui/src/screen/main.rs b/raumklang-gui/src/screen/main.rs index 52efef7..63575eb 100644 --- a/raumklang-gui/src/screen/main.rs +++ b/raumklang-gui/src/screen/main.rs @@ -1,7 +1,6 @@ mod chart; mod frequency_response; mod impulse_response; -mod measurement; mod recording; mod spectral_decay_config; mod spectrogram_config; @@ -19,7 +18,7 @@ use crate::{ widget::sidebar, PickAndLoadError, }; -use raumklang_core::dbfs; +use raumklang_core::{dbfs, WavLoadError}; use impulse_response::{ChartOperation, WindowSettings}; use recording::Recording; @@ -40,6 +39,7 @@ use iced::{ Color, Element, Function, Length, Subscription, Task, Theme, }; use prism::{axis, line_series, Axis, Chart, Labels}; +use rfd::FileHandle; use std::{ collections::{BTreeMap, HashMap}, @@ -55,6 +55,8 @@ pub struct Main { loopback: Option, measurements: ui::measurement::List, + project_path: Option, + signal_cache: canvas::Cache, zoom: chart::Zoom, offset: chart::Offset, @@ -162,7 +164,6 @@ pub enum Message { OpenImpulseResponses, OpenFrequencyResponses, - Measurements(measurement::Message), MeasurementChart(waveform::Interaction), ImpulseResponseSelected(ui::measurement::Id), @@ -195,6 +196,12 @@ pub enum Message { OpenSpectrogramConfig, SpectrogramConfig(spectrogram_config::Message), Measurement(ui::measurement::Message), + + LoadLoopback, + LoopbackLoaded(Loopback), + + LoadMeasurement, + MeasurementLoaded(ui::Measurement), } #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -208,75 +215,92 @@ pub enum TabId { impl Main { pub fn from_project(path: PathBuf, project: data::Project) -> (Self, Task) { - // let load_loopback = project - // .loopback - // .map(|loopback| { - // Task::perform( - // measurement::load_measurement(loopback.0.path, measurement::Kind::Loopback), - // measurement::Message::Loaded, - // ) - // }) - // .unwrap_or_else(Task::none); - - // let load_measurements = project.measurements.into_iter().map(|measurement| { - // Task::perform( - // measurement::load_measurement(measurement.path, measurement::Kind::Normal), - // measurement::Message::Loaded, - // ) - // }); + let load_loopback = project + .loopback + .map(|loopback| { + Task::perform( + Loopback::from_file(loopback.0.path), + Message::LoopbackLoaded, + ) + }) + .unwrap_or_default(); + + let load_measurements = project.measurements.into_iter().map(|measurement| { + Task::perform( + ui::Measurement::from_file(measurement.path), + Message::MeasurementLoaded, + ) + }); ( - Self::default(), - // Self { - // project_path: Some(path), - // ..Default::default() - // }, - // Task::batch([load_loopback, Task::batch(load_measurements)]).map(Message::Measurements), - Task::none(), + Self { + project_path: Some(path), + ..Default::default() + }, + Task::batch([load_loopback, Task::batch(load_measurements)]), ) } pub fn update(&mut self, recent_projects: &mut RecentProjects, msg: Message) -> Task { match msg { - Message::Measurements(message) => { - match message { - measurement::Message::Load(kind) => { - let dialog_caption = kind.to_string(); - - return Task::perform( - measurement::pick_file_and_load_signal(dialog_caption, kind), - measurement::Message::Loaded, - ) - .map(Message::Measurements); + Message::LoadLoopback => { + Task::future(pick_file("Load Loopback ...")).and_then(|path| { + Task::perform(ui::Loopback::from_file(path), Message::LoopbackLoaded) + }) + } + Message::LoadMeasurement => { + Task::future(pick_file("Load measurement ...")).and_then(|path| { + Task::perform(ui::Measurement::from_file(path), Message::MeasurementLoaded) + }) + } + Message::LoopbackLoaded(loopback) => { + self.window = loopback + .loaded() + .map(raumklang_core::Loopback::sample_rate) + .map(SampleRate::from) + .map(Window::new) + .map(Into::into); + + self.loopback = Some(loopback); + + if !self.measurements.is_empty() { + self.state = State::analysis(); + } + + Task::none() + } + Message::MeasurementLoaded(measurement) => { + let is_loopback_loaded = self.loopback.as_ref().is_some_and(Loopback::is_loaded); + + if is_loopback_loaded && self.measurements.is_empty() { + self.state = State::analysis(); + } + + self.measurements.push(measurement); + + Task::none() + } + Message::Measurement(msg) => { + match msg { + ui::measurement::Message::Select(selected) => { + self.selected = Some(selected); + self.signal_cache.clear(); } - measurement::Message::Loaded(Ok(result)) => match Arc::into_inner(result) { - Some(measurement::LoadedKind::Loopback(loopback)) => { - self.window = loopback - .loaded() - .map(raumklang_core::Loopback::sample_rate) - .map(SampleRate::from) - .map(Window::new) - .map(Into::into); - - self.loopback = Some(loopback); - - if !self.measurements.is_empty() { - self.state = State::analysis() - } + ui::measurement::Message::Remove(id) => { + self.measurements.remove(id); + + if self.measurements.loaded().next().is_none() { + self.state = State::Collecting } - Some(measurement::LoadedKind::Normal(measurement)) => { - if self.measurements.is_empty() { - self.state = State::analysis(); - } - self.measurements.push(measurement); + if let State::Analysing { + ref mut analyses, .. + } = self.state + { + analyses.remove(&id); } - None => {} - }, - measurement::Message::Loaded(Err(err)) => { - log::error!("{err}"); } - } + }; Task::none() } @@ -359,30 +383,6 @@ impl Main { Task::none() } } - Message::Measurement(msg) => { - match msg { - ui::measurement::Message::Select(selected) => { - self.selected = Some(selected); - self.signal_cache.clear(); - } - ui::measurement::Message::Remove(id) => { - self.measurements.remove(id); - - if self.measurements.is_empty() { - self.state = State::Collecting - } - - if let State::Analysing { - ref mut analyses, .. - } = self.state - { - analyses.remove(&id); - } - } - }; - - Task::none() - } Message::OpenMeasurements => { let State::Analysing { active_tab: ref mut tab, @@ -1481,9 +1481,7 @@ impl Main { let loopback = Category::new("Loopback") .push_button( button("+") - .on_press_maybe(Some(Message::Measurements(measurement::Message::Load( - measurement::Kind::Loopback, - )))) + .on_press(Message::LoadLoopback) .style(button::secondary), ) .push_button( @@ -1496,23 +1494,22 @@ impl Main { loopback.view(active).map(Message::Measurement) })); - let measurements = - Category::new("Measurements") - .push_button(button("+").style(button::secondary).on_press( - Message::Measurements(measurement::Message::Load( - measurement::Kind::Normal, - )), - )) - .push_button( - button(icon::record()) - .on_press(Message::StartRecording(recording::Kind::Measurement)) - .style(button::secondary), - ) - .extend_entries(self.measurements.iter().map(|measurement| { - let active = self.selected - == Some(ui::measurement::Selected::Measurement(measurement.id())); - measurement.view(active).map(Message::Measurement) - })); + let measurements = Category::new("Measurements") + .push_button( + button("+") + .style(button::secondary) + .on_press(Message::LoadMeasurement), + ) + .push_button( + button(icon::record()) + .on_press(Message::StartRecording(recording::Kind::Measurement)) + .style(button::secondary), + ) + .extend_entries(self.measurements.iter().map(|measurement| { + let active = self.selected + == Some(ui::measurement::Selected::Measurement(measurement.id())); + measurement.view(active).map(Message::Measurement) + })); container(scrollable( column![loopback, measurements].spacing(20).padding(10), @@ -2107,26 +2104,23 @@ fn impulse_response_item<'a>( impl Default for Main { fn default() -> Self { Self { - // state: State::CollectingMeasuremnts { - // recording: None, - // loopback: None, - // }, state: State::default(), + selected: None, loopback: None, measurements: ui::measurement::List::default(), - // project_path: None, - selected: None, - smoothing: frequency_response::Smoothing::default(), - // modal: Modal::None, - signal_cache: canvas::Cache::default(), + + project_path: None, + zoom: chart::Zoom::default(), offset: chart::Offset::default(), + smoothing: frequency_response::Smoothing::default(), // spectral_decay_config: data::spectral_decay::Config::default(), // spectrogram_config: spectrogram::Preferences::default(), window: None, charts: Charts::default(), + signal_cache: canvas::Cache::default(), } } } @@ -2420,3 +2414,14 @@ async fn pick_project_file() -> Result { Ok(handle.path().to_path_buf()) } + +pub async fn pick_file(title: impl AsRef) -> Option { + let handle = rfd::AsyncFileDialog::new() + .set_title(format!("Choose {} file", title.as_ref())) + .add_filter("wav", &["wav", "wave"]) + .add_filter("all", &["*"]) + .pick_file() + .await; + + handle.as_ref().map(FileHandle::path).map(Path::to_path_buf) +} diff --git a/raumklang-gui/src/screen/main/measurement.rs b/raumklang-gui/src/screen/main/measurement.rs deleted file mode 100644 index 79646b7..0000000 --- a/raumklang-gui/src/screen/main/measurement.rs +++ /dev/null @@ -1,77 +0,0 @@ -use crate::ui::{self}; - -use raumklang_core::WavLoadError; -use rfd::FileHandle; - -use std::{fmt::Display, path::Path, sync::Arc}; - -#[derive(Debug, Clone)] -pub enum Message { - Load(Kind), - Loaded(Result, Arc>), -} - -#[derive(Debug, Clone, Copy)] -pub enum Kind { - Loopback, - Normal, -} - -impl Display for Kind { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let text = match self { - Kind::Loopback => "Loopback", - Kind::Normal => "Measurement", - }; - - write!(f, "{}", text) - } -} - -#[allow(clippy::large_enum_variant)] -#[derive(Debug)] -pub enum LoadedKind { - Loopback(ui::Loopback), - Normal(ui::Measurement), -} - -pub async fn load_measurement( - path: impl AsRef, - kind: Kind, -) -> Result, Arc> { - match kind { - Kind::Loopback => Ok(LoadedKind::Loopback( - ui::measurement::Loopback::from_file(path).await, - )), - Kind::Normal => Ok(LoadedKind::Normal(ui::Measurement::from_file(path).await)), - } - .map(Arc::new) - .map_err(Arc::new) -} - -pub async fn pick_file_and_load_signal( - file_type: impl AsRef, - kind: Kind, -) -> Result, Arc> { - let handle = pick_file(file_type).await.unwrap(); - - load_measurement(handle.path(), kind).await -} - -pub async fn pick_file(file_type: impl AsRef) -> Result { - rfd::AsyncFileDialog::new() - .set_title(format!("Choose {} file", file_type.as_ref())) - .add_filter("wav", &["wav", "wave"]) - .add_filter("all", &["*"]) - .pick_file() - .await - .ok_or(Error::DialogClosed) -} - -#[derive(Debug, Clone, thiserror::Error)] -pub enum Error { - // #[error("error while loading file: {0}")] - // File(PathBuf, Arc), - #[error("dialog closed")] - DialogClosed, -} From dadc4753f308d0c766aa0dfcccc5228d4cd6f43c Mon Sep 17 00:00:00 2001 From: Hendrik Kunert Date: Thu, 29 Jan 2026 21:02:07 +0100 Subject: [PATCH 13/30] Add export of IRs --- raumklang-gui/src/screen/main.rs | 341 +++++++----------- .../src/screen/main/impulse_response.rs | 23 -- raumklang-gui/src/ui/analysis.rs | 22 +- raumklang-gui/src/ui/frequency_response.rs | 9 +- raumklang-gui/src/ui/impulse_response.rs | 129 +++++-- 5 files changed, 249 insertions(+), 275 deletions(-) diff --git a/raumklang-gui/src/screen/main.rs b/raumklang-gui/src/screen/main.rs index 63575eb..b46a388 100644 --- a/raumklang-gui/src/screen/main.rs +++ b/raumklang-gui/src/screen/main.rs @@ -11,10 +11,10 @@ use crate::{ }, icon, load_project, log, screen::main::{ - chart::waveform, impulse_response::processing_overlay, - spectral_decay_config::SpectralDecayConfig, spectrogram_config::SpectrogramConfig, + chart::waveform, spectral_decay_config::SpectralDecayConfig, + spectrogram_config::SpectrogramConfig, }, - ui::{self, analysis, Analysis, Loopback}, + ui::{self, analysis, measurement, Analysis, ImpulseResponse, Loopback, Measurement}, widget::sidebar, PickAndLoadError, }; @@ -44,16 +44,17 @@ use rfd::FileHandle; use std::{ collections::{BTreeMap, HashMap}, fmt::Display, + future::Future, path::{Path, PathBuf}, sync::Arc, }; pub struct Main { state: State, - selected: Option, + selected: Option, - loopback: Option, - measurements: ui::measurement::List, + loopback: Option, + measurements: measurement::List, project_path: Option, @@ -75,8 +76,8 @@ enum State { Collecting, Analysing { active_tab: Tab, - selected: Option, - analyses: BTreeMap, + selected: Option, + analyses: BTreeMap, }, } @@ -154,54 +155,55 @@ pub enum ModalAction { pub enum Message { NewProject, LoadProject, - SaveProject, - RecentProject(usize), ProjectLoaded(Result<(Arc, PathBuf), PickAndLoadError>), + SaveProject, ProjectSaved(Result), + LoadRecentProject(usize), + + LoadLoopback, + LoopbackLoaded(Loopback), + + LoadMeasurement, + MeasurementLoaded(Measurement), + + Measurement(measurement::Message), - TabSelected(TabId), OpenMeasurements, OpenImpulseResponses, OpenFrequencyResponses, - MeasurementChart(waveform::Interaction), - ImpulseResponseSelected(ui::measurement::Id), + ImpulseResponseSelected(measurement::Id), + ImpulseResponseComputed(measurement::Id, data::ImpulseResponse), - FrequencyResponseToggled(ui::measurement::Id, bool), - SmoothingChanged(frequency_response::Smoothing), - FrequencyResponseSmoothed((ui::measurement::Id, Box<[f32]>)), - FrequencyResponseComputed(ui::measurement::Id, data::FrequencyResponse), + FrequencyResponseComputed(measurement::Id, data::FrequencyResponse), + ImpulseResponseSaved(measurement::Id, Option>), - ImpulseResponses(impulse_response::Message), - SaveImpulseResponseFileDialog(ui::measurement::Id), - SaveImpulseResponse(ui::measurement::Id, Arc), - ImpulseResponsesSaved(Arc), + FrequencyResponseToggled(measurement::Id, bool), + SmoothingChanged(frequency_response::Smoothing), + FrequencyResponseSmoothed((measurement::Id, Box<[f32]>)), ShiftKeyPressed, ShiftKeyReleased, - ImpulseResponseComputed(ui::measurement::Id, data::ImpulseResponse), + MeasurementChart(waveform::Interaction), + ImpulseResponse(ui::measurement::Id, ui::impulse_response::Message), + SaveImpulseResponse(measurement::Id, Arc), + // SaveImpulseResponseFileDialog(measurement::Id), Recording(recording::Message), StartRecording(recording::Kind), - SpectralDecayComputed(ui::measurement::Id, data::SpectralDecay), + SpectralDecayComputed(measurement::Id, data::SpectralDecay), Spectrogram(chart::spectrogram::Interaction), - SpectrogramComputed(ui::measurement::Id, data::Spectrogram), + SpectrogramComputed(measurement::Id, data::Spectrogram), Modal(ModalAction), OpenSpectralDecayConfig, SpectralDecayConfig(spectral_decay_config::Message), OpenSpectrogramConfig, SpectrogramConfig(spectrogram_config::Message), - Measurement(ui::measurement::Message), - - LoadLoopback, - LoopbackLoaded(Loopback), - - LoadMeasurement, - MeasurementLoaded(ui::Measurement), + ImpulseResponseChart(impulse_response::Message), } #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -227,7 +229,7 @@ impl Main { let load_measurements = project.measurements.into_iter().map(|measurement| { Task::perform( - ui::Measurement::from_file(measurement.path), + Measurement::from_file(measurement.path), Message::MeasurementLoaded, ) }); @@ -243,14 +245,11 @@ impl Main { pub fn update(&mut self, recent_projects: &mut RecentProjects, msg: Message) -> Task { match msg { - Message::LoadLoopback => { - Task::future(pick_file("Load Loopback ...")).and_then(|path| { - Task::perform(ui::Loopback::from_file(path), Message::LoopbackLoaded) - }) - } + Message::LoadLoopback => Task::future(pick_file("Load Loopback ...")) + .and_then(|path| Task::perform(Loopback::from_file(path), Message::LoopbackLoaded)), Message::LoadMeasurement => { Task::future(pick_file("Load measurement ...")).and_then(|path| { - Task::perform(ui::Measurement::from_file(path), Message::MeasurementLoaded) + Task::perform(Measurement::from_file(path), Message::MeasurementLoaded) }) } Message::LoopbackLoaded(loopback) => { @@ -282,11 +281,11 @@ impl Main { } Message::Measurement(msg) => { match msg { - ui::measurement::Message::Select(selected) => { + measurement::Message::Select(selected) => { self.selected = Some(selected); self.signal_cache.clear(); } - ui::measurement::Message::Remove(id) => { + measurement::Message::Remove(id) => { self.measurements.remove(id); if self.measurements.loaded().next().is_none() { @@ -304,7 +303,7 @@ impl Main { Task::none() } - Message::ImpulseResponseSelected(id) => { + Message::ImpulseResponse(id, ui::impulse_response::Message::Select) => { let State::Analysing { active_tab: tab, selected, @@ -326,6 +325,46 @@ impl Main { Tab::Spectrogram => todo!(), } } + Message::ImpulseResponse(id, ui::impulse_response::Message::OpenSaveFileDialog) => { + Task::future(choose_impulse_response_file_path()) + .and_then(move |path| Task::done(Message::SaveImpulseResponse(id, path))) + } + Message::SaveImpulseResponse(id, path) => { + let State::Analysing { + ref mut analyses, .. + } = self.state + else { + return Task::none(); + }; + + // FIXME: ugly as hell + let ir = analyses + .get(&id) + .and_then(Analysis::impulse_response) + .cloned(); + + let ir_fut = self.impulse_response_future(id); + + Task::sip( + sipper(async move |mut progress| { + let ir = if let Some(ir) = ir { + ir + } else { + let ir = ir_fut?.await; + + progress.send(ir.clone()).await; + + ui::ImpulseResponse::from_data(ir) + }; + + save_impulse_response(path.clone(), ir).await; + + Some(path) + }), + Message::ImpulseResponseComputed.with(id), + Message::ImpulseResponseSaved.with(id), + ) + } Message::ImpulseResponseComputed(id, new_ir) => { let State::Analysing { selected, @@ -337,7 +376,7 @@ impl Main { return Task::none(); }; - let new_ir = ui::ImpulseResponse::from_data(new_ir); + let new_ir = ImpulseResponse::from_data(new_ir); let analysis = analyses.entry(id).or_default(); analysis.apply(analysis::Event::ImpulseResponseComputed(new_ir.clone())); @@ -428,11 +467,7 @@ impl Main { *tab = Tab::FrequencyResponses; // FIXME get rid of vec alloc - let ids: Vec<_> = self - .measurements - .loaded() - .map(ui::Measurement::id) - .collect(); + let ids: Vec<_> = self.measurements.loaded().map(Measurement::id).collect(); let tasks = ids .into_iter() @@ -1226,7 +1261,7 @@ impl Main { .filter_map(|(i, p)| p.to_str().map(|f| (i, f))) .map(|(i, s)| { button(s) - .on_press(Message::RecentProject(i)) + .on_press(Message::LoadRecentProject(i)) .style(button::subtle) .width(Length::Fill) .into() @@ -1490,7 +1525,7 @@ impl Main { .style(button::secondary), ) .push_entry_maybe(self.loopback.as_ref().map(|loopback| { - let active = self.selected == Some(ui::measurement::Selected::Loopback); + let active = self.selected == Some(measurement::Selected::Loopback); loopback.view(active).map(Message::Measurement) })); @@ -1506,8 +1541,8 @@ impl Main { .style(button::secondary), ) .extend_entries(self.measurements.iter().map(|measurement| { - let active = self.selected - == Some(ui::measurement::Selected::Measurement(measurement.id())); + let active = + self.selected == Some(measurement::Selected::Measurement(measurement.id())); measurement.view(active).map(Message::Measurement) })); @@ -1543,13 +1578,13 @@ impl Main { let content = if let Some(measurement) = self.selected.and_then(|selected| match selected { - ui::measurement::Selected::Loopback => self + measurement::Selected::Loopback => self .loopback .as_ref() .and_then(Loopback::loaded) .map(AsRef::as_ref), - ui::measurement::Selected::Measurement(id) => { - self.measurements.get(id).and_then(ui::Measurement::signal) + measurement::Selected::Measurement(id) => { + self.measurements.get(id).and_then(Measurement::signal) } }) { chart::waveform(measurement, &self.signal_cache, self.zoom, self.offset) @@ -1571,27 +1606,30 @@ impl Main { pub fn impulse_responses_tab<'a>( &'a self, - selected: Option, + selected: Option, chart: &'a impulse_response::Chart, window_settings: &'a WindowSettings, - analyses: &'a BTreeMap, + analyses: &'a BTreeMap, ) -> Element<'a, Message> { let sidebar = { let header = sidebar::header("Impulse Responses"); let entries = self.measurements.iter().flat_map(|measurement| { + let active = selected == Some(measurement.id()); let signal = measurement.signal()?; - let impulse_response = analyses + let progress = analyses .get(&measurement.id()) .map(Analysis::impulse_response_progress); - Some(impulse_response_item( - selected, - measurement.id(), + let entry = ui::impulse_response::view( &measurement.name, - signal, - impulse_response, - )) + signal.modified, + progress, + active, + ) + .map(Message::ImpulseResponse.with(measurement.id())); + + Some(entry) }); container(column![header, scrollable(column(entries))].spacing(6)) @@ -1612,7 +1650,7 @@ impl Main { .map_or(placeholder, |impulse_response| { chart .view(impulse_response, window_settings) - .map(Message::ImpulseResponses) + .map(Message::ImpulseResponseChart) }) }; @@ -1629,7 +1667,7 @@ impl Main { fn frequency_responses_tab<'a>( &'a self, chart_settings: &'a frequency_response::ChartData, - analyses: &'a BTreeMap, + analyses: &'a BTreeMap, ) -> Element<'a, Message> { let sidebar = { let header = sidebar::header("Frequency Responses"); @@ -1644,7 +1682,7 @@ impl Main { Message::FrequencyResponseToggled.with(measurement.id()), ); - Some(sidebar::item(content, false)) + Some(content) }); container(column![header, scrollable(column(entries).spacing(6))].spacing(6)) @@ -2005,7 +2043,33 @@ impl Main { // is_loopback_loaded && self.measurements.iter().any(ui::Measurement::is_loaded) // } - fn compute_impulse_response(&mut self, id: ui::measurement::Id) -> Task { + fn impulse_response_future( + &mut self, + id: measurement::Id, + ) -> Option> { + let State::Analysing { + ref mut analyses, .. + } = self.state + else { + return None; + }; + + let analysis = analyses.entry(id).or_default(); + + let Some(loopback) = self.loopback.as_ref().and_then(Loopback::loaded) else { + return None; + }; + + let measurement = self + .measurements + .get(id) + .and_then(Measurement::signal) + .unwrap(); + + analysis.compute_impulse_response(loopback.clone(), measurement.clone()) + } + + fn compute_impulse_response(&mut self, id: measurement::Id) -> Task { let State::Analysing { ref mut analyses, .. } = self.state @@ -2022,7 +2086,7 @@ impl Main { let measurement = self .measurements .get(id) - .and_then(ui::Measurement::signal) + .and_then(Measurement::signal) .unwrap(); analysis @@ -2031,7 +2095,7 @@ impl Main { .unwrap_or_default() } - fn compute_frequency_response(&mut self, id: ui::measurement::Id) -> Task { + fn compute_frequency_response(&mut self, id: measurement::Id) -> Task { let State::Analysing { ref mut analyses, .. } = self.state @@ -2048,59 +2112,6 @@ impl Main { } } -fn impulse_response_item<'a>( - selected: Option, - id: ui::measurement::Id, - name: &'a str, - signal: &'a raumklang_core::Measurement, - impulse_response: Option, -) -> Element<'a, Message> { - let is_active = selected.is_some_and(|selected| selected == id); - - let entry = { - let dt: DateTime = signal.modified.into(); - let ir_btn = button( - column![ - text(name).size(16).wrapping(Wrapping::WordOrGlyph), - text!("{}", dt.format("%x %X")).size(10) - ] - .clip(true) - .spacing(6), - ) - .on_press_with(move || Message::ImpulseResponseSelected(id)) - .width(Length::Fill) - .style(move |theme: &Theme, status| { - let background = theme.extended_palette().background; - let base = button::subtle(theme, status); - - if is_active { - base.with_background(background.weak.color) - } else { - base - } - }); - - let save_btn = button(icon::download().size(10)) - .style(button::secondary) - .on_press_with(move || Message::SaveImpulseResponseFileDialog(id)); - - let content = row![ - ir_btn, - rule::vertical(1.0), - right(save_btn).width(Length::Shrink).padding([0, 6]) - ]; - - sidebar::item(content, is_active) - }; - - match impulse_response { - Some(ui::impulse_response::Progress::Computing) => { - impulse_response::processing_overlay("Impulse Response", entry) - } - _ => entry, - } -} - impl Default for Main { fn default() -> Self { Self { @@ -2108,7 +2119,7 @@ impl Default for Main { selected: None, loopback: None, - measurements: ui::measurement::List::default(), + measurements: measurement::List::default(), project_path: None, @@ -2133,10 +2144,10 @@ async fn choose_impulse_response_file_path() -> Option> { .save_file() .await .as_ref() - .map(|h| h.path().to_path_buf().into()) + .map(|h| h.path().into()) } -async fn save_impulse_response(path: Arc, impulse_response: ui::ImpulseResponse) { +async fn save_impulse_response(path: Arc, impulse_response: ImpulseResponse) { tokio::task::spawn_blocking(move || { let spec = hound::WavSpec { channels: 1, @@ -2226,86 +2237,6 @@ async fn save_impulse_response(path: Arc, impulse_response: ui::ImpulseRes // } // } -impl TabId { - pub fn iter() -> impl Iterator { - [ - TabId::Measurements, - TabId::ImpulseResponses, - TabId::FrequencyResponses, - TabId::SpectralDecay, - TabId::Spectrogram, - ] - .into_iter() - } - - pub fn view<'a>(self, is_analysing: bool) -> Element<'a, Message> { - let mut row = row![]; - - for tab in TabId::iter() { - let is_active = self == tab; - - let is_enabled = match tab { - TabId::Measurements => true, - TabId::ImpulseResponses - | TabId::FrequencyResponses - | TabId::SpectralDecay - | TabId::Spectrogram => is_analysing, - }; - - let button = button(text(tab.to_string()).size(16)) - .padding(10) - .style(move |theme: &Theme, status| { - if is_active { - let palette = theme.extended_palette(); - - button::Style { - background: Some(palette.background.base.color.into()), - text_color: palette.background.base.text, - ..button::text(theme, status) - } - } else { - button::text(theme, status) - } - }) - .on_press_maybe(if is_enabled { - Some(Message::TabSelected(tab)) - } else { - None - }); - - row = row.push(button); - } - - row.spacing(5).align_y(Alignment::Center).into() - } -} - -impl Display for TabId { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let label = match self { - TabId::Measurements => "Measurements", - TabId::ImpulseResponses => "Impulse Responses", - TabId::FrequencyResponses => "Frequency Responses", - TabId::SpectralDecay => "Spectral Decay", - TabId::Spectrogram => "Spectorgram", - }; - - write!(f, "{}", label) - } -} - -impl From<&Tab> for TabId { - fn from(tab: &Tab) -> Self { - match tab { - Tab::Measurements { .. } => TabId::Measurements, - Tab::ImpulseResponses { .. } => TabId::ImpulseResponses, - Tab::FrequencyResponses => TabId::FrequencyResponses, - Tab::SpectralDecay => TabId::SpectralDecay, - Tab::Spectrogram => TabId::Spectrogram, - } - } -} - fn modal<'a, Message>( base: impl Into>, content: impl Into>, diff --git a/raumklang-gui/src/screen/main/impulse_response.rs b/raumklang-gui/src/screen/main/impulse_response.rs index 04503c0..2bcb56d 100644 --- a/raumklang-gui/src/screen/main/impulse_response.rs +++ b/raumklang-gui/src/screen/main/impulse_response.rs @@ -115,26 +115,3 @@ impl WindowSettings { Self { window } } } - -pub fn processing_overlay<'a, Message>( - status: &'a str, - entry: impl Into>, -) -> Element<'a, Message> -where - Message: 'a, -{ - stack([ - container(entry).style(container::bordered_box).into(), - container(column![text("Computing..."), text(status).size(12)]) - .center(Length::Fill) - .style(|theme| container::Style { - border: container::rounded_box(theme).border, - background: Some(iced::Background::Color(Color::from_rgba( - 0.0, 0.0, 0.0, 0.8, - ))), - ..Default::default() - }) - .into(), - ]) - .into() -} diff --git a/raumklang-gui/src/ui/analysis.rs b/raumklang-gui/src/ui/analysis.rs index 5b824cf..5a6ffe1 100644 --- a/raumklang-gui/src/ui/analysis.rs +++ b/raumklang-gui/src/ui/analysis.rs @@ -10,6 +10,17 @@ use crate::{ #[derive(Debug, Clone, Default)] pub struct Analysis(State); +#[derive(Debug, Clone, Default)] +enum State { + #[default] + None, + ImpulseResponseComputing(FrequencyResponse), + ImpulseResponse { + impulse_response: ImpulseResponse, + frequency_response: FrequencyResponse, + }, +} + pub enum Event { ImpulseResponseComputed(ImpulseResponse), FrequencyResponseComputed(data::FrequencyResponse), @@ -113,14 +124,3 @@ impl Analysis { )) } } - -#[derive(Debug, Clone, Default)] -enum State { - #[default] - None, - ImpulseResponseComputing(FrequencyResponse), - ImpulseResponse { - impulse_response: ImpulseResponse, - frequency_response: FrequencyResponse, - }, -} diff --git a/raumklang-gui/src/ui/frequency_response.rs b/raumklang-gui/src/ui/frequency_response.rs index 52399f4..a74ae2d 100644 --- a/raumklang-gui/src/ui/frequency_response.rs +++ b/raumklang-gui/src/ui/frequency_response.rs @@ -1,6 +1,7 @@ use std::fmt::{self, Display}; use crate::ui::impulse_response; +use crate::widget::sidebar; use crate::{data, icon}; use iced::widget::stack; @@ -48,7 +49,7 @@ impl FrequencyResponse { on_toggle: impl Fn(bool) -> Message + 'a, ) -> Element<'a, Message> where - Message: 'a, + Message: Clone + 'a, { let item = { let color_dot = icon::record().color(self.color).align_y(Alignment::Center); @@ -80,7 +81,7 @@ impl FrequencyResponse { .into() }; - match impulse_response_progess { + let content = match impulse_response_progess { impulse_response::Progress::None | impulse_response::Progress::Computing => { processing_overlay("Impulse Response", item) } @@ -90,7 +91,9 @@ impl FrequencyResponse { } Progress::Finished => item, }, - } + }; + + sidebar::item(content, false).into() } pub(crate) fn result(&self) -> Option<&data::FrequencyResponse> { diff --git a/raumklang-gui/src/ui/impulse_response.rs b/raumklang-gui/src/ui/impulse_response.rs index e041f40..8d020f2 100644 --- a/raumklang-gui/src/ui/impulse_response.rs +++ b/raumklang-gui/src/ui/impulse_response.rs @@ -1,40 +1,23 @@ -use crate::data::{self, SampleRate}; +use std::time::SystemTime; -#[derive(Debug, Clone, Default)] -pub enum State { - #[default] - None, - Computing, - Computed(ImpulseResponse), -} +use crate::{ + data::{self, SampleRate}, + icon, + ui::impulse_response, + widget::sidebar, +}; -impl State { - pub fn progress(&self) -> Progress { - match self { - State::None => Progress::None, - State::Computing => Progress::Computing, - State::Computed(_) => Progress::Finished, - } - } - - pub(crate) fn computed(&mut self, impulse_response: ImpulseResponse) { - *self = State::Computed(impulse_response) - } - - pub(crate) fn result(&self) -> Option<&ImpulseResponse> { - let State::Computed(ir) = self else { - return None; - }; - - Some(ir) - } -} +use chrono::{DateTime, Utc}; +use iced::{ + widget::{button, column, container, right, row, rule, stack, text, text::Wrapping}, + Color, Element, + Length::{Fill, Shrink}, +}; #[derive(Debug, Clone)] -pub enum Progress { - None, - Computing, - Finished, +pub enum Message { + Select, + OpenSaveFileDialog, } #[derive(Debug, Clone)] @@ -69,3 +52,83 @@ impl ImpulseResponse { } } } + +pub fn view<'a>( + name: &'a str, + date_time: SystemTime, + progress: Option, + active: bool, +) -> Element<'a, Message> { + let entry = { + let dt: DateTime = date_time.into(); + let ir_btn = button( + column![ + text(name).size(16).wrapping(Wrapping::WordOrGlyph), + text!("{}", dt.format("%x %X")).size(10) + ] + .clip(true) + .spacing(6), + ) + .on_press_with(move || Message::Select) + .width(Fill) + .style(move |theme, status| { + let base = button::subtle(theme, status); + let background = theme.extended_palette().background; + + if active { + base.with_background(background.weak.color) + } else { + base + } + }); + + let save_btn = button(icon::download().size(10)) + .style(button::secondary) + .on_press_with(move || Message::OpenSaveFileDialog); + + let content = row![ + ir_btn, + rule::vertical(1.0), + right(save_btn).width(Shrink).padding([0, 6]) + ]; + + sidebar::item(content, active) + }; + + match progress { + Some(impulse_response::Progress::Computing) => { + processing_overlay("Impulse Response", entry) + } + _ => entry, + } +} + +#[derive(Debug, Clone)] +pub enum Progress { + None, + Computing, + Finished, +} + +fn processing_overlay<'a, Message>( + status: &'a str, + entry: impl Into>, +) -> Element<'a, Message> +where + Message: 'a, +{ + stack([ + container(entry).style(container::bordered_box).into(), + container(column![text("Computing..."), text(status).size(12)]) + .center(Fill) + .style(|theme| container::Style { + border: container::rounded_box(theme).border, + background: Some(iced::Background::Color(Color::from_rgba( + 0.0, 0.0, 0.0, 0.8, + ))), + ..Default::default() + }) + .into(), + ]) + .into() +} From 88abc9fe63ec6108a35ed40ad83586852a4c90ed Mon Sep 17 00:00:00 2001 From: Hendrik Kunert Date: Thu, 29 Jan 2026 21:21:04 +0100 Subject: [PATCH 14/30] Enable toggler for FRs --- raumklang-gui/src/screen/main.rs | 27 ++++++++++++++++++++++++--- raumklang-gui/src/ui/analysis.rs | 10 ++++++++++ 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/raumklang-gui/src/screen/main.rs b/raumklang-gui/src/screen/main.rs index b46a388..52a48c4 100644 --- a/raumklang-gui/src/screen/main.rs +++ b/raumklang-gui/src/screen/main.rs @@ -422,6 +422,27 @@ impl Main { Task::none() } } + Message::FrequencyResponseToggled(id, state) => { + let State::Analysing { + ref mut analyses, .. + } = self.state + else { + return Task::none(); + }; + + let Some(fr) = analyses + .get_mut(&id) + .and_then(Analysis::frequency_response_mut) + else { + return Task::none(); + }; + + fr.is_shown = state; + + self.charts.frequency_responses.cache.clear(); + + Task::none() + } Message::OpenMeasurements => { let State::Analysing { active_tab: ref mut tab, @@ -1701,11 +1722,11 @@ impl Main { )] }; - let frequency_responses = analyses.values().map(Analysis::frequency_response); + let frequency_responses = analyses.values().flat_map(Analysis::frequency_response); - let content = if frequency_responses.clone().any(|o| o.is_some()) { + let content = if frequency_responses.clone().any(|fr| fr.is_shown) { let series_list = frequency_responses - .flatten() + .filter(|fr| fr.is_shown) .flat_map(|item| { let Some(frequency_response) = item.result() else { return [None, None]; diff --git a/raumklang-gui/src/ui/analysis.rs b/raumklang-gui/src/ui/analysis.rs index 5a6ffe1..1cf2207 100644 --- a/raumklang-gui/src/ui/analysis.rs +++ b/raumklang-gui/src/ui/analysis.rs @@ -76,6 +76,16 @@ impl Analysis { } } + pub(crate) fn frequency_response_mut(&mut self) -> Option<&mut FrequencyResponse> { + match &mut self.0 { + State::None => None, + State::ImpulseResponseComputing(frequency_response) + | State::ImpulseResponse { + frequency_response, .. + } => Some(frequency_response), + } + } + pub(crate) fn impulse_response_progress(&self) -> impulse_response::Progress { match self.0 { State::None => impulse_response::Progress::None, From be12289d29eebb990e0e3f06ef751a6400d86faf Mon Sep 17 00:00:00 2001 From: Hendrik Kunert Date: Thu, 29 Jan 2026 22:29:54 +0100 Subject: [PATCH 15/30] Refactor FR smoothing and IR compuation --- raumklang-gui/src/data/impulse_response.rs | 136 ++++++- raumklang-gui/src/screen/main.rs | 338 ++++++++++++------ .../src/screen/main/frequency_response.rs | 5 +- raumklang-gui/src/ui/analysis.rs | 210 +++++------ raumklang-gui/src/ui/frequency_response.rs | 4 +- raumklang-gui/src/ui/impulse_response.rs | 85 +++-- 6 files changed, 510 insertions(+), 268 deletions(-) diff --git a/raumklang-gui/src/data/impulse_response.rs b/raumklang-gui/src/data/impulse_response.rs index 8fe26a2..cbab517 100644 --- a/raumklang-gui/src/data/impulse_response.rs +++ b/raumklang-gui/src/data/impulse_response.rs @@ -1,26 +1,128 @@ +use std::{future::Future, path::Path, pin::Pin, sync::Arc}; + +use iced::{ + futures::{future::Shared, FutureExt}, + task::{sipper, Sipper}, +}; + +#[derive(Debug, Clone, Default)] +pub struct ImpulseResponse(State); + #[derive(Debug, Clone)] -pub struct ImpulseResponse { - pub origin: raumklang_core::ImpulseResponse, + +pub enum Event { + Started, +} + +#[derive(Debug, Clone, Default)] +enum State { + #[default] + None, + Computing(Shared + Send>>>), + Computed(Arc), } impl ImpulseResponse { - pub fn from_data(impulse_response: raumklang_core::ImpulseResponse) -> Self { - Self { - origin: impulse_response, + pub fn compute( + self, + loopback: &raumklang_core::Loopback, + measurement: &raumklang_core::Measurement, + ) -> Option> { + if let State::Computing(_) = self.0 { + return None; + } + + if let State::Computed(_) = self.0 { + return None; } + + let loopback = loopback.clone(); + let measurement = measurement.clone(); + + let sipper = sipper(async move |mut progress| { + let computation = async { + let impulse_response = tokio::task::spawn_blocking(move || { + raumklang_core::ImpulseResponse::from_signals(&loopback, &measurement) + }) + .await + .unwrap() + .unwrap(); + + ImpulseResponse(State::Computed(Arc::new(impulse_response))) + } + .boxed(); + let shared = computation.shared(); + progress + .send(ImpulseResponse(State::Computing(shared.clone()))) + .await; + + shared.await + }); + + Some(sipper) + } + + pub fn save( + self, + path: Arc, + loopback: &raumklang_core::Loopback, + measurement: &raumklang_core::Measurement, + ) -> impl Sipper>, Self> { + let fut = self.compute(loopback, measurement).unwrap(); + + sipper(async move |mut progress| { + let ir = fut.run(&progress).await; + + progress.send(ir.clone()).await; + // tokio::task::spawn_blocking(move || { + // let spec = hound::WavSpec { + // channels: 1, + // sample_rate: ir.sample_rate, + // bits_per_sample: 32, + // sample_format: hound::SampleFormat::Float, + // }; + + // let mut writer = hound::WavWriter::create(path, spec).unwrap(); + // for s in ir.data { + // writer.write_sample(s).unwrap(); + // } + // writer.finalize().unwrap(); + // }) + // .await + // .unwrap(); + + Some(path) + }) + } + + pub fn inner(&self) -> Option<&raumklang_core::ImpulseResponse> { + match self.0 { + State::None => None, + State::Computing(_) => None, + State::Computed(ref impulse_response) => Some(impulse_response), + } + } + + pub fn progress(&self) -> Progress { + match self.0 { + State::None => Progress::None, + State::Computing(_) => Progress::Computing, + State::Computed(_) => Progress::Computed, + } + } + + pub fn into_inner(self) -> Option> { + let State::Computed(inner) = self.0 else { + return None; + }; + + Some(inner) } } -pub async fn compute( - loopback: raumklang_core::Loopback, - measurement: raumklang_core::Measurement, -) -> ImpulseResponse { - let impulse_response = tokio::task::spawn_blocking(move || { - raumklang_core::ImpulseResponse::from_signals(&loopback, &measurement) - }) - .await - .unwrap() - .unwrap(); - - ImpulseResponse::from_data(impulse_response) +#[derive(Debug, Clone)] +pub enum Progress { + None, + Computing, + Computed, } diff --git a/raumklang-gui/src/screen/main.rs b/raumklang-gui/src/screen/main.rs index 52a48c4..5e545b1 100644 --- a/raumklang-gui/src/screen/main.rs +++ b/raumklang-gui/src/screen/main.rs @@ -14,7 +14,7 @@ use crate::{ chart::waveform, spectral_decay_config::SpectralDecayConfig, spectrogram_config::SpectrogramConfig, }, - ui::{self, analysis, measurement, Analysis, ImpulseResponse, Loopback, Measurement}, + ui::{self, analysis, measurement, Analysis, Loopback, Measurement}, widget::sidebar, PickAndLoadError, }; @@ -27,13 +27,14 @@ use chrono::{DateTime, Utc}; use generic_overlay::generic_overlay::{dropdown_menu, dropdown_root}; use iced::{ + advanced::Renderer, alignment::{Horizontal, Vertical}, futures::{FutureExt, TryFutureExt}, keyboard, padding, - task::sipper, + task::{sipper, Sipper}, widget::{ button, canvas, center, column, container, opaque, pick_list, right, row, rule, scrollable, - space, stack, text, text::Wrapping, Button, + space, stack, text, Button, }, Alignment::{self, Center}, Color, Element, Function, Length, Subscription, Task, Theme, @@ -179,8 +180,8 @@ pub enum Message { ImpulseResponseSaved(measurement::Id, Option>), FrequencyResponseToggled(measurement::Id, bool), - SmoothingChanged(frequency_response::Smoothing), - FrequencyResponseSmoothed((measurement::Id, Box<[f32]>)), + ChangeSmoothing(frequency_response::Smoothing), + FrequencyResponseSmoothed(measurement::Id, Box<[f32]>), ShiftKeyPressed, ShiftKeyReleased, @@ -315,21 +316,22 @@ impl Main { }; *selected = Some(id); - analyses.entry(id).or_default(); + self.charts.impulse_responses.data_cache.clear(); match tab { Tab::Measurements { .. } => Task::none(), - Tab::ImpulseResponses { .. } => self.compute_impulse_response(id), + Tab::ImpulseResponses { .. } => compute_impulse_response( + analyses, + id, + self.loopback.as_ref(), + &self.measurements, + ), Tab::FrequencyResponses => Task::none(), Tab::SpectralDecay => todo!(), Tab::Spectrogram => todo!(), } } - Message::ImpulseResponse(id, ui::impulse_response::Message::OpenSaveFileDialog) => { - Task::future(choose_impulse_response_file_path()) - .and_then(move |path| Task::done(Message::SaveImpulseResponse(id, path))) - } - Message::SaveImpulseResponse(id, path) => { + Message::ImpulseResponse(id, ui::impulse_response::Message::Save) => { let State::Analysing { ref mut analyses, .. } = self.state @@ -337,25 +339,25 @@ impl Main { return Task::none(); }; - // FIXME: ugly as hell - let ir = analyses - .get(&id) - .and_then(Analysis::impulse_response) - .cloned(); + let loopback = self.loopback.as_ref().and_then(Loopback::loaded).unwrap(); + let measurement = self + .measurements + .get(id) + .and_then(Measurement::signal) + .unwrap(); - let ir_fut = self.impulse_response_future(id); + let analysis = analyses.entry(id).or_default(); + let fut = analysis.impulse_response.compute(loopback, measurement); + dbg!(fut.is_some()); Task::sip( sipper(async move |mut progress| { - let ir = if let Some(ir) = ir { - ir - } else { - let ir = ir_fut?.await; + let path = choose_impulse_response_file_path().await?; - progress.send(ir.clone()).await; + let ir = fut?.run(&progress).await; + progress.send(ir.clone()).await; - ui::ImpulseResponse::from_data(ir) - }; + let ir = ui::ImpulseResponse::from_data(&ir)?; save_impulse_response(path.clone(), ir).await; @@ -365,7 +367,13 @@ impl Main { Message::ImpulseResponseSaved.with(id), ) } - Message::ImpulseResponseComputed(id, new_ir) => { + Message::SaveImpulseResponse(id, path) => Task::none(), + Message::ImpulseResponseSaved(id, path) => { + eprintln!("IR (#{:?}) saved to: {:?}", id, path); + + Task::none() + } + Message::ImpulseResponseComputed(id, impulse_response) => { let State::Analysing { selected, ref active_tab, @@ -376,24 +384,33 @@ impl Main { return Task::none(); }; - let new_ir = ImpulseResponse::from_data(new_ir); + // let new_ir = ImpulseResponse::from_data(new_ir); + log::debug!("IR progress: {:?}", impulse_response.progress()); let analysis = analyses.entry(id).or_default(); - analysis.apply(analysis::Event::ImpulseResponseComputed(new_ir.clone())); + analysis.impulse_response = + ui::impulse_response::State::from_data(impulse_response); + // analysis.apply(analysis::Event::ImpulseResponseComputed(new_ir.clone())); - if selected.is_some_and(|selected| selected == id) { - self.charts - .impulse_responses - .x_range - .get_or_insert(0.0..=new_ir.data.len() as f32); + // if selected.is_some_and(|selected| selected == id) { + // self.charts + // .impulse_responses + // .x_range + // .get_or_insert(0.0..=new_ir.data.len() as f32); - self.charts.impulse_responses.data_cache.clear(); - } + // self.charts.impulse_responses.data_cache.clear(); + // } match active_tab { Tab::Measurements { .. } => Task::none(), Tab::ImpulseResponses { .. } => Task::none(), - Tab::FrequencyResponses => self.compute_frequency_response(id), + Tab::FrequencyResponses => compute_frequency_response( + analyses, + id, + self.loopback.as_ref(), + &self.measurements, + self.window.as_ref().cloned().unwrap(), + ), Tab::SpectralDecay => todo!(), Tab::Spectrogram => todo!(), } @@ -409,14 +426,14 @@ impl Main { }; let analysis = analyses.entry(id).or_default(); - analysis.apply(analysis::Event::FrequencyResponseComputed(new_fr.clone())); + // analysis.apply(analysis::Event::FrequencyResponseComputed(new_fr.clone())); self.charts.frequency_responses.cache.clear(); if let Some(fraction) = self.smoothing.fraction() { Task::perform( - frequency_response::smooth_frequency_response(id, new_fr, fraction), - Message::FrequencyResponseSmoothed, + frequency_response::smooth_frequency_response(new_fr, fraction), + Message::FrequencyResponseSmoothed.with(id), ) } else { Task::none() @@ -443,6 +460,65 @@ impl Main { Task::none() } + Message::ChangeSmoothing(smoothing) => { + let State::Analysing { + ref mut analyses, .. + } = self.state + else { + return Task::none(); + }; + + self.smoothing = smoothing; + + if let Some(fraction) = smoothing.fraction() { + let tasks = analyses + .iter() + .flat_map(|(id, analysis)| { + let fr = analysis.frequency_response()?; + // TODO: refactor FR model + let data = fr.data.as_ref()?; + + Some((id, data.clone())) + }) + .map(|(id, fr)| { + Task::perform( + frequency_response::smooth_frequency_response(fr, fraction), + Message::FrequencyResponseSmoothed.with(*id), + ) + }); + + Task::batch(tasks) + } else { + analyses + .values_mut() + .flat_map(Analysis::frequency_response_mut) + .for_each(|fr| fr.smoothed = None); + + self.charts.frequency_responses.cache.clear(); + + Task::none() + } + } + Message::FrequencyResponseSmoothed(id, smoothed) => { + let State::Analysing { + ref mut analyses, .. + } = self.state + else { + return Task::none(); + }; + + let Some(fr) = analyses + .get_mut(&id) + .and_then(Analysis::frequency_response_mut) + else { + return Task::none(); + }; + + fr.smoothed = Some(smoothed); + self.charts.frequency_responses.cache.clear(); + + Task::none() + } Message::OpenMeasurements => { let State::Analysing { active_tab: ref mut tab, @@ -478,21 +554,31 @@ impl Main { Message::OpenFrequencyResponses => { let State::Analysing { - active_tab: ref mut tab, + ref mut active_tab, + ref mut analyses, .. } = self.state else { return Task::none(); }; - *tab = Tab::FrequencyResponses; + *active_tab = Tab::FrequencyResponses; // FIXME get rid of vec alloc - let ids: Vec<_> = self.measurements.loaded().map(Measurement::id).collect(); + // let ids: Vec<_> = self.measurements.loaded().map(Measurement::id).collect(); - let tasks = ids - .into_iter() - .map(|id| self.compute_frequency_response(id)); + // let tasks = ids + // .into_iter() + // .map(|id| self.compute_frequency_response(id)); + let tasks = self.measurements.loaded().map(Measurement::id).map(|id| { + compute_frequency_response( + analyses, + id, + self.loopback.as_ref(), + &self.measurements, + self.window.as_ref().cloned().unwrap(), + ) + }); Task::batch(tasks) } @@ -1640,7 +1726,7 @@ impl Main { let signal = measurement.signal()?; let progress = analyses .get(&measurement.id()) - .map(Analysis::impulse_response_progress); + .map(|a| a.impulse_response.progress()); let entry = ui::impulse_response::view( &measurement.name, @@ -1662,17 +1748,18 @@ impl Main { }; let content = { - let placeholder = center(text("Impulse response not computed, yet.")).into(); + let placeholder = center(text("Impulse response not computed, yet.")); selected .as_ref() .and_then(|id| analyses.get(id)) .and_then(Analysis::impulse_response) - .map_or(placeholder, |impulse_response| { + .map(|impulse_response| { chart .view(impulse_response, window_settings) .map(Message::ImpulseResponseChart) }) + .unwrap_or(placeholder.into()) }; row![ @@ -1699,7 +1786,7 @@ impl Main { let content = frequency_response.view( &measurement.name, - analysis.impulse_response_progress(), + analysis.impulse_response.progress(), Message::FrequencyResponseToggled.with(measurement.id()), ); @@ -1718,7 +1805,7 @@ impl Main { row![pick_list( frequency_response::Smoothing::ALL, Some(&self.smoothing), - Message::SmoothingChanged, + Message::ChangeSmoothing, )] }; @@ -2064,73 +2151,93 @@ impl Main { // is_loopback_loaded && self.measurements.iter().any(ui::Measurement::is_loaded) // } - fn impulse_response_future( - &mut self, - id: measurement::Id, - ) -> Option> { - let State::Analysing { - ref mut analyses, .. - } = self.state - else { - return None; - }; - - let analysis = analyses.entry(id).or_default(); - - let Some(loopback) = self.loopback.as_ref().and_then(Loopback::loaded) else { - return None; - }; - - let measurement = self - .measurements - .get(id) - .and_then(Measurement::signal) - .unwrap(); - - analysis.compute_impulse_response(loopback.clone(), measurement.clone()) - } - - fn compute_impulse_response(&mut self, id: measurement::Id) -> Task { - let State::Analysing { - ref mut analyses, .. - } = self.state - else { - return Task::none(); - }; - - let analysis = analyses.entry(id).or_default(); + // fn impulse_response_future( + // &mut self, + // id: measurement::Id, + // ) -> Option> { + // let State::Analysing { + // ref mut analyses, .. + // } = self.state + // else { + // return None; + // }; - let Some(loopback) = self.loopback.as_ref().and_then(Loopback::loaded) else { - return Task::none(); - }; + // let analysis = analyses.entry(id).or_default(); - let measurement = self - .measurements - .get(id) - .and_then(Measurement::signal) - .unwrap(); + // let Some(loopback) = self.loopback.as_ref().and_then(Loopback::loaded) else { + // return None; + // }; - analysis - .compute_impulse_response(loopback.clone(), measurement.clone()) - .map(|f| Task::perform(f, Message::ImpulseResponseComputed.with(id))) - .unwrap_or_default() - } + // let measurement = self + // .measurements + // .get(id) + // .and_then(Measurement::signal) + // .unwrap(); - fn compute_frequency_response(&mut self, id: measurement::Id) -> Task { - let State::Analysing { - ref mut analyses, .. - } = self.state - else { - return Task::none(); - }; + // analysis.compute_impulse_response(loopback.clone(), measurement.clone()) + // } +} - let analysis = analyses.entry(id).or_default(); +fn compute_frequency_response( + analyses: &mut BTreeMap, + id: measurement::Id, + loopback: Option<&Loopback>, + measurements: &measurement::List, + window: data::Window, +) -> Task { + // let Some(loopback) = loopback.and_then(Loopback::loaded).cloned() else { + // return Task::none(); + // }; + + // let Some(measurement) = measurements.get(id).and_then(Measurement::signal).cloned() else { + // return Task::none(); + // }; + + // let analysis = analyses.entry(id).or_default(); + + // Task::sip( + // analysis + // .impulse_response + // .clone() + // .compute(loopback, measurement), + // Message::ImpulseResponseComputed.with(id), + // Message::ImpulseResponseComputed.with(id), + // ) + // analysis + // .compute_frequency_response(window) + // .map(|f| Task::perform(f, Message::FrequencyResponseComputed.with(id))) + // .unwrap_or_else(|| compute_impulse_response(analyses, id, loopback, measurements)) + Task::none() +} - analysis - .compute_frequency_response(self.window.as_ref().cloned().unwrap()) - .map(|f| Task::perform(f, Message::FrequencyResponseComputed.with(id))) - .unwrap_or_else(|| self.compute_impulse_response(id)) - } +fn compute_impulse_response( + analyses: &mut BTreeMap, + id: measurement::Id, + loopback: Option<&Loopback>, + measurements: &measurement::List, +) -> Task { + let Some(loopback) = loopback.and_then(Loopback::loaded) else { + return Task::none(); + }; + + let Some(measurement) = measurements.get(id).and_then(Measurement::signal) else { + return Task::none(); + }; + + let analysis = analyses.entry(id).or_default(); + + analysis + .impulse_response + .clone() + .compute(loopback, measurement) + .map(|sipper| { + Task::sip( + sipper, + Message::ImpulseResponseComputed.with(id), + Message::ImpulseResponseComputed.with(id), + ) + }) + .unwrap_or_default() } impl Default for Main { @@ -2160,6 +2267,8 @@ impl Default for Main { async fn choose_impulse_response_file_path() -> Option> { rfd::AsyncFileDialog::new() .set_title("Save Impulse Response ...") + // FIXME: remove + .set_file_name("test.wave") .add_filter("wav", &["wav", "wave"]) .add_filter("all", &["*"]) .save_file() @@ -2168,17 +2277,18 @@ async fn choose_impulse_response_file_path() -> Option> { .map(|h| h.path().into()) } -async fn save_impulse_response(path: Arc, impulse_response: ImpulseResponse) { +// TODO: error handling +async fn save_impulse_response(path: Arc, ir: ui::ImpulseResponse) { tokio::task::spawn_blocking(move || { let spec = hound::WavSpec { channels: 1, - sample_rate: impulse_response.sample_rate.into(), + sample_rate: ir.sample_rate.into(), bits_per_sample: 32, sample_format: hound::SampleFormat::Float, }; let mut writer = hound::WavWriter::create(path, spec).unwrap(); - for s in impulse_response.data.iter().copied() { + for s in ir.data { writer.write_sample(s).unwrap(); } writer.finalize().unwrap(); diff --git a/raumklang-gui/src/screen/main/frequency_response.rs b/raumklang-gui/src/screen/main/frequency_response.rs index 1770206..9c61c6a 100644 --- a/raumklang-gui/src/screen/main/frequency_response.rs +++ b/raumklang-gui/src/screen/main/frequency_response.rs @@ -75,10 +75,9 @@ impl fmt::Display for Smoothing { } pub async fn smooth_frequency_response( - id: ui::measurement::Id, frequency_response: data::FrequencyResponse, fraction: u8, -) -> (ui::measurement::Id, Box<[f32]>) { +) -> Box<[f32]> { let data = tokio::task::spawn_blocking(move || { data::smooth_fractional_octave(&frequency_response.data.clone(), fraction) }) @@ -86,5 +85,5 @@ pub async fn smooth_frequency_response( .unwrap() .into_boxed_slice(); - (id, data) + data } diff --git a/raumklang-gui/src/ui/analysis.rs b/raumklang-gui/src/ui/analysis.rs index 1cf2207..eddea28 100644 --- a/raumklang-gui/src/ui/analysis.rs +++ b/raumklang-gui/src/ui/analysis.rs @@ -8,129 +8,117 @@ use crate::{ }; #[derive(Debug, Clone, Default)] -pub struct Analysis(State); - -#[derive(Debug, Clone, Default)] -enum State { - #[default] - None, - ImpulseResponseComputing(FrequencyResponse), - ImpulseResponse { - impulse_response: ImpulseResponse, - frequency_response: FrequencyResponse, - }, +pub struct Analysis { + pub impulse_response: impulse_response::State, } -pub enum Event { - ImpulseResponseComputed(ImpulseResponse), - FrequencyResponseComputed(data::FrequencyResponse), -} +// #[derive(Debug, Clone, Default)] +// enum State { +// #[default] +// None, +// ImpulseResponseComputing(FrequencyResponse), +// ImpulseResponse { +// impulse_response: ImpulseResponse, +// frequency_response: FrequencyResponse, +// }, +// } + +// pub enum Event { +// ImpulseResponseComputed(ImpulseResponse), +// FrequencyResponseComputed(data::FrequencyResponse), +// } impl Analysis { - pub(crate) fn apply(&mut self, event: Event) { - match event { - Event::ImpulseResponseComputed(impulse_response) => { - let State::ImpulseResponseComputing(frequency_response) = mem::take(&mut self.0) - else { - return; - }; - - self.0 = State::ImpulseResponse { - impulse_response: impulse_response, - frequency_response, - } - } - Event::FrequencyResponseComputed(fr) => { - let State::ImpulseResponse { - ref mut frequency_response, - .. - } = self.0 - else { - return; - }; - - frequency_response.computed(fr); - } - } - } + // pub(crate) fn apply(&mut self, event: Event) { + // match event { + // Event::ImpulseResponseComputed(impulse_response) => { + // let State::ImpulseResponseComputing(frequency_response) = mem::take(&mut self.0) + // else { + // return; + // }; + + // self.0 = State::ImpulseResponse { + // impulse_response: impulse_response, + // frequency_response, + // } + // } + // Event::FrequencyResponseComputed(fr) => { + // let State::ImpulseResponse { + // ref mut frequency_response, + // .. + // } = self.0 + // else { + // return; + // }; + + // frequency_response.computed(fr); + // } + // } + // } pub(crate) fn impulse_response(&self) -> Option<&ImpulseResponse> { - let State::ImpulseResponse { - ref impulse_response, - .. - } = self.0 - else { - return None; - }; - - Some(impulse_response) + self.impulse_response.inner() } pub(crate) fn frequency_response(&self) -> Option<&FrequencyResponse> { - match &self.0 { - State::None => None, - State::ImpulseResponseComputing(frequency_response) - | State::ImpulseResponse { - frequency_response, .. - } => Some(frequency_response), - } + None + // match &self.0 { + // State::None => None, + // State::ImpulseResponseComputing(frequency_response) + // | State::ImpulseResponse { + // frequency_response, .. + // } => Some(frequency_response), + // } } pub(crate) fn frequency_response_mut(&mut self) -> Option<&mut FrequencyResponse> { - match &mut self.0 { - State::None => None, - State::ImpulseResponseComputing(frequency_response) - | State::ImpulseResponse { - frequency_response, .. - } => Some(frequency_response), - } + None + // match &mut self.0 { + // State::None => None, + // State::ImpulseResponseComputing(frequency_response) + // | State::ImpulseResponse { + // frequency_response, .. + // } => Some(frequency_response), + // } } - pub(crate) fn impulse_response_progress(&self) -> impulse_response::Progress { - match self.0 { - State::None => impulse_response::Progress::None, - State::ImpulseResponseComputing(_) => impulse_response::Progress::Computing, - State::ImpulseResponse { .. } => impulse_response::Progress::Finished, - } - } - - pub(crate) fn compute_impulse_response( - &mut self, - loopback: Loopback, - measurement: Measurement, - ) -> Option> { - match self.0 { - State::ImpulseResponse { .. } => None, - State::ImpulseResponseComputing(_) => None, - State::None => { - self.0 = State::ImpulseResponseComputing(FrequencyResponse::new()); - - Some(data::impulse_response::compute(loopback, measurement)) - } - } - } - - pub(crate) fn compute_frequency_response( - &mut self, - window: data::Window, - ) -> Option> { - let State::ImpulseResponse { - ref impulse_response, - ref mut frequency_response, - } = self.0 - else { - return None; - }; - - if let frequency_response::Progress::Finished = frequency_response.progress { - return None; - } - - frequency_response.progress = frequency_response::Progress::Computing; - - Some(data::frequency_response::compute( - impulse_response.origin.clone(), - window, - )) - } + // pub(crate) fn compute_impulse_response( + // &mut self, + // loopback: Loopback, + // measurement: Measurement, + // ) -> Option> { + // match self.0 { + // State::ImpulseResponse { .. } => None, + // State::ImpulseResponseComputing(_) => None, + // State::None => { + // self.0 = State::ImpulseResponseComputing(FrequencyResponse::new()); + + // Some(data::impulse_response::compute(loopback, measurement)) + // } + // } + // } + + // pub(crate) fn compute_frequency_response( + // &mut self, + // window: data::Window, + // ) -> Option> { + // let State::ImpulseResponse { + // ref impulse_response, + // ref mut frequency_response, + // } = self.0 + // else { + // return None; + // }; + + // if let frequency_response::Progress::Finished = frequency_response.progress { + // return None; + // } + + // frequency_response.progress = frequency_response::Progress::Computing; + + // Some(data::frequency_response::compute( + // impulse_response.origin.clone(), + // window, + // )) + // } } diff --git a/raumklang-gui/src/ui/frequency_response.rs b/raumklang-gui/src/ui/frequency_response.rs index a74ae2d..07c7974 100644 --- a/raumklang-gui/src/ui/frequency_response.rs +++ b/raumklang-gui/src/ui/frequency_response.rs @@ -1,8 +1,8 @@ use std::fmt::{self, Display}; -use crate::ui::impulse_response; use crate::widget::sidebar; use crate::{data, icon}; +use data::impulse_response; use iced::widget::stack; use iced::widget::text::IntoFragment; @@ -85,7 +85,7 @@ impl FrequencyResponse { impulse_response::Progress::None | impulse_response::Progress::Computing => { processing_overlay("Impulse Response", item) } - impulse_response::Progress::Finished => match self.progress { + impulse_response::Progress::Computed => match self.progress { Progress::None | Progress::Computing => { processing_overlay(self.progress.to_string(), item) } diff --git a/raumklang-gui/src/ui/impulse_response.rs b/raumklang-gui/src/ui/impulse_response.rs index 8d020f2..6c9bfb9 100644 --- a/raumklang-gui/src/ui/impulse_response.rs +++ b/raumklang-gui/src/ui/impulse_response.rs @@ -1,36 +1,87 @@ -use std::time::SystemTime; +use std::{sync::Arc, time::SystemTime}; use crate::{ + data::impulse_response, data::{self, SampleRate}, icon, - ui::impulse_response, widget::sidebar, }; use chrono::{DateTime, Utc}; use iced::{ - widget::{button, column, container, right, row, rule, stack, text, text::Wrapping}, + task::Sipper, + widget::{button, column, container, right, row, rule, stack, text}, Color, Element, Length::{Fill, Shrink}, + Task, }; #[derive(Debug, Clone)] pub enum Message { Select, - OpenSaveFileDialog, + Save, } #[derive(Debug, Clone)] pub struct ImpulseResponse { pub sample_rate: SampleRate, pub data: Vec, - pub origin: raumklang_core::ImpulseResponse, + pub origin: data::ImpulseResponse, +} + +#[derive(Debug, Clone)] +pub enum State { + Computing(data::ImpulseResponse), + Computed(ImpulseResponse), +} + +impl State { + pub(crate) fn from_data(impulse_response: data::ImpulseResponse) -> State { + match ImpulseResponse::from_data(&impulse_response) { + Some(ir) => State::Computed(ir), + None => State::Computing(impulse_response), + } + } + + pub(crate) fn progress(&self) -> impulse_response::Progress { + match self { + State::Computing(ir) => ir.progress(), + State::Computed(_) => impulse_response::Progress::Computed, + } + } + + pub(crate) fn inner(&self) -> Option<&ImpulseResponse> { + match self { + State::Computing(_) => None, + State::Computed(ref impulse_response) => Some(impulse_response), + } + } + + pub(crate) fn compute( + &self, + loopback: &raumklang_core::Loopback, + measurement: &raumklang_core::Measurement, + ) -> Option> { + match self { + State::Computing(ref impulse_response) => { + impulse_response.clone().compute(loopback, measurement) + } + State::Computed(_) => None, + } + } +} + +impl Default for State { + fn default() -> Self { + Self::Computing(data::ImpulseResponse::default()) + } } impl ImpulseResponse { - pub fn from_data(impulse_response: data::ImpulseResponse) -> Self { + pub fn from_data(data: &data::ImpulseResponse) -> Option { + let impulse_response = data.inner()?; + let max = impulse_response - .origin .data .iter() .map(|s| s.re.abs()) @@ -38,18 +89,17 @@ impl ImpulseResponse { .unwrap(); let normalized = impulse_response - .origin .data .iter() .map(|s| s.re) .map(|s| s / max.abs()) .collect(); - Self { - sample_rate: SampleRate::new(impulse_response.origin.sample_rate), + Some(Self { + sample_rate: SampleRate::new(impulse_response.sample_rate), data: normalized, - origin: impulse_response.origin, - } + origin: data.clone(), + }) } } @@ -63,7 +113,7 @@ pub fn view<'a>( let dt: DateTime = date_time.into(); let ir_btn = button( column![ - text(name).size(16).wrapping(Wrapping::WordOrGlyph), + text(name).size(16).wrapping(text::Wrapping::WordOrGlyph), text!("{}", dt.format("%x %X")).size(10) ] .clip(true) @@ -84,7 +134,7 @@ pub fn view<'a>( let save_btn = button(icon::download().size(10)) .style(button::secondary) - .on_press_with(move || Message::OpenSaveFileDialog); + .on_press_with(move || Message::Save); let content = row![ ir_btn, @@ -103,13 +153,6 @@ pub fn view<'a>( } } -#[derive(Debug, Clone)] -pub enum Progress { - None, - Computing, - Finished, -} - fn processing_overlay<'a, Message>( status: &'a str, entry: impl Into>, From 0e66079fe5b307a297ca7971b9d09bc77c0e1ca7 Mon Sep 17 00:00:00 2001 From: Hendrik Kunert Date: Sat, 31 Jan 2026 12:26:12 +0100 Subject: [PATCH 16/30] Refactor FR computation and IR saving --- raumklang-gui/src/data/impulse_response.rs | 79 ++-------- raumklang-gui/src/screen/main.rs | 162 +++++++++------------ raumklang-gui/src/screen/main/chart.rs | 2 +- raumklang-gui/src/ui/analysis.rs | 3 +- raumklang-gui/src/ui/frequency_response.rs | 49 ++++--- raumklang-gui/src/ui/impulse_response.rs | 35 +++-- 6 files changed, 134 insertions(+), 196 deletions(-) diff --git a/raumklang-gui/src/data/impulse_response.rs b/raumklang-gui/src/data/impulse_response.rs index cbab517..a1e5cf9 100644 --- a/raumklang-gui/src/data/impulse_response.rs +++ b/raumklang-gui/src/data/impulse_response.rs @@ -1,24 +1,15 @@ -use std::{future::Future, path::Path, pin::Pin, sync::Arc}; +use std::sync::Arc; -use iced::{ - futures::{future::Shared, FutureExt}, - task::{sipper, Sipper}, -}; +use iced::task::{sipper, Sipper}; #[derive(Debug, Clone, Default)] pub struct ImpulseResponse(State); -#[derive(Debug, Clone)] - -pub enum Event { - Started, -} - #[derive(Debug, Clone, Default)] enum State { #[default] None, - Computing(Shared + Send>>>), + Computing, Computed(Arc), } @@ -28,7 +19,7 @@ impl ImpulseResponse { loopback: &raumklang_core::Loopback, measurement: &raumklang_core::Measurement, ) -> Option> { - if let State::Computing(_) = self.0 { + if let State::Computing = self.0 { return None; } @@ -40,65 +31,25 @@ impl ImpulseResponse { let measurement = measurement.clone(); let sipper = sipper(async move |mut progress| { - let computation = async { - let impulse_response = tokio::task::spawn_blocking(move || { - raumklang_core::ImpulseResponse::from_signals(&loopback, &measurement) - }) - .await - .unwrap() - .unwrap(); + progress.send(ImpulseResponse(State::Computing)).await; - ImpulseResponse(State::Computed(Arc::new(impulse_response))) - } - .boxed(); - let shared = computation.shared(); - progress - .send(ImpulseResponse(State::Computing(shared.clone()))) - .await; + let impulse_response = tokio::task::spawn_blocking(move || { + raumklang_core::ImpulseResponse::from_signals(&loopback, &measurement) + }) + .await + .unwrap() + .unwrap(); - shared.await + ImpulseResponse(State::Computed(Arc::new(impulse_response))) }); Some(sipper) } - pub fn save( - self, - path: Arc, - loopback: &raumklang_core::Loopback, - measurement: &raumklang_core::Measurement, - ) -> impl Sipper>, Self> { - let fut = self.compute(loopback, measurement).unwrap(); - - sipper(async move |mut progress| { - let ir = fut.run(&progress).await; - - progress.send(ir.clone()).await; - // tokio::task::spawn_blocking(move || { - // let spec = hound::WavSpec { - // channels: 1, - // sample_rate: ir.sample_rate, - // bits_per_sample: 32, - // sample_format: hound::SampleFormat::Float, - // }; - - // let mut writer = hound::WavWriter::create(path, spec).unwrap(); - // for s in ir.data { - // writer.write_sample(s).unwrap(); - // } - // writer.finalize().unwrap(); - // }) - // .await - // .unwrap(); - - Some(path) - }) - } - - pub fn inner(&self) -> Option<&raumklang_core::ImpulseResponse> { + pub fn result(&self) -> Option<&raumklang_core::ImpulseResponse> { match self.0 { State::None => None, - State::Computing(_) => None, + State::Computing => None, State::Computed(ref impulse_response) => Some(impulse_response), } } @@ -106,7 +57,7 @@ impl ImpulseResponse { pub fn progress(&self) -> Progress { match self.0 { State::None => Progress::None, - State::Computing(_) => Progress::Computing, + State::Computing => Progress::Computing, State::Computed(_) => Progress::Computed, } } diff --git a/raumklang-gui/src/screen/main.rs b/raumklang-gui/src/screen/main.rs index 5e545b1..ac90dba 100644 --- a/raumklang-gui/src/screen/main.rs +++ b/raumklang-gui/src/screen/main.rs @@ -41,6 +41,7 @@ use iced::{ }; use prism::{axis, line_series, Axis, Chart, Labels}; use rfd::FileHandle; +use tracing::instrument::WithSubscriber; use std::{ collections::{BTreeMap, HashMap}, @@ -177,7 +178,7 @@ pub enum Message { ImpulseResponseComputed(measurement::Id, data::ImpulseResponse), FrequencyResponseComputed(measurement::Id, data::FrequencyResponse), - ImpulseResponseSaved(measurement::Id, Option>), + ImpulseResponseSaved(measurement::Id, Arc), FrequencyResponseToggled(measurement::Id, bool), ChangeSmoothing(frequency_response::Smoothing), @@ -205,6 +206,7 @@ pub enum Message { OpenSpectrogramConfig, SpectrogramConfig(spectrogram_config::Message), ImpulseResponseChart(impulse_response::Message), + SaveImpulseResponseToFile(measurement::Id, Option>), } #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -332,6 +334,20 @@ impl Main { } } Message::ImpulseResponse(id, ui::impulse_response::Message::Save) => { + let State::Analysing { .. } = self.state else { + return Task::none(); + }; + + Task::perform( + choose_impulse_response_file_path(), + Message::SaveImpulseResponseToFile.with(id), + ) + } + Message::SaveImpulseResponseToFile(id, path) => { + let Some(path) = path else { + return Task::none(); + }; + let State::Analysing { ref mut analyses, .. } = self.state @@ -339,35 +355,24 @@ impl Main { return Task::none(); }; - let loopback = self.loopback.as_ref().and_then(Loopback::loaded).unwrap(); - let measurement = self - .measurements - .get(id) - .and_then(Measurement::signal) - .unwrap(); - let analysis = analyses.entry(id).or_default(); - let fut = analysis.impulse_response.compute(loopback, measurement); - dbg!(fut.is_some()); - - Task::sip( - sipper(async move |mut progress| { - let path = choose_impulse_response_file_path().await?; - - let ir = fut?.run(&progress).await; - progress.send(ir.clone()).await; - - let ir = ui::ImpulseResponse::from_data(&ir)?; - - save_impulse_response(path.clone(), ir).await; - - Some(path) - }), - Message::ImpulseResponseComputed.with(id), - Message::ImpulseResponseSaved.with(id), - ) + if let Some(ir) = analysis.impulse_response.result().cloned() { + Task::perform(save_impulse_response(path.clone(), ir.clone()), move |_| { + Message::ImpulseResponseSaved(id, path) + }) + } else { + compute_impulse_response( + analyses, + id, + self.loopback.as_ref(), + &self.measurements, + ) + .chain(Task::done(Message::SaveImpulseResponseToFile( + id, + Some(path), + ))) + } } - Message::SaveImpulseResponse(id, path) => Task::none(), Message::ImpulseResponseSaved(id, path) => { eprintln!("IR (#{:?}) saved to: {:?}", id, path); @@ -375,7 +380,6 @@ impl Main { } Message::ImpulseResponseComputed(id, impulse_response) => { let State::Analysing { - selected, ref active_tab, ref mut analyses, .. @@ -384,22 +388,9 @@ impl Main { return Task::none(); }; - // let new_ir = ImpulseResponse::from_data(new_ir); - - log::debug!("IR progress: {:?}", impulse_response.progress()); let analysis = analyses.entry(id).or_default(); analysis.impulse_response = ui::impulse_response::State::from_data(impulse_response); - // analysis.apply(analysis::Event::ImpulseResponseComputed(new_ir.clone())); - - // if selected.is_some_and(|selected| selected == id) { - // self.charts - // .impulse_responses - // .x_range - // .get_or_insert(0.0..=new_ir.data.len() as f32); - - // self.charts.impulse_responses.data_cache.clear(); - // } match active_tab { Tab::Measurements { .. } => Task::none(), @@ -425,19 +416,21 @@ impl Main { return Task::none(); }; - let analysis = analyses.entry(id).or_default(); - // analysis.apply(analysis::Event::FrequencyResponseComputed(new_fr.clone())); - - self.charts.frequency_responses.cache.clear(); - - if let Some(fraction) = self.smoothing.fraction() { + let task = if let Some(fraction) = self.smoothing.fraction() { Task::perform( - frequency_response::smooth_frequency_response(new_fr, fraction), + frequency_response::smooth_frequency_response(new_fr.clone(), fraction), Message::FrequencyResponseSmoothed.with(id), ) } else { Task::none() - } + }; + + let analysis = analyses.entry(id).or_default(); + analysis.frequency_response.set_result(new_fr); + + self.charts.frequency_responses.cache.clear(); + + task } Message::FrequencyResponseToggled(id, state) => { let State::Analysing { @@ -471,21 +464,14 @@ impl Main { self.smoothing = smoothing; if let Some(fraction) = smoothing.fraction() { - let tasks = analyses - .iter() - .flat_map(|(id, analysis)| { - let fr = analysis.frequency_response()?; - // TODO: refactor FR model - let data = fr.data.as_ref()?; - - Some((id, data.clone())) - }) - .map(|(id, fr)| { - Task::perform( - frequency_response::smooth_frequency_response(fr, fraction), - Message::FrequencyResponseSmoothed.with(*id), - ) - }); + let tasks = analyses.iter().flat_map(|(id, analysis)| { + let fr = analysis.frequency_response.result()?; + + Some(Task::perform( + frequency_response::smooth_frequency_response(fr.clone(), fraction), + Message::FrequencyResponseSmoothed.with(*id), + )) + }); Task::batch(tasks) } else { @@ -1782,9 +1768,8 @@ impl Main { let entries = self.measurements.iter().flat_map(|measurement| { let analysis = analyses.get(&measurement.id())?; - let frequency_response = analysis.frequency_response()?; - let content = frequency_response.view( + let content = analysis.frequency_response.view( &measurement.name, analysis.impulse_response.progress(), Message::FrequencyResponseToggled.with(measurement.id()), @@ -2185,29 +2170,24 @@ fn compute_frequency_response( measurements: &measurement::List, window: data::Window, ) -> Task { - // let Some(loopback) = loopback.and_then(Loopback::loaded).cloned() else { - // return Task::none(); - // }; - - // let Some(measurement) = measurements.get(id).and_then(Measurement::signal).cloned() else { - // return Task::none(); - // }; - - // let analysis = analyses.entry(id).or_default(); - - // Task::sip( - // analysis - // .impulse_response - // .clone() - // .compute(loopback, measurement), - // Message::ImpulseResponseComputed.with(id), - // Message::ImpulseResponseComputed.with(id), - // ) - // analysis - // .compute_frequency_response(window) - // .map(|f| Task::perform(f, Message::FrequencyResponseComputed.with(id))) - // .unwrap_or_else(|| compute_impulse_response(analyses, id, loopback, measurements)) - Task::none() + let analysis = analyses.entry(id).or_default(); + + if analysis.frequency_response.result().is_some() { + return Task::none(); + } + + if let Some(ir) = analysis.impulse_response.result() { + // TODO move into analysis itself + analysis.frequency_response.state = ui::frequency_response::State::Computing; + Task::perform( + data::frequency_response::compute(ir.data.clone(), window), + Message::FrequencyResponseComputed.with(id), + ) + } else { + analysis.frequency_response.state = + ui::frequency_response::State::WaitingForImpulseResponse; + compute_impulse_response(analyses, id, loopback, measurements) + } } fn compute_impulse_response( @@ -2288,7 +2268,7 @@ async fn save_impulse_response(path: Arc, ir: ui::ImpulseResponse) { }; let mut writer = hound::WavWriter::create(path, spec).unwrap(); - for s in ir.data { + for s in ir.normalized { writer.write_sample(s).unwrap(); } writer.finalize().unwrap(); diff --git a/raumklang-gui/src/screen/main/chart.rs b/raumklang-gui/src/screen/main/chart.rs index 6cba9e9..90c471c 100644 --- a/raumklang-gui/src/screen/main/chart.rs +++ b/raumklang-gui/src/screen/main/chart.rs @@ -102,7 +102,7 @@ pub fn impulse_response<'a>( canvas::Canvas::new(BarChart { window, datapoints: impulse_response - .data + .normalized .iter() .copied() .map(f32::abs) diff --git a/raumklang-gui/src/ui/analysis.rs b/raumklang-gui/src/ui/analysis.rs index eddea28..2d3a174 100644 --- a/raumklang-gui/src/ui/analysis.rs +++ b/raumklang-gui/src/ui/analysis.rs @@ -10,6 +10,7 @@ use crate::{ #[derive(Debug, Clone, Default)] pub struct Analysis { pub impulse_response: impulse_response::State, + pub frequency_response: FrequencyResponse, } // #[derive(Debug, Clone, Default)] @@ -57,7 +58,7 @@ impl Analysis { // } pub(crate) fn impulse_response(&self) -> Option<&ImpulseResponse> { - self.impulse_response.inner() + self.impulse_response.result() } pub(crate) fn frequency_response(&self) -> Option<&FrequencyResponse> { diff --git a/raumklang-gui/src/ui/frequency_response.rs b/raumklang-gui/src/ui/frequency_response.rs index 07c7974..1bf7103 100644 --- a/raumklang-gui/src/ui/frequency_response.rs +++ b/raumklang-gui/src/ui/frequency_response.rs @@ -18,10 +18,19 @@ use rand::Rng as _; pub struct FrequencyResponse { pub color: iced::Color, pub is_shown: bool, - pub progress: Progress, - pub data: Option, + // pub data: Option, pub smoothed: Option>, + + pub state: State, +} + +#[derive(Debug, Clone)] +pub enum State { + None, + WaitingForImpulseResponse, + Computing, + Computed(data::FrequencyResponse), } impl FrequencyResponse { @@ -31,15 +40,10 @@ impl FrequencyResponse { Self { color, is_shown: true, - progress: Progress::None, - data: None, smoothed: None, - } - } - pub fn computed(&mut self, data: data::FrequencyResponse) { - self.data = Some(data); - self.progress = Progress::Finished; + state: State::None, + } } pub fn view<'a, Message>( @@ -81,23 +85,26 @@ impl FrequencyResponse { .into() }; - let content = match impulse_response_progess { - impulse_response::Progress::None | impulse_response::Progress::Computing => { - processing_overlay("Impulse Response", item) - } - impulse_response::Progress::Computed => match self.progress { - Progress::None | Progress::Computing => { - processing_overlay(self.progress.to_string(), item) - } - Progress::Finished => item, - }, + let content = match self.state { + State::None => item, + State::WaitingForImpulseResponse => processing_overlay("Impulse Response", item), + State::Computing => processing_overlay("Computing ...", item), + State::Computed(_) => item, }; sidebar::item(content, false).into() } - pub(crate) fn result(&self) -> Option<&data::FrequencyResponse> { - self.data.as_ref() + pub fn result(&self) -> Option<&data::FrequencyResponse> { + let State::Computed(ref result) = self.state else { + return None; + }; + + Some(result) + } + + pub fn set_result(&mut self, fr: data::FrequencyResponse) { + self.state = State::Computed(fr) } } diff --git a/raumklang-gui/src/ui/impulse_response.rs b/raumklang-gui/src/ui/impulse_response.rs index 6c9bfb9..f60668e 100644 --- a/raumklang-gui/src/ui/impulse_response.rs +++ b/raumklang-gui/src/ui/impulse_response.rs @@ -1,4 +1,4 @@ -use std::{sync::Arc, time::SystemTime}; +use std::time::SystemTime; use crate::{ data::impulse_response, @@ -13,7 +13,6 @@ use iced::{ widget::{button, column, container, right, row, rule, stack, text}, Color, Element, Length::{Fill, Shrink}, - Task, }; #[derive(Debug, Clone)] @@ -22,13 +21,6 @@ pub enum Message { Save, } -#[derive(Debug, Clone)] -pub struct ImpulseResponse { - pub sample_rate: SampleRate, - pub data: Vec, - pub origin: data::ImpulseResponse, -} - #[derive(Debug, Clone)] pub enum State { Computing(data::ImpulseResponse), @@ -50,7 +42,7 @@ impl State { } } - pub(crate) fn inner(&self) -> Option<&ImpulseResponse> { + pub(crate) fn result(&self) -> Option<&ImpulseResponse> { match self { State::Computing(_) => None, State::Computed(ref impulse_response) => Some(impulse_response), @@ -71,15 +63,16 @@ impl State { } } -impl Default for State { - fn default() -> Self { - Self::Computing(data::ImpulseResponse::default()) - } +#[derive(Debug, Clone)] +pub struct ImpulseResponse { + pub sample_rate: SampleRate, + pub normalized: Vec, + pub data: raumklang_core::ImpulseResponse, } impl ImpulseResponse { pub fn from_data(data: &data::ImpulseResponse) -> Option { - let impulse_response = data.inner()?; + let impulse_response = data.result()?; let max = impulse_response .data @@ -97,12 +90,18 @@ impl ImpulseResponse { Some(Self { sample_rate: SampleRate::new(impulse_response.sample_rate), - data: normalized, - origin: data.clone(), + normalized, + data: impulse_response.clone(), }) } } +impl Default for State { + fn default() -> Self { + Self::Computing(data::ImpulseResponse::default()) + } +} + pub fn view<'a>( name: &'a str, date_time: SystemTime, @@ -119,7 +118,7 @@ pub fn view<'a>( .clip(true) .spacing(6), ) - .on_press_with(move || Message::Select) + .on_press(Message::Select) .width(Fill) .style(move |theme, status| { let base = button::subtle(theme, status); From 2844302378fab04ea9dc6cf1613ae847ad6bf901 Mon Sep 17 00:00:00 2001 From: Hendrik Kunert Date: Sat, 31 Jan 2026 15:07:42 +0100 Subject: [PATCH 17/30] Cleanup --- raumklang-gui/src/screen/main.rs | 116 ++++++++++++--------------- raumklang-gui/src/screen/main/tab.rs | 27 +++---- raumklang-gui/src/ui/analysis.rs | 105 +----------------------- 3 files changed, 64 insertions(+), 184 deletions(-) diff --git a/raumklang-gui/src/screen/main.rs b/raumklang-gui/src/screen/main.rs index ac90dba..866becb 100644 --- a/raumklang-gui/src/screen/main.rs +++ b/raumklang-gui/src/screen/main.rs @@ -70,11 +70,14 @@ pub struct Main { // spectral_decay_config: data::spectral_decay::Config, // spectrogram_config: spectrogram::Preferences, window: Option>, - charts: Charts, + + ir_chart: impulse_response::Chart, } #[allow(clippy::large_enum_variant)] +#[derive(Default)] enum State { + #[default] Collecting, Analysing { active_tab: Tab, @@ -100,17 +103,10 @@ impl State { } } -impl Default for State { - fn default() -> Self { - State::Collecting - } -} - -#[allow(clippy::large_enum_variant)] pub enum Tab { Measurements { recording: Option }, ImpulseResponses { window_settings: WindowSettings }, - FrequencyResponses, + FrequencyResponses { cache: canvas::Cache }, SpectralDecay, Spectrogram, } @@ -121,14 +117,6 @@ impl Default for Tab { } } -#[derive(Debug, Default)] -struct Charts { - impulse_responses: impulse_response::Chart, - frequency_responses: frequency_response::ChartData, - spectral_decay_cache: canvas::Cache, - spectrogram: Spectrogram, -} - #[derive(Debug, Default)] struct Spectrogram { pub zoom: chart::Zoom, @@ -176,9 +164,12 @@ pub enum Message { ImpulseResponseSelected(measurement::Id), ImpulseResponseComputed(measurement::Id, data::ImpulseResponse), + SaveImpulseResponseToFile(measurement::Id, Option>), FrequencyResponseComputed(measurement::Id, data::FrequencyResponse), ImpulseResponseSaved(measurement::Id, Arc), + ImpulseResponseChart(impulse_response::Message), + ImpulseResponse(ui::measurement::Id, ui::impulse_response::Message), FrequencyResponseToggled(measurement::Id, bool), ChangeSmoothing(frequency_response::Smoothing), @@ -189,8 +180,6 @@ pub enum Message { MeasurementChart(waveform::Interaction), - ImpulseResponse(ui::measurement::Id, ui::impulse_response::Message), - SaveImpulseResponse(measurement::Id, Arc), // SaveImpulseResponseFileDialog(measurement::Id), Recording(recording::Message), StartRecording(recording::Kind), @@ -200,13 +189,11 @@ pub enum Message { Spectrogram(chart::spectrogram::Interaction), SpectrogramComputed(measurement::Id, data::Spectrogram), - Modal(ModalAction), OpenSpectralDecayConfig, SpectralDecayConfig(spectral_decay_config::Message), OpenSpectrogramConfig, SpectrogramConfig(spectrogram_config::Message), - ImpulseResponseChart(impulse_response::Message), - SaveImpulseResponseToFile(measurement::Id, Option>), + Modal(ModalAction), } #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -318,7 +305,7 @@ impl Main { }; *selected = Some(id); - self.charts.impulse_responses.data_cache.clear(); + self.ir_chart.data_cache.clear(); match tab { Tab::Measurements { .. } => Task::none(), @@ -328,7 +315,7 @@ impl Main { self.loopback.as_ref(), &self.measurements, ), - Tab::FrequencyResponses => Task::none(), + Tab::FrequencyResponses { .. } => Task::none(), Tab::SpectralDecay => todo!(), Tab::Spectrogram => todo!(), } @@ -395,7 +382,7 @@ impl Main { match active_tab { Tab::Measurements { .. } => Task::none(), Tab::ImpulseResponses { .. } => Task::none(), - Tab::FrequencyResponses => compute_frequency_response( + Tab::FrequencyResponses { .. } => compute_frequency_response( analyses, id, self.loopback.as_ref(), @@ -410,7 +397,9 @@ impl Main { log::debug!("Frequency response computed: {id}"); let State::Analysing { - ref mut analyses, .. + ref mut analyses, + active_tab: Tab::FrequencyResponses { ref cache }, + .. } = self.state else { return Task::none(); @@ -427,35 +416,34 @@ impl Main { let analysis = analyses.entry(id).or_default(); analysis.frequency_response.set_result(new_fr); - - self.charts.frequency_responses.cache.clear(); + cache.clear(); task } Message::FrequencyResponseToggled(id, state) => { let State::Analysing { - ref mut analyses, .. + ref mut analyses, + active_tab: Tab::FrequencyResponses { ref cache }, + .. } = self.state else { return Task::none(); }; - let Some(fr) = analyses - .get_mut(&id) - .and_then(Analysis::frequency_response_mut) - else { + let Some(fr) = analyses.get_mut(&id).map(Analysis::frequency_response_mut) else { return Task::none(); }; fr.is_shown = state; - - self.charts.frequency_responses.cache.clear(); + cache.clear(); Task::none() } Message::ChangeSmoothing(smoothing) => { let State::Analysing { - ref mut analyses, .. + ref mut analyses, + active_tab: Tab::FrequencyResponses { ref cache }, + .. } = self.state else { return Task::none(); @@ -477,31 +465,28 @@ impl Main { } else { analyses .values_mut() - .flat_map(Analysis::frequency_response_mut) + .map(Analysis::frequency_response_mut) .for_each(|fr| fr.smoothed = None); - self.charts.frequency_responses.cache.clear(); + cache.clear(); Task::none() } } Message::FrequencyResponseSmoothed(id, smoothed) => { let State::Analysing { - ref mut analyses, .. + ref mut analyses, + active_tab: Tab::FrequencyResponses { ref cache }, + .. } = self.state else { return Task::none(); }; - let Some(fr) = analyses - .get_mut(&id) - .and_then(Analysis::frequency_response_mut) - else { - return Task::none(); - }; - - fr.smoothed = Some(smoothed); - self.charts.frequency_responses.cache.clear(); + if let Some(fr) = analyses.get_mut(&id).map(|a| &mut a.frequency_response) { + fr.smoothed = Some(smoothed); + cache.clear(); + } Task::none() } @@ -548,14 +533,10 @@ impl Main { return Task::none(); }; - *active_tab = Tab::FrequencyResponses; - - // FIXME get rid of vec alloc - // let ids: Vec<_> = self.measurements.loaded().map(Measurement::id).collect(); + *active_tab = Tab::FrequencyResponses { + cache: canvas::Cache::new(), + }; - // let tasks = ids - // .into_iter() - // .map(|id| self.compute_frequency_response(id)); let tasks = self.measurements.loaded().map(Measurement::id).map(|id| { compute_frequency_response( analyses, @@ -1418,7 +1399,7 @@ impl Main { ), tab( "Frequency Responses", - matches!(active_tab, Some(Tab::FrequencyResponses)), + matches!(active_tab, Some(Tab::FrequencyResponses { .. })), active_tab .is_some() .then_some(Message::OpenFrequencyResponses) @@ -1453,15 +1434,15 @@ impl Main { selected, ref analyses, } => match active_tab { - Tab::Measurements { recording } => self.measurements_tab(), + Tab::Measurements { .. } => self.measurements_tab(), Tab::ImpulseResponses { window_settings } => self.impulse_responses_tab( selected, - &self.charts.impulse_responses, + &self.ir_chart, window_settings, analyses, ), - Tab::FrequencyResponses => { - self.frequency_responses_tab(&self.charts.frequency_responses, analyses) + Tab::FrequencyResponses { cache } => { + self.frequency_responses_tab(cache, analyses) } Tab::SpectralDecay => todo!(), Tab::Spectrogram => todo!(), @@ -1760,7 +1741,7 @@ impl Main { fn frequency_responses_tab<'a>( &'a self, - chart_settings: &'a frequency_response::ChartData, + cache: &'a canvas::Cache, analyses: &'a BTreeMap, ) -> Element<'a, Message> { let sidebar = { @@ -1794,9 +1775,12 @@ impl Main { )] }; - let frequency_responses = analyses.values().flat_map(Analysis::frequency_response); + let frequency_responses = analyses.values().map(|a| &a.frequency_response); + let chart_needed = frequency_responses + .clone() + .any(|fr| fr.result().is_some() && fr.is_shown); - let content = if frequency_responses.clone().any(|fr| fr.is_shown) { + let content = if chart_needed { let series_list = frequency_responses .filter(|fr| fr.is_shown) .flat_map(|item| { @@ -1841,10 +1825,9 @@ impl Main { .collect(), ), ) - .x_range(chart_settings.x_range.clone().unwrap_or(20.0..=22_500.0)) .y_labels(Labels::default().format(&|v| format!("{v:.0}"))) .extend_series(series_list) - .cache(&chart_settings.cache); + .cache(&cache); container(chart) } else { @@ -2238,8 +2221,9 @@ impl Default for Main { // spectrogram_config: spectrogram::Preferences::default(), window: None, - charts: Charts::default(), signal_cache: canvas::Cache::default(), + + ir_chart: impulse_response::Chart::default(), } } } diff --git a/raumklang-gui/src/screen/main/tab.rs b/raumklang-gui/src/screen/main/tab.rs index fcef599..49c0aa6 100644 --- a/raumklang-gui/src/screen/main/tab.rs +++ b/raumklang-gui/src/screen/main/tab.rs @@ -1,19 +1,16 @@ -pub mod frequency_responses; -pub mod impulse_responses; -pub mod measurements; +mod impulse_response; -pub use frequency_responses::FrequencyResponses; -pub use impulse_responses::ImpulseReponses; -pub use measurements::Measurements; +pub use impulse_response::ImpulseResponses; +pub use impulse_response::WindowSettings; -pub enum Tab { - Measurements(Measurements), - ImpulseResponses, - FrequencyResponses, -} +use crate::screen::main::recording::Recording; -impl Default for Tab { - fn default() -> Self { - Self::Measurements(Measurements::new()) - } +use iced::widget::canvas; + +pub enum Tab { + Measurements { recording: Option }, + ImpulseResponses { window_settings: WindowSettings }, + FrequencyResponses { cache: canvas::Cache }, + SpectralDecay, + Spectrogram, } diff --git a/raumklang-gui/src/ui/analysis.rs b/raumklang-gui/src/ui/analysis.rs index 2d3a174..6ddce37 100644 --- a/raumklang-gui/src/ui/analysis.rs +++ b/raumklang-gui/src/ui/analysis.rs @@ -13,113 +13,12 @@ pub struct Analysis { pub frequency_response: FrequencyResponse, } -// #[derive(Debug, Clone, Default)] -// enum State { -// #[default] -// None, -// ImpulseResponseComputing(FrequencyResponse), -// ImpulseResponse { -// impulse_response: ImpulseResponse, -// frequency_response: FrequencyResponse, -// }, -// } - -// pub enum Event { -// ImpulseResponseComputed(ImpulseResponse), -// FrequencyResponseComputed(data::FrequencyResponse), -// } - impl Analysis { - // pub(crate) fn apply(&mut self, event: Event) { - // match event { - // Event::ImpulseResponseComputed(impulse_response) => { - // let State::ImpulseResponseComputing(frequency_response) = mem::take(&mut self.0) - // else { - // return; - // }; - - // self.0 = State::ImpulseResponse { - // impulse_response: impulse_response, - // frequency_response, - // } - // } - // Event::FrequencyResponseComputed(fr) => { - // let State::ImpulseResponse { - // ref mut frequency_response, - // .. - // } = self.0 - // else { - // return; - // }; - - // frequency_response.computed(fr); - // } - // } - // } - pub(crate) fn impulse_response(&self) -> Option<&ImpulseResponse> { self.impulse_response.result() } - pub(crate) fn frequency_response(&self) -> Option<&FrequencyResponse> { - None - // match &self.0 { - // State::None => None, - // State::ImpulseResponseComputing(frequency_response) - // | State::ImpulseResponse { - // frequency_response, .. - // } => Some(frequency_response), - // } - } - - pub(crate) fn frequency_response_mut(&mut self) -> Option<&mut FrequencyResponse> { - None - // match &mut self.0 { - // State::None => None, - // State::ImpulseResponseComputing(frequency_response) - // | State::ImpulseResponse { - // frequency_response, .. - // } => Some(frequency_response), - // } + pub(crate) fn frequency_response_mut(&mut self) -> &mut FrequencyResponse { + &mut self.frequency_response } - - // pub(crate) fn compute_impulse_response( - // &mut self, - // loopback: Loopback, - // measurement: Measurement, - // ) -> Option> { - // match self.0 { - // State::ImpulseResponse { .. } => None, - // State::ImpulseResponseComputing(_) => None, - // State::None => { - // self.0 = State::ImpulseResponseComputing(FrequencyResponse::new()); - - // Some(data::impulse_response::compute(loopback, measurement)) - // } - // } - // } - - // pub(crate) fn compute_frequency_response( - // &mut self, - // window: data::Window, - // ) -> Option> { - // let State::ImpulseResponse { - // ref impulse_response, - // ref mut frequency_response, - // } = self.0 - // else { - // return None; - // }; - - // if let frequency_response::Progress::Finished = frequency_response.progress { - // return None; - // } - - // frequency_response.progress = frequency_response::Progress::Computing; - - // Some(data::frequency_response::compute( - // impulse_response.origin.clone(), - // window, - // )) - // } } From a263d966d535de36db717ff43a666e063952b75a Mon Sep 17 00:00:00 2001 From: Hendrik Kunert Date: Sat, 31 Jan 2026 21:00:39 +0100 Subject: [PATCH 18/30] Refactor spectral decay tab --- raumklang-gui/src/data/spectral_decay.rs | 154 ++--- raumklang-gui/src/screen/main.rs | 635 +++++++++++------- .../src/screen/main/spectral_decay_config.rs | 19 +- raumklang-gui/src/ui.rs | 1 + raumklang-gui/src/ui/analysis.rs | 10 +- raumklang-gui/src/ui/impulse_response.rs | 32 +- raumklang-gui/src/ui/spectral_decay.rs | 77 ++- raumklang-gui/src/ui/spectrogram.rs | 6 +- raumklang-gui/src/widget.rs | 28 +- 9 files changed, 557 insertions(+), 405 deletions(-) diff --git a/raumklang-gui/src/data/spectral_decay.rs b/raumklang-gui/src/data/spectral_decay.rs index 03eeee8..a521378 100644 --- a/raumklang-gui/src/data/spectral_decay.rs +++ b/raumklang-gui/src/data/spectral_decay.rs @@ -9,80 +9,6 @@ use rustfft::{ use crate::data::{smooth_fractional_octave, SampleRate, Samples}; -#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)] -pub struct Config { - pub shift: Shift, - pub left_window_width: WindowWidth, - pub right_window_width: WindowWidth, - pub smoothing_fraction: u8, -} - -#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)] -pub struct Shift(Duration); - -#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)] -pub struct WindowWidth(Duration); - -#[derive(Debug, thiserror::Error)] -pub enum ValidationError { - #[error("Must be in range: 0..50")] - Range, - #[error("Not a number.")] - NotANumber, -} - -impl Shift { - pub(crate) fn from_millis_string(str: &str) -> Result { - let millis = str.parse().map_err(|_| ValidationError::NotANumber)?; - - if !(1..=50).contains(&millis) { - return Err(ValidationError::Range); - } - - Ok(Self(Duration::from_millis(millis))) - } - - pub(crate) fn as_millis(&self) -> u128 { - self.0.as_millis() - } - - fn from_millis(millis: u64) -> Self { - Self(Duration::from_millis(millis)) - } -} - -impl From<&Shift> for Duration { - fn from(shift: &Shift) -> Self { - shift.0 - } -} - -impl WindowWidth { - pub(crate) fn from_millis_string(str: &str) -> Result { - let millis = str.parse().map_err(|_| ValidationError::NotANumber)?; - - if !(0..=500).contains(&millis) { - return Err(ValidationError::Range); - } - - Ok(Self(Duration::from_millis(millis))) - } - - pub(crate) fn as_millis(&self) -> u128 { - self.0.as_millis() - } - - fn from_millis(millis: u64) -> Self { - Self(Duration::from_millis(millis)) - } -} - -impl From<&WindowWidth> for Duration { - fn from(value: &WindowWidth) -> Self { - value.0 - } -} - #[derive(Clone)] pub struct SpectralDecay(Vec); @@ -96,6 +22,12 @@ impl SpectralDecay { } } +impl fmt::Debug for SpectralDecay { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Spectral Decay of size: {} slices", self.0.len()) + } +} + pub(crate) async fn compute( ir: raumklang_core::ImpulseResponse, preferences: Config, @@ -170,12 +102,72 @@ pub(crate) async fn compute( .unwrap() } -impl fmt::Debug for SpectralDecay { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "Spectral Decay of size: {} slices", self.0.len()) +#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)] +pub struct Shift(Duration); + +impl Shift { + pub(crate) fn from_millis_string(str: &str) -> Result { + let millis = str.parse().map_err(|_| ValidationError::NotANumber)?; + + if !(1..=50).contains(&millis) { + return Err(ValidationError::Range); + } + + Ok(Self(Duration::from_millis(millis))) + } + + pub(crate) fn as_millis(&self) -> u128 { + self.0.as_millis() + } + + fn from_millis(millis: u64) -> Self { + Self(Duration::from_millis(millis)) } } +impl From<&Shift> for Duration { + fn from(shift: &Shift) -> Self { + shift.0 + } +} + +#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)] +pub struct WindowWidth(Duration); + +impl WindowWidth { + pub(crate) fn from_millis_string(str: &str) -> Result { + let millis = str.parse().map_err(|_| ValidationError::NotANumber)?; + + if !(0..=500).contains(&millis) { + return Err(ValidationError::Range); + } + + Ok(Self(Duration::from_millis(millis))) + } + + pub(crate) fn as_millis(&self) -> u128 { + self.0.as_millis() + } + + fn from_millis(millis: u64) -> Self { + Self(Duration::from_millis(millis)) + } +} + +impl From<&WindowWidth> for Duration { + fn from(value: &WindowWidth) -> Self { + value.0 + } +} + +#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)] +pub struct Config { + pub shift: Shift, + pub left_window_width: WindowWidth, + pub right_window_width: WindowWidth, + pub smoothing_fraction: u8, +} + impl Default for Config { fn default() -> Self { Self { @@ -186,3 +178,11 @@ impl Default for Config { } } } + +#[derive(Debug, thiserror::Error)] +pub enum ValidationError { + #[error("Must be in range: 0..50")] + Range, + #[error("Not a number.")] + NotANumber, +} diff --git a/raumklang-gui/src/screen/main.rs b/raumklang-gui/src/screen/main.rs index 866becb..f3ca6dd 100644 --- a/raumklang-gui/src/screen/main.rs +++ b/raumklang-gui/src/screen/main.rs @@ -15,7 +15,7 @@ use crate::{ spectrogram_config::SpectrogramConfig, }, ui::{self, analysis, measurement, Analysis, Loopback, Measurement}, - widget::sidebar, + widget::{processing_overlay, sidebar}, PickAndLoadError, }; use raumklang_core::{dbfs, WavLoadError}; @@ -27,7 +27,9 @@ use chrono::{DateTime, Utc}; use generic_overlay::generic_overlay::{dropdown_menu, dropdown_root}; use iced::{ - advanced::Renderer, + advanced::{ + graphics::text::cosmic_text::skrifa::raw::tables::layout::SelectedScript, Renderer, + }, alignment::{Horizontal, Vertical}, futures::{FutureExt, TryFutureExt}, keyboard, padding, @@ -53,6 +55,7 @@ use std::{ pub struct Main { state: State, + modal: Modal, selected: Option, loopback: Option, @@ -65,9 +68,8 @@ pub struct Main { offset: chart::Offset, smoothing: frequency_response::Smoothing, - // modal: Modal, // project_path: Option, - // spectral_decay_config: data::spectral_decay::Config, + spectral_decay_config: data::spectral_decay::Config, // spectrogram_config: spectrogram::Preferences, window: Option>, @@ -107,7 +109,7 @@ pub enum Tab { Measurements { recording: Option }, ImpulseResponses { window_settings: WindowSettings }, FrequencyResponses { cache: canvas::Cache }, - SpectralDecay, + SpectralDecay { cache: canvas::Cache }, Spectrogram, } @@ -161,6 +163,7 @@ pub enum Message { OpenMeasurements, OpenImpulseResponses, OpenFrequencyResponses, + OpenSpectralDecays, ImpulseResponseSelected(measurement::Id), ImpulseResponseComputed(measurement::Id, data::ImpulseResponse), @@ -235,6 +238,96 @@ impl Main { pub fn update(&mut self, recent_projects: &mut RecentProjects, msg: Message) -> Task { match msg { + Message::OpenMeasurements => { + let State::Analysing { + active_tab: ref mut tab, + .. + } = self.state + else { + return Task::none(); + }; + + *tab = Tab::Measurements { recording: None }; + + Task::none() + } + Message::OpenImpulseResponses => { + let State::Analysing { + active_tab: ref mut tab, + .. + } = self.state + else { + return Task::none(); + }; + + let Some(window) = &self.window else { + return Task::none(); + }; + + *tab = Tab::ImpulseResponses { + window_settings: WindowSettings::new(window.clone()), + }; + + return Task::none(); + } + + Message::OpenFrequencyResponses => { + let State::Analysing { + ref mut active_tab, + ref mut analyses, + .. + } = self.state + else { + return Task::none(); + }; + + *active_tab = Tab::FrequencyResponses { + cache: canvas::Cache::new(), + }; + + let tasks = self.measurements.loaded().map(Measurement::id).map(|id| { + compute_frequency_response( + analyses, + id, + self.loopback.as_ref(), + &self.measurements, + self.window.as_ref().cloned().unwrap(), + ) + }); + + Task::batch(tasks) + } + Message::OpenSpectralDecays => { + let State::Analysing { + selected, + ref mut active_tab, + ref mut analyses, + .. + } = self.state + else { + return Task::none(); + }; + + *active_tab = Tab::SpectralDecay { + cache: canvas::Cache::new(), + }; + + if let Some(id) = selected { + // FIXME duplicate + // compute spectral decay + let analysis = analyses.entry(id).or_default(); + if let Some(computation) = analysis.spectral_decay.compute_spectral_decay( + &analysis.impulse_response, + self.spectral_decay_config, + ) { + Task::perform(computation, Message::SpectralDecayComputed.with(id)) + } else { + Task::none() + } + } else { + Task::none() + } + } Message::LoadLoopback => Task::future(pick_file("Load Loopback ...")) .and_then(|path| Task::perform(Loopback::from_file(path), Message::LoopbackLoaded)), Message::LoadMeasurement => { @@ -293,6 +386,16 @@ impl Main { Task::none() } + Message::MeasurementChart(interaction) => { + match interaction { + waveform::Interaction::ZoomChanged(zoom) => self.zoom = zoom, + waveform::Interaction::OffsetChanged(offset) => self.offset = offset, + } + + self.signal_cache.clear(); + + Task::none() + } Message::ImpulseResponse(id, ui::impulse_response::Message::Select) => { let State::Analysing { active_tab: tab, @@ -316,7 +419,14 @@ impl Main { &self.measurements, ), Tab::FrequencyResponses { .. } => Task::none(), - Tab::SpectralDecay => todo!(), + Tab::SpectralDecay { .. } => compute_spectral_decay( + id, + analyses, + self.spectral_decay_config, + self.loopback.as_ref(), + &self.measurements, + ), + Tab::Spectrogram => todo!(), } } @@ -389,7 +499,14 @@ impl Main { &self.measurements, self.window.as_ref().cloned().unwrap(), ), - Tab::SpectralDecay => todo!(), + Tab::SpectralDecay { .. } => analysis + .spectral_decay + .compute_spectral_decay( + &analysis.impulse_response, + self.spectral_decay_config, + ) + .map(|f| Task::perform(f, Message::SpectralDecayComputed.with(id))) + .unwrap_or_default(), Tab::Spectrogram => todo!(), } } @@ -490,42 +607,40 @@ impl Main { Task::none() } - Message::OpenMeasurements => { - let State::Analysing { - active_tab: ref mut tab, - .. - } = self.state - else { - return Task::none(); - }; - *tab = Tab::Measurements { recording: None }; - - Task::none() - } - Message::OpenImpulseResponses => { + Message::SpectralDecayComputed(id, sd) => { let State::Analysing { - active_tab: ref mut tab, + ref mut analyses, + active_tab: Tab::SpectralDecay { ref cache }, .. } = self.state else { return Task::none(); }; - let Some(window) = &self.window else { + let Some(analysis) = analyses.get_mut(&id) else { return Task::none(); }; - *tab = Tab::ImpulseResponses { - window_settings: WindowSettings::new(window.clone()), - }; + analysis.spectral_decay.set_result(sd); + cache.clear(); - return Task::none(); + Task::none() } + Message::OpenSpectralDecayConfig => { + self.modal = Modal::SpectralDecayConfig(SpectralDecayConfig::new( + self.spectral_decay_config, + )); + + Task::none() + } + Message::SpectralDecayConfig(msg) => { + let Modal::SpectralDecayConfig(modal) = &mut self.modal else { + return Task::none(); + }; - Message::OpenFrequencyResponses => { let State::Analysing { - ref mut active_tab, + selected, ref mut analyses, .. } = self.state @@ -533,21 +648,31 @@ impl Main { return Task::none(); }; - *active_tab = Tab::FrequencyResponses { - cache: canvas::Cache::new(), - }; - - let tasks = self.measurements.loaded().map(Measurement::id).map(|id| { - compute_frequency_response( - analyses, - id, - self.loopback.as_ref(), - &self.measurements, - self.window.as_ref().cloned().unwrap(), - ) - }); - - Task::batch(tasks) + match modal.update(msg) { + spectral_decay_config::Action::None => Task::none(), + spectral_decay_config::Action::Discard => { + self.modal = Modal::None; + Task::none() + } + spectral_decay_config::Action::Apply(config) => { + self.modal = Modal::None; + self.spectral_decay_config = config; + + analyses.values_mut().for_each(|a| a.spectral_decay.reset()); + + selected + .map(|id| { + compute_spectral_decay( + id, + analyses, + config, + self.loopback.as_ref(), + &self.measurements, + ) + }) + .unwrap_or_default() + } + } } _ => Task::none(), } @@ -1406,8 +1531,8 @@ impl Main { ), tab( "Spectral Decays", - matches!(active_tab, Some(Tab::SpectralDecay)), - None // Message::OpenFrequencyResponses + matches!(active_tab, Some(Tab::SpectralDecay { .. })), + active_tab.is_some().then_some(Message::OpenSpectralDecays) ), tab( "Spectrogram", @@ -1444,13 +1569,54 @@ impl Main { Tab::FrequencyResponses { cache } => { self.frequency_responses_tab(cache, analyses) } - Tab::SpectralDecay => todo!(), + Tab::SpectralDecay { cache } => { + self.spectral_decay_tab(selected, &analyses, &cache) + } Tab::Spectrogram => todo!(), }, } }; - container(column![header, container(content).padding(10)]).into() + let content = container(column![header, container(content).padding(10)]); + + match self.modal { + Modal::None => content.into(), + Modal::PendingWindow { .. } => { + let pending_window = { + container( + column![ + text("Window pending!").size(18), + column![ + text("You have modified the window used for frequency response computations."), + text("You need to discard or apply your changes before proceeding."), + ].spacing(5), + row![ + space::horizontal(), + button("Discard") + .style(button::danger) + .on_press(Message::Modal(ModalAction::Discard)), + button("Apply") + .style(button::success) + .on_press(Message::Modal(ModalAction::Apply)) + ] + .spacing(5) + ] + .spacing(10)) + .padding(20) + .width(400) + .style(container::bordered_box) + }; + + modal(content, pending_window) + } + Modal::SpectralDecayConfig(ref config) => { + modal(content, config.view().map(Message::SpectralDecayConfig)) + } + Modal::SpectrogramConfig(ref spectrogram_config) => modal( + content, + spectrogram_config.view().map(Message::SpectrogramConfig), + ), + } } // pub fn view<'a>(&'a self, recent_projects: &'a RecentProjects) -> Element<'a, Message> { // let header = { @@ -1545,44 +1711,6 @@ impl Main { // let content = container(column![header, container(content).padding(10)]); - // match self.modal { - // Modal::None => content.into(), - // Modal::PendingWindow { .. } => { - // let pending_window = { - // container( - // column![ - // text("Window pending!").size(18), - // column![ - // text("You have modified the window used for frequency response computations."), - // text("You need to discard or apply your changes before proceeding."), - // ].spacing(5), - // row![ - // space::horizontal(), - // button("Discard") - // .style(button::danger) - // .on_press(Message::Modal(ModalAction::Discard)), - // button("Apply") - // .style(button::success) - // .on_press(Message::Modal(ModalAction::Apply)) - // ] - // .spacing(5) - // ] - // .spacing(10)) - // .padding(20) - // .width(400) - // .style(container::bordered_box) - // }; - - // modal(content, pending_window) - // } - // Modal::SpectralDecayConfig(ref config) => { - // modal(content, config.view().map(Message::SpectralDecayConfig)) - // } - // Modal::SpectrogramConfig(ref spectrogram_config) => modal( - // content, - // spectrogram_config.view().map(Message::SpectrogramConfig), - // ), - // } // } fn measurements_tab<'a>(&'a self) -> Element<'a, Message> { @@ -1844,127 +1972,137 @@ impl Main { .into() } - // pub fn spectral_decay_tab<'a>( - // &'a self, - // selected: Option, - // cache: &'a canvas::Cache, - // ) -> Element<'a, Message> { - // let sidebar = { - // let header = { - // let config_btn = button(icon::settings().center()) - // .style(button::subtle) - // .on_press(Message::OpenSpectralDecayConfig); - // Category::new("Spectral Decays").push_button(config_btn) - // }; + pub fn spectral_decay_tab<'a>( + &'a self, + selected: Option, + analyses: &'a BTreeMap, + cache: &'a canvas::Cache, + ) -> Element<'a, Message> { + let sidebar = { + let header = { + let config_btn = button(icon::settings().center()) + .style(button::subtle) + .on_press(Message::OpenSpectralDecayConfig); + Category::new("Spectral Decays").push_button(config_btn) + }; - // let entries = self.measurements.iter().flat_map(|measurement| { - // let id = measurement.id(); - // let is_active = selected.is_some_and(|s| s == id); + let entries = self.measurements.iter().flat_map(|measurement| { + let id = measurement.id(); + let is_active = selected.is_some_and(|s| s == id); - // let signal = measurement.signal()?; - // let entry = { - // // TODO: refactor, basically the same btn as IR and Spectrogram - // let dt: DateTime = signal.modified.into(); - // let btn = button( - // column![ - // text(&measurement.name) - // .size(16) - // .wrapping(Wrapping::WordOrGlyph), - // text!("{}", dt.format("%x %X")).size(10) - // ] - // .clip(true) - // .spacing(6), - // ) - // .on_press_with(move || Message::ImpulseResponseSelected(id)) - // .width(Length::Fill) - // .style(move |theme: &Theme, status| { - // let base = button::subtle(theme, status); - // let background = theme.extended_palette().background; + let signal = measurement.signal()?; - // if is_active { - // base.with_background(background.weak.color) - // } else { - // base - // } - // }); + let entry = { + // TODO: refactor, basically the same btn as IR and Spectrogram + let dt: DateTime = signal.modified.into(); + let btn = button( + column![ + text(&measurement.name) + .size(16) + .wrapping(text::Wrapping::WordOrGlyph), + text!("{}", dt.format("%x %X")).size(10) + ] + .clip(true) + .spacing(6), + ) + // FIXME message type + .on_press_with(move || { + Message::ImpulseResponse(id, ui::impulse_response::Message::Select) + }) + .width(Length::Fill) + .style(move |theme: &Theme, status| { + let base = button::subtle(theme, status); + let background = theme.extended_palette().background; - // sidebar::item(btn, is_active) - // }; + if is_active { + base.with_background(background.weak.color) + } else { + base + } + }); - // let analysis = measurement.analysis()?; - // let entry = match analysis.spectral_decay_progress() { - // ui::spectral_decay::Progress::None => entry, - // ui::spectral_decay::Progress::ComputingImpulseResponse => { - // processing_overlay("Impulse Response", entry) - // } - // ui::spectral_decay::Progress::Computing => { - // processing_overlay("Spectral Decay", entry) - // } - // ui::spectral_decay::Progress::Finished => entry, - // }; + sidebar::item(btn, is_active) + }; - // Some(entry) - // }); + // FIXME + let analysis = analyses.get(&id); - // container(column![header, scrollable(column(entries))].spacing(6)) - // .padding(6) - // .style(|theme| { - // container::rounded_box(theme) - // .background(theme.extended_palette().background.weakest.color) - // }) - // }; + let entry = if let Some(analysis) = analysis { + match analysis.spectral_decay.progress() { + ui::spectral_decay::Progress::None => entry, + ui::spectral_decay::Progress::WaitingForImpulseResponse => { + processing_overlay("Impulse Response", entry) + } + ui::spectral_decay::Progress::Computing => { + processing_overlay("Computing ...", entry) + } + ui::spectral_decay::Progress::Finished => entry, + } + } else { + entry + }; - // let content = if let Some(decay) = self - // .measurements - // .iter() - // .find(|m| Some(m.id()) == selected) - // .and_then(ui::Measurement::analysis) - // .and_then(|analysis| analysis.spectral_decay.result()) - // { - // let gradient = colorous::MAGMA; + Some(entry) + }); - // let series_list = decay.iter().enumerate().map(|(fr_index, fr)| { - // let sample_rate = fr.sample_rate; - // let len = fr.data.len() * 2 + 1; - // let resolution = sample_rate as f32 / len as f32; + container(column![header, scrollable(column(entries))].spacing(6)) + .padding(6) + .style(|theme| { + container::rounded_box(theme) + .background(theme.extended_palette().background.weakest.color) + }) + }; - // let closure = move |(i, s)| (i as f32 * resolution, dbfs(s)); + let spectral_decay = selected + .and_then(|id| analyses.get(&id)) + .map(|a| &a.spectral_decay); - // 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 content = if let Some(decay) = spectral_decay.and_then(ui::SpectralDecay::result) { + let gradient = colorous::MAGMA; - // 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 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; - // container(chart) - // } else { - // container(text("Please select a frequency respone.")) - // }; + let closure = move |(i, s)| (i as f32 * resolution, dbfs(s)); - // row![ - // container(sidebar) - // .width(Length::FillPortion(2)) - // .style(container::bordered_box), - // container(content).width(Length::FillPortion(5)) - // ] - // .spacing(10) - // .into() - // } + 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); + + container(chart) + } else { + center(text("Please select a frequency respone.").size(18)) + }; + + row![ + container(sidebar) + .width(Length::FillPortion(2)) + .style(container::bordered_box), + container(content).width(Length::FillPortion(5)) + ] + .spacing(10) + .into() + } // fn spectrogram_tab<'a>( // &'a self, @@ -2146,6 +2284,36 @@ impl Main { // } } +fn compute_impulse_response( + analyses: &mut BTreeMap, + id: measurement::Id, + loopback: Option<&Loopback>, + measurements: &measurement::List, +) -> Task { + let Some(loopback) = loopback.and_then(Loopback::loaded) else { + return Task::none(); + }; + + let Some(measurement) = measurements.get(id).and_then(Measurement::signal) else { + return Task::none(); + }; + + let analysis = analyses.entry(id).or_default(); + + analysis + .impulse_response + .clone() + .compute(loopback, measurement) + .map(|sipper| { + Task::sip( + sipper, + Message::ImpulseResponseComputed.with(id), + Message::ImpulseResponseComputed.with(id), + ) + }) + .unwrap_or_default() +} + fn compute_frequency_response( analyses: &mut BTreeMap, id: measurement::Id, @@ -2173,40 +2341,30 @@ fn compute_frequency_response( } } -fn compute_impulse_response( - analyses: &mut BTreeMap, +fn compute_spectral_decay( id: measurement::Id, + analyses: &mut BTreeMap, + config: data::spectral_decay::Config, loopback: Option<&Loopback>, measurements: &measurement::List, ) -> Task { - let Some(loopback) = loopback.and_then(Loopback::loaded) else { - return Task::none(); - }; - - let Some(measurement) = measurements.get(id).and_then(Measurement::signal) else { - return Task::none(); - }; - let analysis = analyses.entry(id).or_default(); - analysis - .impulse_response - .clone() - .compute(loopback, measurement) - .map(|sipper| { - Task::sip( - sipper, - Message::ImpulseResponseComputed.with(id), - Message::ImpulseResponseComputed.with(id), - ) - }) - .unwrap_or_default() + if let Some(computation) = analysis + .spectral_decay + .compute_spectral_decay(&analysis.impulse_response, config) + { + Task::perform(computation, Message::SpectralDecayComputed.with(id)) + } else { + compute_impulse_response(analyses, id, loopback, measurements) + } } impl Default for Main { fn default() -> Self { Self { state: State::default(), + modal: Modal::None, selected: None, loopback: None, @@ -2214,10 +2372,11 @@ impl Default for Main { project_path: None, + spectral_decay_config: data::spectral_decay::Config::default(), + zoom: chart::Zoom::default(), offset: chart::Offset::default(), smoothing: frequency_response::Smoothing::default(), - // spectral_decay_config: data::spectral_decay::Config::default(), // spectrogram_config: spectrogram::Preferences::default(), window: None, @@ -2261,52 +2420,6 @@ async fn save_impulse_response(path: Arc, ir: ui::ImpulseResponse) { .unwrap(); } -// fn compute_frequency_response( -// loopback: &ui::Loopback, -// measurement: &mut ui::Measurement, -// window: &Window, -// impulse_response: &mut ui::impulse_response::State, -// frequency_response: &mut ui::FrequencyResponse, -// ) -> Task { -// let id = measurement.id(); - -// if let Some(impulse_response) = impulse_response.result() { -// frequency_response.progress = ui::frequency_response::Progress::Computing; - -// Task::perform( -// data::frequency_response::compute(impulse_response.origin.clone(), window.clone()), -// Message::FrequencyResponseComputed.with(id), -// ) -// } else { -// compute_impulse_response(loopback, measurement, impulse_response) -// } -// } - -// fn compute_spectral_decay( -// loopback: &ui::Loopback, -// measurement: &mut ui::Measurement, -// config: data::spectral_decay::Config, -// ) -> Task { -// let Some(analysis) = measurement.analysis_mut() else { -// return Task::none(); -// }; - -// if analysis.spectral_decay.result().is_some() { -// return Task::none(); -// } - -// if let Some(impulse_response) = analysis.impulse_response.result() { -// analysis.spectral_decay = ui::spectral_decay::State::Computing; - -// Task::perform( -// data::spectral_decay::compute(impulse_response.origin.clone(), config), -// Message::SpectralDecayComputed.with(measurement.id()), -// ) -// } else { -// compute_impulse_response(loopback, measurement) -// } -// } - // fn compute_spectrogram( // loopback: &ui::Loopback, // measurement: &mut ui::Measurement, diff --git a/raumklang-gui/src/screen/main/spectral_decay_config.rs b/raumklang-gui/src/screen/main/spectral_decay_config.rs index c9e6652..e09715b 100644 --- a/raumklang-gui/src/screen/main/spectral_decay_config.rs +++ b/raumklang-gui/src/screen/main/spectral_decay_config.rs @@ -22,8 +22,9 @@ pub(crate) enum Message { } pub(crate) enum Action { - Apply(spectral_decay::Config), + None, Discard, + Apply(spectral_decay::Config), } #[derive(Debug)] @@ -54,29 +55,29 @@ impl SpectralDecayConfig { self.right_window_width = config.right_window_width.as_millis().to_string(); } - pub(crate) fn update(&mut self, message: Message) -> Option { + pub(crate) fn update(&mut self, message: Message) -> Action { match message { - Message::Apply(config) => Some(Action::Apply(config)), - Message::Discard => Some(Action::Discard), + Message::Apply(config) => Action::Apply(config), + Message::Discard => Action::Discard, Message::ShiftChanged(shift) => { self.shift = shift; - None + Action::None } Message::LeftWidthChanged(left_width) => { self.left_window_width = left_width; - None + Action::None } Message::RightWidthChanged(right_width) => { self.right_window_width = right_width; - None + Action::None } Message::ResetToDefault => { self.reset_to_default(); - None + Action::None } Message::ResetToPrevious => { self.reset_to_config(self.prev_config); - None + Action::None } } } diff --git a/raumklang-gui/src/ui.rs b/raumklang-gui/src/ui.rs index 27a9ff6..2a4f9b2 100644 --- a/raumklang-gui/src/ui.rs +++ b/raumklang-gui/src/ui.rs @@ -9,3 +9,4 @@ 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/analysis.rs b/raumklang-gui/src/ui/analysis.rs index 6ddce37..6b0fe94 100644 --- a/raumklang-gui/src/ui/analysis.rs +++ b/raumklang-gui/src/ui/analysis.rs @@ -1,16 +1,12 @@ -use std::{future::Future, mem}; - -use raumklang_core::{Loopback, Measurement}; - -use crate::{ - data, - ui::{frequency_response, impulse_response, FrequencyResponse, ImpulseResponse}, +use crate::ui::{ + impulse_response, spectral_decay::SpectralDecay, FrequencyResponse, ImpulseResponse, }; #[derive(Debug, Clone, Default)] pub struct Analysis { pub impulse_response: impulse_response::State, pub frequency_response: FrequencyResponse, + pub spectral_decay: SpectralDecay, } impl Analysis { diff --git a/raumklang-gui/src/ui/impulse_response.rs b/raumklang-gui/src/ui/impulse_response.rs index f60668e..bfa4994 100644 --- a/raumklang-gui/src/ui/impulse_response.rs +++ b/raumklang-gui/src/ui/impulse_response.rs @@ -1,17 +1,16 @@ use std::time::SystemTime; use crate::{ - data::impulse_response, - data::{self, SampleRate}, + data::{self, impulse_response, SampleRate}, icon, - widget::sidebar, + widget::{processing_overlay, sidebar}, }; use chrono::{DateTime, Utc}; use iced::{ task::Sipper, - widget::{button, column, container, right, row, rule, stack, text}, - Color, Element, + widget::{button, column, right, row, rule, text}, + Element, Length::{Fill, Shrink}, }; @@ -151,26 +150,3 @@ pub fn view<'a>( _ => entry, } } - -fn processing_overlay<'a, Message>( - status: &'a str, - entry: impl Into>, -) -> Element<'a, Message> -where - Message: 'a, -{ - stack([ - container(entry).style(container::bordered_box).into(), - container(column![text("Computing..."), text(status).size(12)]) - .center(Fill) - .style(|theme| container::Style { - border: container::rounded_box(theme).border, - background: Some(iced::Background::Color(Color::from_rgba( - 0.0, 0.0, 0.0, 0.8, - ))), - ..Default::default() - }) - .into(), - ]) - .into() -} diff --git a/raumklang-gui/src/ui/spectral_decay.rs b/raumklang-gui/src/ui/spectral_decay.rs index 18cd157..b8dc054 100644 --- a/raumklang-gui/src/ui/spectral_decay.rs +++ b/raumklang-gui/src/ui/spectral_decay.rs @@ -1,31 +1,76 @@ -use crate::data; +use std::future::Future; + +use iced::Task; + +use crate::{ + data, + ui::{self, impulse_response}, + Message, +}; + +#[derive(Debug, Clone, Default)] +pub struct SpectralDecay(State); #[derive(Debug, Clone, Default)] -pub enum State { +enum State { #[default] None, + WaitingForImpulseResponse, Computing, Computed(data::SpectralDecay), } -#[derive(Debug, Clone)] -pub enum Progress { - None, - ComputingImpulseResponse, - Computing, - Finished, -} +impl SpectralDecay { + pub fn result(&self) -> Option<&data::SpectralDecay> { + let State::Computed(result) = &self.0 else { + return None; + }; -impl State { - pub(crate) fn computed(&mut self, decay: data::SpectralDecay) { - *self = State::Computed(decay) + Some(result) + } + pub fn progress(&self) -> Progress { + match self.0 { + State::None => Progress::None, + State::WaitingForImpulseResponse => Progress::WaitingForImpulseResponse, + State::Computing => Progress::Computing, + State::Computed(_) => Progress::Finished, + } } - pub(crate) fn result(&self) -> Option<&data::SpectralDecay> { - let State::Computed(spectral_decay) = self else { + pub fn compute_spectral_decay( + &mut self, + impulse_response: &impulse_response::State, + config: data::spectral_decay::Config, + ) -> Option> { + if self.result().is_some() { return None; - }; + } - Some(spectral_decay) + if let Some(impulse_response) = impulse_response.result() { + self.0 = ui::spectral_decay::State::Computing; + + let computation = data::spectral_decay::compute(impulse_response.data.clone(), config); + + Some(computation) + } else { + self.0 = ui::spectral_decay::State::WaitingForImpulseResponse; + None + } + } + + pub fn set_result(&mut self, spectral_decay: data::SpectralDecay) { + self.0 = State::Computed(spectral_decay); } + + pub fn reset(&mut self) { + self.0 = State::None + } +} + +#[derive(Debug, Clone)] +pub enum Progress { + None, + WaitingForImpulseResponse, + Computing, + Finished, } diff --git a/raumklang-gui/src/ui/spectrogram.rs b/raumklang-gui/src/ui/spectrogram.rs index 9a813a2..5ec2a32 100644 --- a/raumklang-gui/src/ui/spectrogram.rs +++ b/raumklang-gui/src/ui/spectrogram.rs @@ -9,11 +9,7 @@ pub enum State { } impl State { - pub(crate) fn computed(&mut self, data: data::Spectrogram) { - *self = State::Computed(data) - } - - pub(crate) fn result(&self) -> Option<&data::Spectrogram> { + pub fn result(&self) -> Option<&data::Spectrogram> { let State::Computed(data) = self else { return None; }; diff --git a/raumklang-gui/src/widget.rs b/raumklang-gui/src/widget.rs index 86a3b4a..1d04e69 100644 --- a/raumklang-gui/src/widget.rs +++ b/raumklang-gui/src/widget.rs @@ -5,8 +5,9 @@ pub use meter::RmsPeakMeter; use iced::{ alignment::Horizontal::Right, - widget::{text, text_input, tooltip}, - Element, Font, + widget::{column, container, stack, text, text_input, tooltip}, + Color, Element, Font, + Length::Fill, }; use std::fmt; @@ -46,3 +47,26 @@ pub fn number_input<'a, E: fmt::Display, Message: Clone + 'a>( } .into() } + +pub fn processing_overlay<'a, Message>( + status: &'a str, + entry: impl Into>, +) -> Element<'a, Message> +where + Message: 'a, +{ + stack([ + container(entry).style(container::bordered_box).into(), + container(column![text("Computing..."), text(status).size(12)]) + .center(Fill) + .style(|theme| container::Style { + border: container::rounded_box(theme).border, + background: Some(iced::Background::Color(Color::from_rgba( + 0.0, 0.0, 0.0, 0.8, + ))), + ..Default::default() + }) + .into(), + ]) + .into() +} From 4c7b977c8f981e3b0b4cd5042880af15ac167e0a Mon Sep 17 00:00:00 2001 From: Hendrik Kunert Date: Sat, 31 Jan 2026 22:43:27 +0100 Subject: [PATCH 19/30] Refactor modal --- raumklang-gui/src/screen/main.rs | 65 ++++--------------- raumklang-gui/src/screen/main/modal.rs | 16 +++++ .../src/screen/main/modal/pending_window.rs | 38 +++++++++++ .../main/{ => modal}/spectral_decay_config.rs | 16 ++--- 4 files changed, 73 insertions(+), 62 deletions(-) create mode 100644 raumklang-gui/src/screen/main/modal.rs create mode 100644 raumklang-gui/src/screen/main/modal/pending_window.rs rename raumklang-gui/src/screen/main/{ => modal}/spectral_decay_config.rs (93%) diff --git a/raumklang-gui/src/screen/main.rs b/raumklang-gui/src/screen/main.rs index f3ca6dd..65f052c 100644 --- a/raumklang-gui/src/screen/main.rs +++ b/raumklang-gui/src/screen/main.rs @@ -1,18 +1,20 @@ mod chart; mod frequency_response; mod impulse_response; +mod modal; mod recording; -mod spectral_decay_config; mod spectrogram_config; +use modal::Modal; + use crate::{ data::{ self, project, spectrogram, window, Project, RecentProjects, SampleRate, Samples, Window, }, icon, load_project, log, screen::main::{ - chart::waveform, spectral_decay_config::SpectralDecayConfig, - spectrogram_config::SpectrogramConfig, + chart::waveform, + modal::{pending_window, spectral_decay_config, SpectralDecayConfig}, }, ui::{self, analysis, measurement, Analysis, Loopback, Measurement}, widget::{processing_overlay, sidebar}, @@ -126,23 +128,6 @@ struct Spectrogram { pub cache: canvas::Cache, } -#[derive(Default, Debug)] -enum Modal { - #[default] - None, - PendingWindow { - goto_tab: TabId, - }, - SpectralDecayConfig(SpectralDecayConfig), - SpectrogramConfig(SpectrogramConfig), -} - -#[derive(Debug, Clone)] -pub enum ModalAction { - Discard, - Apply, -} - #[derive(Debug, Clone)] pub enum Message { NewProject, @@ -196,7 +181,7 @@ pub enum Message { SpectralDecayConfig(spectral_decay_config::Message), OpenSpectrogramConfig, SpectrogramConfig(spectrogram_config::Message), - Modal(ModalAction), + Modal(pending_window::Message), } #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -1581,41 +1566,13 @@ impl Main { match self.modal { Modal::None => content.into(), - Modal::PendingWindow { .. } => { - let pending_window = { - container( - column![ - text("Window pending!").size(18), - column![ - text("You have modified the window used for frequency response computations."), - text("You need to discard or apply your changes before proceeding."), - ].spacing(5), - row![ - space::horizontal(), - button("Discard") - .style(button::danger) - .on_press(Message::Modal(ModalAction::Discard)), - button("Apply") - .style(button::success) - .on_press(Message::Modal(ModalAction::Apply)) - ] - .spacing(5) - ] - .spacing(10)) - .padding(20) - .width(400) - .style(container::bordered_box) - }; - - modal(content, pending_window) - } Modal::SpectralDecayConfig(ref config) => { modal(content, config.view().map(Message::SpectralDecayConfig)) - } - Modal::SpectrogramConfig(ref spectrogram_config) => modal( - content, - spectrogram_config.view().map(Message::SpectrogramConfig), - ), + } // Modal::SpectrogramConfig(ref spectrogram_config) => modal( + // content, + // spectrogram_config.view().map(Message::SpectrogramConfig), + // ), + // Modal::PendingWindow { .. } => modal::pending_window().map(Message::Modal), } } // pub fn view<'a>(&'a self, recent_projects: &'a RecentProjects) -> Element<'a, Message> { diff --git a/raumklang-gui/src/screen/main/modal.rs b/raumklang-gui/src/screen/main/modal.rs new file mode 100644 index 0000000..049e205 --- /dev/null +++ b/raumklang-gui/src/screen/main/modal.rs @@ -0,0 +1,16 @@ +pub mod pending_window; +pub mod spectral_decay_config; + +pub use pending_window::pending_window; +pub use spectral_decay_config::SpectralDecayConfig; + +#[derive(Default, Debug)] +pub enum Modal { + #[default] + None, + // PendingWindow { + // goto_tab: TabId, + // }, + SpectralDecayConfig(SpectralDecayConfig), + // SpectrogramConfig(SpectrogramConfig), +} diff --git a/raumklang-gui/src/screen/main/modal/pending_window.rs b/raumklang-gui/src/screen/main/modal/pending_window.rs new file mode 100644 index 0000000..c3fd7de --- /dev/null +++ b/raumklang-gui/src/screen/main/modal/pending_window.rs @@ -0,0 +1,38 @@ +use iced::{ + widget::{button, column, container, row, space, text}, + Element, +}; + +#[derive(Debug, Clone)] +pub enum Message { + Discard, + Apply, +} + +pub fn pending_window() -> Element<'static, Message> { + container( + column![ + text("Window pending!").size(18), + column![ + text("You have modified the window used for frequency response computations."), + text("You need to discard or apply your changes before proceeding."), + ] + .spacing(5), + row![ + space::horizontal(), + button("Discard") + .style(button::danger) + .on_press(Message::Discard), + button("Apply") + .style(button::success) + .on_press(Message::Apply) + ] + .spacing(5) + ] + .spacing(10), + ) + .padding(20) + .width(400) + .style(container::bordered_box) + .into() +} diff --git a/raumklang-gui/src/screen/main/spectral_decay_config.rs b/raumklang-gui/src/screen/main/modal/spectral_decay_config.rs similarity index 93% rename from raumklang-gui/src/screen/main/spectral_decay_config.rs rename to raumklang-gui/src/screen/main/modal/spectral_decay_config.rs index e09715b..b22ff2e 100644 --- a/raumklang-gui/src/screen/main/spectral_decay_config.rs +++ b/raumklang-gui/src/screen/main/modal/spectral_decay_config.rs @@ -11,7 +11,7 @@ use iced::{ }; #[derive(Debug, Clone)] -pub(crate) enum Message { +pub enum Message { Discard, ResetToDefault, ResetToPrevious, @@ -21,14 +21,14 @@ pub(crate) enum Message { Apply(spectral_decay::Config), } -pub(crate) enum Action { +pub enum Action { None, Discard, Apply(spectral_decay::Config), } #[derive(Debug)] -pub(crate) struct SpectralDecayConfig { +pub struct SpectralDecayConfig { shift: String, left_window_width: String, right_window_width: String, @@ -36,7 +36,7 @@ pub(crate) struct SpectralDecayConfig { } impl SpectralDecayConfig { - pub(crate) fn new(config: spectral_decay::Config) -> Self { + pub fn new(config: spectral_decay::Config) -> Self { Self { shift: config.shift.as_millis().to_string(), left_window_width: config.left_window_width.as_millis().to_string(), @@ -45,17 +45,17 @@ impl SpectralDecayConfig { } } - pub(crate) fn reset_to_default(&mut self) { + pub fn reset_to_default(&mut self) { self.reset_to_config(spectral_decay::Config::default()); } - pub(crate) fn reset_to_config(&mut self, config: spectral_decay::Config) { + pub fn reset_to_config(&mut self, config: spectral_decay::Config) { self.shift = config.shift.as_millis().to_string(); self.left_window_width = config.left_window_width.as_millis().to_string(); self.right_window_width = config.right_window_width.as_millis().to_string(); } - pub(crate) fn update(&mut self, message: Message) -> Action { + pub fn update(&mut self, message: Message) -> Action { match message { Message::Apply(config) => Action::Apply(config), Message::Discard => Action::Discard, @@ -82,7 +82,7 @@ impl SpectralDecayConfig { } } - pub(crate) fn view(&self) -> Element<'_, Message> { + pub fn view(&self) -> Element<'_, Message> { let shift = Shift::from_millis_string(&self.shift); let left_window_width = WindowWidth::from_millis_string(&self.left_window_width); let right_window_width = WindowWidth::from_millis_string(&self.right_window_width); From d26e3e6217b561e3f819a116381e28a440a6882b Mon Sep 17 00:00:00 2001 From: Hendrik Kunert Date: Sat, 31 Jan 2026 23:32:57 +0100 Subject: [PATCH 20/30] Refactor spectrogram --- raumklang-gui/src/screen/main.rs | 329 ++++++++++++++----------- raumklang-gui/src/ui/analysis.rs | 4 +- raumklang-gui/src/ui/spectral_decay.rs | 6 +- raumklang-gui/src/ui/spectrogram.rs | 49 +++- 4 files changed, 242 insertions(+), 146 deletions(-) diff --git a/raumklang-gui/src/screen/main.rs b/raumklang-gui/src/screen/main.rs index 65f052c..7c2d729 100644 --- a/raumklang-gui/src/screen/main.rs +++ b/raumklang-gui/src/screen/main.rs @@ -70,12 +70,13 @@ pub struct Main { offset: chart::Offset, smoothing: frequency_response::Smoothing, - // project_path: Option, - spectral_decay_config: data::spectral_decay::Config, - // spectrogram_config: spectrogram::Preferences, window: Option>, ir_chart: impulse_response::Chart, + spectrogram: Spectrogram, + + spectral_decay_config: data::spectral_decay::Config, + spectrogram_config: spectrogram::Preferences, } #[allow(clippy::large_enum_variant)] @@ -149,6 +150,7 @@ pub enum Message { OpenImpulseResponses, OpenFrequencyResponses, OpenSpectralDecays, + OpenSpectrograms, ImpulseResponseSelected(measurement::Id), ImpulseResponseComputed(measurement::Id, data::ImpulseResponse), @@ -298,17 +300,39 @@ impl Main { }; if let Some(id) = selected { - // FIXME duplicate - // compute spectral decay - let analysis = analyses.entry(id).or_default(); - if let Some(computation) = analysis.spectral_decay.compute_spectral_decay( - &analysis.impulse_response, + compute_spectral_decay( + id, + analyses, self.spectral_decay_config, - ) { - Task::perform(computation, Message::SpectralDecayComputed.with(id)) - } else { - Task::none() - } + self.loopback.as_ref(), + &self.measurements, + ) + } else { + Task::none() + } + } + Message::OpenSpectrograms => { + let State::Analysing { + selected, + ref mut active_tab, + ref mut analyses, + .. + } = self.state + else { + return Task::none(); + }; + + *active_tab = Tab::Spectrogram; + self.spectrogram.cache.clear(); + + if let Some(id) = selected { + compute_spectrogram( + id, + analyses, + &self.spectrogram_config, + self.loopback.as_ref(), + &self.measurements, + ) } else { Task::none() } @@ -412,7 +436,13 @@ impl Main { &self.measurements, ), - Tab::Spectrogram => todo!(), + Tab::Spectrogram => compute_spectrogram( + id, + analyses, + &self.spectrogram_config, + self.loopback.as_ref(), + &self.measurements, + ), } } Message::ImpulseResponse(id, ui::impulse_response::Message::Save) => { @@ -486,13 +516,16 @@ impl Main { ), Tab::SpectralDecay { .. } => analysis .spectral_decay - .compute_spectral_decay( - &analysis.impulse_response, - self.spectral_decay_config, - ) + .compute(&analysis.impulse_response, self.spectral_decay_config) .map(|f| Task::perform(f, Message::SpectralDecayComputed.with(id))) .unwrap_or_default(), - Tab::Spectrogram => todo!(), + Tab::Spectrogram => compute_spectrogram( + id, + analyses, + &self.spectrogram_config, + self.loopback.as_ref(), + &self.measurements, + ), } } Message::FrequencyResponseComputed(id, new_fr) => { @@ -659,6 +692,25 @@ impl Main { } } } + Message::SpectrogramComputed(id, spectrogram) => { + let State::Analysing { + selected, + ref mut analyses, + .. + } = self.state + else { + return Task::none(); + }; + + let analysis = analyses.entry(id).or_default(); + analysis.spectrogram.set_result(spectrogram); + + if selected.is_some_and(|selected| selected == id) { + self.spectrogram.cache.clear(); + } + + Task::none() + } _ => Task::none(), } } @@ -1522,7 +1574,7 @@ impl Main { tab( "Spectrogram", matches!(active_tab, Some(Tab::Spectrogram)), - None // Message::OpenFrequencyResponses + active_tab.is_some().then_some(Message::OpenSpectrograms) ), ] .spacing(5) @@ -1557,7 +1609,7 @@ impl Main { Tab::SpectralDecay { cache } => { self.spectral_decay_tab(selected, &analyses, &cache) } - Tab::Spectrogram => todo!(), + Tab::Spectrogram => self.spectrogram_tab(selected, analyses, &self.spectrogram), }, } }; @@ -2061,103 +2113,109 @@ impl Main { .into() } - // fn spectrogram_tab<'a>( - // &'a self, - // selected: Option, - // spectrogram: &'a Spectrogram, - // ) -> Element<'a, Message> { - // let sidebar = { - // let header = { - // let config_btn = button(icon::settings().center()) - // .style(button::subtle) - // .on_press(Message::OpenSpectrogramConfig); - // Category::new("Spectrograms").push_button(config_btn) - // }; + fn spectrogram_tab<'a>( + &'a self, + selected: Option, + analyses: &'a BTreeMap, + spectrogram: &'a Spectrogram, + ) -> Element<'a, Message> { + let sidebar = { + let header = { + let config_btn = button(icon::settings().center()) + .style(button::subtle) + .on_press(Message::OpenSpectrogramConfig); + Category::new("Spectrograms").push_button(config_btn) + }; - // let entries = self.measurements.iter().flat_map(|measurement| { - // let id = measurement.id(); - // let is_active = selected.is_some_and(|selected| selected == id); - - // let signal = measurement.signal()?; - // let entry = { - // let dt: DateTime = signal.modified.into(); - // let btn = button( - // column![ - // text(&measurement.name) - // .size(16) - // .wrapping(Wrapping::WordOrGlyph), - // text!("{}", dt.format("%x %X")).size(10) - // ] - // .clip(true) - // .spacing(6), - // ) - // .on_press_with(move || Message::ImpulseResponseSelected(id)) - // .width(Length::Fill) - // .style(move |theme: &Theme, status| { - // let base = button::subtle(theme, status); - // let background = theme.extended_palette().background; - - // if is_active { - // base.with_background(background.weak.color) - // } else { - // base - // } - // }); + let entries = self.measurements.iter().flat_map(|measurement| { + let id = measurement.id(); + let is_active = selected.is_some_and(|selected| selected == id); - // sidebar::item(btn, is_active) - // }; + let signal = measurement.signal()?; + let entry = { + let dt: DateTime = signal.modified.into(); + let btn = button( + column![ + text(&measurement.name) + .size(16) + .wrapping(text::Wrapping::WordOrGlyph), + text!("{}", dt.format("%x %X")).size(10) + ] + .clip(true) + .spacing(6), + ) + .on_press_with(move || { + Message::ImpulseResponse(id, ui::impulse_response::Message::Select) + }) + .width(Length::Fill) + .style(move |theme: &Theme, status| { + let base = button::subtle(theme, status); + let background = theme.extended_palette().background; - // let analysis = measurement.analysis()?; - // let entry = match analysis.spectrogram_progress() { - // ui::spectrogram::Progress::None => entry, - // ui::spectrogram::Progress::ComputingImpulseResponse => { - // processing_overlay("Impulse Response", entry) - // } - // ui::spectrogram::Progress::Computing => { - // processing_overlay("Spectral Decay", entry) - // } - // ui::spectrogram::Progress::Finished => entry, - // }; + if is_active { + base.with_background(background.weak.color) + } else { + base + } + }); - // Some(entry) - // }); + sidebar::item(btn, is_active) + }; - // container(column![header, scrollable(column(entries))].spacing(6)) - // .padding(6) - // .style(|theme| { - // container::rounded_box(theme) - // .background(theme.extended_palette().background.weakest.color) - // }) - // }; + let spectrogram = &analyses.get(&id).map(|a| &a.spectrogram); + let entry = if let Some(spectrogram) = spectrogram { + match spectrogram.progress() { + ui::spectrogram::Progress::None => entry, + ui::spectrogram::Progress::ComputingImpulseResponse => { + processing_overlay("Impulse Response", entry) + } + ui::spectrogram::Progress::Computing => { + processing_overlay("Spectrogram", entry) + } + ui::spectrogram::Progress::Finished => entry, + } + } else { + entry + }; - // let spectrogram_data = selected - // .and_then(|id| self.measurements.get(id)) - // .and_then(ui::Measurement::analysis) - // .and_then(|analysis| analysis.spectrogram.result()); - - // let content = if let Some(data) = spectrogram_data { - // let chart = chart::spectrogram( - // data, - // &spectrogram.cache, - // spectrogram.zoom, - // spectrogram.offset, - // ) - // .map(Message::Spectrogram); - - // container(chart) - // } else { - // container(text("Please select a frequency respone.")) - // }; + Some(entry) + }); - // row![ - // container(sidebar) - // .width(Length::FillPortion(2)) - // .style(container::bordered_box), - // container(content).width(Length::FillPortion(5)) - // ] - // .spacing(10) - // .into() - // } + container(column![header, scrollable(column(entries))].spacing(6)) + .padding(6) + .style(|theme| { + container::rounded_box(theme) + .background(theme.extended_palette().background.weakest.color) + }) + }; + + let spectrogram_data = selected + .and_then(|id| analyses.get(&id)) + .and_then(|analysis| analysis.spectrogram.result()); + + let content = if let Some(data) = spectrogram_data { + let chart = chart::spectrogram( + data, + &spectrogram.cache, + spectrogram.zoom, + spectrogram.offset, + ) + .map(Message::Spectrogram); + + container(chart) + } else { + container(text("Please select a frequency respone.")) + }; + + row![ + container(sidebar) + .width(Length::FillPortion(2)) + .style(container::bordered_box), + container(content).width(Length::FillPortion(5)) + ] + .spacing(10) + .into() + } pub fn subscription(&self) -> Subscription { use keyboard::key; @@ -2309,7 +2367,7 @@ fn compute_spectral_decay( if let Some(computation) = analysis .spectral_decay - .compute_spectral_decay(&analysis.impulse_response, config) + .compute(&analysis.impulse_response, config) { Task::perform(computation, Message::SpectralDecayComputed.with(id)) } else { @@ -2317,6 +2375,25 @@ fn compute_spectral_decay( } } +fn compute_spectrogram( + id: measurement::Id, + analyses: &mut BTreeMap, + config: &spectrogram::Preferences, + loopback: Option<&ui::Loopback>, + measurements: &measurement::List, +) -> Task { + let analysis = analyses.entry(id).or_default(); + + if let Some(computation) = analysis + .spectrogram + .compute(&analysis.impulse_response, config) + { + Task::perform(computation, Message::SpectrogramComputed.with(id)) + } else { + compute_impulse_response(analyses, id, loopback, measurements) + } +} + impl Default for Main { fn default() -> Self { Self { @@ -2334,12 +2411,13 @@ impl Default for Main { zoom: chart::Zoom::default(), offset: chart::Offset::default(), smoothing: frequency_response::Smoothing::default(), - // spectrogram_config: spectrogram::Preferences::default(), window: None, signal_cache: canvas::Cache::default(), ir_chart: impulse_response::Chart::default(), + spectrogram: Spectrogram::default(), + spectrogram_config: spectrogram::Preferences::default(), } } } @@ -2377,31 +2455,6 @@ async fn save_impulse_response(path: Arc, ir: ui::ImpulseResponse) { .unwrap(); } -// fn compute_spectrogram( -// loopback: &ui::Loopback, -// measurement: &mut ui::Measurement, -// config: &spectrogram::Preferences, -// ) -> Task { -// let Some(analysis) = measurement.analysis_mut() else { -// return Task::none(); -// }; - -// if analysis.spectrogram.result().is_some() { -// return Task::none(); -// } - -// if let Some(impulse_response) = analysis.impulse_response.result() { -// analysis.spectrogram = ui::spectrogram::State::Computing; - -// Task::perform( -// data::spectrogram::compute(impulse_response.origin.clone(), *config), -// Message::SpectrogramComputed.with(measurement.id()), -// ) -// } else { -// compute_impulse_response(loopback, measurement) -// } -// } - fn modal<'a, Message>( base: impl Into>, content: impl Into>, diff --git a/raumklang-gui/src/ui/analysis.rs b/raumklang-gui/src/ui/analysis.rs index 6b0fe94..aba8791 100644 --- a/raumklang-gui/src/ui/analysis.rs +++ b/raumklang-gui/src/ui/analysis.rs @@ -1,5 +1,6 @@ use crate::ui::{ - impulse_response, spectral_decay::SpectralDecay, FrequencyResponse, ImpulseResponse, + impulse_response, spectral_decay::SpectralDecay, spectrogram::Spectrogram, FrequencyResponse, + ImpulseResponse, }; #[derive(Debug, Clone, Default)] @@ -7,6 +8,7 @@ pub struct Analysis { pub impulse_response: impulse_response::State, pub frequency_response: FrequencyResponse, pub spectral_decay: SpectralDecay, + pub spectrogram: Spectrogram, } impl Analysis { diff --git a/raumklang-gui/src/ui/spectral_decay.rs b/raumklang-gui/src/ui/spectral_decay.rs index b8dc054..b175ffb 100644 --- a/raumklang-gui/src/ui/spectral_decay.rs +++ b/raumklang-gui/src/ui/spectral_decay.rs @@ -37,7 +37,7 @@ impl SpectralDecay { } } - pub fn compute_spectral_decay( + pub fn compute( &mut self, impulse_response: &impulse_response::State, config: data::spectral_decay::Config, @@ -47,13 +47,13 @@ impl SpectralDecay { } if let Some(impulse_response) = impulse_response.result() { - self.0 = ui::spectral_decay::State::Computing; + self.0 = State::Computing; let computation = data::spectral_decay::compute(impulse_response.data.clone(), config); Some(computation) } else { - self.0 = ui::spectral_decay::State::WaitingForImpulseResponse; + self.0 = State::WaitingForImpulseResponse; None } } diff --git a/raumklang-gui/src/ui/spectrogram.rs b/raumklang-gui/src/ui/spectrogram.rs index 5ec2a32..6be5f0c 100644 --- a/raumklang-gui/src/ui/spectrogram.rs +++ b/raumklang-gui/src/ui/spectrogram.rs @@ -1,21 +1,62 @@ -use crate::data; +use std::future::Future; + +use crate::data::{self, spectrogram}; + +#[derive(Debug, Clone, Default)] +pub struct Spectrogram(State); #[derive(Debug, Clone, Default)] -pub enum State { +enum State { #[default] None, + WaitingForImpulseResponse, Computing, Computed(data::Spectrogram), } -impl State { +impl Spectrogram { pub fn result(&self) -> Option<&data::Spectrogram> { - let State::Computed(data) = self else { + let State::Computed(ref data) = self.0 else { return None; }; Some(data) } + + pub fn progress(&self) -> Progress { + match self.0 { + State::None => Progress::None, + State::WaitingForImpulseResponse => Progress::ComputingImpulseResponse, + State::Computing => Progress::Computing, + State::Computed(_) => Progress::Finished, + } + } + + pub fn compute( + &mut self, + impulse_response: &super::impulse_response::State, + config: &spectrogram::Preferences, + ) -> Option> { + if self.result().is_some() { + return None; + } + + if let Some(impulse_response) = impulse_response.result() { + self.0 = State::Computing; + + let computation = + data::spectrogram::compute(impulse_response.data.clone(), config.clone()); + + Some(computation) + } else { + self.0 = State::WaitingForImpulseResponse; + None + } + } + + pub fn set_result(&mut self, spectrogram: data::Spectrogram) { + self.0 = State::Computed(spectrogram); + } } #[derive(Debug, Clone)] From 91abb3adba14c1aeb6b72f75bbbc02c37a6eacc8 Mon Sep 17 00:00:00 2001 From: Hendrik Kunert Date: Mon, 2 Feb 2026 11:10:41 +0100 Subject: [PATCH 21/30] Refactor spectrogram config --- raumklang-gui/src/screen/main.rs | 60 ++++++++++++++++--- raumklang-gui/src/screen/main/modal.rs | 4 +- .../main/{ => modal}/spectrogram_config.rs | 12 ++-- raumklang-gui/src/ui/spectrogram.rs | 4 ++ 4 files changed, 66 insertions(+), 14 deletions(-) rename raumklang-gui/src/screen/main/{ => modal}/spectrogram_config.rs (95%) diff --git a/raumklang-gui/src/screen/main.rs b/raumklang-gui/src/screen/main.rs index 7c2d729..0d7961e 100644 --- a/raumklang-gui/src/screen/main.rs +++ b/raumklang-gui/src/screen/main.rs @@ -3,7 +3,6 @@ mod frequency_response; mod impulse_response; mod modal; mod recording; -mod spectrogram_config; use modal::Modal; @@ -14,7 +13,7 @@ use crate::{ icon, load_project, log, screen::main::{ chart::waveform, - modal::{pending_window, spectral_decay_config, SpectralDecayConfig}, + modal::{pending_window, spectral_decay_config, spectrogram_config, SpectralDecayConfig}, }, ui::{self, analysis, measurement, Analysis, Loopback, Measurement}, widget::{processing_overlay, sidebar}, @@ -711,6 +710,54 @@ impl Main { Task::none() } + Message::OpenSpectrogramConfig => { + self.modal = Modal::SpectrogramConfig(modal::SpectrogramConfig::new( + self.spectrogram_config, + )); + + Task::none() + } + Message::SpectrogramConfig(message) => { + let Modal::SpectrogramConfig(config) = &mut self.modal else { + return Task::none(); + }; + + let State::Analysing { + selected, + ref mut analyses, + .. + } = self.state + else { + return Task::none(); + }; + + match config.update(message) { + spectrogram_config::Action::None => Task::none(), + spectrogram_config::Action::Close => { + self.modal = Modal::None; + + Task::none() + } + spectrogram_config::Action::ConfigChanged(preferences) => { + self.modal = Modal::None; + self.spectrogram_config = preferences; + + analyses.values_mut().for_each(|a| a.spectrogram.reset()); + + if let Some(id) = selected { + analyses + .get_mut(&id) + .and_then(|a| { + a.spectrogram.compute(&a.impulse_response, &preferences) + }) + .map(|f| Task::perform(f, Message::SpectrogramComputed.with(id))) + .unwrap_or_default() + } else { + Task::none() + } + } + } + } _ => Task::none(), } } @@ -1620,11 +1667,10 @@ impl Main { Modal::None => content.into(), Modal::SpectralDecayConfig(ref config) => { modal(content, config.view().map(Message::SpectralDecayConfig)) - } // Modal::SpectrogramConfig(ref spectrogram_config) => modal( - // content, - // spectrogram_config.view().map(Message::SpectrogramConfig), - // ), - // Modal::PendingWindow { .. } => modal::pending_window().map(Message::Modal), + } + Modal::SpectrogramConfig(ref config) => { + modal(content, config.view().map(Message::SpectrogramConfig)) + } } } // pub fn view<'a>(&'a self, recent_projects: &'a RecentProjects) -> Element<'a, Message> { diff --git a/raumklang-gui/src/screen/main/modal.rs b/raumklang-gui/src/screen/main/modal.rs index 049e205..ceaf19e 100644 --- a/raumklang-gui/src/screen/main/modal.rs +++ b/raumklang-gui/src/screen/main/modal.rs @@ -1,8 +1,10 @@ pub mod pending_window; pub mod spectral_decay_config; +pub mod spectrogram_config; pub use pending_window::pending_window; pub use spectral_decay_config::SpectralDecayConfig; +pub use spectrogram_config::SpectrogramConfig; #[derive(Default, Debug)] pub enum Modal { @@ -12,5 +14,5 @@ pub enum Modal { // goto_tab: TabId, // }, SpectralDecayConfig(SpectralDecayConfig), - // SpectrogramConfig(SpectrogramConfig), + SpectrogramConfig(SpectrogramConfig), } diff --git a/raumklang-gui/src/screen/main/spectrogram_config.rs b/raumklang-gui/src/screen/main/modal/spectrogram_config.rs similarity index 95% rename from raumklang-gui/src/screen/main/spectrogram_config.rs rename to raumklang-gui/src/screen/main/modal/spectrogram_config.rs index bddac71..cbc195f 100644 --- a/raumklang-gui/src/screen/main/spectrogram_config.rs +++ b/raumklang-gui/src/screen/main/modal/spectrogram_config.rs @@ -9,7 +9,7 @@ use iced::{ }; #[derive(Debug, Clone)] -pub(crate) enum Message { +pub enum Message { Close, ResetToDefault, ResetToPrevious, @@ -19,14 +19,14 @@ pub(crate) enum Message { Apply(spectrogram::Preferences), } -pub(crate) enum Action { +pub enum Action { None, Close, ConfigChanged(spectrogram::Preferences), } #[derive(Debug, Clone)] -pub(crate) struct SpectrogramConfig { +pub struct SpectrogramConfig { window_width: String, span_before_peak: String, span_after_peak: String, @@ -34,7 +34,7 @@ pub(crate) struct SpectrogramConfig { } impl SpectrogramConfig { - pub(crate) fn new(config: spectrogram::Preferences) -> Self { + pub fn new(config: spectrogram::Preferences) -> Self { Self { window_width: config.window_width.as_millis().to_string(), span_before_peak: config.span_before_peak.as_millis().to_string(), @@ -44,7 +44,7 @@ impl SpectrogramConfig { } #[must_use] - pub(crate) fn update(&mut self, message: Message) -> Action { + pub fn update(&mut self, message: Message) -> Action { match message { Message::Close => Action::Close, Message::WindowWidthChanged(width) => { @@ -71,7 +71,7 @@ impl SpectrogramConfig { } } - pub(crate) fn view(&self) -> Element<'_, Message> { + pub fn view(&self) -> Element<'_, Message> { let window_width = self.window_width.parse().map(Duration::from_millis); let span_before_peak = self.span_before_peak.parse().map(Duration::from_millis); let span_after_peak = self.span_after_peak.parse().map(Duration::from_millis); diff --git a/raumklang-gui/src/ui/spectrogram.rs b/raumklang-gui/src/ui/spectrogram.rs index 6be5f0c..99010a4 100644 --- a/raumklang-gui/src/ui/spectrogram.rs +++ b/raumklang-gui/src/ui/spectrogram.rs @@ -57,6 +57,10 @@ impl Spectrogram { pub fn set_result(&mut self, spectrogram: data::Spectrogram) { self.0 = State::Computed(spectrogram); } + + pub fn reset(&mut self) { + self.0 = State::None + } } #[derive(Debug, Clone)] From eb32d07614f1c77ec6d001212c058f85cb20811d Mon Sep 17 00:00:00 2001 From: Hendrik Kunert Date: Mon, 2 Feb 2026 12:46:50 +0100 Subject: [PATCH 22/30] Refactor IR window settings --- raumklang-gui/Cargo.toml | 2 +- raumklang-gui/src/data/impulse_response.rs | 4 +- raumklang-gui/src/screen/main.rs | 393 +++++++++++------- raumklang-gui/src/screen/main/chart.rs | 19 +- .../src/screen/main/impulse_response.rs | 31 +- raumklang-gui/src/screen/main/modal.rs | 8 +- raumklang-gui/src/screen/main/tab.rs | 28 +- raumklang-gui/src/ui/impulse_response.rs | 12 +- raumklang-gui/src/ui/spectral_decay.rs | 5 +- raumklang-gui/src/ui/spectrogram.rs | 2 +- 10 files changed, 292 insertions(+), 212 deletions(-) diff --git a/raumklang-gui/Cargo.toml b/raumklang-gui/Cargo.toml index 0a99f9b..b8c94ad 100644 --- a/raumklang-gui/Cargo.toml +++ b/raumklang-gui/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "gui" version = "0.1.0" -edition = "2021" +edition = "2024" [[bin]] name = "raumklang" diff --git a/raumklang-gui/src/data/impulse_response.rs b/raumklang-gui/src/data/impulse_response.rs index a1e5cf9..30525a4 100644 --- a/raumklang-gui/src/data/impulse_response.rs +++ b/raumklang-gui/src/data/impulse_response.rs @@ -1,6 +1,6 @@ use std::sync::Arc; -use iced::task::{sipper, Sipper}; +use iced::task::{Sipper, sipper}; #[derive(Debug, Clone, Default)] pub struct ImpulseResponse(State); @@ -18,7 +18,7 @@ impl ImpulseResponse { self, loopback: &raumklang_core::Loopback, measurement: &raumklang_core::Measurement, - ) -> Option> { + ) -> Option + use<>> { if let State::Computing = self.0 { return None; } diff --git a/raumklang-gui/src/screen/main.rs b/raumklang-gui/src/screen/main.rs index 0d7961e..29f2843 100644 --- a/raumklang-gui/src/screen/main.rs +++ b/raumklang-gui/src/screen/main.rs @@ -3,23 +3,25 @@ mod frequency_response; mod impulse_response; mod modal; mod recording; +mod tab; use modal::Modal; +use tab::Tab; use crate::{ + PickAndLoadError, data::{ - self, project, spectrogram, window, Project, RecentProjects, SampleRate, Samples, Window, + self, Project, RecentProjects, SampleRate, Samples, Window, project, spectrogram, window, }, icon, load_project, log, screen::main::{ chart::waveform, - modal::{pending_window, spectral_decay_config, spectrogram_config, SpectralDecayConfig}, + modal::{SpectralDecayConfig, pending_window, spectral_decay_config, spectrogram_config}, }, - ui::{self, analysis, measurement, Analysis, Loopback, Measurement}, + ui::{self, Analysis, Loopback, Measurement, analysis, measurement}, widget::{processing_overlay, sidebar}, - PickAndLoadError, }; -use raumklang_core::{dbfs, WavLoadError}; +use raumklang_core::{WavLoadError, dbfs}; use impulse_response::{ChartOperation, WindowSettings}; use recording::Recording; @@ -28,21 +30,21 @@ use chrono::{DateTime, Utc}; use generic_overlay::generic_overlay::{dropdown_menu, dropdown_root}; use iced::{ + Alignment::{self, Center}, + Color, Element, Function, Length, Subscription, Task, Theme, advanced::{ - graphics::text::cosmic_text::skrifa::raw::tables::layout::SelectedScript, Renderer, + Renderer, graphics::text::cosmic_text::skrifa::raw::tables::layout::SelectedScript, }, alignment::{Horizontal, Vertical}, futures::{FutureExt, TryFutureExt}, keyboard, padding, - task::{sipper, Sipper}, + task::{Sipper, sipper}, widget::{ - button, canvas, center, column, container, opaque, pick_list, right, row, rule, scrollable, - space, stack, text, Button, + Button, button, canvas, center, column, container, opaque, pick_list, right, row, rule, + scrollable, space, stack, text, }, - Alignment::{self, Center}, - Color, Element, Function, Length, Subscription, Task, Theme, }; -use prism::{axis, line_series, Axis, Chart, Labels}; +use prism::{Axis, Chart, Labels, axis, line_series}; use rfd::FileHandle; use tracing::instrument::WithSubscriber; @@ -50,6 +52,7 @@ use std::{ collections::{BTreeMap, HashMap}, fmt::Display, future::Future, + mem, path::{Path, PathBuf}, sync::Arc, }; @@ -107,20 +110,6 @@ impl State { } } -pub enum Tab { - Measurements { recording: Option }, - ImpulseResponses { window_settings: WindowSettings }, - FrequencyResponses { cache: canvas::Cache }, - SpectralDecay { cache: canvas::Cache }, - Spectrogram, -} - -impl Default for Tab { - fn default() -> Self { - Self::Measurements { recording: None } - } -} - #[derive(Debug, Default)] struct Spectrogram { pub zoom: chart::Zoom, @@ -139,25 +128,23 @@ pub enum Message { LoadLoopback, LoopbackLoaded(Loopback), - LoadMeasurement, MeasurementLoaded(Measurement), - Measurement(measurement::Message), - OpenMeasurements, - OpenImpulseResponses, - OpenFrequencyResponses, - OpenSpectralDecays, - OpenSpectrograms, - + OpenTab(tab::Id), + // OpenMeasurements, + // OpenImpulseResponses, + // OpenFrequencyResponses, + // OpenSpectralDecays, + // OpenSpectrograms, ImpulseResponseSelected(measurement::Id), ImpulseResponseComputed(measurement::Id, data::ImpulseResponse), SaveImpulseResponseToFile(measurement::Id, Option>), FrequencyResponseComputed(measurement::Id, data::FrequencyResponse), ImpulseResponseSaved(measurement::Id, Arc), - ImpulseResponseChart(impulse_response::Message), + ImpulseResponseChart(impulse_response::ChartOperation), ImpulseResponse(ui::measurement::Id, ui::impulse_response::Message), FrequencyResponseToggled(measurement::Id, bool), @@ -182,7 +169,7 @@ pub enum Message { SpectralDecayConfig(spectral_decay_config::Message), OpenSpectrogramConfig, SpectrogramConfig(spectrogram_config::Message), - Modal(pending_window::Message), + PendingWindow(pending_window::Message), } #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -224,116 +211,140 @@ impl Main { pub fn update(&mut self, recent_projects: &mut RecentProjects, msg: Message) -> Task { match msg { - Message::OpenMeasurements => { + Message::OpenTab(tab) => { let State::Analysing { - active_tab: ref mut tab, + ref active_tab, .. + // selected, + // analyses, } = self.state else { return Task::none(); }; - *tab = Tab::Measurements { recording: None }; - - Task::none() - } - Message::OpenImpulseResponses => { - let State::Analysing { - active_tab: ref mut tab, - .. - } = self.state - else { + if let Tab::ImpulseResponses { window_settings } = active_tab + && !matches!(tab, tab::Id::ImpulseResponses) + && self + .window + .as_ref() + .is_none_or(|window| &window_settings.window != window) + { + self.modal = Modal::PendingWindow { goto_tab: tab }; return Task::none(); - }; + } - let Some(window) = &self.window else { - return Task::none(); - }; + match tab { + tab::Id::Measurements => { + let State::Analysing { + active_tab: ref mut tab, + .. + } = self.state + else { + return Task::none(); + }; - *tab = Tab::ImpulseResponses { - window_settings: WindowSettings::new(window.clone()), - }; + *tab = Tab::Measurements { recording: None }; - return Task::none(); - } + Task::none() + } + tab::Id::ImpulseResponses => { + let State::Analysing { + active_tab: ref mut tab, + .. + } = self.state + else { + return Task::none(); + }; - Message::OpenFrequencyResponses => { - let State::Analysing { - ref mut active_tab, - ref mut analyses, - .. - } = self.state - else { - return Task::none(); - }; + let Some(window) = &self.window else { + return Task::none(); + }; - *active_tab = Tab::FrequencyResponses { - cache: canvas::Cache::new(), - }; + *tab = Tab::ImpulseResponses { + window_settings: WindowSettings::new(window.clone()), + }; - let tasks = self.measurements.loaded().map(Measurement::id).map(|id| { - compute_frequency_response( - analyses, - id, - self.loopback.as_ref(), - &self.measurements, - self.window.as_ref().cloned().unwrap(), - ) - }); + return Task::none(); + } + tab::Id::FrequencyResponses => { + let State::Analysing { + ref mut active_tab, + ref mut analyses, + .. + } = self.state + else { + return Task::none(); + }; + + *active_tab = Tab::FrequencyResponses { + cache: canvas::Cache::new(), + }; + + let tasks = self.measurements.loaded().map(Measurement::id).map(|id| { + compute_frequency_response( + analyses, + id, + self.loopback.as_ref(), + &self.measurements, + self.window.as_ref().cloned().unwrap(), + ) + }); - Task::batch(tasks) - } - Message::OpenSpectralDecays => { - let State::Analysing { - selected, - ref mut active_tab, - ref mut analyses, - .. - } = self.state - else { - return Task::none(); - }; + Task::batch(tasks) + } + tab::Id::SpectralDecays => { + let State::Analysing { + selected, + ref mut active_tab, + ref mut analyses, + .. + } = self.state + else { + return Task::none(); + }; - *active_tab = Tab::SpectralDecay { - cache: canvas::Cache::new(), - }; + *active_tab = Tab::SpectralDecays { + cache: canvas::Cache::new(), + }; - if let Some(id) = selected { - compute_spectral_decay( - id, - analyses, - self.spectral_decay_config, - self.loopback.as_ref(), - &self.measurements, - ) - } else { - Task::none() - } - } - Message::OpenSpectrograms => { - let State::Analysing { - selected, - ref mut active_tab, - ref mut analyses, - .. - } = self.state - else { - return Task::none(); - }; + if let Some(id) = selected { + compute_spectral_decay( + id, + analyses, + self.spectral_decay_config, + self.loopback.as_ref(), + &self.measurements, + ) + } else { + Task::none() + } + } + tab::Id::Spectrograms => { + let State::Analysing { + selected, + ref mut active_tab, + ref mut analyses, + .. + } = self.state + else { + return Task::none(); + }; - *active_tab = Tab::Spectrogram; - self.spectrogram.cache.clear(); + *active_tab = Tab::Spectrograms; + self.spectrogram.cache.clear(); - if let Some(id) = selected { - compute_spectrogram( - id, - analyses, - &self.spectrogram_config, - self.loopback.as_ref(), - &self.measurements, - ) - } else { - Task::none() + if let Some(id) = selected { + compute_spectrogram( + id, + analyses, + &self.spectrogram_config, + self.loopback.as_ref(), + &self.measurements, + ) + } else { + Task::none() + } + } } } Message::LoadLoopback => Task::future(pick_file("Load Loopback ...")) @@ -427,7 +438,7 @@ impl Main { &self.measurements, ), Tab::FrequencyResponses { .. } => Task::none(), - Tab::SpectralDecay { .. } => compute_spectral_decay( + Tab::SpectralDecays { .. } => compute_spectral_decay( id, analyses, self.spectral_decay_config, @@ -435,7 +446,7 @@ impl Main { &self.measurements, ), - Tab::Spectrogram => compute_spectrogram( + Tab::Spectrograms => compute_spectrogram( id, analyses, &self.spectrogram_config, @@ -499,9 +510,10 @@ impl Main { return Task::none(); }; - let analysis = analyses.entry(id).or_default(); - analysis.impulse_response = - ui::impulse_response::State::from_data(impulse_response); + analyses.entry(id).and_modify(|analysis| { + analysis.impulse_response = + ui::impulse_response::State::from_data(impulse_response); + }); match active_tab { Tab::Measurements { .. } => Task::none(), @@ -513,12 +525,14 @@ impl Main { &self.measurements, self.window.as_ref().cloned().unwrap(), ), - Tab::SpectralDecay { .. } => analysis - .spectral_decay - .compute(&analysis.impulse_response, self.spectral_decay_config) - .map(|f| Task::perform(f, Message::SpectralDecayComputed.with(id))) - .unwrap_or_default(), - Tab::Spectrogram => compute_spectrogram( + Tab::SpectralDecays { .. } => compute_spectral_decay( + id, + analyses, + self.spectral_decay_config, + self.loopback.as_ref(), + &self.measurements, + ), + Tab::Spectrograms => compute_spectrogram( id, analyses, &self.spectrogram_config, @@ -527,6 +541,33 @@ impl Main { ), } } + Message::PendingWindow(action) => { + let State::Analysing { + active_tab, + analyses, + .. + } = &mut self.state + else { + return Task::none(); + }; + + let Modal::PendingWindow { goto_tab } = mem::take(&mut self.modal) else { + return Task::none(); + }; + + let tab = mem::take(active_tab); + if let Tab::ImpulseResponses { window_settings } = tab { + match action { + pending_window::Message::Discard => self.ir_chart.overlay_cache.clear(), + pending_window::Message::Apply => { + self.window = Some(window_settings.window); + analyses.values_mut().for_each(|a| *a = Analysis::default()); + } + } + } + + self.update(recent_projects, Message::OpenTab(goto_tab)) + } Message::FrequencyResponseComputed(id, new_fr) => { log::debug!("Frequency response computed: {id}"); @@ -628,7 +669,7 @@ impl Main { Message::SpectralDecayComputed(id, sd) => { let State::Analysing { ref mut analyses, - active_tab: Tab::SpectralDecay { ref cache }, + active_tab: Tab::SpectralDecays { ref cache }, .. } = self.state else { @@ -723,10 +764,8 @@ impl Main { }; let State::Analysing { - selected, - ref mut analyses, - .. - } = self.state + selected, analyses, .. + } = &mut self.state else { return Task::none(); }; @@ -747,10 +786,10 @@ impl Main { if let Some(id) = selected { analyses .get_mut(&id) - .and_then(|a| { + .and_then(move |a| { a.spectrogram.compute(&a.impulse_response, &preferences) }) - .map(|f| Task::perform(f, Message::SpectrogramComputed.with(id))) + .map(|f| Task::perform(f, Message::SpectrogramComputed.with(*id))) .unwrap_or_default() } else { Task::none() @@ -758,6 +797,37 @@ impl Main { } } } + Message::ImpulseResponseChart(operation) => { + let State::Analysing { + active_tab: + Tab::ImpulseResponses { + ref mut window_settings, + }, + .. + } = self.state + else { + return Task::none(); + }; + + if let ChartOperation::Interaction(ref interaction) = operation { + match interaction { + chart::Interaction::HandleMoved(index, new_pos) => { + let mut handles: window::Handles = Into::into(&window_settings.window); + handles.update(*index, *new_pos); + window_settings.window.update(handles); + } + chart::Interaction::ZoomChanged(zoom) => { + self.ir_chart.zoom = *zoom; + } + chart::Interaction::OffsetChanged(offset) => { + self.ir_chart.offset = *offset; + } + } + } + self.ir_chart.update(operation); + + Task::none() + } _ => Task::none(), } } @@ -1571,7 +1641,7 @@ impl Main { .width(Length::Fill) }; - let tab = |s, is_active, message| { + let tab = |s, is_active, id: Option<_>| { button(text(s).size(20)) .padding(10) .style(move |theme: &Theme, status| { @@ -1589,39 +1659,35 @@ impl Main { base } }) - .on_press_maybe(message) + .on_press_maybe(id.map(Message::OpenTab)) }; let active_tab = self.state.active_tab(); let tabs = row![ tab( "Measurements", - active_tab.is_none(), - Some(Message::OpenMeasurements) + active_tab.is_none() || matches!(active_tab, Some(Tab::Measurements { .. })), + Some(tab::Id::Measurements) ), tab( "Impulse Responses", matches!(active_tab, Some(Tab::ImpulseResponses { .. })), - active_tab - .is_some() - .then_some(Message::OpenImpulseResponses) + active_tab.is_some().then_some(tab::Id::ImpulseResponses) ), tab( "Frequency Responses", matches!(active_tab, Some(Tab::FrequencyResponses { .. })), - active_tab - .is_some() - .then_some(Message::OpenFrequencyResponses) + active_tab.is_some().then_some(tab::Id::FrequencyResponses) ), tab( "Spectral Decays", - matches!(active_tab, Some(Tab::SpectralDecay { .. })), - active_tab.is_some().then_some(Message::OpenSpectralDecays) + matches!(active_tab, Some(Tab::SpectralDecays { .. })), + active_tab.is_some().then_some(tab::Id::SpectralDecays) ), tab( "Spectrogram", - matches!(active_tab, Some(Tab::Spectrogram)), - active_tab.is_some().then_some(Message::OpenSpectrograms) + matches!(active_tab, Some(Tab::Spectrograms)), + active_tab.is_some().then_some(tab::Id::Spectrograms) ), ] .spacing(5) @@ -1653,10 +1719,12 @@ impl Main { Tab::FrequencyResponses { cache } => { self.frequency_responses_tab(cache, analyses) } - Tab::SpectralDecay { cache } => { + Tab::SpectralDecays { cache } => { self.spectral_decay_tab(selected, &analyses, &cache) } - Tab::Spectrogram => self.spectrogram_tab(selected, analyses, &self.spectrogram), + Tab::Spectrograms => { + self.spectrogram_tab(selected, analyses, &self.spectrogram) + } }, } }; @@ -1665,6 +1733,9 @@ impl Main { match self.modal { Modal::None => content.into(), + Modal::PendingWindow { .. } => { + modal(content, modal::pending_window().map(Message::PendingWindow)) + } Modal::SpectralDecayConfig(ref config) => { modal(content, config.view().map(Message::SpectralDecayConfig)) } diff --git a/raumklang-gui/src/screen/main/chart.rs b/raumklang-gui/src/screen/main/chart.rs index 90c471c..eaa4a6e 100644 --- a/raumklang-gui/src/screen/main/chart.rs +++ b/raumklang-gui/src/screen/main/chart.rs @@ -4,12 +4,15 @@ pub mod waveform; use waveform::Waveform; use crate::{ - data::{self, chart, window::Handles, Samples, Window}, + data::{self, Samples, Window, chart, window::Handles}, screen::main::chart::spectrogram::Spectrogram, ui, }; use iced::{ + Element, Event, Font, + Length::Fill, + Pixels, Point, Rectangle, Renderer, Size, Theme, Vector, advanced::{ graphics::text::Paragraph, text::{self, Paragraph as _}, @@ -21,9 +24,7 @@ use iced::{ container, text::{Fragment, IntoFragment}, }, - window, Element, Event, Font, - Length::Fill, - Pixels, Point, Rectangle, Renderer, Size, Theme, Vector, + window, }; use std::{ @@ -318,8 +319,8 @@ where x_axis, y_axis, plane, - ref mut hovered_handle, - ref mut dragging, + hovered_handle, + dragging, .. } = state else { @@ -411,7 +412,7 @@ where let State::Initialized { hovered_handle, plane, - ref mut dragging, + dragging, .. } = state else { @@ -435,8 +436,8 @@ where let State::Initialized { x_axis, plane, - ref mut hovered_handle, - ref mut dragging, + hovered_handle, + dragging, .. } = state else { diff --git a/raumklang-gui/src/screen/main/impulse_response.rs b/raumklang-gui/src/screen/main/impulse_response.rs index 2bcb56d..62e792d 100644 --- a/raumklang-gui/src/screen/main/impulse_response.rs +++ b/raumklang-gui/src/screen/main/impulse_response.rs @@ -6,17 +6,12 @@ use crate::{ }; use iced::{ - widget::{canvas, column, container, pick_list, row, stack, text}, - Alignment, Color, Element, Length, + Alignment, Element, Length, + widget::{canvas, column, container, pick_list, row}, }; use std::ops::RangeInclusive; -#[derive(Debug, Clone)] -pub enum Message { - Chart(ChartOperation), -} - #[derive(Debug, Clone)] pub enum ChartOperation { TimeUnitChanged(data::chart::TimeSeriesUnit), @@ -57,12 +52,12 @@ impl Chart { &'a self, impulse_response: &'a ImpulseResponse, window_settings: &'a WindowSettings, - ) -> Element<'a, Message> { + ) -> Element<'a, ChartOperation> { let header = { pick_list( &data::chart::AmplitudeUnit::ALL[..], Some(&self.amplitude_unit), - |unit| Message::Chart(ChartOperation::AmplitudeUnitChanged(unit)), + |unit| ChartOperation::AmplitudeUnitChanged(unit), ) }; @@ -78,19 +73,20 @@ impl Chart { &self.data_cache, &self.overlay_cache, ) - .map(ChartOperation::Interaction) - .map(Message::Chart), + .map(ChartOperation::Interaction), ) .style(container::rounded_box) }; let footer = { - row![container(pick_list( - &data::chart::TimeSeriesUnit::ALL[..], - Some(&self.time_unit), - |unit| Message::Chart(ChartOperation::TimeUnitChanged(unit)) - )) - .align_right(Length::Fill)] + row![ + container(pick_list( + &data::chart::TimeSeriesUnit::ALL[..], + Some(&self.time_unit), + |unit| ChartOperation::TimeUnitChanged(unit) + )) + .align_right(Length::Fill) + ] .align_y(Alignment::Center) }; @@ -106,6 +102,7 @@ impl Chart { } } +// FIXME pub struct WindowSettings { pub window: data::Window, } diff --git a/raumklang-gui/src/screen/main/modal.rs b/raumklang-gui/src/screen/main/modal.rs index ceaf19e..3b15b54 100644 --- a/raumklang-gui/src/screen/main/modal.rs +++ b/raumklang-gui/src/screen/main/modal.rs @@ -6,13 +6,15 @@ pub use pending_window::pending_window; pub use spectral_decay_config::SpectralDecayConfig; pub use spectrogram_config::SpectrogramConfig; +use crate::screen::main::{impulse_response::WindowSettings, tab}; + #[derive(Default, Debug)] pub enum Modal { #[default] None, - // PendingWindow { - // goto_tab: TabId, - // }, + PendingWindow { + goto_tab: tab::Id, + }, SpectralDecayConfig(SpectralDecayConfig), SpectrogramConfig(SpectrogramConfig), } diff --git a/raumklang-gui/src/screen/main/tab.rs b/raumklang-gui/src/screen/main/tab.rs index 49c0aa6..5de30e3 100644 --- a/raumklang-gui/src/screen/main/tab.rs +++ b/raumklang-gui/src/screen/main/tab.rs @@ -1,16 +1,26 @@ -mod impulse_response; - -pub use impulse_response::ImpulseResponses; -pub use impulse_response::WindowSettings; - -use crate::screen::main::recording::Recording; - use iced::widget::canvas; +use crate::screen::main::{impulse_response::WindowSettings, recording::Recording}; + pub enum Tab { Measurements { recording: Option }, ImpulseResponses { window_settings: WindowSettings }, FrequencyResponses { cache: canvas::Cache }, - SpectralDecay, - Spectrogram, + SpectralDecays { cache: canvas::Cache }, + Spectrograms, +} + +#[derive(Debug, Clone, Copy)] +pub enum Id { + Measurements, + ImpulseResponses, + FrequencyResponses, + SpectralDecays, + Spectrograms, +} + +impl Default for Tab { + fn default() -> Self { + Self::Measurements { recording: None } + } } diff --git a/raumklang-gui/src/ui/impulse_response.rs b/raumklang-gui/src/ui/impulse_response.rs index bfa4994..dd8c5d6 100644 --- a/raumklang-gui/src/ui/impulse_response.rs +++ b/raumklang-gui/src/ui/impulse_response.rs @@ -1,17 +1,17 @@ use std::time::SystemTime; use crate::{ - data::{self, impulse_response, SampleRate}, + data::{self, SampleRate, impulse_response}, icon, widget::{processing_overlay, sidebar}, }; use chrono::{DateTime, Utc}; use iced::{ - task::Sipper, - widget::{button, column, right, row, rule, text}, Element, Length::{Fill, Shrink}, + task::Sipper, + widget::{button, column, right, row, rule, text}, }; #[derive(Debug, Clone)] @@ -44,7 +44,7 @@ impl State { pub(crate) fn result(&self) -> Option<&ImpulseResponse> { match self { State::Computing(_) => None, - State::Computed(ref impulse_response) => Some(impulse_response), + State::Computed(impulse_response) => Some(impulse_response), } } @@ -52,9 +52,9 @@ impl State { &self, loopback: &raumklang_core::Loopback, measurement: &raumklang_core::Measurement, - ) -> Option> { + ) -> Option + use<>> { match self { - State::Computing(ref impulse_response) => { + State::Computing(impulse_response) => { impulse_response.clone().compute(loopback, measurement) } State::Computed(_) => None, diff --git a/raumklang-gui/src/ui/spectral_decay.rs b/raumklang-gui/src/ui/spectral_decay.rs index b175ffb..f5031bf 100644 --- a/raumklang-gui/src/ui/spectral_decay.rs +++ b/raumklang-gui/src/ui/spectral_decay.rs @@ -3,9 +3,8 @@ use std::future::Future; use iced::Task; use crate::{ - data, + Message, data, ui::{self, impulse_response}, - Message, }; #[derive(Debug, Clone, Default)] @@ -41,7 +40,7 @@ impl SpectralDecay { &mut self, impulse_response: &impulse_response::State, config: data::spectral_decay::Config, - ) -> Option> { + ) -> Option + use<>> { if self.result().is_some() { return None; } diff --git a/raumklang-gui/src/ui/spectrogram.rs b/raumklang-gui/src/ui/spectrogram.rs index 99010a4..064b69d 100644 --- a/raumklang-gui/src/ui/spectrogram.rs +++ b/raumklang-gui/src/ui/spectrogram.rs @@ -36,7 +36,7 @@ impl Spectrogram { &mut self, impulse_response: &super::impulse_response::State, config: &spectrogram::Preferences, - ) -> Option> { + ) -> Option + use<>> { if self.result().is_some() { return None; } From 05d3b01cb64aafe4f5e73648a3d2ab9c4d9c9d55 Mon Sep 17 00:00:00 2001 From: Hendrik Kunert Date: Mon, 2 Feb 2026 13:01:24 +0100 Subject: [PATCH 23/30] Remove unnecessary `WindowSettings` wrapper --- raumklang-gui/src/data/window.rs | 2 +- raumklang-gui/src/screen/main.rs | 31 +++++++++---------- .../src/screen/main/impulse_response.rs | 17 ++-------- raumklang-gui/src/screen/main/modal.rs | 2 +- raumklang-gui/src/screen/main/tab.rs | 4 +-- 5 files changed, 22 insertions(+), 34 deletions(-) diff --git a/raumklang-gui/src/data/window.rs b/raumklang-gui/src/data/window.rs index 23781af..921d957 100644 --- a/raumklang-gui/src/data/window.rs +++ b/raumklang-gui/src/data/window.rs @@ -7,7 +7,7 @@ use super::{SampleRate, Samples}; use std::time::Duration; #[derive(Debug, Clone, PartialEq)] -pub struct Window { +pub struct Window { sample_rate: SampleRate, left_type: raumklang_core::Window, left_width: D, diff --git a/raumklang-gui/src/screen/main.rs b/raumklang-gui/src/screen/main.rs index 29f2843..134d99d 100644 --- a/raumklang-gui/src/screen/main.rs +++ b/raumklang-gui/src/screen/main.rs @@ -23,7 +23,7 @@ use crate::{ }; use raumklang_core::{WavLoadError, dbfs}; -use impulse_response::{ChartOperation, WindowSettings}; +use impulse_response::ChartOperation; use recording::Recording; use chrono::{DateTime, Utc}; @@ -222,12 +222,12 @@ impl Main { return Task::none(); }; - if let Tab::ImpulseResponses { window_settings } = active_tab + if let Tab::ImpulseResponses { pending_window } = active_tab && !matches!(tab, tab::Id::ImpulseResponses) && self .window .as_ref() - .is_none_or(|window| &window_settings.window != window) + .is_none_or(|window| pending_window != window) { self.modal = Modal::PendingWindow { goto_tab: tab }; return Task::none(); @@ -261,7 +261,7 @@ impl Main { }; *tab = Tab::ImpulseResponses { - window_settings: WindowSettings::new(window.clone()), + pending_window: window.clone(), }; return Task::none(); @@ -556,11 +556,11 @@ impl Main { }; let tab = mem::take(active_tab); - if let Tab::ImpulseResponses { window_settings } = tab { + if let Tab::ImpulseResponses { pending_window } = tab { match action { pending_window::Message::Discard => self.ir_chart.overlay_cache.clear(), pending_window::Message::Apply => { - self.window = Some(window_settings.window); + self.window = Some(pending_window); analyses.values_mut().for_each(|a| *a = Analysis::default()); } } @@ -799,12 +799,9 @@ impl Main { } Message::ImpulseResponseChart(operation) => { let State::Analysing { - active_tab: - Tab::ImpulseResponses { - ref mut window_settings, - }, + active_tab: Tab::ImpulseResponses { pending_window }, .. - } = self.state + } = &mut self.state else { return Task::none(); }; @@ -812,9 +809,9 @@ impl Main { if let ChartOperation::Interaction(ref interaction) = operation { match interaction { chart::Interaction::HandleMoved(index, new_pos) => { - let mut handles: window::Handles = Into::into(&window_settings.window); + let mut handles = window::Handles::from(&*pending_window); handles.update(*index, *new_pos); - window_settings.window.update(handles); + pending_window.update(handles); } chart::Interaction::ZoomChanged(zoom) => { self.ir_chart.zoom = *zoom; @@ -1710,7 +1707,9 @@ impl Main { ref analyses, } => match active_tab { Tab::Measurements { .. } => self.measurements_tab(), - Tab::ImpulseResponses { window_settings } => self.impulse_responses_tab( + Tab::ImpulseResponses { + pending_window: window_settings, + } => self.impulse_responses_tab( selected, &self.ir_chart, window_settings, @@ -1936,7 +1935,7 @@ impl Main { &'a self, selected: Option, chart: &'a impulse_response::Chart, - window_settings: &'a WindowSettings, + window: &'a Window, analyses: &'a BTreeMap, ) -> Element<'a, Message> { let sidebar = { @@ -1977,7 +1976,7 @@ impl Main { .and_then(Analysis::impulse_response) .map(|impulse_response| { chart - .view(impulse_response, window_settings) + .view(impulse_response, window) .map(Message::ImpulseResponseChart) }) .unwrap_or(placeholder.into()) diff --git a/raumklang-gui/src/screen/main/impulse_response.rs b/raumklang-gui/src/screen/main/impulse_response.rs index 62e792d..6d33ac0 100644 --- a/raumklang-gui/src/screen/main/impulse_response.rs +++ b/raumklang-gui/src/screen/main/impulse_response.rs @@ -1,7 +1,7 @@ use super::chart; use crate::{ - data::{self}, + data::{self, Window}, ui::ImpulseResponse, }; @@ -51,7 +51,7 @@ impl Chart { pub(crate) fn view<'a>( &'a self, impulse_response: &'a ImpulseResponse, - window_settings: &'a WindowSettings, + window: &'a Window, ) -> Element<'a, ChartOperation> { let header = { pick_list( @@ -64,7 +64,7 @@ impl Chart { let chart = { container( chart::impulse_response( - &window_settings.window, + &window, impulse_response, &self.time_unit, &self.amplitude_unit, @@ -101,14 +101,3 @@ impl Chart { self.shift_key_pressed = true } } - -// FIXME -pub struct WindowSettings { - pub window: data::Window, -} - -impl WindowSettings { - pub(crate) fn new(window: data::Window) -> Self { - Self { window } - } -} diff --git a/raumklang-gui/src/screen/main/modal.rs b/raumklang-gui/src/screen/main/modal.rs index 3b15b54..040ef12 100644 --- a/raumklang-gui/src/screen/main/modal.rs +++ b/raumklang-gui/src/screen/main/modal.rs @@ -6,7 +6,7 @@ pub use pending_window::pending_window; pub use spectral_decay_config::SpectralDecayConfig; pub use spectrogram_config::SpectrogramConfig; -use crate::screen::main::{impulse_response::WindowSettings, tab}; +use crate::screen::main::tab; #[derive(Default, Debug)] pub enum Modal { diff --git a/raumklang-gui/src/screen/main/tab.rs b/raumklang-gui/src/screen/main/tab.rs index 5de30e3..3b82b32 100644 --- a/raumklang-gui/src/screen/main/tab.rs +++ b/raumklang-gui/src/screen/main/tab.rs @@ -1,10 +1,10 @@ use iced::widget::canvas; -use crate::screen::main::{impulse_response::WindowSettings, recording::Recording}; +use crate::{data::Window, screen::main::recording::Recording}; pub enum Tab { Measurements { recording: Option }, - ImpulseResponses { window_settings: WindowSettings }, + ImpulseResponses { pending_window: Window }, FrequencyResponses { cache: canvas::Cache }, SpectralDecays { cache: canvas::Cache }, Spectrograms, From 98d3aa8552903561547cbbaed4837fca1026242e Mon Sep 17 00:00:00 2001 From: Hendrik Kunert Date: Mon, 2 Feb 2026 17:20:54 +0100 Subject: [PATCH 24/30] Improve measurements button styling --- raumklang-gui/fonts/icons.toml | 3 ++- raumklang-gui/fonts/icons.ttf | Bin 7300 -> 7476 bytes raumklang-gui/src/icon.rs | 6 ++++- raumklang-gui/src/screen/main.rs | 24 ++++++------------- raumklang-gui/src/ui/measurement.rs | 10 ++++---- raumklang-gui/src/ui/measurement/loopback.rs | 8 ++----- raumklang-gui/src/widget/sidebar.rs | 9 ++++++- 7 files changed, 28 insertions(+), 32 deletions(-) diff --git a/raumklang-gui/fonts/icons.toml b/raumklang-gui/fonts/icons.toml index 3f9d8ba..ac5ece7 100644 --- a/raumklang-gui/fonts/icons.toml +++ b/raumklang-gui/fonts/icons.toml @@ -1,8 +1,9 @@ module = "icon" [glyphs] -delete = "fontawesome-trash" +plus = "fontawesome-plus" record = "typicons-record" +delete = "fontawesome-trash" download = "typicons-download" settings = "fontawesome-cog-alt" reset = "fontawesome-ccw" diff --git a/raumklang-gui/fonts/icons.ttf b/raumklang-gui/fonts/icons.ttf index e9ef9d79fa8f846be38b1f72355516bc592ea54a..033906d956075fda38d6ca65d7f97760566817b9 100644 GIT binary patch delta 692 zcmZ8fO=uHA6n<}Kvm4Vit-)+WLRsCKAkl)2P0;4hgMx^lpww0mbsO^oP3&S~NhGuf zt0xa?Ks>3J78C>_TD=G&z3EAmUOen46GA9Iow8jY z-70tzytiPN9nSQe0zV6GUbhSBH@8KM~y?dmYH2iL5UN@t*Z$xRCCXm}&CwYsp`l;`@7$^CLM zrvH7CDezLsg;J~SSB@|vew*{QXUE!+NJEb904E7=bb!4S+3El;w7Sp%{Ad$5m4Gw< zT5Ci4V0UX<_(d04bZ8_@s!;DhJbG|`j%O+BI^ue`Qc!At1m+DT7B*QG9Y*JeHY{B)XsDx*qS^y4|m|DxAlcz!4W cofQ_WvWqp=*-fgGzget3?GD$MyFd8;0=ANs_y7O^ delta 535 zcmYjNJuE{}6h8O9*Vm@%m864&p@Zns_*n=#SZs(OLXf(u+M%y~s-(ovz$Otqqr}3% zB#M|ASR`GH5`#rF79k`x)puVbac{nR&iCDO&OP^>cWkt8``1`nZu9&HDCBB?!w9Y7(i4a0b;k@I7$d5LuH&z1`r4;QpjA zq8EC$2#yE&v6-67OpI40(12`Y&`d|o)yvLHqT0evag3#`Ar|5d@76ABnMe~u3{ip< z+Y7?F@y#@;)CJv5F5-mfz~}SPM^Se5-Cs}=ZLvCKb G(SHHC{(I{H diff --git a/raumklang-gui/src/icon.rs b/raumklang-gui/src/icon.rs index 5bc4caf..9b9c74b 100644 --- a/raumklang-gui/src/icon.rs +++ b/raumklang-gui/src/icon.rs @@ -1,6 +1,6 @@ // Generated automatically by iced_fontello at build time. // Do not edit manually. Source: ../fonts/icons.toml -// a31a04bdecfefc20c79309ff6399870e033e64c8b1bb66096a94c9e027c9d20c +// 44042cbeec84dc8662b531772612b8dd3be566793229f21ef656918a300a1863 use iced::Font; use iced::widget::{Text, text}; @@ -14,6 +14,10 @@ pub fn download<'a>() -> Text<'a> { icon("\u{1F4E5}") } +pub fn plus<'a>() -> Text<'a> { + icon("\u{2B}") +} + pub fn record<'a>() -> Text<'a> { icon("\u{E0A5}") } diff --git a/raumklang-gui/src/screen/main.rs b/raumklang-gui/src/screen/main.rs index 134d99d..dbe45ae 100644 --- a/raumklang-gui/src/screen/main.rs +++ b/raumklang-gui/src/screen/main.rs @@ -1841,15 +1841,10 @@ impl Main { fn measurements_tab<'a>(&'a self) -> Element<'a, Message> { let sidebar = { let loopback = Category::new("Loopback") + .push_button(sidebar::button(icon::plus()).on_press(Message::LoadLoopback)) .push_button( - button("+") - .on_press(Message::LoadLoopback) - .style(button::secondary), - ) - .push_button( - button(icon::record()) - .on_press(Message::StartRecording(recording::Kind::Loopback)) - .style(button::secondary), + sidebar::button(icon::record()) + .on_press(Message::StartRecording(recording::Kind::Loopback)), ) .push_entry_maybe(self.loopback.as_ref().map(|loopback| { let active = self.selected == Some(measurement::Selected::Loopback); @@ -1857,15 +1852,10 @@ impl Main { })); let measurements = Category::new("Measurements") + .push_button(sidebar::button(icon::plus()).on_press(Message::LoadMeasurement)) .push_button( - button("+") - .style(button::secondary) - .on_press(Message::LoadMeasurement), - ) - .push_button( - button(icon::record()) - .on_press(Message::StartRecording(recording::Kind::Measurement)) - .style(button::secondary), + sidebar::button(icon::record()) + .on_press(Message::StartRecording(recording::Kind::Measurement)), ) .extend_entries(self.measurements.iter().map(|measurement| { let active = @@ -2641,7 +2631,7 @@ where pub fn view(self) -> Element<'a, Message> { let header = row![sidebar::header(self.title),] .padding(padding::right(6)) - .extend(self.buttons.into_iter().map(|btn| btn.width(30).into())) + .extend(self.buttons.into_iter().map(|btn| btn.into())) .spacing(6) .align_y(Alignment::Center); diff --git a/raumklang-gui/src/ui/measurement.rs b/raumklang-gui/src/ui/measurement.rs index 3723149..bf71b5b 100644 --- a/raumklang-gui/src/ui/measurement.rs +++ b/raumklang-gui/src/ui/measurement.rs @@ -4,10 +4,10 @@ pub use loopback::Loopback; use chrono::{DateTime, Utc}; use iced::{ - widget::{button, column, right, row, rule, text}, Alignment::Center, Element, Length::{Fill, Shrink}, + widget::{button, column, right, row, rule, text}, }; use std::{ @@ -121,11 +121,9 @@ impl Measurement { .width(Fill) .clip(true); - let delete_btn = button(icon::delete().align_x(Center).align_y(Center)) - .on_press_with(move || Message::Remove(self.id)) - .width(30) - .height(30) - .style(button::danger); + let delete_btn = sidebar::button(icon::delete()) + .style(button::danger) + .on_press_with(move || Message::Remove(self.id)); let content = row![ measurement_btn, diff --git a/raumklang-gui/src/ui/measurement/loopback.rs b/raumklang-gui/src/ui/measurement/loopback.rs index c61ee32..4314449 100644 --- a/raumklang-gui/src/ui/measurement/loopback.rs +++ b/raumklang-gui/src/ui/measurement/loopback.rs @@ -3,9 +3,9 @@ use super::{Message, Selected}; use crate::{icon, widget::sidebar}; use iced::{ - widget::{button, column, right, row, rule, text, tooltip}, Element, Length::{Fill, Shrink}, + widget::{button, column, right, row, rule, text, tooltip}, }; use chrono::{DateTime, Utc}; @@ -97,11 +97,7 @@ impl Loopback { }) .width(Fill); - let delete_btn = button(icon::delete()) - // .on_press(Message::RemoveLoopback) - .width(30) - .height(30) - .style(button::danger); + let delete_btn = sidebar::button(icon::delete()).style(button::danger); let content = row![ measurement_btn, diff --git a/raumklang-gui/src/widget/sidebar.rs b/raumklang-gui/src/widget/sidebar.rs index 13f4906..d3d2640 100644 --- a/raumklang-gui/src/widget/sidebar.rs +++ b/raumklang-gui/src/widget/sidebar.rs @@ -1,7 +1,7 @@ use iced::{ - widget::{container, text}, Element, Font, Length::Fill, + widget::{self, Button, Text, container, text}, }; pub fn header<'a, Message: Clone + 'a>(title: impl text::IntoFragment<'a>) -> Element<'a, Message> { @@ -10,6 +10,13 @@ pub fn header<'a, Message: Clone + 'a>(title: impl text::IntoFragment<'a>) -> El .into() } +pub fn button<'a, Message>(title: Text<'a>) -> Button<'a, Message> { + widget::button(title.size(16).center()) + .width(26) + .height(26) + .style(widget::button::secondary) +} + pub fn item<'a, Message: Clone + 'a>( content: impl Into>, is_active: bool, From d45d0460333a7cc3abf696f902f8d454a002b1ca Mon Sep 17 00:00:00 2001 From: Hendrik Kunert Date: Mon, 2 Feb 2026 18:28:41 +0100 Subject: [PATCH 25/30] Re-integrate `Recording` --- raumklang-gui/src/screen/main.rs | 70 +++++++++++++++++++++-------- raumklang-gui/src/ui/measurement.rs | 2 +- 2 files changed, 52 insertions(+), 20 deletions(-) diff --git a/raumklang-gui/src/screen/main.rs b/raumklang-gui/src/screen/main.rs index dbe45ae..f9b98b9 100644 --- a/raumklang-gui/src/screen/main.rs +++ b/raumklang-gui/src/screen/main.rs @@ -60,6 +60,7 @@ use std::{ pub struct Main { state: State, modal: Modal, + recording: Option, selected: Option, loopback: Option, @@ -825,6 +826,42 @@ impl Main { Task::none() } + Message::StartRecording(kind) => { + self.recording = Some(Recording::new(kind)); + Task::none() + } + Message::Recording(msg) => { + let Some(recording) = &mut self.recording else { + return Task::none(); + }; + + match recording.update(msg) { + recording::Action::None => Task::none(), + recording::Action::Cancel => { + self.recording = None; + Task::none() + } + recording::Action::Task(task) => task.map(Message::Recording), + recording::Action::Finished(result) => { + match result { + recording::Result::Loopback(loopback) => { + self.loopback = + Some(ui::Loopback::new("Loopback".to_string(), loopback)); + } + recording::Result::Measurement(measurement) => { + self.measurements.push(ui::Measurement::new( + "Measurement".to_string(), + None, + Some(measurement), + )); + } + } + + self.recording = None; + Task::none() + } + } + } _ => Task::none(), } } @@ -1728,7 +1765,11 @@ impl Main { } }; - let content = container(column![header, container(content).padding(10)]); + let content = if let Some(recording) = &self.recording { + container(recording.view().map(Message::Recording)).padding(10) + } else { + container(column![header, container(content).padding(10)]) + }; match self.modal { Modal::None => content.into(), @@ -2345,23 +2386,13 @@ impl Main { _ => None, }); - // let recording = match &self.state { - // State::CollectingMeasuremnts { - // recording: Some(recording), - // .. - // } - // | State::Analysing { - // active_tab: - // Tab::Measurements { - // recording: Some(recording), - // }, - // .. - // } => recording.subscription().map(Message::Recording), - // _ => Subscription::none(), - // }; - - // Subscription::batch([hotkeys, recording]) - Subscription::batch([hotkeys]) + let recording = self + .recording + .as_ref() + .map(Recording::subscription) + .unwrap_or(Subscription::none()); + + Subscription::batch([hotkeys, recording.map(Message::Recording)]) } // fn analysing_possible(&self) -> bool { @@ -2505,6 +2536,7 @@ impl Default for Main { Self { state: State::default(), modal: Modal::None, + recording: None, selected: None, loopback: None, @@ -2672,7 +2704,7 @@ async fn pick_project_file() -> Result { pub async fn pick_file(title: impl AsRef) -> Option { let handle = rfd::AsyncFileDialog::new() - .set_title(format!("Choose {} file", title.as_ref())) + .set_title(title.as_ref()) .add_filter("wav", &["wav", "wave"]) .add_filter("all", &["*"]) .pick_file() diff --git a/raumklang-gui/src/ui/measurement.rs b/raumklang-gui/src/ui/measurement.rs index bf71b5b..b03d679 100644 --- a/raumklang-gui/src/ui/measurement.rs +++ b/raumklang-gui/src/ui/measurement.rs @@ -52,7 +52,7 @@ enum State { } impl Measurement { - pub(crate) fn new( + pub fn new( name: String, path: Option, signal: Option, From 46d0b70f398673926f27fd030ec5166cec8a5041 Mon Sep 17 00:00:00 2001 From: Hendrik Kunert Date: Mon, 2 Feb 2026 21:25:39 +0100 Subject: [PATCH 26/30] Re-integrate project loading/saving --- raumklang-gui/src/main.rs | 5 +- raumklang-gui/src/screen/main.rs | 126 +++++++++++++++++++++++-------- 2 files changed, 95 insertions(+), 36 deletions(-) diff --git a/raumklang-gui/src/main.rs b/raumklang-gui/src/main.rs index 07963ac..da74fa9 100644 --- a/raumklang-gui/src/main.rs +++ b/raumklang-gui/src/main.rs @@ -8,12 +8,11 @@ mod ui; mod widget; use screen::{ - landing, + Screen, landing, main::{self}, - Screen, }; -use data::{project, RecentProjects}; +use data::{RecentProjects, project}; use iced::{Element, Font, Subscription, Task, Theme}; diff --git a/raumklang-gui/src/screen/main.rs b/raumklang-gui/src/screen/main.rs index f9b98b9..8747fae 100644 --- a/raumklang-gui/src/screen/main.rs +++ b/raumklang-gui/src/screen/main.rs @@ -124,7 +124,7 @@ pub enum Message { LoadProject, ProjectLoaded(Result<(Arc, PathBuf), PickAndLoadError>), SaveProject, - ProjectSaved(Result), + ProjectSaved(Result), LoadRecentProject(usize), LoadLoopback, @@ -212,14 +212,71 @@ impl Main { pub fn update(&mut self, recent_projects: &mut RecentProjects, msg: Message) -> Task { match msg { + Message::NewProject => { + *self = Self::default(); + Task::none() + } + Message::LoadProject => Task::future(pick_project_file_to_load()) + .and_then(|path| Task::perform(load_project(path), Message::ProjectLoaded)), + Message::LoadRecentProject(index) => { + let Some(path) = recent_projects.get(index) else { + return Task::none(); + }; + + Task::perform(load_project(path.clone()), Message::ProjectLoaded) + } + Message::ProjectLoaded(Ok((project, path))) => { + let Some(project) = Arc::into_inner(project) else { + return Task::none(); + }; + + let (view, tasks) = Self::from_project(path, project); + + *self = view; + tasks + } + Message::SaveProject => { + let loopback = self + .loopback + .as_ref() + .cloned() + .and_then(|l| l.path) + .map(|path| project::Loopback(project::Measurement { path })); + + let measurements = self + .measurements + .loaded() + .flat_map(|m| m.path.as_ref()) + .cloned() + .map(|path| project::Measurement { path }) + .collect(); + + let project = Project { + loopback, + measurements, + }; + + if let Some(path) = self.project_path.as_ref() { + let path = path.clone(); + Task::perform(project.save(path.clone()), |res| { + Message::ProjectSaved(res.map(|_| path)) + }) + } else { + Task::future(pick_project_file_to_save()).and_then(move |path| { + Task::perform(project.clone().save(path.clone()), |res| { + Message::ProjectSaved(res.map(|_| path)) + }) + }) + } + } + Message::ProjectSaved(Ok(path)) => { + self.project_path = Some(path.clone()); + recent_projects.insert(path); + + Task::future(recent_projects.clone().save()).discard() + } Message::OpenTab(tab) => { - let State::Analysing { - ref active_tab, - .. - // selected, - // analyses, - } = self.state - else { + let State::Analysing { ref active_tab, .. } = self.state else { return Task::none(); }; @@ -348,13 +405,12 @@ impl Main { } } } - Message::LoadLoopback => Task::future(pick_file("Load Loopback ...")) + Message::LoadLoopback => Task::future(pick_measurement_file("Load Loopback ...")) .and_then(|path| Task::perform(Loopback::from_file(path), Message::LoopbackLoaded)), - Message::LoadMeasurement => { - Task::future(pick_file("Load measurement ...")).and_then(|path| { + Message::LoadMeasurement => Task::future(pick_measurement_file("Load measurement ...")) + .and_then(|path| { Task::perform(Measurement::from_file(path), Message::MeasurementLoaded) - }) - } + }), Message::LoopbackLoaded(loopback) => { self.window = loopback .loaded() @@ -1905,7 +1961,9 @@ impl Main { })); container(scrollable( - column![loopback, measurements].spacing(20).padding(10), + column![loopback, rule::horizontal(1), measurements] + .spacing(10) + .padding(10), )) .style(|theme| { container::rounded_box(theme) @@ -2684,25 +2742,7 @@ where } } -#[derive(Debug, Clone, thiserror::Error)] -pub enum PickAndSaveError { - #[error("dialog closed")] - DialogClosed, - #[error(transparent)] - File(#[from] project::Error), -} - -async fn pick_project_file() -> Result { - let handle = rfd::AsyncFileDialog::new() - .set_title("Save project file ...") - .save_file() - .await - .ok_or(PickAndSaveError::DialogClosed)?; - - Ok(handle.path().to_path_buf()) -} - -pub async fn pick_file(title: impl AsRef) -> Option { +pub async fn pick_measurement_file(title: impl AsRef) -> Option { let handle = rfd::AsyncFileDialog::new() .set_title(title.as_ref()) .add_filter("wav", &["wav", "wave"]) @@ -2712,3 +2752,23 @@ pub async fn pick_file(title: impl AsRef) -> Option { handle.as_ref().map(FileHandle::path).map(Path::to_path_buf) } + +async fn pick_project_file_to_load() -> Option { + let handle = rfd::AsyncFileDialog::new() + .set_title("Load project...") + .add_filter("json", &["json"]) + .add_filter("all", &["*"]) + .pick_file() + .await?; + + Some(handle.path().to_path_buf()) +} + +async fn pick_project_file_to_save() -> Option { + let handle = rfd::AsyncFileDialog::new() + .set_title("Save project file ...") + .save_file() + .await?; + + Some(handle.path().to_path_buf()) +} From a3e917a383f262e1565fcfe3d2780d89a6d27687 Mon Sep 17 00:00:00 2001 From: Hendrik Kunert Date: Mon, 2 Feb 2026 21:39:47 +0100 Subject: [PATCH 27/30] Cleanup --- raumklang-gui/src/data/impulse_response.rs | 8 --- raumklang-gui/src/screen/main.rs | 69 +++++++++---------- .../src/screen/main/frequency_response.rs | 18 +---- .../src/screen/main/impulse_response.rs | 3 - raumklang-gui/src/screen/main/tab.rs | 24 ++++--- raumklang-gui/src/ui/frequency_response.rs | 29 +------- raumklang-gui/src/ui/measurement.rs | 13 ---- raumklang-gui/src/ui/spectral_decay.rs | 9 +-- 8 files changed, 52 insertions(+), 121 deletions(-) diff --git a/raumklang-gui/src/data/impulse_response.rs b/raumklang-gui/src/data/impulse_response.rs index 30525a4..5efb8af 100644 --- a/raumklang-gui/src/data/impulse_response.rs +++ b/raumklang-gui/src/data/impulse_response.rs @@ -61,14 +61,6 @@ impl ImpulseResponse { State::Computed(_) => Progress::Computed, } } - - pub fn into_inner(self) -> Option> { - let State::Computed(inner) = self.0 else { - return None; - }; - - Some(inner) - } } #[derive(Debug, Clone)] diff --git a/raumklang-gui/src/screen/main.rs b/raumklang-gui/src/screen/main.rs index 8747fae..0fd9371 100644 --- a/raumklang-gui/src/screen/main.rs +++ b/raumklang-gui/src/screen/main.rs @@ -18,10 +18,10 @@ use crate::{ chart::waveform, modal::{SpectralDecayConfig, pending_window, spectral_decay_config, spectrogram_config}, }, - ui::{self, Analysis, Loopback, Measurement, analysis, measurement}, + ui::{self, Analysis, Loopback, Measurement, measurement}, widget::{processing_overlay, sidebar}, }; -use raumklang_core::{WavLoadError, dbfs}; +use raumklang_core::dbfs; use impulse_response::ChartOperation; use recording::Recording; @@ -32,26 +32,18 @@ use generic_overlay::generic_overlay::{dropdown_menu, dropdown_root}; use iced::{ Alignment::{self, Center}, Color, Element, Function, Length, Subscription, Task, Theme, - advanced::{ - Renderer, graphics::text::cosmic_text::skrifa::raw::tables::layout::SelectedScript, - }, alignment::{Horizontal, Vertical}, - futures::{FutureExt, TryFutureExt}, keyboard, padding, - task::{Sipper, sipper}, widget::{ - Button, button, canvas, center, column, container, opaque, pick_list, right, row, rule, - scrollable, space, stack, text, + Button, button, canvas, center, column, container, opaque, pick_list, row, rule, + scrollable, stack, text, }, }; use prism::{Axis, Chart, Labels, axis, line_series}; use rfd::FileHandle; -use tracing::instrument::WithSubscriber; use std::{ - collections::{BTreeMap, HashMap}, - fmt::Display, - future::Future, + collections::BTreeMap, mem, path::{Path, PathBuf}, sync::Arc, @@ -134,12 +126,6 @@ pub enum Message { Measurement(measurement::Message), OpenTab(tab::Id), - // OpenMeasurements, - // OpenImpulseResponses, - // OpenFrequencyResponses, - // OpenSpectralDecays, - // OpenSpectrograms, - ImpulseResponseSelected(measurement::Id), ImpulseResponseComputed(measurement::Id, data::ImpulseResponse), SaveImpulseResponseToFile(measurement::Id, Option>), @@ -157,29 +143,19 @@ pub enum Message { MeasurementChart(waveform::Interaction), - // SaveImpulseResponseFileDialog(measurement::Id), Recording(recording::Message), StartRecording(recording::Kind), - SpectralDecayComputed(measurement::Id, data::SpectralDecay), - - Spectrogram(chart::spectrogram::Interaction), - SpectrogramComputed(measurement::Id, data::Spectrogram), - OpenSpectralDecayConfig, SpectralDecayConfig(spectral_decay_config::Message), + SpectralDecayComputed(measurement::Id, data::SpectralDecay), + OpenSpectrogramConfig, SpectrogramConfig(spectrogram_config::Message), - PendingWindow(pending_window::Message), -} + SpectrogramComputed(measurement::Id, data::Spectrogram), + Spectrogram(chart::spectrogram::Interaction), -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum TabId { - Measurements, - ImpulseResponses, - FrequencyResponses, - SpectralDecay, - Spectrogram, + PendingWindow(pending_window::Message), } impl Main { @@ -301,7 +277,7 @@ impl Main { return Task::none(); }; - *tab = Tab::Measurements { recording: None }; + *tab = Tab::Measurements; Task::none() } @@ -808,6 +784,20 @@ impl Main { Task::none() } + Message::Spectrogram(interaction) => { + match interaction { + chart::spectrogram::Interaction::ZoomChanged(zoom) => { + self.spectrogram.zoom = zoom + } + chart::spectrogram::Interaction::OffsetChanged(offset) => { + self.spectrogram.offset = offset + } + } + + self.spectrogram.cache.clear(); + + Task::none() + } Message::OpenSpectrogramConfig => { self.modal = Modal::SpectrogramConfig(modal::SpectrogramConfig::new( self.spectrogram_config, @@ -918,6 +908,14 @@ impl Main { } } } + Message::ShiftKeyPressed => { + self.ir_chart.shift_key_pressed(); + Task::none() + } + Message::ShiftKeyReleased => { + self.ir_chart.shift_key_released(); + Task::none() + } _ => Task::none(), } } @@ -2094,7 +2092,6 @@ impl Main { let content = analysis.frequency_response.view( &measurement.name, - analysis.impulse_response.progress(), Message::FrequencyResponseToggled.with(measurement.id()), ); diff --git a/raumklang-gui/src/screen/main/frequency_response.rs b/raumklang-gui/src/screen/main/frequency_response.rs index 9c61c6a..455bc94 100644 --- a/raumklang-gui/src/screen/main/frequency_response.rs +++ b/raumklang-gui/src/screen/main/frequency_response.rs @@ -1,20 +1,6 @@ -use std::{ - fmt::{self}, - ops::RangeInclusive, -}; +use std::fmt::{self}; -use crate::{ - data, - ui::{self}, -}; - -use iced::widget::canvas; - -#[derive(Debug, Default)] -pub struct ChartData { - pub x_range: Option>, - pub cache: canvas::Cache, -} +use crate::data; #[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] pub enum Smoothing { diff --git a/raumklang-gui/src/screen/main/impulse_response.rs b/raumklang-gui/src/screen/main/impulse_response.rs index 6d33ac0..2a22c9e 100644 --- a/raumklang-gui/src/screen/main/impulse_response.rs +++ b/raumklang-gui/src/screen/main/impulse_response.rs @@ -10,8 +10,6 @@ use iced::{ widget::{canvas, column, container, pick_list, row}, }; -use std::ops::RangeInclusive; - #[derive(Debug, Clone)] pub enum ChartOperation { TimeUnitChanged(data::chart::TimeSeriesUnit), @@ -21,7 +19,6 @@ pub enum ChartOperation { #[derive(Debug, Default)] pub struct Chart { - pub x_range: Option>, shift_key_pressed: bool, pub amplitude_unit: data::chart::AmplitudeUnit, pub time_unit: data::chart::TimeSeriesUnit, diff --git a/raumklang-gui/src/screen/main/tab.rs b/raumklang-gui/src/screen/main/tab.rs index 3b82b32..500a1aa 100644 --- a/raumklang-gui/src/screen/main/tab.rs +++ b/raumklang-gui/src/screen/main/tab.rs @@ -1,12 +1,20 @@ use iced::widget::canvas; -use crate::{data::Window, screen::main::recording::Recording}; +use crate::data::Window; +#[derive(Default)] pub enum Tab { - Measurements { recording: Option }, - ImpulseResponses { pending_window: Window }, - FrequencyResponses { cache: canvas::Cache }, - SpectralDecays { cache: canvas::Cache }, + #[default] + Measurements, + ImpulseResponses { + pending_window: Window, + }, + FrequencyResponses { + cache: canvas::Cache, + }, + SpectralDecays { + cache: canvas::Cache, + }, Spectrograms, } @@ -18,9 +26,3 @@ pub enum Id { SpectralDecays, Spectrograms, } - -impl Default for Tab { - fn default() -> Self { - Self::Measurements { recording: None } - } -} diff --git a/raumklang-gui/src/ui/frequency_response.rs b/raumklang-gui/src/ui/frequency_response.rs index 1bf7103..beac1d7 100644 --- a/raumklang-gui/src/ui/frequency_response.rs +++ b/raumklang-gui/src/ui/frequency_response.rs @@ -1,15 +1,12 @@ -use std::fmt::{self, Display}; - use crate::widget::sidebar; use crate::{data, icon}; -use data::impulse_response; +use iced::Alignment; use iced::widget::stack; use iced::widget::text::IntoFragment; -use iced::Alignment; use iced::{ - widget::{column, container, row, text, toggler}, Element, Length, + widget::{column, container, row, text, toggler}, }; use rand::Rng as _; @@ -19,7 +16,6 @@ pub struct FrequencyResponse { pub color: iced::Color, pub is_shown: bool, - // pub data: Option, pub smoothed: Option>, pub state: State, @@ -49,7 +45,6 @@ impl FrequencyResponse { pub fn view<'a, Message>( &'a self, measurement_name: &'a str, - impulse_response_progess: impulse_response::Progress, on_toggle: impl Fn(bool) -> Message + 'a, ) -> Element<'a, Message> where @@ -114,26 +109,6 @@ impl Default for FrequencyResponse { } } -#[derive(Debug, Clone, Copy, Default)] -pub enum Progress { - #[default] - None, - Computing, - Finished, -} - -impl Display for Progress { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let text = match self { - Self::None => "Not Started", - Self::Computing => "Impulse Response", - Self::Finished => "Finished", - }; - - write!(f, "{}", text) - } -} - fn random_color() -> iced::Color { const MAX_COLOR_VALUE: u8 = 255; diff --git a/raumklang-gui/src/ui/measurement.rs b/raumklang-gui/src/ui/measurement.rs index b03d679..d3eee4b 100644 --- a/raumklang-gui/src/ui/measurement.rs +++ b/raumklang-gui/src/ui/measurement.rs @@ -4,7 +4,6 @@ pub use loopback::Loopback; use chrono::{DateTime, Utc}; use iced::{ - Alignment::Center, Element, Length::{Fill, Shrink}, widget::{button, column, right, row, rule, text}, @@ -161,18 +160,10 @@ impl List { self.0.iter() } - pub fn iter_mut(&mut self) -> impl Iterator { - self.0.iter_mut() - } - pub fn loaded(&self) -> impl Iterator { self.0.iter().filter(|m| m.is_loaded()) } - pub fn loaded_mut(&mut self) -> impl Iterator { - self.0.iter_mut().filter(|m| m.is_loaded()) - } - pub fn push(&mut self, measurement: Measurement) { self.0.push(measurement); } @@ -192,10 +183,6 @@ impl List { self.0.iter().find(|m| m.id == id) } - pub fn get_mut(&mut self, id: Id) -> Option<&mut Measurement> { - self.0.iter_mut().find(|m| m.id == id) - } - pub fn is_empty(&self) -> bool { self.0.is_empty() } diff --git a/raumklang-gui/src/ui/spectral_decay.rs b/raumklang-gui/src/ui/spectral_decay.rs index f5031bf..730f228 100644 --- a/raumklang-gui/src/ui/spectral_decay.rs +++ b/raumklang-gui/src/ui/spectral_decay.rs @@ -1,11 +1,6 @@ -use std::future::Future; - -use iced::Task; +use crate::{data, ui::impulse_response}; -use crate::{ - Message, data, - ui::{self, impulse_response}, -}; +use std::future::Future; #[derive(Debug, Clone, Default)] pub struct SpectralDecay(State); From 39a7f2171f26faf4e8e30f66d06c3ff14b1b4b0f Mon Sep 17 00:00:00 2001 From: Hendrik Kunert Date: Mon, 2 Feb 2026 21:46:14 +0100 Subject: [PATCH 28/30] Remove unused src files --- raumklang-gui/src/audio/driver.rs | 7 - raumklang-gui/src/data/project/file.rs | 46 - raumklang-gui/src/data/store.rs | 220 ----- raumklang-gui/src/screen/main.rs | 905 ------------------ .../src/screen/main/chart/impulse_response.rs | 0 raumklang-gui/src/ui/measurement.rs | 5 +- 6 files changed, 1 insertion(+), 1182 deletions(-) delete mode 100644 raumklang-gui/src/audio/driver.rs delete mode 100644 raumklang-gui/src/data/project/file.rs delete mode 100644 raumklang-gui/src/data/store.rs delete mode 100644 raumklang-gui/src/screen/main/chart/impulse_response.rs diff --git a/raumklang-gui/src/audio/driver.rs b/raumklang-gui/src/audio/driver.rs deleted file mode 100644 index 811d6a8..0000000 --- a/raumklang-gui/src/audio/driver.rs +++ /dev/null @@ -1,7 +0,0 @@ -#[derive(Debug, Clone)] -pub enum Notification { - OutPortConnected(String), - OutPortDisconnected, - InPortConnected(String), - InPortDisconnected, -} diff --git a/raumklang-gui/src/data/project/file.rs b/raumklang-gui/src/data/project/file.rs deleted file mode 100644 index 997a0c3..0000000 --- a/raumklang-gui/src/data/project/file.rs +++ /dev/null @@ -1,46 +0,0 @@ -use std::{ - io, - path::{Path, PathBuf}, -}; - -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] -pub struct File { - pub loopback: Option, - pub measurements: Vec, -} - -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] -pub struct Loopback(Measurement); - -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] -pub struct Measurement { - pub path: PathBuf, -} - -#[derive(thiserror::Error, Debug, Clone)] -pub enum Error { - #[error("could not load file: {0}")] - Io(io::ErrorKind), - #[error("could not parse file: {0}")] - Json(String), -} - -impl File { - pub async fn load(path: impl AsRef) -> Result { - let path = path.as_ref(); - let content = tokio::fs::read(path) - .await - .map_err(|err| Error::Io(err.kind()))?; - - let project = - serde_json::from_slice(&content).map_err(|err| Error::Json(err.to_string()))?; - - Ok(project) - } -} - -impl Loopback { - pub fn path(&self) -> &PathBuf { - &self.0.path - } -} diff --git a/raumklang-gui/src/data/store.rs b/raumklang-gui/src/data/store.rs deleted file mode 100644 index 234361b..0000000 --- a/raumklang-gui/src/data/store.rs +++ /dev/null @@ -1,220 +0,0 @@ -use std::{collections::HashMap, hash::Hash, marker::PhantomData, ops::AddAssign, slice}; - -pub struct Store { - all: Vec, Id>>, - loaded: IdMap, - not_loaded: IdMap, -} - -#[derive(Debug)] -struct IdMap { - next_id: Id, - data: HashMap, T>, -} - -#[derive(Debug)] -pub struct Id { - inner: usize, - phantom: PhantomData, -} - -#[derive(Debug)] -pub enum MeasurementState { - Loaded(L), - Loading, - NotLoaded(O), -} - -impl Store { - pub fn new() -> Self { - Self { - not_loaded: IdMap::new(), - loaded: IdMap::new(), - all: Vec::new(), - } - } - - pub fn insert(&mut self, state: MeasurementState) { - let id = match state { - MeasurementState::NotLoaded(om) => { - let id = self.not_loaded.insert(om); - MeasurementState::NotLoaded(id) - } - MeasurementState::Loaded(m) => { - let id = self.loaded.insert(m); - MeasurementState::Loaded(id) - } - }; - - self.all.push(id); - } - - pub fn get(&self, index: usize) -> Option> { - let id = self.all.get(index); - - id.map(|m| match m { - MeasurementState::Loaded(id) => self.loaded.get(id).map(MeasurementState::Loaded), - MeasurementState::NotLoaded(id) => { - self.not_loaded.get(id).map(MeasurementState::NotLoaded) - } - })? - } - - pub fn get_loaded_id(&self, index: usize) -> Option> { - self.all.get(index).map(|m| match m { - MeasurementState::Loaded(id) => Some(*id), - MeasurementState::NotLoaded(_) => None, - })? - } - - pub fn get_loaded_by_id(&self, id: &Id) -> Option<&L> { - self.loaded.get(id) - } - - pub fn iter(&self) -> Iter { - Iter { - inner: self.all.iter(), - loaded: &self.loaded, - not_loaded: &self.not_loaded, - } - } - - pub fn loaded(&self) -> impl Iterator, &L)> { - self.all.iter().filter_map(|state| match state { - MeasurementState::Loaded(id) => self.loaded.get(id).map(|m| (id, m)), - MeasurementState::NotLoaded(_) => None, - }) - } - - pub fn is_loaded_empty(&self) -> bool { - self.loaded.is_empty() - } - - pub fn remove(&mut self, id: usize) -> Option> { - let state = self.all.remove(id); - - match state { - MeasurementState::Loaded(id) => self.loaded.remove(&id).map(MeasurementState::Loaded), - MeasurementState::NotLoaded(id) => { - self.not_loaded.remove(&id).map(MeasurementState::NotLoaded) - } - } - } - - pub fn is_empty(&self) -> bool { - self.all.is_empty() - } -} - -impl<'a, L, O> From> for Option<&'a L> { - fn from(value: MeasurementState<&'a L, &O>) -> Self { - match value { - MeasurementState::Loaded(m) => Some(m), - MeasurementState::NotLoaded(_) => None, - } - } -} - -pub struct Iter<'a, L, O> { - inner: slice::Iter<'a, MeasurementState, Id>>, - loaded: &'a IdMap, - not_loaded: &'a IdMap, -} - -impl<'a, L, O> Iterator for Iter<'a, L, O> { - type Item = MeasurementState<&'a L, &'a O>; - - fn next(&mut self) -> Option { - self.inner.next().map(|state| match state { - MeasurementState::NotLoaded(id) => { - MeasurementState::NotLoaded(self.not_loaded.get(id).unwrap()) - } - MeasurementState::Loaded(id) => MeasurementState::Loaded(self.loaded.get(id).unwrap()), - }) - } -} - -impl IdMap { - fn new() -> Self { - Self { - next_id: Id::new(), - data: HashMap::new(), - } - } - - fn insert(&mut self, entry: T) -> Id { - let cur_id = self.next_id; - - self.data.insert(cur_id, entry); - self.next_id += 1; - - cur_id - } - - fn get(&self, key: &Id) -> Option<&T> { - self.data.get(key) - } - - fn remove(&mut self, id: &Id) -> Option { - self.data.remove(id) - } - - fn is_empty(&self) -> bool { - self.data.is_empty() - } -} - -impl Id { - fn new() -> Self { - Self { - inner: 0, - phantom: PhantomData, - } - } -} - -impl PartialEq for Id { - fn eq(&self, other: &Self) -> bool { - self.inner == other.inner - } -} - -impl Eq for Id {} - -impl Hash for Id { - fn hash(&self, state: &mut H) { - self.inner.hash(state); - } -} - -impl Clone for Id { - fn clone(&self) -> Self { - *self - } -} - -impl Copy for Id {} - -impl AddAssign for Id { - fn add_assign(&mut self, rhs: usize) { - self.inner += rhs - } -} - -#[cfg(test)] -mod test { - use super::{MeasurementState, Store}; - - #[test] - fn loaded_returns_items_in_correct_order() { - let mut store: Store<&str, ()> = Store::new(); - - store.insert(MeasurementState::Loaded("a")); - store.insert(MeasurementState::Loaded("c")); - store.insert(MeasurementState::Loaded("b")); - - let items: Vec<_> = store.loaded().map(|(_, i)| *i).collect(); - - assert_eq!(items, vec!["a", "c", "b"]) - } -} diff --git a/raumklang-gui/src/screen/main.rs b/raumklang-gui/src/screen/main.rs index 0fd9371..eae72c4 100644 --- a/raumklang-gui/src/screen/main.rs +++ b/raumklang-gui/src/screen/main.rs @@ -920,777 +920,6 @@ impl Main { } } - // pub fn update( - // - // &mut self, - // recent_projects: &mut RecentProjects, - // message: Message, - // ) -> Task { - // match message { - // Message::TabSelected(id) => { - // let State::Analysing { - // ref mut active_tab, - // ref window, - // ref mut selected, - // ref loopback, - // .. - // } = self.state - // else { - // return Task::none(); - // }; - - // let (tab, tasks) = match (&active_tab, id) { - // (Tab::Measurements { .. }, TabId::Measurements) - // | (Tab::ImpulseResponses { .. }, TabId::ImpulseResponses) - // | (Tab::FrequencyResponses, TabId::FrequencyResponses) => return Task::none(), - // ( - // Tab::ImpulseResponses { - // ref window_settings, - // .. - // }, - // tab_id, - // ) if window_settings.window != *window => { - // self.modal = Modal::PendingWindow { goto_tab: tab_id }; - // return Task::none(); - // } - // (_, TabId::Measurements) => { - // (Tab::Measurements { recording: None }, Task::none()) - // } - // (_, TabId::ImpulseResponses) => ( - // Tab::ImpulseResponses { - // window_settings: WindowSettings::new(window.clone()), - // }, - // Task::none(), - // ), - // (_, TabId::FrequencyResponses) => { - // let tasks = self.measurements.loaded_mut().map(|measurement| { - // compute_frequency_response(loopback, measurement, window) - // }); - // (Tab::FrequencyResponses, Task::batch(tasks)) - // } - // (_, TabId::SpectralDecay) => (Tab::SpectralDecay, Task::none()), - // (_, TabId::Spectrogram) => (Tab::Spectrogram, Task::none()), - // }; - - // *active_tab = tab; - // *selected = None; - - // tasks - // } - // Message::Measurements(message) => { - // match message { - // measurement::Message::Load(kind) => { - // let dialog_caption = kind.to_string(); - - // return Task::perform( - // measurement::pick_file_and_load_signal(dialog_caption, kind), - // measurement::Message::Loaded, - // ) - // .map(Message::Measurements); - // } - // measurement::Message::Loaded(Ok(result)) => match Arc::into_inner(result) { - // Some(measurement::LoadedKind::Loopback(new)) => match &mut self.state { - // State::CollectingMeasuremnts { loopback, .. } => *loopback = Some(new), - // State::Analysing { loopback, .. } => *loopback = new, - // }, - // Some(measurement::LoadedKind::Normal(measurement)) => { - // self.measurements.push(measurement) - // } - // None => {} - // }, - // measurement::Message::Loaded(Err(err)) => { - // log::error!("{err}"); - // } - // measurement::Message::Remove(id) => { - // self.measurements.remove(id); - // } - // measurement::Message::Select(selected) => { - // self.selected = Some(selected); - // self.signal_cache.clear(); - // } - // } - - // let is_analysing_possible = self.analysing_possible(); - // let state = std::mem::take(&mut self.state); - // self.state = match (state, is_analysing_possible) { - // ( - // State::CollectingMeasuremnts { - // recording, - // loopback, - // }, - // false, - // ) => State::CollectingMeasuremnts { - // recording, - // loopback, - // }, - // ( - // State::CollectingMeasuremnts { - // recording, - // loopback: Some(loopback), - // }, - // true, - // ) => State::Analysing { - // active_tab: Tab::Measurements { recording }, - // window: Window::new(SampleRate::from( - // loopback - // .loaded() - // .map_or(44_100, |l| l.as_ref().sample_rate()), - // )) - // .into(), - // selected: None, - // charts: Charts::default(), - // loopback, - // }, - // (old_state, true) => old_state, - // (State::Analysing { loopback, .. }, false) => State::CollectingMeasuremnts { - // recording: None, - // loopback: Some(loopback), - // }, - // }; - - // Task::none() - // } - // Message::ImpulseResponseSelected(id) => { - // log::debug!("Impulse response selected: {id}"); - - // let State::Analysing { - // selected: ref mut selected_analysis, - // ref mut charts, - // ref active_tab, - // ref loopback, - // .. - // } = self.state - // else { - // return Task::none(); - // }; - - // *selected_analysis = Some(id); - // charts.impulse_responses.data_cache.clear(); - - // let Some(measurement) = self.measurements.get_mut(id) else { - // return Task::none(); - // }; - - // match active_tab { - // Tab::Measurements { .. } => Task::none(), - // Tab::ImpulseResponses { .. } => compute_impulse_response(loopback, measurement), - // Tab::FrequencyResponses => Task::none(), - // Tab::SpectralDecay => { - // compute_spectral_decay(loopback, measurement, self.spectral_decay_config) - // } - // Tab::Spectrogram => { - // compute_spectrogram(loopback, measurement, &self.spectrogram_config) - // } - // } - // } - // Message::ImpulseResponseComputed(id, impulse_response) => { - // let State::Analysing { - // window, - // active_tab, - // selected: selected_analysis, - // charts, - // loopback, - // .. - // } = &mut self.state - // else { - // return Task::none(); - // }; - - // let impulse_response = ui::ImpulseResponse::from_data(impulse_response); - - // let Some(measurement) = self.measurements.get_mut(id) else { - // return Task::none(); - // }; - - // let Some(analysis) = measurement.analysis_mut() else { - // return Task::none(); - // }; - - // analysis.impulse_response.computed(impulse_response.clone()); - - // if selected_analysis.is_some_and(|selected| selected == id) { - // charts - // .impulse_responses - // .x_range - // .get_or_insert(0.0..=impulse_response.data.len() as f32); - - // charts.impulse_responses.data_cache.clear(); - // } - - // if let Tab::FrequencyResponses = active_tab { - // compute_frequency_response(loopback, measurement, window) - // } else if let Tab::SpectralDecay = active_tab { - // compute_spectral_decay(loopback, measurement, self.spectral_decay_config) - // } else if let Tab::Spectrogram = active_tab { - // compute_spectrogram(loopback, measurement, &self.spectrogram_config) - // } else { - // Task::none() - // } - // } - // Message::ImpulseResponses(impulse_response::Message::Chart(operation)) => { - // let State::Analysing { - // active_tab: - // Tab::ImpulseResponses { - // ref mut window_settings, - // .. - // }, - // ref mut charts, - // .. - // } = self.state - // else { - // return Task::none(); - // }; - - // if let ChartOperation::Interaction(ref interaction) = operation { - // match interaction { - // chart::Interaction::HandleMoved(index, new_pos) => { - // let mut handles: window::Handles = Into::into(&window_settings.window); - // handles.update(*index, *new_pos); - // window_settings.window.update(handles); - // } - // chart::Interaction::ZoomChanged(zoom) => { - // charts.impulse_responses.zoom = *zoom; - // } - // chart::Interaction::OffsetChanged(offset) => { - // charts.impulse_responses.offset = *offset; - // } - // } - // } - - // charts.impulse_responses.update(operation); - - // Task::none() - // } - // Message::FrequencyResponseComputed(id, frequency_response) => { - // log::debug!("Frequency response computed: {id}"); - - // let State::Analysing { ref mut charts, .. } = self.state else { - // return Task::none(); - // }; - - // let Some(measurement) = self.measurements.get_mut(id) else { - // return Task::none(); - // }; - - // let Some(analysis) = measurement.analysis_mut() else { - // return Task::none(); - // }; - - // analysis - // .frequency_response - // .computed(frequency_response.clone()); - - // charts.frequency_responses.cache.clear(); - - // if let Some(fraction) = self.smoothing.fraction() { - // Task::perform( - // frequency_response::smooth_frequency_response( - // id, - // frequency_response, - // fraction, - // ), - // Message::FrequencyResponseSmoothed, - // ) - // } else { - // Task::none() - // } - // } - // Message::FrequencyResponseToggled(id, state) => { - // let State::Analysing { ref mut charts, .. } = self.state else { - // return Task::none(); - // }; - - // let Some(frequency_response) = self - // .measurements - // .get_mut(id) - // .and_then(ui::Measurement::analysis_mut) - // .map(|analysis| &mut analysis.frequency_response) - // else { - // return Task::none(); - // }; - - // frequency_response.is_shown = state; - - // charts.frequency_responses.cache.clear(); - - // Task::none() - // } - // Message::SmoothingChanged(smoothing) => { - // let State::Analysing { ref mut charts, .. } = self.state else { - // return Task::none(); - // }; - - // self.smoothing = smoothing; - - // if let Some(fraction) = smoothing.fraction() { - // let tasks = self.measurements.iter().flat_map(|measurement| { - // let fr = measurement - // .analysis() - // .as_ref() - // .and_then(|a| a.frequency_response.data.clone())?; - - // Some(Task::perform( - // frequency_response::smooth_frequency_response( - // measurement.id(), - // fr, - // fraction, - // ), - // Message::FrequencyResponseSmoothed, - // )) - // }); - - // Task::batch(tasks) - // } else { - // self.measurements - // .iter_mut() - // .flat_map(ui::Measurement::analysis_mut) - // .map(|analysis| &mut analysis.frequency_response) - // .for_each(|fr| fr.smoothed = None); - - // charts.frequency_responses.cache.clear(); - - // Task::none() - // } - // } - // Message::FrequencyResponseSmoothed((id, smoothed_data)) => { - // let State::Analysing { ref mut charts, .. } = self.state else { - // return Task::none(); - // }; - - // let Some(frequency_response) = self - // .measurements - // .get_mut(id) - // .and_then(ui::Measurement::analysis_mut) - // .map(|analysis| &mut analysis.frequency_response) - // else { - // return Task::none(); - // }; - - // frequency_response.smoothed = Some(smoothed_data); - // charts.frequency_responses.cache.clear(); - - // Task::none() - // } - // Message::ShiftKeyPressed => { - // let State::Analysing { ref mut charts, .. } = self.state else { - // return Task::none(); - // }; - - // charts.impulse_responses.shift_key_pressed(); - - // Task::none() - // } - // Message::ShiftKeyReleased => { - // let State::Analysing { ref mut charts, .. } = self.state else { - // return Task::none(); - // }; - - // charts.impulse_responses.shift_key_released(); - - // Task::none() - // } - // Message::Modal(action) => { - // let Modal::PendingWindow { goto_tab } = std::mem::take(&mut self.modal) else { - // return Task::none(); - // }; - - // let State::Analysing { - // ref mut active_tab, - // ref mut window, - // .. - // } = self.state - // else { - // return Task::none(); - // }; - - // let Tab::ImpulseResponses { - // ref mut window_settings, - // .. - // } = active_tab - // else { - // return Task::none(); - // }; - - // match action { - // ModalAction::Discard => { - // *window_settings = WindowSettings::new(window.clone()); - // } - // ModalAction::Apply => { - // self.measurements - // .iter_mut() - // .flat_map(ui::Measurement::analysis_mut) - // .for_each(|analysis| { - // analysis.frequency_response = ui::FrequencyResponse::default(); - // analysis.spectral_decay = ui::spectral_decay::State::default(); - // analysis.spectrogram = ui::spectrogram::State::default(); - // }); - - // *window = window_settings.window.clone(); - // } - // } - - // Task::done(Message::TabSelected(goto_tab)) - // } - // Message::Recording(message) => { - // let (State::CollectingMeasuremnts { - // ref mut recording, - // loopback: Some(ref mut loopback), - // } - // | State::Analysing { - // active_tab: Tab::Measurements { ref mut recording }, - // ref mut loopback, - // .. - // }) = self.state - // else { - // return Task::none(); - // }; - - // if let Some(view) = recording { - // match view.update(message) { - // recording::Action::None => Task::none(), - // recording::Action::Cancel => { - // *recording = None; - // Task::none() - // } - // recording::Action::Finished(result) => { - // match result { - // recording::Result::Loopback(signal) => { - // *loopback = ui::Loopback::new("Loopback".to_string(), signal) - // } - // recording::Result::Measurement(signal) => { - // self.measurements.push(ui::Measurement::new( - // "Measurement".to_string(), - // None, - // Some(signal), - // )) - // } - // } - // *recording = None; - // Task::none() - // } - // recording::Action::Task(task) => task.map(Message::Recording), - // } - // } else { - // Task::none() - // } - // } - // Message::StartRecording(kind) => match &mut self.state { - // State::CollectingMeasuremnts { recording, .. } - // | State::Analysing { - // active_tab: Tab::Measurements { recording }, - // .. - // } => { - // *recording = Some(Recording::new(kind)); - // Task::none() - // } - // _ => Task::none(), - // }, - // Message::MeasurementChart(interaction) => { - // match interaction { - // waveform::Interaction::ZoomChanged(zoom) => self.zoom = zoom, - // waveform::Interaction::OffsetChanged(offset) => self.offset = offset, - // } - - // self.signal_cache.clear(); - - // Task::none() - // } - // Message::SaveImpulseResponseFileDialog(id) => { - // Task::future(choose_impulse_response_file_path()) - // .and_then(Task::done) - // .map(Message::SaveImpulseResponse.with(id)) - // } - // Message::SaveImpulseResponse(id, path) => { - // let State::Analysing { - // active_tab: Tab::ImpulseResponses { .. }, - // .. - // } = &self.state - // else { - // return Task::none(); - // }; - - // if let Some(impulse_response) = self - // .measurements - // .get(id) - // .and_then(ui::Measurement::analysis) - // .and_then(|analysis| analysis.impulse_response.result()) - // .cloned() - // { - // Task::perform( - // save_impulse_response(path.clone(), impulse_response), - // |_| Message::ImpulseResponsesSaved(path), - // ) - // } else { - // self.compute_impulse_response(id) - // .chain(Task::done(Message::SaveImpulseResponse(id, path))) - // } - // } - // Message::ImpulseResponsesSaved(path) => { - // log::debug!("Impulse response saved to: {}", path.display()); - - // Task::none() - // } - // Message::NewProject => { - // *self = Self::default(); - - // Task::none() - // } - // Message::LoadProject => Task::perform( - // crate::pick_project_file().then(async |res| { - // let path = res?; - // load_project(path).await - // }), - // Message::ProjectLoaded, - // ), - // Message::ProjectLoaded(Ok((project, path))) => match Arc::into_inner(project) { - // Some(project) => { - // recent_projects.insert(path.clone()); - - // let (screen, tasks) = Self::from_project(path, project); - // *self = screen; - - // Task::batch([ - // tasks, - // Task::future(recent_projects.clone().save()).discard(), - // ]) - // } - // None => Task::none(), - // }, - // Message::ProjectLoaded(Err(err)) => { - // log::debug!("Loading project failed: {err}"); - - // Task::none() - // } - // Message::RecentProject(id) => match recent_projects.get(id) { - // Some(path) => Task::perform(load_project(path.clone()), Message::ProjectLoaded), - // None => Task::none(), - // }, - // Message::SaveProject => { - // let loopback = if let State::CollectingMeasuremnts { ref loopback, .. } = self.state - // { - // loopback.as_ref() - // } else if let State::Analysing { ref loopback, .. } = self.state { - // Some(loopback) - // } else { - // None - // }; - - // let loopback = loopback - // .as_ref() - // .and_then(|l| l.path.clone()) - // .map(|path| project::Loopback(project::Measurement { path })); - - // let measurements = self - // .measurements - // .iter() - // .flat_map(|m| m.path.clone()) - // .map(|path| project::Measurement { path }) - // .collect(); - - // let project = Project { - // loopback, - // measurements, - // }; - - // if let Some(path) = self.project_path.clone() { - // Task::perform( - // project - // .save(path.clone()) - // .map_ok(move |_| path) - // .map_err(PickAndSaveError::File), - // Message::ProjectSaved, - // ) - // } else { - // Task::perform( - // pick_project_file().then(async |res| { - // let path = res?; - // project.save(path.clone()).await?; - - // Ok(path) - // }), - // Message::ProjectSaved, - // ) - // } - // } - // Message::ProjectSaved(Ok(path)) => { - // log::debug!("Project saved."); - - // self.project_path = Some(path); - - // Task::none() - // } - // Message::ProjectSaved(Err(err)) => { - // log::debug!("Saving project failed: {err}"); - // Task::none() - // } - // Message::SpectralDecayComputed(id, decay) => { - // log::debug!( - // "Spectral decay for measurement (ID: {}) with: {} slices, computed.", - // id, - // decay.len() - // ); - - // let Some(spectral_decay) = self - // .measurements - // .get_mut(id) - // .and_then(ui::Measurement::analysis_mut) - // .map(|analysis| &mut analysis.spectral_decay) - // else { - // return Task::none(); - // }; - - // spectral_decay.computed(decay); - - // if let State::Analysing { charts, .. } = &mut self.state { - // charts.spectral_decay_cache.clear(); - // }; - - // Task::none() - // } - // Message::Spectrogram(interaction) => { - // log::debug!("Spectrogram chart: {interaction:?}."); - - // let State::Analysing { charts, .. } = &mut self.state else { - // return Task::none(); - // }; - - // match interaction { - // chart::spectrogram::Interaction::ZoomChanged(zoom) => { - // charts.spectrogram.zoom = zoom - // } - // chart::spectrogram::Interaction::OffsetChanged(offset) => { - // charts.spectrogram.offset = offset - // } - // } - - // charts.spectrogram.cache.clear(); - - // Task::none() - // } - // Message::SpectrogramComputed(id, data) => { - // log::debug!( - // "Spectrogram for measurement (ID: {}) with: {} slices, computed.", - // id, - // data.len() - // ); - - // let Some(spectrogram) = self - // .measurements - // .get_mut(id) - // .and_then(ui::Measurement::analysis_mut) - // .map(|analysis| &mut analysis.spectrogram) - // else { - // return Task::none(); - // }; - - // spectrogram.computed(data); - - // if let State::Analysing { charts, .. } = &mut self.state { - // charts.spectrogram.cache.clear(); - // }; - - // Task::none() - // } - // Message::OpenSpectralDecayConfig => { - // self.modal = Modal::SpectralDecayConfig(SpectralDecayConfig::new( - // self.spectral_decay_config, - // )); - - // Task::none() - // } - // Message::SpectralDecayConfig(message) => { - // let Modal::SpectralDecayConfig(config) = &mut self.modal else { - // return Task::none(); - // }; - - // let State::Analysing { - // selected: selected_analysis, - // ref loopback, - // .. - // } = self.state - // else { - // return Task::none(); - // }; - - // match config.update(message) { - // Some(action) => { - // self.modal = Modal::None; - - // if let spectral_decay_config::Action::Apply(config) = action { - // self.spectral_decay_config = config; - - // self.measurements - // .iter_mut() - // .flat_map(ui::Measurement::analysis_mut) - // .for_each(|analysis| { - // analysis.spectral_decay = ui::spectral_decay::State::default() - // }); - - // selected_analysis - // .and_then(|id| self.measurements.iter_mut().find(|m| m.id() == id)) - // .map_or(Task::none(), |measurement| { - // compute_spectral_decay( - // loopback, - // measurement, - // self.spectral_decay_config, - // ) - // }) - // } else { - // Task::none() - // } - // } - // None => Task::none(), - // } - // } - // Message::OpenSpectrogramConfig => { - // self.modal = - // Modal::SpectrogramConfig(SpectrogramConfig::new(self.spectrogram_config)); - - // Task::none() - // } - // Message::SpectrogramConfig(message) => { - // let Modal::SpectrogramConfig(config) = &mut self.modal else { - // return Task::none(); - // }; - - // let State::Analysing { - // selected: selected_analysis, - // ref loopback, - // .. - // } = self.state - // else { - // return Task::none(); - // }; - - // match config.update(message) { - // spectrogram_config::Action::None => Task::none(), - // spectrogram_config::Action::Close => { - // self.modal = Modal::None; - - // Task::none() - // } - // spectrogram_config::Action::ConfigChanged(preferences) => { - // self.modal = Modal::None; - // self.spectrogram_config = preferences; - - // self.measurements - // .iter_mut() - // .flat_map(ui::Measurement::analysis_mut) - // .for_each(|analysis| { - // analysis.spectrogram = ui::spectrogram::State::default() - // }); - - // selected_analysis - // .and_then(|id| self.measurements.get_mut(id)) - // .map_or(Task::none(), |measurement| { - // compute_spectrogram(loopback, measurement, &self.spectrogram_config) - // }) - // } - // } - // } - // } - // } - pub fn view<'a>(&'a self, recent_projects: &'a RecentProjects) -> Element<'a, Message> { let header = { let project_menu = { @@ -1838,100 +1067,6 @@ impl Main { } } } - // pub fn view<'a>(&'a self, recent_projects: &'a RecentProjects) -> Element<'a, Message> { - // let header = { - // let project_menu = { - // let recent_project_entries = column( - // recent_projects - // .iter() - // .enumerate() - // .filter_map(|(i, p)| p.file_name().map(|f| (i, f))) - // .filter_map(|(i, p)| p.to_str().map(|f| (i, f))) - // .map(|(i, s)| { - // button(s) - // .on_press(Message::RecentProject(i)) - // .style(button::subtle) - // .width(Length::Fill) - // .into() - // }), - // ) - // .width(Length::Fill); - // column![ - // button("New") - // .on_press(Message::NewProject) - // .style(button::subtle) - // .width(Length::Fill), - // button("Save") - // .on_press(Message::SaveProject) - // .style(button::subtle) - // .width(Length::Fill), - // button("Open ...") - // .on_press(Message::LoadProject) - // .style(button::subtle) - // .width(Length::Fill), - // dropdown_menu("Open recent ...", recent_project_entries) - // .style(button::subtle) - // .width(Length::Fill), - // ] - // .width(Length::Fill) - // }; - - // container(column![ - // dropdown_root("Project", project_menu).style(button::secondary), - // match &self.state { - // State::CollectingMeasuremnts { .. } => TabId::Measurements.view(false), - // State::Analysing { active_tab, .. } => TabId::from(active_tab).view(true), - // } - // ]) - // .width(Length::Fill) - // .style(container::dark) - // }; - - // let content = match &self.state { - // State::CollectingMeasuremnts { - // recording, - // loopback, - // } => { - // if let Some(recording) = recording { - // recording.view().map(Message::Recording) - // } else { - // self.measurements_tab(loopback.as_ref()) - // } - // } - // State::Analysing { - // active_tab, - // selected: selected_analysis, - // charts, - // loopback, - // .. - // } => match active_tab { - // Tab::Measurements { recording } => { - // if let Some(recording) = recording { - // recording.view().map(Message::Recording) - // } else { - // self.measurements_tab(Some(loopback)) - // } - // } - // Tab::ImpulseResponses { - // window_settings, .. - // } => self.impulse_responses_tab( - // *selected_analysis, - // &charts.impulse_responses, - // window_settings, - // ), - // Tab::FrequencyResponses => { - // self.frequency_responses_tab(&charts.frequency_responses) - // } - // Tab::SpectralDecay => { - // self.spectral_decay_tab(*selected_analysis, &charts.spectral_decay_cache) - // } - // Tab::Spectrogram => self.spectrogram_tab(*selected_analysis, &charts.spectrogram), - // }, - // }; - - // let content = container(column![header, container(content).padding(10)]); - - // } fn measurements_tab<'a>(&'a self) -> Element<'a, Message> { let sidebar = { @@ -2449,46 +1584,6 @@ impl Main { Subscription::batch([hotkeys, recording.map(Message::Recording)]) } - - // fn analysing_possible(&self) -> bool { - // let is_loopback_loaded = if let State::CollectingMeasuremnts { - // loopback: Some(ref loopback), - // .. - // } - // | State::Analysing { ref loopback, .. } = self.state - // { - // loopback.is_loaded() - // } else { - // return false; - // }; - - // is_loopback_loaded && self.measurements.iter().any(ui::Measurement::is_loaded) - // } - // fn impulse_response_future( - // &mut self, - // id: measurement::Id, - // ) -> Option> { - // let State::Analysing { - // ref mut analyses, .. - // } = self.state - // else { - // return None; - // }; - - // let analysis = analyses.entry(id).or_default(); - - // let Some(loopback) = self.loopback.as_ref().and_then(Loopback::loaded) else { - // return None; - // }; - - // let measurement = self - // .measurements - // .get(id) - // .and_then(Measurement::signal) - // .unwrap(); - - // analysis.compute_impulse_response(loopback.clone(), measurement.clone()) - // } } fn compute_impulse_response( diff --git a/raumklang-gui/src/screen/main/chart/impulse_response.rs b/raumklang-gui/src/screen/main/chart/impulse_response.rs deleted file mode 100644 index e69de29..0000000 diff --git a/raumklang-gui/src/ui/measurement.rs b/raumklang-gui/src/ui/measurement.rs index d3eee4b..cf268ab 100644 --- a/raumklang-gui/src/ui/measurement.rs +++ b/raumklang-gui/src/ui/measurement.rs @@ -44,10 +44,7 @@ pub struct Id(usize); #[derive(Debug, Clone)] enum State { NotLoaded, - Loaded { - signal: raumklang_core::Measurement, - // analysis: Analysis, - }, + Loaded { signal: raumklang_core::Measurement }, } impl Measurement { From bdea9a17f26022b7e1727121f17f707bacdfba5d Mon Sep 17 00:00:00 2001 From: Hendrik Kunert Date: Tue, 3 Feb 2026 13:21:25 +0100 Subject: [PATCH 29/30] Fix clippy issues --- raumklang-gui/src/data/spectrogram.rs | 4 +-- raumklang-gui/src/screen/main.rs | 32 ++++++++++--------- .../src/screen/main/frequency_response.rs | 6 ++-- .../src/screen/main/impulse_response.rs | 6 ++-- .../screen/main/modal/spectrogram_config.rs | 6 ++-- raumklang-gui/src/ui/frequency_response.rs | 2 +- 6 files changed, 28 insertions(+), 28 deletions(-) diff --git a/raumklang-gui/src/data/spectrogram.rs b/raumklang-gui/src/data/spectrogram.rs index f6b9768..1737689 100644 --- a/raumklang-gui/src/data/spectrogram.rs +++ b/raumklang-gui/src/data/spectrogram.rs @@ -2,13 +2,13 @@ use core::slice; use std::{fmt, sync::Arc, time::Duration}; use rustfft::{ - num_complex::{Complex, Complex32}, FftPlanner, + num_complex::{Complex, Complex32}, }; use crate::data::{SampleRate, Samples}; -#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)] +#[derive(Debug, Clone, PartialEq, PartialOrd)] pub struct Preferences { pub span_before_peak: Duration, pub span_after_peak: Duration, diff --git a/raumklang-gui/src/screen/main.rs b/raumklang-gui/src/screen/main.rs index eae72c4..627ade9 100644 --- a/raumklang-gui/src/screen/main.rs +++ b/raumklang-gui/src/screen/main.rs @@ -298,7 +298,7 @@ impl Main { pending_window: window.clone(), }; - return Task::none(); + Task::none() } tab::Id::FrequencyResponses => { let State::Analysing { @@ -463,7 +463,7 @@ impl Main { self.ir_chart.data_cache.clear(); match tab { - Tab::Measurements { .. } => Task::none(), + Tab::Measurements => Task::none(), Tab::ImpulseResponses { .. } => compute_impulse_response( analyses, id, @@ -549,7 +549,7 @@ impl Main { }); match active_tab { - Tab::Measurements { .. } => Task::none(), + Tab::Measurements => Task::none(), Tab::ImpulseResponses { .. } => Task::none(), Tab::FrequencyResponses { .. } => compute_frequency_response( analyses, @@ -800,7 +800,7 @@ impl Main { } Message::OpenSpectrogramConfig => { self.modal = Modal::SpectrogramConfig(modal::SpectrogramConfig::new( - self.spectrogram_config, + self.spectrogram_config.clone(), )); Task::none() @@ -826,21 +826,23 @@ impl Main { } spectrogram_config::Action::ConfigChanged(preferences) => { self.modal = Modal::None; - self.spectrogram_config = preferences; - - analyses.values_mut().for_each(|a| a.spectrogram.reset()); - if let Some(id) = selected { + let task = if let Some(id) = selected { analyses - .get_mut(&id) - .and_then(move |a| { + .get_mut(id) + .and_then(|a| { a.spectrogram.compute(&a.impulse_response, &preferences) }) .map(|f| Task::perform(f, Message::SpectrogramComputed.with(*id))) .unwrap_or_default() } else { Task::none() - } + }; + + self.spectrogram_config = preferences; + analyses.values_mut().for_each(|a| a.spectrogram.reset()); + + task } } } @@ -983,7 +985,7 @@ impl Main { let tabs = row![ tab( "Measurements", - active_tab.is_none() || matches!(active_tab, Some(Tab::Measurements { .. })), + active_tab.is_none() || matches!(active_tab, Some(Tab::Measurements)), Some(tab::Id::Measurements) ), tab( @@ -1026,7 +1028,7 @@ impl Main { selected, ref analyses, } => match active_tab { - Tab::Measurements { .. } => self.measurements_tab(), + Tab::Measurements => self.measurements_tab(), Tab::ImpulseResponses { pending_window: window_settings, } => self.impulse_responses_tab( @@ -1039,7 +1041,7 @@ impl Main { self.frequency_responses_tab(cache, analyses) } Tab::SpectralDecays { cache } => { - self.spectral_decay_tab(selected, &analyses, &cache) + self.spectral_decay_tab(selected, analyses, cache) } Tab::Spectrograms => { self.spectrogram_tab(selected, analyses, &self.spectrogram) @@ -1301,7 +1303,7 @@ impl Main { ) .y_labels(Labels::default().format(&|v| format!("{v:.0}"))) .extend_series(series_list) - .cache(&cache); + .cache(cache); container(chart) } else { diff --git a/raumklang-gui/src/screen/main/frequency_response.rs b/raumklang-gui/src/screen/main/frequency_response.rs index 455bc94..7750ef5 100644 --- a/raumklang-gui/src/screen/main/frequency_response.rs +++ b/raumklang-gui/src/screen/main/frequency_response.rs @@ -64,12 +64,10 @@ pub async fn smooth_frequency_response( frequency_response: data::FrequencyResponse, fraction: u8, ) -> Box<[f32]> { - let data = tokio::task::spawn_blocking(move || { + tokio::task::spawn_blocking(move || { data::smooth_fractional_octave(&frequency_response.data.clone(), fraction) }) .await .unwrap() - .into_boxed_slice(); - - data + .into_boxed_slice() } diff --git a/raumklang-gui/src/screen/main/impulse_response.rs b/raumklang-gui/src/screen/main/impulse_response.rs index 2a22c9e..9621566 100644 --- a/raumklang-gui/src/screen/main/impulse_response.rs +++ b/raumklang-gui/src/screen/main/impulse_response.rs @@ -54,14 +54,14 @@ impl Chart { pick_list( &data::chart::AmplitudeUnit::ALL[..], Some(&self.amplitude_unit), - |unit| ChartOperation::AmplitudeUnitChanged(unit), + ChartOperation::AmplitudeUnitChanged, ) }; let chart = { container( chart::impulse_response( - &window, + window, impulse_response, &self.time_unit, &self.amplitude_unit, @@ -80,7 +80,7 @@ impl Chart { container(pick_list( &data::chart::TimeSeriesUnit::ALL[..], Some(&self.time_unit), - |unit| ChartOperation::TimeUnitChanged(unit) + ChartOperation::TimeUnitChanged )) .align_right(Length::Fill) ] diff --git a/raumklang-gui/src/screen/main/modal/spectrogram_config.rs b/raumklang-gui/src/screen/main/modal/spectrogram_config.rs index cbc195f..f580d5e 100644 --- a/raumklang-gui/src/screen/main/modal/spectrogram_config.rs +++ b/raumklang-gui/src/screen/main/modal/spectrogram_config.rs @@ -3,9 +3,9 @@ use std::time::Duration; use crate::{data::spectrogram, icon, widget::number_input}; use iced::{ - widget::{button, column, container, row, rule, scrollable, space, text, tooltip}, Alignment::Center, Element, + widget::{button, column, container, row, rule, scrollable, space, text, tooltip}, }; #[derive(Debug, Clone)] @@ -65,7 +65,7 @@ impl SpectrogramConfig { Action::None } Message::ResetToPrevious => { - self.reset_to_config(self.prev_config); + self.reset_to_config(self.prev_config.clone()); Action::None } } @@ -151,7 +151,7 @@ impl SpectrogramConfig { space::horizontal(), tooltip( button(icon::reset().center()) - .on_press_maybe(config.map(|_| Message::ResetToPrevious)) + .on_press_maybe(config.is_some().then_some(Message::ResetToPrevious)) .style(button::secondary), "Reset to defaults.", tooltip::Position::default() diff --git a/raumklang-gui/src/ui/frequency_response.rs b/raumklang-gui/src/ui/frequency_response.rs index beac1d7..3f404c6 100644 --- a/raumklang-gui/src/ui/frequency_response.rs +++ b/raumklang-gui/src/ui/frequency_response.rs @@ -87,7 +87,7 @@ impl FrequencyResponse { State::Computed(_) => item, }; - sidebar::item(content, false).into() + sidebar::item(content, false) } pub fn result(&self) -> Option<&data::FrequencyResponse> { From 75415d8df4331928c105c02fa18557007aadcf56 Mon Sep 17 00:00:00 2001 From: Hendrik Kunert Date: Tue, 3 Feb 2026 13:50:17 +0100 Subject: [PATCH 30/30] Fix formatting --- raumklang-gui/src/audio.rs | 2 +- raumklang-gui/src/audio/loudness.rs | 2 +- raumklang-gui/src/audio/measurement.rs | 6 ++-- raumklang-gui/src/data.rs | 4 +-- raumklang-gui/src/data/recent_projects.rs | 2 +- raumklang-gui/src/data/spectral_decay.rs | 4 +-- raumklang-gui/src/screen.rs | 2 +- raumklang-gui/src/screen/landing.rs | 4 +-- .../src/screen/main/chart/spectrogram.rs | 2 +- .../src/screen/main/chart/waveform.rs | 2 +- .../src/screen/main/modal/pending_window.rs | 2 +- .../main/modal/spectral_decay_config.rs | 2 +- raumklang-gui/src/screen/main/recording.rs | 30 ++++++++++--------- .../screen/main/recording/page/component.rs | 5 ++-- .../screen/main/recording/page/measurement.rs | 4 +-- raumklang-gui/src/ui/analysis.rs | 4 +-- raumklang-gui/src/widget.rs | 4 +-- raumklang-gui/src/widget/meter.rs | 2 +- 18 files changed, 42 insertions(+), 41 deletions(-) diff --git a/raumklang-gui/src/audio.rs b/raumklang-gui/src/audio.rs index dc8101b..a59bb79 100644 --- a/raumklang-gui/src/audio.rs +++ b/raumklang-gui/src/audio.rs @@ -20,8 +20,8 @@ use tokio::sync::mpsc; use tokio::sync::mpsc::error::TryRecvError; use tokio_stream::wrappers::ReceiverStream; -use std::sync::atomic::{self, AtomicBool}; use std::sync::Arc; +use std::sync::atomic::{self, AtomicBool}; use std::thread; use std::time::Duration; diff --git a/raumklang-gui/src/audio/loudness.rs b/raumklang-gui/src/audio/loudness.rs index 1324d97..6237047 100644 --- a/raumklang-gui/src/audio/loudness.rs +++ b/raumklang-gui/src/audio/loudness.rs @@ -4,7 +4,7 @@ use tokio::sync::mpsc::error::TrySendError; use std::time::{Duration, Instant}; -use super::{process::Control, Process}; +use super::{Process, process::Control}; #[derive(Debug, Clone, Copy)] pub struct Loudness { diff --git a/raumklang-gui/src/audio/measurement.rs b/raumklang-gui/src/audio/measurement.rs index 43f17d0..5c8cb59 100644 --- a/raumklang-gui/src/audio/measurement.rs +++ b/raumklang-gui/src/audio/measurement.rs @@ -1,16 +1,16 @@ use crate::log; -use super::{loudness, process::Control, Process}; +use super::{Process, loudness, process::Control}; use ringbuf::{ - traits::{Consumer as _, Producer as _, Split as _}, HeapCons, HeapProd, HeapRb, + traits::{Consumer as _, Producer as _, Split as _}, }; use std::{ sync::{ - atomic::{self, AtomicBool}, Arc, + atomic::{self, AtomicBool}, }, time::Duration, }; diff --git a/raumklang-gui/src/data.rs b/raumklang-gui/src/data.rs index 3ba8498..1759bfe 100644 --- a/raumklang-gui/src/data.rs +++ b/raumklang-gui/src/data.rs @@ -21,8 +21,8 @@ pub use spectral_decay::SpectralDecay; pub use spectrogram::Spectrogram; pub use window::Window; -use ndarray::{concatenate, Array, Array1, ArrayView, Axis}; -use ndarray_interp::interp1d::{cubic_spline::CubicSpline, Interp1DBuilder}; +use ndarray::{Array, Array1, ArrayView, Axis, concatenate}; +use ndarray_interp::interp1d::{Interp1DBuilder, cubic_spline::CubicSpline}; use ndarray_stats::SummaryStatisticsExt; use std::{io, sync::Arc}; diff --git a/raumklang-gui/src/data/recent_projects.rs b/raumklang-gui/src/data/recent_projects.rs index bbe738d..8719bb8 100644 --- a/raumklang-gui/src/data/recent_projects.rs +++ b/raumklang-gui/src/data/recent_projects.rs @@ -1,7 +1,7 @@ use super::Error; use std::{ - collections::{vec_deque, VecDeque}, + collections::{VecDeque, vec_deque}, io, path::{Path, PathBuf}, }; diff --git a/raumklang-gui/src/data/spectral_decay.rs b/raumklang-gui/src/data/spectral_decay.rs index a521378..4accde4 100644 --- a/raumklang-gui/src/data/spectral_decay.rs +++ b/raumklang-gui/src/data/spectral_decay.rs @@ -3,11 +3,11 @@ use std::{fmt, sync::Arc, time::Duration}; use raumklang_core::{Window, WindowBuilder}; use rustfft::{ - num_complex::{Complex, Complex32}, FftPlanner, + num_complex::{Complex, Complex32}, }; -use crate::data::{smooth_fractional_octave, SampleRate, Samples}; +use crate::data::{SampleRate, Samples, smooth_fractional_octave}; #[derive(Clone)] pub struct SpectralDecay(Vec); diff --git a/raumklang-gui/src/screen.rs b/raumklang-gui/src/screen.rs index 4ad9bd1..4027bb4 100644 --- a/raumklang-gui/src/screen.rs +++ b/raumklang-gui/src/screen.rs @@ -5,8 +5,8 @@ pub use landing::landing; pub use main::Main; use iced::{ - widget::{container, text}, Element, Length, + widget::{container, text}, }; #[allow(clippy::large_enum_variant)] diff --git a/raumklang-gui/src/screen/landing.rs b/raumklang-gui/src/screen/landing.rs index 314accf..270502a 100644 --- a/raumklang-gui/src/screen/landing.rs +++ b/raumklang-gui/src/screen/landing.rs @@ -1,10 +1,10 @@ use crate::data::RecentProjects; use iced::{ - font, - widget::{button, column, container, row, scrollable, space, text}, Element, Font, Length::{self, Fill}, + font, + widget::{button, column, container, row, scrollable, space, text}, }; #[derive(Debug, Clone)] diff --git a/raumklang-gui/src/screen/main/chart/spectrogram.rs b/raumklang-gui/src/screen/main/chart/spectrogram.rs index 37e9eba..8f13ae9 100644 --- a/raumklang-gui/src/screen/main/chart/spectrogram.rs +++ b/raumklang-gui/src/screen/main/chart/spectrogram.rs @@ -8,9 +8,9 @@ use crate::{ use super::{HorizontalAxis, Offset, VerticalAxis, Zoom}; use iced::{ + Event, Point, Rectangle, Renderer, Size, Vector, mouse::{self, ScrollDelta}, widget::canvas::{self}, - Event, Point, Rectangle, Renderer, Size, Vector, }; use raumklang_core::dbfs; diff --git a/raumklang-gui/src/screen/main/chart/waveform.rs b/raumklang-gui/src/screen/main/chart/waveform.rs index 36928f6..d3dd881 100644 --- a/raumklang-gui/src/screen/main/chart/waveform.rs +++ b/raumklang-gui/src/screen/main/chart/waveform.rs @@ -1,9 +1,9 @@ use super::{HorizontalAxis, Offset, VerticalAxis, Zoom}; use iced::{ + Event, Point, Rectangle, Renderer, Size, Vector, mouse::{self, ScrollDelta}, widget::canvas::{self}, - Event, Point, Rectangle, Renderer, Size, Vector, }; use std::cmp::Ordering; diff --git a/raumklang-gui/src/screen/main/modal/pending_window.rs b/raumklang-gui/src/screen/main/modal/pending_window.rs index c3fd7de..2f0ed63 100644 --- a/raumklang-gui/src/screen/main/modal/pending_window.rs +++ b/raumklang-gui/src/screen/main/modal/pending_window.rs @@ -1,6 +1,6 @@ use iced::{ - widget::{button, column, container, row, space, text}, Element, + widget::{button, column, container, row, space, text}, }; #[derive(Debug, Clone)] diff --git a/raumklang-gui/src/screen/main/modal/spectral_decay_config.rs b/raumklang-gui/src/screen/main/modal/spectral_decay_config.rs index b22ff2e..8dc3e82 100644 --- a/raumklang-gui/src/screen/main/modal/spectral_decay_config.rs +++ b/raumklang-gui/src/screen/main/modal/spectral_decay_config.rs @@ -5,9 +5,9 @@ use crate::{ }; use iced::{ - widget::{button, column, container, row, rule, scrollable, space, text, tooltip}, Alignment::Center, Element, + widget::{button, column, container, row, rule, scrollable, space, text, tooltip}, }; #[derive(Debug, Clone)] diff --git a/raumklang-gui/src/screen/main/recording.rs b/raumklang-gui/src/screen/main/recording.rs index a00a790..32c138d 100644 --- a/raumklang-gui/src/screen/main/recording.rs +++ b/raumklang-gui/src/screen/main/recording.rs @@ -9,18 +9,18 @@ use crate::{ measurement::config, recording::{self, volume}, }, - screen::main::recording::page::{measurement, Component}, - widget::{meter, RmsPeakMeter}, + screen::main::recording::page::{Component, measurement}, + widget::{RmsPeakMeter, meter}, }; use iced::{ - alignment::{Horizontal, Vertical}, - time, - widget::{self, canvas, column, container, pick_list, row, rule, slider, text, text_input}, Alignment::Center, Element, Length::{Fill, Shrink}, Subscription, Task, + alignment::{Horizontal, Vertical}, + time, + widget::{self, canvas, column, container, pick_list, row, rule, slider, text, text_input}, }; use tokio_stream::wrappers::ReceiverStream; @@ -571,15 +571,17 @@ where column![] .push(self.label.map(text)) .push( - row![text_input("", self.value) - .id(widget::Id::new("from")) - .align_x(Horizontal::Right) - .on_input_maybe(self.on_input) - .style(if self.is_valid { - text_input::default - } else { - number_input_danger - })] + row![ + text_input("", self.value) + .id(widget::Id::new("from")) + .align_x(Horizontal::Right) + .on_input_maybe(self.on_input) + .style(if self.is_valid { + text_input::default + } else { + number_input_danger + }) + ] .push(self.unit.map(text)) .align_y(Vertical::Center) .spacing(3), diff --git a/raumklang-gui/src/screen/main/recording/page/component.rs b/raumklang-gui/src/screen/main/recording/page/component.rs index 39c3049..c5a0ac0 100644 --- a/raumklang-gui/src/screen/main/recording/page/component.rs +++ b/raumklang-gui/src/screen/main/recording/page/component.rs @@ -1,13 +1,12 @@ use crate::data; use iced::{ + Element, Length, alignment::Vertical, widget::{ - button, column, container, row, rule, space, text, + Button, button, column, container, row, rule, space, text, text::{Fragment, IntoFragment}, - Button, }, - Element, Length, }; pub struct Page<'a, Message> { diff --git a/raumklang-gui/src/screen/main/recording/page/measurement.rs b/raumklang-gui/src/screen/main/recording/page/measurement.rs index 898d7a3..044a300 100644 --- a/raumklang-gui/src/screen/main/recording/page/measurement.rs +++ b/raumklang-gui/src/screen/main/recording/page/measurement.rs @@ -7,11 +7,11 @@ use crate::{ }; use iced::{ + Element, Length, Task, alignment::{Horizontal, Vertical}, widget::{canvas, column, container, row, rule, text}, - Element, Length, Task, }; -use prism::{line_series, Chart}; +use prism::{Chart, line_series}; use tokio_stream::wrappers::ReceiverStream; #[derive(Debug)] diff --git a/raumklang-gui/src/ui/analysis.rs b/raumklang-gui/src/ui/analysis.rs index aba8791..0c1adc1 100644 --- a/raumklang-gui/src/ui/analysis.rs +++ b/raumklang-gui/src/ui/analysis.rs @@ -1,6 +1,6 @@ use crate::ui::{ - impulse_response, spectral_decay::SpectralDecay, spectrogram::Spectrogram, FrequencyResponse, - ImpulseResponse, + FrequencyResponse, ImpulseResponse, impulse_response, spectral_decay::SpectralDecay, + spectrogram::Spectrogram, }; #[derive(Debug, Clone, Default)] diff --git a/raumklang-gui/src/widget.rs b/raumklang-gui/src/widget.rs index 1d04e69..af731d9 100644 --- a/raumklang-gui/src/widget.rs +++ b/raumklang-gui/src/widget.rs @@ -4,10 +4,10 @@ pub mod sidebar; pub use meter::RmsPeakMeter; use iced::{ - alignment::Horizontal::Right, - widget::{column, container, stack, text, text_input, tooltip}, Color, Element, Font, Length::Fill, + alignment::Horizontal::Right, + widget::{column, container, stack, text, text_input, tooltip}, }; use std::fmt; diff --git a/raumklang-gui/src/widget/meter.rs b/raumklang-gui/src/widget/meter.rs index f9df1aa..5596b14 100644 --- a/raumklang-gui/src/widget/meter.rs +++ b/raumklang-gui/src/widget/meter.rs @@ -1,10 +1,10 @@ use iced::{ + Font, Pixels, Point, Renderer, Size, advanced::{graphics::text::Paragraph, text::Paragraph as _}, widget::{ canvas, text::{LineHeight, Shaping}, }, - Font, Pixels, Point, Renderer, Size, }; pub struct RmsPeakMeter<'a> {