danceinterpreter_rs/traktor_api/
data_provider.rs1use crate::dataloading::songinfo::SongInfo;
2use crate::traktor_api::{
3 AppMessage, ChannelState, DeckContentState, DeckState, MixerState, ServerMessage, State,
4 StateUpdate,
5};
6use iced::futures::channel::mpsc::UnboundedSender;
7use iced::widget::image;
8use std::collections::HashMap;
9use std::mem;
10use std::net::SocketAddr;
11
12pub const TRAKTOR_SERVER_DEFAULT_ADDR: &str = "127.0.0.1:8080";
13
14#[derive(Debug, Copy, Clone, Eq, PartialEq)]
15pub enum TraktorNextMode {
16 DeckByPosition,
17 DeckByNumber,
18 PlaylistByNumber,
19 PlaylistByName,
20}
21
22#[derive(Debug, Copy, Clone, Eq, PartialEq)]
23pub enum TraktorSyncMode {
24 Relative,
25 AbsoluteByNumber,
26 AbsoluteByName,
27}
28
29#[derive(Debug, Copy, Clone, Eq, PartialEq)]
30pub enum TraktorSyncAction {
31 Relative(isize),
32 PlaylistAbsolute(usize),
33}
34
35pub struct TraktorDataProvider {
36 pub is_enabled: bool,
37 pub address: String,
38 pub submitted_address: String,
39
40 pub next_mode: Option<TraktorNextMode>,
41 pub next_mode_fallback: Option<TraktorNextMode>,
42 pub sync_mode: Option<TraktorSyncMode>,
43
44 channel: Option<UnboundedSender<AppMessage>>,
45
46 time_offset_ms: i64,
47 pub state: Option<State>,
48 covers: HashMap<String, image::Handle>,
49
50 sync_x_fader_is_left: bool,
51
52 cached_song_info: Option<SongInfo>,
53 cached_next_song_info: Option<SongInfo>,
54 cached_sync_action: TraktorSyncAction,
55 should_scroll: bool,
56
57 pub debug_logging: bool,
58 log: Vec<String>,
59}
60
61impl Default for TraktorDataProvider {
62 fn default() -> Self {
63 Self {
64 is_enabled: false,
65 address: String::new(),
66 submitted_address: String::new(),
67 channel: None,
68
69 next_mode: Some(TraktorNextMode::DeckByNumber),
70 next_mode_fallback: None,
71 sync_mode: None,
72
73 time_offset_ms: 0,
74 state: None,
75 covers: HashMap::new(),
76
77 sync_x_fader_is_left: true,
78
79 cached_song_info: None,
80 cached_next_song_info: None,
81 cached_sync_action: TraktorSyncAction::Relative(0),
82 should_scroll: false,
83
84 debug_logging: false,
85 log: Vec::new(),
86 }
87 }
88}
89
90impl TraktorDataProvider {
91 pub fn is_ready(&self) -> bool {
92 self.is_enabled && self.channel.as_ref().is_some_and(|c| !c.is_closed())
93 }
94
95 #[allow(dead_code)]
96 pub fn get_log(&self) -> &[String] {
97 &self.log
98 }
99
100 #[allow(dead_code)]
101 pub fn clear_log(&mut self) {
102 self.log.clear();
103 }
104
105 pub fn reconnect(&mut self) {
106 self.time_offset_ms = 0;
107 self.state = None;
108 self.sync_x_fader_is_left = true;
109 self.update_song_info(&[]);
110
111 self.send_message(AppMessage::Reconnect {
112 debug_logging: self.debug_logging,
113 });
114 }
115
116 pub fn get_socket_addr(&self) -> Option<SocketAddr> {
117 if !self.is_enabled {
118 return None;
119 }
120
121 if self.submitted_address.is_empty() {
122 return TRAKTOR_SERVER_DEFAULT_ADDR.parse().ok();
123 }
124
125 self.submitted_address.parse().ok()
126 }
127
128 pub fn get_song_info(&self) -> Option<&SongInfo> {
129 if !self.is_ready() {
130 return None;
131 }
132
133 self.cached_song_info.as_ref()
134 }
135
136 pub fn get_next_song_info(&self) -> Option<&SongInfo> {
137 if !self.is_ready() {
138 return None;
139 }
140
141 self.cached_next_song_info.as_ref()
142 }
143
144 fn get_deck_score(&self, deck: &DeckState, channel: &ChannelState, mixer: &MixerState) -> f64 {
145 if !deck.content.is_loaded || deck.play_state.speed == 0.0 || channel.volume == 0.0 {
146 return 0.0;
147 }
148
149 if channel.x_fader_left && mixer.x_fader > 0.5 {
150 (1.0 - mixer.x_fader) * 2.0
151 } else if channel.x_fader_right && mixer.x_fader < 0.5 {
152 mixer.x_fader * 2.0
153 } else {
154 1.0
155 }
156 }
157
158 fn update_song_info(&mut self, playlist: &[SongInfo]) {
159 let old_song_info = self.cached_song_info.take();
160 self.cached_next_song_info = None;
161
162 if !self.is_ready() {
163 return;
164 }
165
166 let Some(state) = self.state.as_ref() else {
167 return;
168 };
169
170 let scores = [
171 self.get_deck_score(&state.decks.0, &state.channels.0, &state.mixer),
172 self.get_deck_score(&state.decks.1, &state.channels.1, &state.mixer),
173 self.get_deck_score(&state.decks.2, &state.channels.2, &state.mixer),
174 self.get_deck_score(&state.decks.3, &state.channels.3, &state.mixer),
175 ];
176
177 let Some(max) = scores
178 .iter()
179 .enumerate()
180 .max_by(|(_, a), (_, b)| a.total_cmp(b))
181 else {
182 return;
183 };
184 let max_index = if *max.1 > 0.0 {
185 max.0
186 } else {
187 return;
188 };
189
190 let content = match max_index {
191 0 => &state.decks.0.content,
192 1 => &state.decks.1.content,
193 2 => &state.decks.2.content,
194 3 => &state.decks.3.content,
195 _ => return,
196 };
197
198 let channel = match max_index {
199 0 => &state.channels.0,
200 1 => &state.channels.1,
201 2 => &state.channels.2,
202 3 => &state.channels.3,
203 _ => return,
204 };
205
206 let current_song_info = self.copy_song_info_from_deck(content, playlist);
207 self.cached_song_info = Some(current_song_info.clone());
208
209 if old_song_info != self.cached_song_info {
210 self.should_scroll = true;
211 }
212
213 self.cached_next_song_info = self
214 .try_get_next_with_mode(self.next_mode, channel, playlist)
215 .or_else(|| self.try_get_next_with_mode(self.next_mode_fallback, channel, playlist));
216
217 match self.sync_mode {
218 Some(TraktorSyncMode::AbsoluteByNumber) => {
219 let current_index = playlist
220 .iter()
221 .position(|s| content.number == s.track_number);
222
223 self.cached_sync_action = match current_index {
224 None => TraktorSyncAction::Relative(0),
225 Some(ci) => TraktorSyncAction::PlaylistAbsolute(ci),
226 };
227 }
228 Some(TraktorSyncMode::AbsoluteByName) => {
229 let current_index = playlist
230 .iter()
231 .position(|s| Self::songs_name_match(¤t_song_info, s));
232
233 self.cached_sync_action = match current_index {
234 None => TraktorSyncAction::Relative(0),
235 Some(ci) => TraktorSyncAction::PlaylistAbsolute(ci),
236 };
237 }
238 _ => {}
239 };
240 }
241
242 fn try_get_next_with_mode(
243 &self,
244 mode: Option<TraktorNextMode>,
245 current_channel: &ChannelState,
246 playlist: &[SongInfo],
247 ) -> Option<SongInfo> {
248 let mode = mode?;
249
250 if !self.is_ready() {
251 return None;
252 }
253
254 let state = self.state.as_ref()?;
255
256 let current_song_info = self.cached_song_info.as_ref()?;
257
258 match mode {
259 TraktorNextMode::DeckByPosition => {
260 let is_on_left = if current_channel.x_fader_left {
261 true
262 } else if current_channel.x_fader_right {
263 false
264 } else {
265 return None;
266 };
267
268 let other_side = vec![
269 &state.channels.0,
270 &state.channels.1,
271 &state.channels.2,
272 &state.channels.3,
273 ]
274 .into_iter()
275 .position(|c| {
276 if is_on_left {
277 c.x_fader_right
278 } else {
279 c.x_fader_left
280 }
281 });
282
283 let deck = other_side.and_then(|o| match o {
284 0 => Some(&state.decks.0),
285 1 => Some(&state.decks.1),
286 2 => Some(&state.decks.2),
287 3 => Some(&state.decks.3),
288 _ => None,
289 });
290
291 deck.filter(|d| d.play_state.position < 0.5 * d.content.track_length)
292 .map(|d| self.copy_song_info_from_deck(&d.content, playlist))
293 }
294 TraktorNextMode::DeckByNumber => {
295 let deck = vec![
296 &state.decks.0,
297 &state.decks.1,
298 &state.decks.2,
299 &state.decks.3,
300 ]
301 .into_iter()
302 .find(|d| d.content.number == current_song_info.track_number + 1);
303
304 deck.map(|d| self.copy_song_info_from_deck(&d.content, playlist))
305 }
306 TraktorNextMode::PlaylistByNumber => {
307 let current_index = playlist
308 .iter()
309 .position(|s| current_song_info.track_number == s.track_number);
310
311 current_index.and_then(|ci| playlist.get(ci + 1).cloned())
312 }
313 TraktorNextMode::PlaylistByName => {
314 let current_index = playlist
315 .iter()
316 .position(|s| Self::songs_name_match(current_song_info, s));
317
318 current_index.and_then(|ci| playlist.get(ci + 1).cloned())
319 }
320 }
321 }
322
323 fn copy_song_info_from_deck(
324 &self,
325 content: &DeckContentState,
326 playlist: &[SongInfo],
327 ) -> SongInfo {
328 let mut song_info = SongInfo::new(
329 content.number,
330 content.title.to_owned(),
331 content.artist.to_owned(),
332 content.genre.to_owned(),
333 self.covers.get(&content.file_path).cloned(),
334 );
335
336 if song_info.album_art.is_none() {
337 song_info.album_art = playlist
338 .iter()
339 .find(|s| Self::songs_name_match(&song_info, s))
340 .and_then(|s| s.album_art.clone());
341 }
342
343 song_info
344 }
345
346 pub fn songs_name_match(a: &SongInfo, b: &SongInfo) -> bool {
347 a.artist == b.artist && a.title == b.title
349 }
350
351 fn get_loaded_files(&self) -> Vec<String> {
352 let Some(state) = self.state.as_ref() else {
353 return Vec::new();
354 };
355
356 let mut files: Vec<String> = vec![
357 &state.decks.0.content.file_path,
358 &state.decks.1.content.file_path,
359 &state.decks.2.content.file_path,
360 &state.decks.3.content.file_path,
361 ]
362 .into_iter()
363 .filter(|&f| !f.is_empty())
364 .map(|f| f.to_owned())
365 .collect();
366 files.dedup();
367
368 files
369 }
370
371 pub fn process_message(&mut self, message: ServerMessage, playlist: &[SongInfo]) {
372 match message {
373 ServerMessage::Ready(channel) => {
374 self.channel = Some(channel);
375
376 self.time_offset_ms = 0;
377 self.state = None;
378 self.sync_x_fader_is_left = true;
379 self.update_song_info(playlist);
380
381 self.reconnect();
382 }
383 ServerMessage::Connect {
384 time_offset_ms,
385 initial_state,
386 } => {
387 println!("{:?}", initial_state);
388
389 self.time_offset_ms = time_offset_ms;
390 self.sync_x_fader_is_left = initial_state.mixer.x_fader < 0.5;
391 self.state = Some(*initial_state);
392 self.update_song_info(playlist);
393 }
394 ServerMessage::Update(update) => {
395 println!("{:?}", update);
396
397 if let Some(state) = self.state.as_mut() {
398 if matches!(self.sync_mode, Some(TraktorSyncMode::Relative))
399 && let StateUpdate::Mixer(new_mixer_state) = &update
400 {
401 let x_fader_old = state.mixer.x_fader;
402 let x_fader_new = new_mixer_state.x_fader;
403
404 let mut offset = 0;
405 if x_fader_old > 0.5 && x_fader_new <= 0.5 {
406 if self.sync_x_fader_is_left {
407 offset -= 1;
408 } else {
409 offset += 1;
410 }
411 } else if x_fader_old <= 0.5 && x_fader_new > 0.5 {
412 if self.sync_x_fader_is_left {
413 offset += 1;
414 } else {
415 offset -= 1;
416 }
417 }
418
419 if x_fader_new < 0.2 {
420 self.sync_x_fader_is_left = true;
421 } else if x_fader_new > 0.8 {
422 self.sync_x_fader_is_left = false;
423 }
424
425 self.cached_sync_action = match self.cached_sync_action {
426 TraktorSyncAction::Relative(prev) => {
427 TraktorSyncAction::Relative(prev + offset)
428 }
429 TraktorSyncAction::PlaylistAbsolute(_) => {
430 TraktorSyncAction::Relative(offset)
431 }
432 };
433 }
434
435 state.apply_update(update);
436 }
437
438 self.update_song_info(playlist);
439 }
440 ServerMessage::CoverImage { path, data } => {
441 self.covers.insert(path, image::Handle::from_bytes(data));
442
443 let loaded_files = self.get_loaded_files();
444 self.covers.retain(|path, _| loaded_files.contains(path));
445 }
446 ServerMessage::Log(msg) => {
447 if self.debug_logging {
448 self.log.push(msg);
449 }
450 }
451 }
452 }
453
454 pub fn take_sync_action(&mut self) -> TraktorSyncAction {
455 mem::replace(&mut self.cached_sync_action, TraktorSyncAction::Relative(0))
456 }
457
458 pub fn take_should_scroll(&mut self) -> bool {
459 let should_scroll = self.should_scroll;
460 self.should_scroll = false;
461 should_scroll
462 }
463
464 pub fn get_current_index(&self, playlist: &[SongInfo]) -> Option<usize> {
465 let traktor_song = self.get_song_info()?;
466
467 playlist
468 .iter()
469 .enumerate()
470 .find(|(_i, s)| TraktorDataProvider::songs_name_match(s, traktor_song))
471 .map(|(i, _s)| i)
472 }
473
474 fn send_message(&mut self, message: AppMessage) {
475 if let Some(channel) = self.channel.as_ref()
476 && channel.unbounded_send(message).is_err()
477 {
478 self.channel = None;
479 }
480 }
481}