use std::path::PathBuf;
use dawkey_core::{ClipId, Controller, NoteId, PluginId, TrackId};
use serde::Serialize;
pub(crate) const LANE_H: f32 = 72.0;
pub(crate) const HEADER_W: f32 = 250.0;
pub(crate) const RULER_H: f32 = 28.0;
pub(crate) const FADER_H: f32 = 220.0;
pub(crate) const PAN_W: f32 = 76.0;
pub(crate) fn track_tops(lanes: impl Iterator<Item = (usize, usize)>) -> (Vec<f32>, f32) {
let mut tops = Vec::new();
let mut y = 0.0;
for (takes, autos) in lanes {
tops.push(y);
y += LANE_H + takes as f32 * TAKE_H + autos as f32 * AUTO_H;
}
(tops, y)
}
pub(crate) fn takes_shown(view: &ViewState, track: TrackId, lanes: usize) -> usize {
if lanes > 1 && view.takes_shown.contains(&track) {
lanes
} else {
0
}
}
pub(crate) const MIN_ZOOM: f32 = 2.0;
pub(crate) const MAX_ZOOM: f32 = 4000.0;
pub(crate) const ROW_H: f32 = 12.0;
pub(crate) const KEYS_W: f32 = 56.0;
pub(crate) const MIN_BEAT_ZOOM: f32 = 8.0;
pub(crate) const MAX_BEAT_ZOOM: f32 = 1000.0;
pub(crate) const ROLL_LANE_H: f32 = 96.0;
pub(crate) const SLIDER_W: f32 = 84.0;
pub(crate) const AUTO_H: f32 = 44.0;
pub(crate) const TAKE_H: f32 = 48.0;
pub(crate) const PARAM_W: f32 = 180.0;
pub(crate) const TEMPO_H: f32 = 44.0;
pub(crate) const METER_H: f32 = 24.0;
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum Page {
#[default]
Arrange,
Mixer,
PianoRoll,
}
#[derive(Debug, Clone, PartialEq)]
pub(crate) enum Gesture {
Clip {
clip: ClipId,
grab_s: f64,
preview_start_s: f64,
preview_track: TrackId,
copy: bool,
},
Comp {
track: TrackId,
clip: ClipId,
lane: u32,
anchor_s: f64,
current_s: f64,
},
TrimClip {
clip: ClipId,
edge: ClipEdge,
grab_s: f64,
min_s: f64,
max_s: f64,
start_s: f64,
end_s: f64,
},
Repeat {
clip: ClipId,
grab_s: f64,
max_s: f64,
until_s: f64,
},
Fader {
track: TrackId,
start_db: f32,
merge_key: String,
},
Pan {
track: TrackId,
start_pan: f32,
merge_key: String,
},
Scrub,
Range {
kind: RangeKind,
anchor_s: f64,
current_s: f64,
},
MoveNotes {
clip: ClipId,
notes: Vec<NoteId>,
origin_beats: f64,
origin_pitch: i32,
anchor_beats: f64,
delta_beats: f64,
delta_pitch: i32,
},
ResizeNote {
clip: ClipId,
note: NoteId,
start_beats: f64,
length_beats: f64,
},
Marquee {
clip: ClipId,
base: Vec<NoteId>,
from: (f64, i32),
to: (f64, i32),
},
Velocity {
clip: ClipId,
notes: Vec<(NoteId, u8)>,
delta: i32,
},
ControlPoint {
clip: ClipId,
controller: Controller,
index: usize,
beat: f64,
value: u16,
},
Paint {
clip: ClipId,
lane: RollLane,
path: Vec<(f64, u16)>,
},
PluginParam {
plugin: PluginId,
param: String,
start_value: f64,
min: f64,
max: f64,
stepped: bool,
merge_key: String,
},
OverlayScroll {
start_scroll: f32,
per_px: f32,
max: f32,
},
AutoPoint {
track: TrackId,
lane: usize,
index: usize,
time_s: f64,
value: f64,
},
SynthParam {
track: TrackId,
param: SynthParam,
start_pos: f32,
merge_key: String,
},
TempoPoint {
index: usize,
grab_beats: f64,
start_bpm: f64,
beat: f64,
bpm: f64,
scale: (f64, f64),
},
MeterChange {
index: usize,
grab_beats: f64,
bar: u32,
},
DeviceLatency {
track: TrackId,
start_pos: f32,
merge_key: String,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ClipEdge {
Start,
End,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum PendingAction {
OpenProject,
Close,
}
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct TextEdit {
pub field: TextField,
pub text: String,
pub selected: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case", tag = "kind", content = "index")]
pub enum TextField {
Tempo(usize),
MeterNumerator(usize),
MeterDenominator(usize),
DeviceLatency(TrackId),
PluginParam { plugin: PluginId, param: String },
RecordOffset,
McpPort,
}
pub(crate) fn latency_position(ms: f64) -> f32 {
((ms / dawkey_core::DEVICE_LATENCY_MS_MAX).cbrt() as f32).clamp(0.0, 1.0)
}
pub(crate) fn latency_value(position: f32) -> f64 {
let p = f64::from(position.clamp(0.0, 1.0));
(dawkey_core::DEVICE_LATENCY_MS_MAX * p.powi(3) * 10.0).round() / 10.0
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum SynthParam {
Attack,
Decay,
Sustain,
Release,
Cutoff,
Resonance,
FilterEnv,
}
impl SynthParam {
pub(crate) const ALL: [SynthParam; 7] = [
Self::Attack,
Self::Decay,
Self::Sustain,
Self::Release,
Self::Cutoff,
Self::Resonance,
Self::FilterEnv,
];
pub(crate) fn name(self) -> &'static str {
match self {
Self::Attack => "attack",
Self::Decay => "decay",
Self::Sustain => "sustain",
Self::Release => "release",
Self::Cutoff => "cutoff",
Self::Resonance => "resonance",
Self::FilterEnv => "env",
}
}
pub(crate) fn from_name(name: &str) -> Option<Self> {
Self::ALL.into_iter().find(|p| p.name() == name)
}
pub(crate) fn get(self, p: &dawkey_core::SynthParams) -> f32 {
match self {
Self::Attack => p.attack_ms,
Self::Decay => p.decay_ms,
Self::Sustain => p.sustain,
Self::Release => p.release_ms,
Self::Cutoff => p.cutoff_hz,
Self::Resonance => p.resonance,
Self::FilterEnv => p.filter_env_octaves,
}
}
pub(crate) fn position(self, value: f32) -> f32 {
let p = match self {
Self::Attack => (value / 10_000.0).cbrt(),
Self::Decay | Self::Release => (value / 20_000.0).cbrt(),
Self::Cutoff => (value / 20.0).max(1.0).log10() / 3.0,
Self::Sustain | Self::Resonance => value,
Self::FilterEnv => value / 8.0,
};
p.clamp(0.0, 1.0)
}
pub(crate) fn value(self, position: f32) -> f32 {
let p = position.clamp(0.0, 1.0);
let round = |v: f32, step: f32| (v / step).round() * step;
match self {
Self::Attack => round(10_000.0 * p.powi(3), 0.1),
Self::Decay | Self::Release => round(20_000.0 * p.powi(3), 0.1),
Self::Cutoff => round(20.0 * 1000f32.powf(p), 1.0).clamp(20.0, 20_000.0),
Self::Sustain | Self::Resonance => round(p, 0.01),
Self::FilterEnv => round(8.0 * p, 0.05),
}
}
pub(crate) fn text(self, value: f32) -> String {
let ms = |v: f32| {
if v >= 1000.0 {
format!("{:.2} s", v / 1000.0)
} else {
format!("{v:.0} ms")
}
};
match self {
Self::Attack => format!("Att {}", ms(value)),
Self::Decay => format!("Dec {}", ms(value)),
Self::Sustain => format!("Sus {:.0}%", value * 100.0),
Self::Release => format!("Rel {}", ms(value)),
Self::Cutoff if value >= 1000.0 => format!("Cut {:.1} kHz", value / 1000.0),
Self::Cutoff => format!("Cut {value:.0} Hz"),
Self::Resonance => format!("Res {:.0}%", value * 100.0),
Self::FilterEnv => format!("Env {value:.1} oct"),
}
}
pub(crate) fn command(self, track: TrackId, value: f32) -> dawkey_core::SetSynth {
let mut c = dawkey_core::SetSynth::new(track);
let field = match self {
Self::Attack => &mut c.attack_ms,
Self::Decay => &mut c.decay_ms,
Self::Sustain => &mut c.sustain,
Self::Release => &mut c.release_ms,
Self::Cutoff => &mut c.cutoff_hz,
Self::Resonance => &mut c.resonance,
Self::FilterEnv => &mut c.filter_env_octaves,
};
*field = Some(value);
c
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case", tag = "kind")]
pub enum RollLane {
#[default]
Velocity,
Control {
controller: Controller,
},
}
impl RollLane {
pub(crate) const COMMON: [RollLane; 6] = [
Self::Velocity,
Self::Control {
controller: Controller::PitchBend,
},
Self::Control {
controller: Controller::Cc { number: 1 },
},
Self::Control {
controller: Controller::Cc { number: 11 },
},
Self::Control {
controller: Controller::Cc { number: 64 },
},
Self::Control {
controller: Controller::ChannelPressure,
},
];
pub(crate) fn range(self) -> (u16, u16) {
match self {
Self::Velocity => (1, 127),
Self::Control { controller } => (0, controller.max()),
}
}
pub(crate) fn label(self) -> String {
match self {
Self::Velocity => "Vel".into(),
Self::Control { controller } => match controller {
Controller::PitchBend => "Bend".into(),
Controller::ChannelPressure => "Press".into(),
Controller::PolyPressure { pitch } => format!("PP {pitch}"),
Controller::Program => "Prog".into(),
Controller::Cc { number: 1 } => "Mod".into(),
Controller::Cc { number: 7 } => "Vol".into(),
Controller::Cc { number: 11 } => "Expr".into(),
Controller::Cc { number: 64 } => "Sus".into(),
Controller::Cc { number } => format!("CC {number}"),
Controller::NotePitchBend { pitch } => format!("NB {pitch}"),
Controller::NoteController { pitch, number } => format!("NC {pitch}/{number}"),
Controller::NoteAssignable { pitch, number } => format!("NA {pitch}/{number}"),
},
}
}
pub(crate) fn controller(self) -> Option<Controller> {
match self {
Self::Velocity => None,
Self::Control { controller } => Some(controller),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case", tag = "kind", content = "index")]
pub enum Marker {
Tempo(usize),
Meter(usize),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum RangeKind {
Loop,
Punch,
}
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct ViewState {
pub page: Page,
pub px_per_s: f32,
pub scroll_s: f64,
pub scroll_y: f32,
pub selected_clip: Option<ClipId>,
pub clipboard: Option<ClipId>,
pub snap: bool,
pub editing_clip: Option<ClipId>,
pub selected_notes: Vec<NoteId>,
pub piano_roll: RollView,
pub roll_lane: RollLane,
pub selected_control: Option<usize>,
pub plugin_picker: Option<TrackId>,
pub picker_filter: Option<bool>,
pub picker_query: String,
pub picker_selected: usize,
pub plugin_editor: Option<PluginId>,
pub overlay_scroll: f32,
pub automation_shown: Vec<TrackId>,
pub takes_shown: Vec<TrackId>,
pub selected_point: Option<(TrackId, usize, usize)>,
pub tempo_shown: bool,
pub selected_marker: Option<Marker>,
pub confirm: Option<PendingAction>,
pub text_edit: Option<TextEdit>,
pub recovery: Option<dawkey_studio::Recovery>,
pub prefs_open: bool,
pub export_dialog: bool,
pub export: ExportSettings,
pub automation_write: bool,
pub automation_mode: AutomationMode,
#[serde(skip)]
pub(crate) gesture: Option<Gesture>,
#[serde(skip)]
pub(crate) writing: Vec<AutomationWrite>,
#[serde(skip)]
pub(crate) pass: Option<AutomationPass>,
#[serde(skip)]
pub(crate) passes: u32,
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize)]
pub struct ExportSettings {
pub format: dawkey_media::FileFormat,
pub sample: dawkey_media::SampleFormat,
pub sample_rate: Option<u32>,
pub range: ExportRange,
pub tail_s: f64,
}
impl Default for ExportSettings {
fn default() -> Self {
Self {
format: dawkey_media::FileFormat::Wav,
sample: dawkey_media::SampleFormat::Int24,
sample_rate: None,
range: ExportRange::Song,
tail_s: 0.0,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum ExportRange {
Song,
Loop,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum AutomationMode {
#[default]
Touch,
Latch,
Write,
}
impl AutomationMode {
pub(crate) fn next(self) -> Self {
match self {
Self::Touch => Self::Latch,
Self::Latch => Self::Write,
Self::Write => Self::Touch,
}
}
pub(crate) fn label(self) -> &'static str {
match self {
Self::Touch => "Touch",
Self::Latch => "Latch",
Self::Write => "Write",
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct AutomationWrite {
pub(crate) track: TrackId,
pub(crate) target: dawkey_core::AutomationTarget,
pub(crate) from_s: f64,
pub(crate) points: Vec<(f64, f64)>,
}
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct AutomationPass {
pub(crate) merge_key: String,
pub(crate) every_lane: bool,
}
impl Default for ViewState {
fn default() -> Self {
Self {
page: Page::Arrange,
px_per_s: 80.0,
scroll_s: 0.0,
scroll_y: 0.0,
selected_clip: None,
clipboard: None,
snap: true,
editing_clip: None,
selected_notes: Vec::new(),
piano_roll: RollView::default(),
roll_lane: RollLane::default(),
selected_control: None,
plugin_picker: None,
picker_filter: None,
picker_query: String::new(),
picker_selected: 0,
plugin_editor: None,
overlay_scroll: 0.0,
automation_shown: Vec::new(),
takes_shown: Vec::new(),
selected_point: None,
tempo_shown: false,
selected_marker: None,
confirm: None,
text_edit: None,
recovery: None,
prefs_open: false,
export_dialog: false,
export: ExportSettings::default(),
automation_write: false,
automation_mode: AutomationMode::default(),
gesture: None,
writing: Vec::new(),
pass: None,
passes: 0,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize)]
pub struct RollView {
pub px_per_beat: f32,
pub scroll_beats: f64,
pub scroll_y: f32,
pub grid_beats: f64,
}
impl Default for RollView {
fn default() -> Self {
Self {
px_per_beat: 80.0,
scroll_beats: 0.0,
scroll_y: (127 - 84) as f32 * ROW_H,
grid_beats: 0.25,
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize)]
#[serde(rename_all = "snake_case", tag = "request")]
pub enum PlatformRequest {
ChooseAudioFiles {
track: Option<TrackId>,
position_s: f64,
},
ChooseProjectToOpen,
ChooseSaveLocation {
#[serde(default, skip_serializing_if = "Option::is_none")]
then: Option<PendingAction>,
},
ChooseFolder {
kind: crate::prefs::PluginKind,
},
ChooseExportLocation {
name: String,
format: dawkey_media::FileFormat,
},
}
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct ImportRequest {
pub path: PathBuf,
pub track: Option<TrackId>,
pub position_s: f64,
}
impl ImportRequest {
pub fn is_midi(&self) -> bool {
self.path
.extension()
.and_then(|e| e.to_str())
.is_some_and(|e| matches!(e.to_ascii_lowercase().as_str(), "mid" | "midi" | "smf"))
}
}