Skip to main content

danceinterpreter_rs/traktor_api/
data_provider.rs

1use 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::fmt::Display;
10use std::mem;
11use std::net::SocketAddr;
12
13pub const TRAKTOR_SERVER_DEFAULT_ADDR: &str = "127.0.0.1:8080";
14
15#[derive(Debug, Copy, Clone, Eq, PartialEq)]
16pub enum TraktorNextMode {
17    DeckByPosition,
18    DeckByNumber,
19    PlaylistByNumber,
20    PlaylistByName,
21}
22
23#[derive(Debug, Copy, Clone, Eq, PartialEq)]
24pub enum TraktorSyncMode {
25    Relative,
26    AbsoluteByNumber,
27    AbsoluteByName,
28}
29
30#[derive(Debug, Copy, Clone, Eq, PartialEq)]
31pub enum TraktorSyncAction {
32    Relative(isize),
33    PlaylistAbsolute(usize),
34}
35
36impl Display for TraktorNextMode {
37    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
38        write!(f, "{:?}", self)
39    }
40}
41
42impl Display for TraktorSyncMode {
43    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
44        write!(f, "{:?}", self)
45    }
46}
47
48impl Display for TraktorSyncAction {
49    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
50        write!(f, "{:?}", self)
51    }
52}
53
54#[derive(Debug, Clone)]
55pub enum TraktorMessage {
56    ServerMessage(Box<ServerMessage>),
57    EnableSync(bool),
58    SetSyncMode(TraktorSyncMode),
59    SetNextMode(TraktorNextMode),
60    SetNextModeFallback(TraktorNextMode),
61    EnableServer(bool),
62    ChangeAddress(String),
63    SubmitAddress,
64    ChangeAndSubmitAddress(String),
65    EnableDebugLogging(bool),
66    Reconnect,
67}
68
69#[derive(Debug, Clone, Copy)]
70pub enum ConnectionState {
71    Disconnected,
72    CoverLoader,
73    Traktor,
74    Connected,
75}
76
77pub struct TraktorDataProvider {
78    pub address: String,
79    pub submitted_address: String,
80    enabled: bool,
81
82    pub next_mode: TraktorNextMode,
83    pub next_mode_fallback: TraktorNextMode,
84    pub sync: bool,
85    pub sync_mode: TraktorSyncMode,
86
87    channel: Option<UnboundedSender<AppMessage>>,
88
89    time_offset_ms: i64,
90    pub state: Option<State>,
91    covers: HashMap<String, image::Handle>,
92    pub cover_loader_addr: Option<SocketAddr>,
93
94    sync_x_fader_is_left: bool,
95
96    cached_song_info: Option<SongInfo>,
97    cached_next_song_info: Option<SongInfo>,
98    cached_sync_action: TraktorSyncAction,
99    should_scroll: bool,
100
101    pub debug_logging: bool,
102    log: Vec<String>,
103}
104
105impl Default for TraktorDataProvider {
106    fn default() -> Self {
107        Self {
108            enabled: false,
109            address: String::new(),
110            submitted_address: String::new(),
111            channel: None,
112
113            next_mode: TraktorNextMode::DeckByNumber,
114            next_mode_fallback: TraktorNextMode::DeckByPosition,
115            sync: false,
116            sync_mode: TraktorSyncMode::Relative,
117
118            time_offset_ms: 0,
119            state: None,
120            covers: HashMap::new(),
121
122            sync_x_fader_is_left: true,
123
124            cached_song_info: None,
125            cached_next_song_info: None,
126            cached_sync_action: TraktorSyncAction::Relative(0),
127            should_scroll: false,
128
129            cover_loader_addr: None,
130
131            debug_logging: false,
132            log: Vec::new(),
133        }
134    }
135}
136
137impl TraktorDataProvider {
138    pub fn set_enabled(&mut self, enabled: bool) {
139        self.enabled = enabled;
140        self.cover_loader_addr = None;
141        self.state = None;
142    }
143    pub fn is_ready(&self) -> bool {
144        self.enabled && self.channel.as_ref().is_some_and(|c| !c.is_closed())
145    }
146
147    #[allow(dead_code)]
148    pub fn get_log(&self) -> &[String] {
149        &self.log
150    }
151
152    #[allow(dead_code)]
153    pub fn clear_log(&mut self) {
154        self.log.clear();
155    }
156
157    pub fn reconnect(&mut self) {
158        self.time_offset_ms = 0;
159        self.state = None;
160        self.sync_x_fader_is_left = true;
161        self.update_song_info(&[]);
162
163        self.send_message(AppMessage::Reconnect {
164            debug_logging: self.debug_logging,
165        });
166    }
167
168    pub fn get_socket_addr(&self) -> Option<SocketAddr> {
169        if !self.enabled {
170            return None;
171        }
172
173        if self.submitted_address.is_empty() {
174            return TRAKTOR_SERVER_DEFAULT_ADDR.parse().ok();
175        }
176
177        self.submitted_address.parse().ok()
178    }
179
180    pub fn get_song_info(&self) -> Option<&SongInfo> {
181        if !self.is_ready() {
182            return None;
183        }
184
185        self.cached_song_info.as_ref()
186    }
187
188    pub fn get_next_song_info(&self) -> Option<&SongInfo> {
189        if !self.is_ready() {
190            return None;
191        }
192
193        self.cached_next_song_info.as_ref()
194    }
195
196    pub fn get_connection_state(&self) -> ConnectionState {
197        if !self.enabled {
198            ConnectionState::Disconnected
199        } else if self.cover_loader_addr.is_some() && self.state.is_some() {
200            ConnectionState::Connected
201        } else if self.state.is_some() {
202            ConnectionState::Traktor
203        } else if self.cover_loader_addr.is_some() {
204            ConnectionState::CoverLoader
205        } else {
206            ConnectionState::Disconnected
207        }
208    }
209
210    fn get_deck_score(&self, deck: &DeckState, channel: &ChannelState, mixer: &MixerState) -> f64 {
211        if !deck.content.is_loaded || deck.play_state.speed == 0.0 || channel.volume == 0.0 {
212            return 0.0;
213        }
214
215        if channel.x_fader_left && mixer.x_fader > 0.5 {
216            (1.0 - mixer.x_fader) * 2.0
217        } else if channel.x_fader_right && mixer.x_fader < 0.5 {
218            mixer.x_fader * 2.0
219        } else {
220            1.0
221        }
222    }
223
224    fn update_song_info(&mut self, playlist: &[SongInfo]) {
225        let old_song_info = self.cached_song_info.take();
226        self.cached_next_song_info = None;
227
228        if !self.is_ready() {
229            return;
230        }
231
232        let Some(state) = self.state.as_ref() else {
233            return;
234        };
235
236        let scores = (0..4)
237            .map(|i| self.get_deck_score(&state.decks[i], &state.channels[i], &state.mixer))
238            .collect::<Vec<f64>>();
239
240        let Some(max) = scores
241            .iter()
242            .enumerate()
243            .max_by(|(_, a), (_, b)| a.total_cmp(b))
244        else {
245            return;
246        };
247        let max_index = if *max.1 > 0.0 {
248            max.0
249        } else {
250            return;
251        };
252
253        let content = &state.decks[max_index].content;
254        let channel = &state.channels[max_index];
255
256        let current_song_info = self.copy_song_info_from_deck(content, playlist);
257        self.cached_song_info = Some(current_song_info.clone());
258
259        if old_song_info != self.cached_song_info {
260            self.should_scroll = true;
261        }
262
263        self.cached_next_song_info = self
264            .try_get_next_with_mode(false, channel, playlist)
265            .or_else(|| self.try_get_next_with_mode(true, channel, playlist));
266
267        match self.sync_mode {
268            TraktorSyncMode::AbsoluteByNumber => {
269                let current_index = playlist
270                    .iter()
271                    .position(|s| content.number == s.track_number);
272
273                self.cached_sync_action = match current_index {
274                    None => TraktorSyncAction::Relative(0),
275                    Some(ci) => TraktorSyncAction::PlaylistAbsolute(ci),
276                };
277            }
278            TraktorSyncMode::AbsoluteByName => {
279                let current_index = playlist
280                    .iter()
281                    .position(|s| Self::songs_name_match(&current_song_info, s));
282
283                self.cached_sync_action = match current_index {
284                    None => TraktorSyncAction::Relative(0),
285                    Some(ci) => TraktorSyncAction::PlaylistAbsolute(ci),
286                };
287            }
288            _ => {}
289        };
290    }
291
292    fn try_get_next_with_mode(
293        &self,
294        fallback: bool,
295        current_channel: &ChannelState,
296        playlist: &[SongInfo],
297    ) -> Option<SongInfo> {
298        let mode = if !fallback {
299            self.next_mode
300        } else {
301            self.next_mode_fallback
302        };
303
304        if !self.is_ready() {
305            return None;
306        }
307
308        let state = self.state.as_ref()?;
309
310        let current_song_info = self.cached_song_info.as_ref()?;
311
312        match mode {
313            TraktorNextMode::DeckByPosition => {
314                let is_on_left = if current_channel.x_fader_left {
315                    true
316                } else if current_channel.x_fader_right {
317                    false
318                } else {
319                    return None;
320                };
321
322                let other_side = state.channels.iter().position(|c| {
323                    if is_on_left {
324                        c.x_fader_right
325                    } else {
326                        c.x_fader_left
327                    }
328                });
329
330                let deck = other_side.map(|o| &state.decks[o]);
331                deck.filter(|d| d.play_state.position < 0.5 * d.content.track_length)
332                    .map(|d| self.copy_song_info_from_deck(&d.content, playlist))
333            }
334            TraktorNextMode::DeckByNumber => {
335                let deck = state
336                    .decks
337                    .iter()
338                    .find(|d| d.content.number == current_song_info.track_number + 1);
339
340                deck.map(|d| self.copy_song_info_from_deck(&d.content, playlist))
341            }
342            TraktorNextMode::PlaylistByNumber => {
343                let current_index = playlist
344                    .iter()
345                    .position(|s| current_song_info.track_number == s.track_number);
346
347                current_index.and_then(|ci| playlist.get(ci + 1).cloned())
348            }
349            TraktorNextMode::PlaylistByName => {
350                let current_index = playlist
351                    .iter()
352                    .position(|s| Self::songs_name_match(current_song_info, s));
353
354                current_index.and_then(|ci| playlist.get(ci + 1).cloned())
355            }
356        }
357    }
358
359    fn copy_song_info_from_deck(
360        &self,
361        content: &DeckContentState,
362        playlist: &[SongInfo],
363    ) -> SongInfo {
364        let mut song_info = SongInfo::new(
365            content.number,
366            content.title.to_owned(),
367            content.artist.to_owned(),
368            content.genre.to_owned(),
369            self.covers.get(&content.file_path).cloned(),
370        );
371
372        if song_info.album_art.is_none() {
373            song_info.album_art = playlist
374                .iter()
375                .find(|s| Self::songs_name_match(&song_info, s))
376                .and_then(|s| s.album_art.clone());
377        }
378
379        song_info
380    }
381
382    pub fn songs_name_match(a: &SongInfo, b: &SongInfo) -> bool {
383        // TODO: maybe change this to levenshtein or sth
384        a.artist == b.artist && a.title == b.title
385    }
386
387    fn get_loaded_files(&self) -> Vec<String> {
388        let Some(state) = self.state.as_ref() else {
389            return Vec::new();
390        };
391
392        let mut files: Vec<String> = state
393            .decks
394            .iter()
395            .map(|d| &d.content.file_path)
396            .filter(|&f| !f.is_empty())
397            .map(|f| f.to_owned())
398            .collect();
399        files.dedup();
400
401        files
402    }
403
404    pub fn process_message(&mut self, message: ServerMessage, playlist: &[SongInfo]) {
405        match message {
406            ServerMessage::Ready(channel) => {
407                self.channel = Some(channel);
408
409                self.time_offset_ms = 0;
410                self.state = None;
411                self.sync_x_fader_is_left = true;
412                self.update_song_info(playlist);
413
414                self.reconnect();
415            }
416            ServerMessage::Connect {
417                time_offset_ms,
418                initial_state,
419            } => {
420                self.time_offset_ms = time_offset_ms;
421                self.sync_x_fader_is_left = initial_state.mixer.x_fader < 0.5;
422                self.state = Some(*initial_state);
423                self.update_song_info(playlist);
424            }
425            ServerMessage::Update(update) => {
426                if let Some(state) = self.state.as_mut() {
427                    if matches!(self.sync_mode, TraktorSyncMode::Relative)
428                        && let StateUpdate::Mixer(new_mixer_state) = &update
429                    {
430                        let x_fader_old = state.mixer.x_fader;
431                        let x_fader_new = new_mixer_state.x_fader;
432
433                        let mut offset = 0;
434                        if x_fader_old > 0.5 && x_fader_new <= 0.5 {
435                            if self.sync_x_fader_is_left {
436                                offset -= 1;
437                            } else {
438                                offset += 1;
439                            }
440                        } else if x_fader_old <= 0.5 && x_fader_new > 0.5 {
441                            if self.sync_x_fader_is_left {
442                                offset += 1;
443                            } else {
444                                offset -= 1;
445                            }
446                        }
447
448                        if x_fader_new < 0.2 {
449                            self.sync_x_fader_is_left = true;
450                        } else if x_fader_new > 0.8 {
451                            self.sync_x_fader_is_left = false;
452                        }
453
454                        self.cached_sync_action = match self.cached_sync_action {
455                            TraktorSyncAction::Relative(prev) => {
456                                TraktorSyncAction::Relative(prev + offset)
457                            }
458                            TraktorSyncAction::PlaylistAbsolute(_) => {
459                                TraktorSyncAction::Relative(offset)
460                            }
461                        };
462                    }
463
464                    state.apply_update(update);
465                }
466                self.update_song_info(playlist);
467            }
468            ServerMessage::CoverImage { path, data } => {
469                self.covers.insert(path, image::Handle::from_bytes(data));
470
471                let loaded_files = self.get_loaded_files();
472                self.covers.retain(|path, _| loaded_files.contains(path));
473            }
474            ServerMessage::Log(msg) => {
475                if self.debug_logging {
476                    self.log.push(msg);
477                }
478            }
479            ServerMessage::ClientChanged(addr) => {
480                self.cover_loader_addr = addr;
481            }
482        }
483    }
484
485    pub fn take_sync_action(&mut self) -> TraktorSyncAction {
486        mem::replace(&mut self.cached_sync_action, TraktorSyncAction::Relative(0))
487    }
488
489    pub fn take_should_scroll(&mut self) -> bool {
490        let should_scroll = self.should_scroll;
491        self.should_scroll = false;
492        should_scroll
493    }
494
495    pub fn get_current_index(&self, playlist: &[SongInfo]) -> Option<usize> {
496        let traktor_song = self.get_song_info()?;
497
498        playlist
499            .iter()
500            .enumerate()
501            .find(|(_i, s)| TraktorDataProvider::songs_name_match(s, traktor_song))
502            .map(|(i, _s)| i)
503    }
504
505    fn send_message(&mut self, message: AppMessage) {
506        if let Some(channel) = self.channel.as_ref()
507            && channel.unbounded_send(message).is_err()
508        {
509            self.channel = None;
510        }
511    }
512
513    pub fn enabled(&self) -> bool {
514        self.enabled
515    }
516}
517
518#[cfg(test)]
519mod tests {
520    use super::*;
521    use crate::traktor_api::DeckPlayState;
522    use iced::futures::channel::mpsc;
523    use std::array;
524
525    /// After receiving a [ClientChanged](ServerMessage::ClientChanged) message with a [SockedAddr](SocketAddr), the provider should be in the [CoverLoader](ConnectionState::CoverLoader) state.
526    /// After receiving a [ClientChanged](ServerMessage::ClientChanged) message without a [SockedAddr](SocketAddr), the provider should return to the [Disconnected](ConnectionState::Disconnected) state.
527    #[test]
528    fn connecting_cover_loader_sets_connection_state() {
529        // Keep the receivers alive so the channel counts as "open" (ready).
530        let (tx, _rx) = mpsc::unbounded();
531        let mut provider = TraktorDataProvider {
532            enabled: true,
533            channel: Some(tx),
534            ..Default::default()
535        };
536
537        assert!(matches!(
538            provider.get_connection_state(),
539            ConnectionState::Disconnected
540        ));
541
542        provider.process_message(
543            ServerMessage::ClientChanged(Some("127.0.0.1:8080".parse().unwrap())),
544            &[],
545        );
546        assert!(matches!(
547            provider.get_connection_state(),
548            ConnectionState::CoverLoader
549        ));
550
551        provider.process_message(ServerMessage::ClientChanged(None), &[]);
552        assert!(matches!(
553            provider.get_connection_state(),
554            ConnectionState::Disconnected
555        ));
556    }
557
558    /// After receiving a [Connect](ServerMessage::Connect) message, the provider should be in the [Traktor](ConnectionState::Traktor) state.
559    #[test]
560    fn connecting_traktor_sets_connection_state() {
561        // Keep the receivers alive so the channel counts as "open" (ready).
562        let (tx, _rx) = mpsc::unbounded();
563        let mut provider = TraktorDataProvider {
564            enabled: true,
565            channel: Some(tx),
566            ..Default::default()
567        };
568
569        assert!(matches!(
570            provider.get_connection_state(),
571            ConnectionState::Disconnected
572        ));
573
574        let connect_message = ServerMessage::Connect {
575            time_offset_ms: 0,
576            initial_state: Box::new(get_sample_state()),
577        };
578
579        provider.process_message(connect_message, &[]);
580        assert!(matches!(
581            provider.get_connection_state(),
582            ConnectionState::Traktor
583        ));
584    }
585
586    /// After receiving a [ClientChanged](ServerMessage::ClientChanged) message with a [SockedAddr](SocketAddr), the provider should be in the [CoverLoader](ConnectionState::CoverLoader) state.
587    /// After receiving a [Connect](ServerMessage::Connect) message, the provider should be in the [Connected](ConnectionState::Connected) state.
588    /// After receiving a [ClientChanged](ServerMessage::ClientChanged) message without a [SockedAddr](SocketAddr), the provider should return to the [Traktor](ConnectionState::Traktor) state.
589    #[test]
590    fn connecting_cover_loader_and_traktor_sets_connection_state() {
591        // Keep the receivers alive so the channel counts as "open" (ready).
592        let (tx, _rx) = mpsc::unbounded();
593        let mut provider = TraktorDataProvider {
594            enabled: true,
595            channel: Some(tx),
596            ..Default::default()
597        };
598
599        assert!(matches!(
600            provider.get_connection_state(),
601            ConnectionState::Disconnected
602        ));
603
604        provider.process_message(
605            ServerMessage::ClientChanged(Some("127.0.0.1:8080".parse().unwrap())),
606            &[],
607        );
608        assert!(matches!(
609            provider.get_connection_state(),
610            ConnectionState::CoverLoader
611        ));
612
613        let connect_message = ServerMessage::Connect {
614            time_offset_ms: 0,
615            initial_state: Box::new(get_sample_state()),
616        };
617
618        provider.process_message(connect_message, &[]);
619        assert!(matches!(
620            provider.get_connection_state(),
621            ConnectionState::Connected
622        ));
623
624        provider.process_message(ServerMessage::ClientChanged(None), &[]);
625        assert!(matches!(
626            provider.get_connection_state(),
627            ConnectionState::Traktor
628        ));
629
630        provider.set_enabled(false);
631
632        assert!(matches!(
633            provider.get_connection_state(),
634            ConnectionState::Disconnected
635        ));
636    }
637
638    fn get_sample_state() -> State {
639        let deck_state = DeckState {
640            content: DeckContentState {
641                is_loaded: false,
642                number: 0,
643                title: "".to_string(),
644                artist: "".to_string(),
645                album: "".to_string(),
646                genre: "".to_string(),
647                comment: "".to_string(),
648                comment2: "".to_string(),
649                label: "".to_string(),
650                key: "".to_string(),
651                file_path: "".to_string(),
652                track_length: 0.0,
653                bpm: 0.0,
654            },
655            play_state: DeckPlayState {
656                timestamp: 0,
657                position: 0.0,
658                speed: 0.0,
659            },
660        };
661        State {
662            mixer: MixerState {
663                x_fader: 0.0,
664                master_volume: 0.0,
665                cue_volume: 0.0,
666                cue_mix: 0.0,
667                mic_volume: 0.0,
668            },
669            channels: [ChannelState {
670                cue: false,
671                volume: 0.0,
672                x_fader_left: false,
673                x_fader_right: false,
674            }; 4],
675            decks: array::from_fn(|_| deck_state.clone()),
676        }
677    }
678}