1use crate::{DeckId, LoadStatus, SurfaceRegion};
2use serde::{Deserialize, Serialize};
3
4fn default_scratch_grip() -> f32 {
5 1.0
6}
7
8fn default_scratch_grab_impulse() -> f32 {
9 0.22
10}
11
12#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
13#[serde(tag = "type", rename_all = "snake_case")]
14pub enum PlayerEvent {
15 HydrateDeck {
16 deck: DeckId,
17 loaded: bool,
18 status: LoadStatus,
19 duration_seconds: f64,
20 current_seconds: f64,
21 playing: bool,
22 transport_on: bool,
23 needle_lifted: bool,
24 playback_rate: f32,
25 channel_gain: f32,
26 },
27 SetActiveDeck {
28 deck: DeckId,
29 },
30 SetLoadState {
31 deck: DeckId,
32 status: LoadStatus,
33 loaded: bool,
34 duration_seconds: f64,
35 },
36 SetSourceSampleRate {
37 deck: DeckId,
38 sample_rate: f64,
39 },
40 PlaybackPositionObserved {
41 deck: DeckId,
42 seconds: f64,
43 },
44 PlaybackEnded {
45 deck: DeckId,
46 },
47 ToggleTransport {
48 deck: DeckId,
49 },
50 SetTransport {
51 deck: DeckId,
52 running: bool,
53 },
54 TogglePlayback {
55 deck: DeckId,
56 },
57 SetNeedle {
58 deck: DeckId,
59 lifted: bool,
60 observed_playback_seconds: f64,
61 },
62 Seek {
63 deck: DeckId,
64 seconds: f64,
65 },
66 SetPlaybackRate {
67 deck: DeckId,
68 rate: f32,
69 },
70 StartTimedRegion {
71 region: SurfaceRegion,
72 now_ms: f64,
73 duration_seconds: f64,
74 },
75 StopTimedRegion {
76 region: SurfaceRegion,
77 completed: bool,
78 },
79 TimedRegionElapsed {
80 region: SurfaceRegion,
81 },
82 StartClipLoop {
83 clip_id: String,
84 },
85 StopClipLoop,
86 BeginScratch {
87 deck: DeckId,
88 pointer_id: i32,
89 playback_seconds: f64,
90 rotation_degrees: f64,
91 #[serde(default)]
92 rate: f32,
93 #[serde(default = "default_scratch_grab_impulse")]
94 impulse: f32,
95 #[serde(default = "default_scratch_grip")]
96 grip: f32,
97 #[serde(default)]
98 output_frame: u64,
99 },
100 MoveScratch {
101 deck: DeckId,
102 position_frames: f64,
103 rendered_position_frames: f64,
104 rate: f32,
105 rotation_degrees: f64,
106 impulse: f32,
107 #[serde(default = "default_scratch_grip")]
108 grip: f32,
109 #[serde(default)]
110 output_frame: u64,
111 },
112 ScratchRenderedPosition {
113 deck: DeckId,
114 rendered_position_frames: f64,
115 },
116 EndScratch {
117 deck: DeckId,
118 rendered_position_frames: f64,
119 rotation_degrees: f64,
120 resume_playback: bool,
121 save_sample: bool,
122 can_platter_handoff: bool,
123 #[serde(default)]
124 output_frame: u64,
125 },
126 SetCrossfader {
127 value: f32,
128 },
129 SetChannelGain {
130 deck: DeckId,
131 value: f32,
132 },
133 SetTwoDeckMode {
134 enabled: bool,
135 },
136 Tick {
137 now_ms: f64,
138 },
139}
140
141#[cfg(test)]
142mod tests {
143 use super::*;
144
145 #[test]
146 fn historical_scratch_events_default_to_compatible_begin_intent() {
147 let begin: PlayerEvent = serde_json::from_value(serde_json::json!({
148 "type": "begin_scratch",
149 "deck": "a",
150 "pointer_id": 1,
151 "playback_seconds": 2.0,
152 "rotation_degrees": 0.0
153 }))
154 .unwrap();
155 let movement: PlayerEvent = serde_json::from_value(serde_json::json!({
156 "type": "move_scratch",
157 "deck": "a",
158 "position_frames": 96_000.0,
159 "rendered_position_frames": 96_000.0,
160 "rate": -1.0,
161 "rotation_degrees": -20.0,
162 "impulse": 0.0
163 }))
164 .unwrap();
165 let end: PlayerEvent = serde_json::from_value(serde_json::json!({
166 "type": "end_scratch",
167 "deck": "a",
168 "rendered_position_frames": 96_000.0,
169 "rotation_degrees": 0.0,
170 "resume_playback": true,
171 "save_sample": false,
172 "can_platter_handoff": true
173 }))
174 .unwrap();
175
176 assert!(matches!(
177 begin,
178 PlayerEvent::BeginScratch { rate, impulse, grip, output_frame, .. }
179 if rate == 0.0 && impulse == 0.22 && grip == 1.0 && output_frame == 0
180 ));
181 assert!(matches!(
182 movement,
183 PlayerEvent::MoveScratch { grip, output_frame, .. }
184 if grip == 1.0 && output_frame == 0
185 ));
186 assert!(matches!(
187 end,
188 PlayerEvent::EndScratch { output_frame, .. } if output_frame == 0
189 ));
190 }
191}