rust_rocket/
simple.rs

1#![cfg(feature = "simple")]
2
3//! An opinionated abstraction for the lower level [`client`](crate::client) and [`player`](crate::player) API.
4//!
5//! Requires the `simple` feature.
6//! All errors are printed to stderr, and the connection to the tracker will be automatically re-established
7//! as long as [`poll_events`](Rocket::poll_events) is called frequently enough.
8//!
9//! # Usage
10//!
11//! First, install a rocket tracker ([original Qt editor](https://github.com/rocket/rocket)
12//! or [emoon's OpenGL-based editor](https://github.com/emoon/rocket)).
13//!
14//! The [`Rocket`] type in this module compiles to different code depending on crate feature `player`.
15//! When the feature is not enabled, the [`Rocket`] type uses [`RocketClient`](crate::RocketClient) internally.
16//! When `player` is enabled, the [`Rocket`] type uses [`RocketPlayer`](crate::RocketPlayer) internally.
17//!
18//! Enable the feature in your production's Cargo.toml:
19//! ```toml
20//! [features]
21//! player = ["rust-rocket/player"]
22//!
23//! [dependencies]
24//! rust-rocket = { version = "0", features = ["simple"] }
25//! ```
26//!
27//! And build your release accordingly:
28//! ```console
29//! cargo run                                 # Editing without player feature
30//! cargo build --release --features player   # Release built with player feature
31//! ```
32//!
33//! A main loop may look like this:
34//! ```rust,no_run
35//! # use std::time::Duration;
36//! # use rust_rocket::simple::{Rocket, Event};
37//! struct MusicPlayer; // Your music player, not included in this crate
38//! # impl MusicPlayer {
39//! #     fn new() -> Self { Self }
40//! #     fn get_time(&self) -> Duration { Duration::ZERO }
41//! #     fn get_bpm(&self) -> f32 { 120. }
42//! #     fn seek(&self, _to: Duration) {}
43//! #     fn pause(&self, _state: bool) {}
44//! # }
45//!
46//! fn main() {
47//!     let mut music = MusicPlayer::new(/* ... */);
48//!     let mut rocket = Rocket::new("tracks.bin", music.get_bpm()).unwrap();
49//!
50//!     // Create window, render resources etc...
51//!
52//!     loop {
53//!         // Handle events from the rocket tracker
54//!         while let Some(event) = rocket.poll_events().ok().flatten() {
55//!             match event {
56//!                 Event::Seek(to) => music.seek(to),
57//!                 Event::Pause(state) => music.pause(state),
58//!                 Event::NotConnected => break,
59//!             }
60//!         }
61//!
62//!         // Get current frame's time and keep the tracker updated
63//!         let time = music.get_time();
64//!         rocket.set_time(&time);
65//!
66//!         // Read values with Rocket's get_value function while rendering the frame
67//!         let _ = rocket.get_value("track0");
68//!     }
69//! }
70//! ```
71//!
72//! For a more thorough example, see `examples/simple.rs`.
73//!
74//! # Caveats
75//!
76//! - Can't choose how to handle [`saving the tracks`](crate::RocketClient::save_tracks), this uses [`std::fs::File`]
77//!   and [`bincode`].
78//! - Sub-optimal performance, the implementation does not support caching tracks
79//!   (only [`get_value`](Rocket::get_value), no [`get_track`](crate::RocketClient::get_track)).
80//!   It's unlikely that this causes noticeable slowdown unless you have an abnormally large amount of tracks.
81//! - **Caution**: reconnection will wipe track state. Make sure to save in the editor before closing and reopening it.
82//!
83//! # Benefits
84//!
85//! - Get started quickly!
86//! - Avoid writing `#[cfg(...)]`-attributes in your code.
87//! - Sensible error handling that you may want to write anyway if you're not size-restricted.
88
89use bincode::error::{DecodeError, EncodeError};
90use std::{path::Path, time::Duration};
91
92const SECS_PER_MINUTE: f32 = 60.;
93const ROWS_PER_BEAT: f32 = 8.;
94const PREFIX: &str = "rocket";
95
96/// Print a message to stderr. Prefixed with `prefix: `.
97///
98/// # Example
99///
100/// ```rust
101/// use rust_rocket::simple::print_msg;
102/// print_msg(env!("CARGO_CRATE_NAME"), "Using software renderer");
103/// ```
104pub fn print_msg(prefix: &str, msg: &str) {
105    eprintln!("{prefix}: {msg}");
106}
107
108/// Print an error and its sources to stderr. Prefixed with `prefix: `.
109pub fn print_errors(prefix: &str, error: &dyn std::error::Error) {
110    eprintln!("{prefix}: {error}");
111    let mut error = error.source();
112    while let Some(e) = error {
113        eprintln!("    Caused by: {e}");
114        error = e.source();
115    }
116}
117
118/// An `Event` type.
119#[derive(Debug, Copy, Clone)]
120pub enum Event {
121    /// The tracker changes row, asking you to update your time source.
122    Seek(Duration),
123    /// The tracker pauses or unpauses.
124    Pause(bool),
125    /// The client is not connected. Next calls to [`poll_events`](Rocket::poll_events) will eventually attempt to
126    /// reconnect.
127    ///
128    /// There are three equally sensible ways to handle this variant:
129    ///
130    /// 1. `break`: End your event polling `while let`-loop and proceed to rendering the frame.
131    ///    All [`Rocket`] methods keep working, but without control from the tracker.
132    /// 2. `continue 'main`: Restart your main loop, don't render the frame.
133    ///    This lets you keep calling other event polling functions from other libraries, e.g. SDL or winit.
134    /// 3. `{}`: Ignore it and let your event polling loop continue.
135    ///
136    /// Options 2 and 3 result is a busy wait, e.g. waste a lot of CPU time.
137    /// It's better to combine them with `std::thread::sleep` for at least a few milliseconds in order to mitigate that.
138    ///
139    /// See `simple.rs` in the `examples`-directory.
140    NotConnected,
141}
142
143/// Provides sync values.
144///
145/// # Usage
146///
147/// See [module documentation](crate::simple#Usage).
148pub struct Rocket<P: AsRef<Path>> {
149    path: P,
150    bps: f32,
151    row: f32,
152    #[cfg(not(feature = "player"))]
153    tracker_row: u32,
154    #[cfg(not(feature = "player"))]
155    connected: bool, // This is false when the rocket client has encountered an error
156    #[cfg(not(feature = "player"))]
157    connection_attempted: std::time::Instant,
158    #[cfg(not(feature = "player"))]
159    rocket: Option<crate::RocketClient>, // TODO: Make the client work on borrowed tracks so this Option isn't needed
160    #[cfg(feature = "player")]
161    rocket: crate::RocketPlayer,
162}
163
164impl<P: AsRef<Path>> Rocket<P> {
165    /// Initializes rocket.
166    ///
167    /// # Without `player` feature
168    ///
169    /// Attempts to connect to a rocket tracker.
170    ///
171    /// # With `player` feature
172    ///
173    /// Loads tracks from file specified by `path` using [`bincode`].
174    ///
175    /// # Errors
176    ///
177    /// Any errors that occur are first printed to stderr, then returned to the caller.
178    ///
179    /// An error is returned If the file specified by `path` cannot be read or its contents cannot be decoded.
180    ///
181    /// The return value can be handled by calling [`unwrap`](Result::unwrap) if you want to panic,
182    /// or [`ok`](Result::ok) if you want to ignore the error and continue without using rocket.
183    pub fn new(path: P, bpm: f32) -> Result<Self, DecodeError> {
184        #[cfg(not(feature = "player"))]
185        let rocket = Self::connect().ok();
186
187        #[cfg(feature = "player")]
188        let rocket = {
189            let mut file = match std::fs::File::open(&path) {
190                Ok(file) => file,
191                Err(e) => {
192                    print_msg(
193                        PREFIX,
194                        &format!("Failed to open {}", path.as_ref().display()),
195                    );
196                    print_errors(PREFIX, &e);
197                    return Err(DecodeError::Io {
198                        inner: e,
199                        additional: 0,
200                    });
201                }
202            };
203            let tracks = match bincode::decode_from_std_read(&mut file, bincode::config::standard())
204            {
205                Ok(tracks) => tracks,
206                Err(e) => {
207                    print_msg(
208                        PREFIX,
209                        &format!("Failed to read {}", path.as_ref().display()),
210                    );
211                    print_errors(PREFIX, &e);
212                    return Err(e);
213                }
214            };
215            crate::RocketPlayer::new(tracks)
216        };
217
218        Ok(Self {
219            path,
220            bps: bpm / SECS_PER_MINUTE,
221            row: 0.,
222            #[cfg(not(feature = "player"))]
223            tracker_row: 0,
224            #[cfg(not(feature = "player"))]
225            connected: rocket.is_some(),
226            #[cfg(not(feature = "player"))]
227            connection_attempted: std::time::Instant::now(),
228            rocket,
229        })
230    }
231
232    /// Get value based on previous call to [`set_time`](Self::set_time), by track name.
233    ///
234    /// # Panics
235    ///
236    /// With `player` feature: if the file specified in call to [`new`](Self::new) doesn't contain track with `name`,
237    /// the function handles the error by printing to stderr and panicking.
238    pub fn get_value(&mut self, track: &str) -> f32 {
239        #[cfg(not(feature = "player"))]
240        let track = match self
241            .rocket
242            .as_mut()
243            .and_then(|rocket| rocket.get_track_mut(track).ok())
244        {
245            Some(track) => track,
246            None => {
247                self.connected = false;
248                return 0.;
249            }
250        };
251
252        #[cfg(feature = "player")]
253        let track = self.rocket.get_track(track).unwrap_or_else(|| {
254            print_msg(
255                PREFIX,
256                &format!(
257                    "Track {} doesn't exist in {}",
258                    track,
259                    self.path.as_ref().display()
260                ),
261            );
262            panic!("{}: Can't recover", PREFIX);
263        });
264
265        track.get_value(self.row)
266    }
267
268    /// Update rocket with the current time from your time source, e.g. music player.
269    pub fn set_time(&mut self, time: &Duration) {
270        let beat = time.as_secs_f32() * self.bps;
271        self.row = beat * ROWS_PER_BEAT;
272
273        #[cfg(not(feature = "player"))]
274        {
275            let row = (self.row + 0.5) as u32;
276            if self.connected && row != self.tracker_row {
277                match self.rocket.as_mut().map(|rocket| rocket.set_row(row)) {
278                    Some(Ok(())) => self.tracker_row = row,
279                    Some(Err(ref e)) => {
280                        print_errors(PREFIX, e);
281                        self.connected = false;
282                    }
283                    None => self.connected = false,
284                }
285            }
286        }
287    }
288
289    /// Poll for new events from rocket.
290    ///
291    /// # Without `player` feature
292    ///
293    /// This polls from events from the tracker.
294    /// You should call this at least once per frame.
295    /// It is recommended to keep calling this in a `while` loop until you receive `Ok(None)`.
296    ///
297    /// # Errors
298    ///
299    /// Any errors that occur are first printed to stderr, then returned to the caller.
300    ///
301    /// An error is returned if the file specified in call to [`new`](Self::new) cannot be written to.
302    ///
303    /// The return value can be handled by calling [`unwrap`](Result::unwrap) if you want to panic,
304    /// or `.ok().flatten()` if you want to ignore the error and continue.
305    ///
306    /// # Example
307    ///
308    /// ```rust,no_run
309    /// # use std::time::Duration;
310    /// # use rust_rocket::simple::{Rocket, Event};
311    /// # struct MusicPlayer; // Your music player, not included in this crate
312    /// # impl MusicPlayer {
313    /// #     fn new() -> Self { Self }
314    /// #     fn get_time(&self) -> Duration { Duration::ZERO }
315    /// #     fn seek(&self, _to: Duration) {}
316    /// #     fn pause(&self, _state: bool) {}
317    /// # }
318    /// # let music = MusicPlayer::new();
319    /// # let mut rocket = Rocket::new("tracks.bin", 60.).unwrap();
320    /// while let Some(event) = rocket.poll_events().ok().flatten() {
321    ///     match event {
322    ///         Event::Seek(to) => music.seek(to),
323    ///         Event::Pause(state) => music.pause(state),
324    ///         Event::NotConnected => break,
325    ///     }
326    /// }
327    /// ```
328    ///
329    /// # Tips
330    ///
331    /// There are three sensible ways to handle the `Event::NotConnected` variant:
332    ///
333    /// 1. `break`: End your event polling `while let`-loop and proceed to rendering the frame.
334    ///    All [`Rocket`] methods keep working, but without control from the tracker.
335    /// 2. `continue 'main`: Restart your main loop, don't render the frame.
336    ///    This lets you keep calling other event polling functions from other libraries, e.g. SDL or winit.
337    /// 3. `{}`: Ignore it and let your event polling loop continue.
338    ///
339    /// Options 2 and 3 result is a busy wait, e.g. waste a lot of CPU time.
340    /// It's better to combine them with `std::thread::sleep` for at least a few milliseconds in order to mitigate that.
341    ///
342    /// # With `player` feature
343    ///
344    /// The function is a no-op.
345    pub fn poll_events(&mut self) -> Result<Option<Event>, EncodeError> {
346        #[cfg(not(feature = "player"))]
347        loop {
348            if !self.connected || self.rocket.is_none() {
349                // Don't spam connect
350                if self.connection_attempted.elapsed() < Duration::from_secs(1) {
351                    return Ok(Some(Event::NotConnected));
352                }
353                self.connection_attempted = std::time::Instant::now();
354                match Self::connect() {
355                    Ok(rocket) => {
356                        self.rocket = Some(rocket);
357                        self.connected = true;
358                    }
359                    Err(_) => return Ok(Some(Event::NotConnected)),
360                }
361            }
362            match self.rocket.as_mut().map(|rocket| rocket.poll_events()) {
363                Some(Ok(Some(event))) => {
364                    let handled = match event {
365                        crate::client::Event::SetRow(row) => {
366                            self.tracker_row = row;
367                            let beat = row as f32 / ROWS_PER_BEAT;
368                            Event::Seek(Duration::from_secs_f32(beat / self.bps))
369                        }
370                        crate::client::Event::Pause(flag) => Event::Pause(flag),
371                        crate::client::Event::SaveTracks => {
372                            self.save_tracks()?;
373                            continue;
374                        }
375                    };
376                    return Ok(Some(handled));
377                }
378                Some(Ok(None)) => return Ok(None),
379                Some(Err(ref e)) => {
380                    print_errors(PREFIX, e);
381                    self.connected = false;
382                }
383                None => self.connected = false,
384            }
385        }
386
387        #[cfg(feature = "player")]
388        Ok(None)
389    }
390
391    /// Save a snapshot of the tracks in the session, overwriting the file specified in call to [`new`](Self::new).
392    ///
393    /// # Errors
394    ///
395    /// Any errors that occur are first printed to stderr, then returned to the caller.
396    ///
397    /// An error is returned if the file specified in call to [`new`](Self::new) cannot be written to.
398    ///
399    /// The return value can be handled by calling [`unwrap`](Result::unwrap) if you want to panic,
400    /// or [`ok`](Result::ok) if you want to ignore the error and continue.
401    ///
402    /// # With `player` feature
403    ///
404    /// The function is a no-op.
405    pub fn save_tracks(&self) -> Result<(), EncodeError> {
406        #[cfg(not(feature = "player"))]
407        if let Some(rocket) = &self.rocket {
408            let open_result = std::fs::OpenOptions::new()
409                .write(true)
410                .create(true)
411                .truncate(true)
412                .open(&self.path);
413
414            let mut file = match open_result {
415                Ok(file) => file,
416                Err(e) => {
417                    print_msg(
418                        PREFIX,
419                        &format!("Failed to open {}", self.path.as_ref().display()),
420                    );
421                    print_errors(PREFIX, &e);
422                    return Err(EncodeError::Io { inner: e, index: 0 });
423                }
424            };
425
426            let tracks = rocket.save_tracks();
427            match bincode::encode_into_std_write(tracks, &mut file, bincode::config::standard()) {
428                Ok(_) => {
429                    print_msg(
430                        PREFIX,
431                        &format!("Tracks saved to {}", self.path.as_ref().display()),
432                    );
433                    Ok(())
434                }
435                Err(e) => {
436                    print_msg(
437                        PREFIX,
438                        &format!("Failed to write to {}", self.path.as_ref().display()),
439                    );
440                    print_errors(PREFIX, &e);
441                    Err(e)
442                }
443            }
444        } else {
445            print_msg(
446                PREFIX,
447                &format!(
448                    "Did not connect, not able to save {}",
449                    self.path.as_ref().display()
450                ),
451            );
452            Ok(())
453        }
454
455        #[cfg(feature = "player")]
456        Ok((/* No-op */))
457    }
458
459    #[cfg(not(feature = "player"))]
460    fn connect() -> Result<crate::RocketClient, crate::client::Error> {
461        print_msg(PREFIX, "Connecting...");
462        crate::RocketClient::new()
463    }
464}
465
466#[cfg(feature = "player")]
467impl Rocket<&str> {
468    /// An escape hatch constructor for advanced users who want to handle track loading via other means than `File`.
469    ///
470    /// This function is only available when the `player` feature is enabled, so you should not default to using it.
471    ///
472    /// # Usage
473    ///
474    /// The function makes it possible to load from e.g. [`std::include_bytes!`] in release builds.
475    ///
476    /// ```rust,no_run
477    /// # use rust_rocket::simple::Rocket;
478    /// # const SYNC_DATA: &[u8] = &[];
479    /// // const SYNC_DATA: &[u8] = include_bytes!("tracks.bin");
480    ///
481    /// #[cfg(feature = "player")]
482    /// let rocket = Rocket::from_std_read(&mut SYNC_DATA, 120.).unwrap_or_else(|_| unsafe {
483    ///     std::hint::unreachable_unchecked()
484    /// });
485    /// ```
486    pub fn from_std_read<R: std::io::Read>(read: &mut R, bpm: f32) -> Result<Self, DecodeError> {
487        let tracks = bincode::decode_from_std_read(read, bincode::config::standard())?;
488        let rocket = crate::RocketPlayer::new(tracks);
489        Ok(Self {
490            path: "release",
491            bps: bpm / SECS_PER_MINUTE,
492            row: 0.,
493            rocket,
494        })
495    }
496}