use super::{CLIENT, Maolan};
use crate::{
consts::gui::{METER_DIRTY_EPSILON_DB, METER_QUANTIZE_STEP_DB},
message::{Message, Show},
ui_timing::{
PLAYHEAD_UPDATE_INTERVAL, RECORDING_PREVIEW_PEAKS_UPDATE_INTERVAL,
RECORDING_PREVIEW_UPDATE_INTERVAL,
},
};
use iced::futures::{Stream, StreamExt, stream};
use iced::keyboard::Event as KeyEvent;
use iced::{Subscription, event, keyboard, mouse, window};
use maolan_engine::message::{Action as EngineAction, Message as EngineMessage};
use std::collections::HashMap;
use std::time::Duration;
#[cfg(unix)]
use tokio::signal::unix::{SignalKind, signal};
impl Maolan {
fn should_poll_meters(
mixer_visible: bool,
playing: bool,
paused: bool,
hw_out_meter_db: &[f32],
track_meter_dbs: impl IntoIterator<Item = impl AsRef<[f32]>>,
) -> bool {
if !mixer_visible {
return false;
}
if playing || paused {
return true;
}
hw_out_meter_db.iter().any(|level| *level > -90.0)
|| track_meter_dbs
.into_iter()
.any(|meter_db| meter_db.as_ref().iter().any(|level| *level > -90.0))
}
pub fn subscription(&self) -> Subscription<Message> {
fn quantize_meter_db(level_db: f32) -> f32 {
let step = METER_QUANTIZE_STEP_DB;
((level_db / step).round() * step).clamp(-90.0, 20.0)
}
fn meter_changed(prev: &[f32], next: &[f32]) -> bool {
if prev.len() != next.len() {
return true;
}
prev.iter().zip(next.iter()).any(|(a, b)| {
(quantize_meter_db(*a) - quantize_meter_db(*b)).abs() >= METER_DIRTY_EPSILON_DB
})
}
fn listener() -> impl Stream<Item = Message> {
stream::once(CLIENT.subscribe()).flat_map(|receiver| {
#[cfg(all(unix, not(target_os = "macos")))]
let initial_messages = vec![
Message::RefreshLv2Plugins,
Message::RefreshVst3Plugins,
Message::RefreshClapPlugins,
];
#[cfg(not(all(unix, not(target_os = "macos"))))]
let initial_messages =
vec![Message::RefreshVst3Plugins, Message::RefreshClapPlugins];
let initial = stream::iter(initial_messages);
initial.chain(stream::unfold(
(
receiver,
Vec::<f32>::new(),
HashMap::<String, Vec<f32>>::new(),
),
|(mut rx, mut last_hw_out, mut last_meters)| async move {
loop {
match rx.recv().await {
Some(EngineMessage::Response(r)) => {
if let Ok(action) = &r {
match action {
EngineAction::TrackMeters {
track_name,
output_db,
} => {
let should_forward = match last_meters
.get(track_name)
{
Some(prev) => meter_changed(prev, output_db),
None => true,
};
if !should_forward {
continue;
}
last_meters
.insert(track_name.clone(), output_db.clone());
}
EngineAction::MeterSnapshot {
hw_out_db,
track_meters,
} => {
let hw_changed =
meter_changed(&last_hw_out, hw_out_db);
let track_count_changed =
last_meters.len() != track_meters.len();
let tracks_changed = track_count_changed
|| track_meters.iter().any(
|(track_name, output_db)| match last_meters
.get(track_name)
{
Some(prev) => {
meter_changed(prev, output_db)
}
None => true,
},
);
if !hw_changed && !tracks_changed {
continue;
}
last_hw_out.clear();
last_hw_out.extend_from_slice(hw_out_db);
last_meters.clear();
last_meters.reserve(track_meters.len());
for (track_name, output_db) in track_meters.iter() {
last_meters.insert(
track_name.clone(),
output_db.clone(),
);
}
}
_ => {}
}
}
return Some((
Message::Response(r),
(rx, last_hw_out, last_meters),
));
}
Some(_) => {}
None => return None,
}
}
},
))
})
}
let engine_sub = Subscription::run(listener);
let keyboard_sub = keyboard::listen().map(Self::keyboard_message);
let event_sub = event::listen().map(|event| match event {
event::Event::Mouse(mouse_event) => match mouse_event {
mouse::Event::ButtonPressed(button) => Message::MousePressed(button),
mouse::Event::CursorMoved { .. } => Message::MouseMoved(mouse_event),
mouse::Event::ButtonReleased(_) => Message::MouseReleased,
_ => Message::None,
},
event::Event::Window(window::Event::Resized(size)) => Message::WindowResized(size),
event::Event::Window(window::Event::CloseRequested) => Message::WindowCloseRequested,
_ => Message::None,
});
let shutdown_sub = Subscription::run(|| {
stream::unfold((), |_| async move {
#[cfg(unix)]
{
let mut sigint = signal(SignalKind::interrupt()).ok()?;
let mut sigquit = signal(SignalKind::quit()).ok()?;
let mut sigterm = signal(SignalKind::terminate()).ok()?;
tokio::select! {
_ = sigint.recv() => Some((Message::WindowCloseRequested, ())),
_ = sigquit.recv() => Some((Message::WindowCloseRequested, ())),
_ = sigterm.recv() => Some((Message::WindowCloseRequested, ())),
}
}
#[cfg(not(unix))]
{
tokio::signal::ctrl_c().await.ok()?;
Some((Message::WindowCloseRequested, ()))
}
})
});
let playback_sub = if self.playing && !self.paused {
iced::time::every(PLAYHEAD_UPDATE_INTERVAL).map(|_| Message::PlaybackTick)
} else {
Subscription::none()
};
let should_poll_meters = if self.mixer_visible {
let state = self.state.blocking_read();
Self::should_poll_meters(
self.mixer_visible,
self.playing,
self.paused,
&state.hw_out_meter_db,
state
.tracks
.iter()
.map(|track| track.meter_out_db.as_slice()),
)
} else {
false
};
let meter_poll_sub = if should_poll_meters {
iced::time::every(Duration::from_millis(40)).map(|_| Message::MeterPollTick)
} else {
Subscription::none()
};
let recording_preview_sub = if self.playing
&& !self.paused
&& self.record_armed
&& self.recording_preview_start_sample.is_some()
{
iced::time::every(RECORDING_PREVIEW_UPDATE_INTERVAL)
.map(|_| Message::RecordingPreviewTick)
} else {
Subscription::none()
};
let recording_preview_peaks_sub = if self.playing
&& !self.paused
&& self.record_armed
&& self.recording_preview_start_sample.is_some()
{
iced::time::every(RECORDING_PREVIEW_PEAKS_UPDATE_INTERVAL)
.map(|_| Message::RecordingPreviewPeaksTick)
} else {
Subscription::none()
};
let autosave_sub =
iced::time::every(Duration::from_secs(15)).map(|_| Message::AutosaveSnapshotTick);
let peak_rebuild_sub = if !self.pending_peak_rebuilds.is_empty() {
iced::time::every(Duration::from_millis(16)).map(|_| Message::DrainAudioPeakUpdates)
} else {
Subscription::none()
};
let clap_ui_sub = if self.clap_ui_host.has_pending_ui_work() {
iced::time::every(Duration::from_millis(16)).map(|_| Message::PumpClapUi)
} else {
Subscription::none()
};
#[cfg(all(unix, not(target_os = "macos")))]
let lv2_ui_sub = if self.lv2_ui_host.has_open_windows() {
iced::time::every(Duration::from_millis(16)).map(|_| Message::PumpLv2Ui)
} else {
Subscription::none()
};
let hw_mixer_sub = {
let state = self.state.blocking_read();
if matches!(state.view, crate::state::View::X32) {
mixosc::app::subscription(&self.hw_mixer).map(Message::HwMixer)
} else {
Subscription::none()
}
};
Subscription::batch(vec![
engine_sub,
keyboard_sub,
event_sub,
shutdown_sub,
playback_sub,
meter_poll_sub,
autosave_sub,
peak_rebuild_sub,
clap_ui_sub,
recording_preview_sub,
recording_preview_peaks_sub,
hw_mixer_sub,
#[cfg(all(unix, not(target_os = "macos")))]
lv2_ui_sub,
])
}
fn keyboard_message(event: KeyEvent) -> Message {
match event {
KeyEvent::KeyPressed { key, modifiers, .. } => {
if modifiers.control()
&& let keyboard::Key::Character(ch) = &key
{
let s = ch.to_ascii_lowercase();
if s == "n" {
return Message::NewSession;
}
if s == "o" {
return Message::Show(Show::Open);
}
if s == "i" {
return Message::OpenFileImporter;
}
if s == "e" {
return Message::OpenExporter;
}
if s == "s" {
if modifiers.shift() {
return Message::Show(Show::SaveAs);
}
return Message::Show(Show::Save);
}
if s == "t" {
return Message::Show(Show::AddTrack);
}
if s == "a" {
return Message::SelectAll;
}
if s == "r" {
return Message::TransportRecordToggle;
}
if s == "l" {
return Message::TransportPanic;
}
if s == "z" {
if modifiers.shift() {
return Message::Redo;
}
return Message::Undo;
}
if s == "y" {
return Message::Redo;
}
}
match key {
keyboard::Key::Character(ch) if !modifiers.control() => {
let s = ch.to_ascii_lowercase();
if s == "q" {
Message::PianoQuantizeSelectedNotes
} else if s == "h" {
Message::PianoHumanizeSelectedNotes
} else if s == "g" {
Message::PianoGrooveSelectedNotes
} else {
Message::None
}
}
keyboard::Key::Named(keyboard::key::Named::Space) if modifiers.shift() => {
Message::TransportPause
}
keyboard::Key::Named(keyboard::key::Named::Space) => Message::ToggleTransport,
keyboard::Key::Named(keyboard::key::Named::Home) => Message::JumpToStart,
keyboard::Key::Named(keyboard::key::Named::End) => Message::JumpToEnd,
keyboard::Key::Named(keyboard::key::Named::Shift) => Message::ShiftPressed,
keyboard::Key::Named(keyboard::key::Named::Control) => Message::CtrlPressed,
keyboard::Key::Named(keyboard::key::Named::Delete)
| keyboard::Key::Named(keyboard::key::Named::Backspace) => Message::Remove,
keyboard::Key::Named(keyboard::key::Named::Escape) => Message::EscapePressed,
_ => Message::None,
}
}
KeyEvent::KeyReleased { key, .. } => match key {
keyboard::Key::Named(keyboard::key::Named::Shift) => Message::ShiftReleased,
keyboard::Key::Named(keyboard::key::Named::Control) => Message::CtrlReleased,
_ => Message::None,
},
_ => Message::None,
}
}
}
#[cfg(test)]
mod tests {
use super::Maolan;
use crate::message::Message;
use iced::keyboard::{self, Event as KeyEvent, Key, Modifiers, key::Named};
#[test]
fn polls_meters_while_playing() {
assert!(Maolan::should_poll_meters(
true,
true,
false,
&[-90.0],
[&[-90.0][..]]
));
}
#[test]
fn polls_meters_while_paused() {
assert!(Maolan::should_poll_meters(
true,
false,
true,
&[-90.0],
[&[-90.0][..]]
));
}
#[test]
fn polls_meters_after_stop_when_hw_out_is_still_active() {
assert!(Maolan::should_poll_meters(
true,
false,
false,
&[-24.0],
[&[-90.0][..]]
));
}
#[test]
fn polls_meters_after_stop_when_track_is_still_active() {
assert!(Maolan::should_poll_meters(
true,
false,
false,
&[-90.0],
[&[-18.0][..]]
));
}
#[test]
fn stops_polling_meters_when_transport_is_stopped_and_all_meters_are_silent() {
assert!(!Maolan::should_poll_meters(
true,
false,
false,
&[-90.0, -90.0],
[&[-90.0, -90.0][..], &[-90.0][..]],
));
}
#[test]
fn does_not_poll_meters_when_mixer_is_hidden() {
assert!(!Maolan::should_poll_meters(
false,
true,
false,
&[0.0],
[&[0.0][..]],
));
}
#[test]
fn keyboard_shortcuts_map_to_transport_actions() {
assert!(matches!(
Maolan::keyboard_message(KeyEvent::KeyPressed {
key: Key::Character("r".into()),
modified_key: Key::Character("r".into()),
physical_key: keyboard::key::Physical::Code(keyboard::key::Code::KeyR),
location: keyboard::Location::Standard,
modifiers: Modifiers::CTRL,
text: Some("r".into()),
repeat: false,
}),
Message::TransportRecordToggle
));
assert!(matches!(
Maolan::keyboard_message(KeyEvent::KeyPressed {
key: Key::Character("l".into()),
modified_key: Key::Character("l".into()),
physical_key: keyboard::key::Physical::Code(keyboard::key::Code::KeyL),
location: keyboard::Location::Standard,
modifiers: Modifiers::CTRL,
text: Some("l".into()),
repeat: false,
}),
Message::TransportPanic
));
assert!(matches!(
Maolan::keyboard_message(KeyEvent::KeyPressed {
key: Key::Named(Named::Home),
modified_key: Key::Named(Named::Home),
physical_key: keyboard::key::Physical::Code(keyboard::key::Code::Home),
location: keyboard::Location::Standard,
modifiers: Modifiers::default(),
text: None,
repeat: false,
}),
Message::JumpToStart
));
assert!(matches!(
Maolan::keyboard_message(KeyEvent::KeyPressed {
key: Key::Named(Named::End),
modified_key: Key::Named(Named::End),
physical_key: keyboard::key::Physical::Code(keyboard::key::Code::End),
location: keyboard::Location::Standard,
modifiers: Modifiers::default(),
text: None,
repeat: false,
}),
Message::JumpToEnd
));
}
}