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}