rs_audio/
player.rs

1use std::collections::HashMap;
2use std::io::Error;
3use std::thread;
4use std::{sync::mpsc, time::Duration};
5
6use rodio::source::SineWave;
7use rodio::{OutputStream, Sink, Source};
8
9use crate::{BPMChoice, Note, WaveForm};
10
11/**
12This struct represents a song.<br>
13It contains a list of notes and a BPM (beats per minute) setting.<br><br>
14## Usage:
15```
16use rs_audio::*;
17let song = Song::default(); // creates a default song with one note (A4, 440Hz, 3 seconds, 0.20 volume, sine wave)
18```
19*/
20#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
21pub struct Song {
22    pub bpm: BPMChoice,
23    pub notes: Vec<Note>,
24}
25
26impl Song {
27    pub fn new(notes: Vec<Note>, bpm: BPMChoice) -> Self {
28        Self { bpm, notes }
29    }
30}
31
32impl Default for Song {
33    /**
34    Generates a default song that is useful for debugging purposes.<br>
35    It contains a single sine wave with a frequency of 440 Hz, lasts 3 seconds and has a volume of 0.20.<br>
36    It has a BPM of 120 (the default)<br><br>
37    # Usage
38    ```
39    use rs_audio::*;
40
41    let default_song = Song::default();
42    ```
43    */
44    fn default() -> Self {
45        Self {
46            bpm: BPMChoice::Default,
47            notes: vec![Note::default()],
48        }
49    }
50}
51
52impl Song {
53    /**
54    Saves a song to a JSON file. This can be useful if you want to save your songs somewhere.<br>
55    Note that this can return an error if it fails to:<br>
56    * Convert the song to JSON
57    * Write to the file
58      <br><br>
59    # Usage
60    ```
61    use rs_audio::*;
62
63    let song = Song::default();
64    match Song::save_to_json(&song, "song.json") {
65        Ok(_) => (),
66        Err(e) => eprintln!("{}", e.to_string()),
67    }
68    ```
69    */
70    pub fn save_to_json(song: &Song, filename: &str) -> Result<(), Error> {
71        let json = serde_json::to_string_pretty(song)?;
72        std::fs::write(filename, json)?;
73        Ok(())
74    }
75
76    /**
77    Loads a Song struct from a JSON file. This can be useful if you want to load existing songs from JSONs.<br>
78    Note that this will return an error if it fails to:<br>
79    * Open the file (it may not exist or it could not read it)
80    * Read from the file.
81      <br><br>
82    # Usage
83    ```
84    use rs_audio::*;
85
86    let loaded_song: Song = match Song::load_from_json("song.json") {
87        Ok(s) => s,
88        Err(e) => {
89            eprintln!("{}", e.to_string());
90            std::process::exit(1);
91        }
92    };
93    ```
94    */
95    pub fn load_from_json(filename: &str) -> Result<Song, Error> {
96        let json = std::fs::read_to_string(filename)?;
97        let song = serde_json::from_str(&json)?;
98        Ok(song)
99    }
100}
101
102/**
103This struct manages audio playback.<br>
104It allows playing multiple songs simultaneously, stopping them individually or all at once, and adjusting their volumes<br>
105It handles everything in a dedicated audio thread to ensure smooth playback and to not disrupt any other tasks.<br><br>
106## Usage:
107```
108use rs_audio::*;
109
110
111let mut audio_manager = AudioManager::new();
112
113let song = Song::default();
114
115let track_id = audio_manager.play(song); // play the song and get its track ID
116
117audio_manager.set_volume(track_id, 0.5); // set volume for this track
118audio_manager.stop(track_id); // stop this specific track
119audio_manager.stop_all(); // stop all tracks
120```
121*/
122pub struct AudioManager {
123    tx: mpsc::Sender<AudioCommand>,
124    next_track_id: usize,
125}
126
127#[derive(Debug, Clone)]
128pub(crate) enum AudioCommand {
129    PlayTrack { id: usize, song: Song },
130    StopTrack(usize),
131    SetVolume(usize, f32),
132    StopAll,
133}
134
135impl Default for AudioManager {
136    fn default() -> Self {
137        Self::new()
138    }
139}
140
141impl AudioManager {
142    /**
143    Creates a new AudioManager instance and starts the audio thread.<br>
144    This thread handles all audio playback and control.<br>
145    It uses channels to receive commands from the main thread.<br><br>
146    # Usage:
147    ```
148    use rs_audio::*;
149    let mut audio_manager = AudioManager::new();
150    ```
151    */
152    pub fn new() -> Self {
153        let (tx, rx) = mpsc::channel();
154        let mut _next_track_id = 0;
155
156        // Spawn the dedicated audio thread
157        thread::spawn(move || {
158            let (_stream, handle) = OutputStream::try_default().unwrap();
159            let mut sinks: HashMap<usize, Sink> = HashMap::new();
160
161            // audio thread loop
162            while let Ok(command) = rx.recv() {
163                match command {
164                    AudioCommand::PlayTrack { id, song } => {
165                        // create a new sink for this track
166                        let sink = Sink::try_new(&handle).unwrap();
167
168                        for note in song.notes {
169                            let source = match note.wave {
170                                WaveForm::Sine => Box::new(SineWave::new(note.freq as f32)),
171                                WaveForm::Rest => {
172                                    std::thread::sleep(Duration::from_secs_f64(note.dur));
173                                    continue;
174                                }
175                                _ => Box::new(note.to_approx_sine()),
176                            };
177                            sink.append(
178                                source
179                                    .take_duration(Duration::from_secs_f64(note.dur))
180                                    .amplify(note.vol),
181                            );
182                        }
183
184                        // Store the sink for potential later control
185                        sinks.insert(id, sink);
186                    }
187                    AudioCommand::StopTrack(id) => {
188                        if let Some(sink) = sinks.get(&id) {
189                            sink.stop(); // Stop only this specific track
190                            sinks.remove(&id);
191                        }
192                    }
193                    AudioCommand::SetVolume(id, volume) => {
194                        if let Some(sink) = sinks.get(&id) {
195                            sink.set_volume(volume);
196                        }
197                    }
198                    AudioCommand::StopAll => {
199                        for sink in sinks.values() {
200                            sink.stop();
201                        }
202                        sinks.clear();
203                    }
204                }
205            }
206        });
207
208        AudioManager {
209            tx,
210            next_track_id: 0,
211        }
212    }
213    /// Plays a song and returns a unique track ID for later control (like stopping or adjusting volume).
214    pub fn play(&mut self, song: Song) -> usize {
215        let track_id = self.next_track_id;
216        self.next_track_id += 1;
217
218        let _ = self.tx.send(AudioCommand::PlayTrack { id: track_id, song });
219
220        track_id // return ID for later control
221    }
222
223    /// Stops a specific track using its track ID.
224    pub fn stop(&self, track_id: usize) {
225        let _ = self.tx.send(AudioCommand::StopTrack(track_id));
226    }
227
228    /// Sets the volume for a specific track using its track ID.
229    pub fn set_volume(&self, track_id: usize, volume: f32) {
230        let _ = self.tx.send(AudioCommand::SetVolume(track_id, volume));
231    }
232
233    /// Stops all currently playing tracks.
234    pub fn stop_all(&self) {
235        let _ = self.tx.send(AudioCommand::StopAll);
236    }
237}