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}