-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
comments, changelog, minor improvements here and there
- Loading branch information
Showing
10 changed files
with
246 additions
and
80 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
v0.2.0 | ||
------ | ||
Released XXXX/XX/XX | ||
|
||
* Added stats() function to Opl3Device for retrieving statistics. | ||
* Added an option to the example `play_tune` to save the output as a wav via `hound`. | ||
* Added a `buffered` parameter to `write_register`. This is technically a breaking change, | ||
but nobody's using this yet, so... | ||
|
||
v0.1.2 | ||
------ | ||
Released 2024/06/30 | ||
|
||
* Fixed build and provided a working example of music playback. | ||
|
||
v0.1.0, v0.1.1 | ||
-------------- | ||
Released 2024/06/29, Yanked 2024/06/30 | ||
|
||
* Broken. Less said the better. Was still figuring out how to publish an FFI crate. |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,11 +1,29 @@ | ||
//! This is a example program that plays a simple, 3-channel tune via opl3-rs and the `rodio` audio | ||
//! library. | ||
//! This is an example program that plays a simple, 3-channel tune via opl3-rs and the `rodio` audio | ||
//! library. It can optionally save the output to a wav file via `hound`. | ||
//! This library uses a multithreaded timer callback via the `timer` crate to play the music and | ||
//! generate audio samples which are sent to the main thread via `crossbeam` channels. | ||
//! | ||
//! Original code by Maarten Janssen ([email protected]) 2016-04-13 | ||
//! Most recent version of the library can be found at my GitHub: https://github.com/DhrBaksteen/ArduinoOPL2 | ||
//! Hacked for a OPL2LPT test program by [email protected]. | ||
//! Original code (C) Maarten Janssen ([email protected]) 2016-04-13 | ||
//! https://github.com/DhrBaksteen/ArduinoOPL2 | ||
//! Hacked for a OPL2LPT test program Peter De Wachter ([email protected]). | ||
//! https://github.com/pdewacht/adlipt/issues | ||
//! Rewritten in Rust by Daniel Balsom for opl3-rs | ||
//! | ||
//! Permission is hereby granted, free of charge, to any person obtaining a copy of this software | ||
//! and associated documentation files (the “Software”), to deal in the Software without | ||
//! restriction, including without limitation the rights to use, copy, modify, merge, publish, | ||
//! distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the | ||
//! Software is furnished to do so, subject to the following conditions: | ||
// | ||
// The above copyright notice and this permission notice shall be included in all copies or | ||
// substantial portions of the Software. | ||
// | ||
// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING | ||
// BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND | ||
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, | ||
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE | ||
|
||
use std::io::Write; | ||
use std::sync::{Arc, Mutex}; | ||
|
||
|
@@ -27,6 +45,17 @@ mod opl_instruments; | |
const TIMER_FREQ: i64 = 100; // We will set a timer callback at 100Hz | ||
|
||
fn main() { | ||
// This example uses rodio for playback. | ||
// rodio is built on top of the lower level 'cpal' audio library. If there is something we | ||
// cannot accomplish in rodio, we can fall back to the underlying cpal implementation. | ||
|
||
// We want to retrieve the system's sample rate in order to be able to provide it to opl3-rs. | ||
// However, I haven't found a way to retrieve the sample rate from a rodio stream, so we | ||
// will open the default device via cpal first, query the sample rate, then open the | ||
// rodio output stream from the cpal device. | ||
|
||
// Possible improvement here: enumerate devices and let user select - or - attempt to open | ||
// other devices if the default device fails. | ||
let audio_device = rodio::cpal::default_host() | ||
.default_output_device() | ||
.expect("No audio device found."); | ||
|
@@ -37,6 +66,8 @@ fn main() { | |
.default_output_config() | ||
.expect("Couldn't get device configuration."); | ||
|
||
// We can now retrieve the sample rate. We don't really need the number of channels or sample | ||
// format, but it is displayed for informational purposes. | ||
let sample_rate = config.sample_rate().0; | ||
let channels = config.channels() as usize; | ||
let sample_format = config.sample_format().to_string(); | ||
|
@@ -49,12 +80,21 @@ fn main() { | |
device_name, sample_rate, channels, sample_format | ||
); | ||
|
||
play_music(sample_rate, stream_handle); | ||
//play_note(sample_rate, stream_handle); | ||
// This was a test used during debugging to play a single note. Add a command-line option | ||
// to select between playing the test note or the example music. | ||
//play_note(sample_rate, stream_handle, true); | ||
|
||
play_music(sample_rate, stream_handle, false); | ||
} | ||
|
||
/// Initialize the MusicPlayer but only play a single, sustained note for 1 second. | ||
/// This is useful for debugging and testing the audio output if the music player isn't working. | ||
/// The generated samples are saved to a file named "test.raw". | ||
/// | ||
/// They can be viewed via Audacity by import->raw, data type: signed 16-bit PCM, little-endian, | ||
/// stereo. | ||
#[allow(dead_code)] | ||
fn play_note(sample_rate: u32, stream_handle: rodio::OutputStreamHandle) { | ||
fn play_note(sample_rate: u32, stream_handle: rodio::OutputStreamHandle, save_wav: bool) { | ||
// Create a stereo buffer one second long. (Length = Sample rate * 2 channels) | ||
let mut samples = vec![0; 2 * sample_rate as usize]; | ||
|
||
|
@@ -95,56 +135,75 @@ fn play_note(sample_rate: u32, stream_handle: rodio::OutputStreamHandle) { | |
std::thread::sleep(std::time::Duration::from_secs(1)); | ||
} | ||
|
||
fn play_music(sample_rate: u32, stream_handle: rodio::OutputStreamHandle) { | ||
/// Play music using the MusicPlayer. This function sets up a timer callback to execute OPL3 | ||
/// commands and generate audio samples. The callback is fired at a fixed rate (100Hz), in a | ||
/// separate thread. Crossbeam channels are used to send the generated samples to the main thread. | ||
/// The message type is an enum of type CallbackMessage, and can incorporate either instructions | ||
/// for the main thread or encapsulate audio samples. | ||
fn play_music(sample_rate: u32, stream_handle: rodio::OutputStreamHandle, save_wav: bool) { | ||
// Create a channel to receive the audio samples as they are generated by the timer callback. | ||
// The channel here is unbounded, but you could calculate the number of samples you expect to | ||
// receive and use a bounded channel. I am not sure of the performance differences. | ||
let (s, r) = unbounded(); | ||
|
||
// Create the music player. | ||
// Create and initialize the music player. | ||
let mut player = MusicPlayer::new(sample_rate, s); | ||
// Start the player | ||
player.setup(); | ||
|
||
// Wrap the player in an Arc<Mutex<>> so we can share it with the timer callback. | ||
let player_arc = Arc::new(Mutex::new(player)); | ||
|
||
// Create a rodio 'sink' to play the audio samples. Since there is only one stream, we could | ||
// use the stream_handle.play_raw() method directly, but this is a more general approach. | ||
let sink = rodio::Sink::try_new(&stream_handle).expect("Couldn't create sink!"); | ||
|
||
// Create and set up the timer. The 'Timer' crate is a bit old and unmaintained, but it seems | ||
// to still work well. You could use a different timer crate such as tokio::time. | ||
let freq_duration = Duration::milliseconds(1000 / TIMER_FREQ); | ||
let timer = Timer::new(); | ||
|
||
// Set up the timer callback. The result of schedule_repeating is saved to a guard variable to | ||
// determine the callback's lifetime. | ||
// Set up the timer callback. The result of schedule_repeating() is stored in a guard variable | ||
// to determine the callback's lifetime. The callback thread will end when the guard is dropped. | ||
let _guard = { | ||
let player_arc_clone = Arc::clone(&player_arc); | ||
timer.schedule_repeating(freq_duration, move || { | ||
let mut player_lock = player_arc.lock().unwrap_or_else(|e| { | ||
eprintln!("Error locking player: {:?}", e); | ||
// exit to os | ||
std::process::exit(1); | ||
}); | ||
// Lock the player and call the player's timer callback. | ||
let mut player_lock = player_arc_clone | ||
.lock() | ||
.unwrap_or_else(|e| panic!("Error locking player: {:?}", e)); | ||
player_lock.timer_callback(); | ||
}) | ||
}; | ||
|
||
// Start playing the sink. | ||
// Start playing the rodio audio sink. This may not be strictly necessary as we never paused it. | ||
sink.play(); | ||
|
||
// Loop and receive messages/samples from the callback. | ||
// Loop and receive messages/samples from the callback. We use an outer flag to determine when | ||
// we are done. This flag is set by a CallbackMessage::EndPlayback message. | ||
let mut end_playback = false; | ||
while !end_playback { | ||
// Block until we receive a message. If we receive a read error, we treat it like we | ||
// received an error message from the channel. | ||
let message = r.recv().unwrap_or_else(|e| { | ||
eprintln!("Error receiving channel message: {:?}", e); | ||
// exit to os | ||
CallbackMessage::Error | ||
}); | ||
match message { | ||
CallbackMessage::Error | CallbackMessage::EndPlayback => { | ||
// Either an error occurred, or we were instructed to end playback. | ||
end_playback = true; | ||
} | ||
CallbackMessage::HaveSamples(samples) => { | ||
// We received some audio samples. opl3-rs generates samples in i16 format, so we | ||
// need to convert them to f32 in the range -1.0 to 1.0 for rodio to play them back. | ||
let channel_samples: Vec<f32> = samples | ||
.iter() | ||
.map(|c| *c as f32 / i16::MAX as f32) | ||
.collect(); | ||
|
||
// Create a SamplesBuffer out of our received samples and append them to the sink | ||
// to be played. | ||
let buf = rodio::buffer::SamplesBuffer::new(2, sample_rate, channel_samples); | ||
sink.append(buf); | ||
} | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,11 +1,25 @@ | ||
//! This is a example program that plays a simple, 3-channel tune via opl3-rs and the `rodio` audio | ||
//! library. | ||
//! Music player interface for play_tune example. | ||
//! | ||
//! Original code by Maarten Janssen ([email protected]) 2016-04-13 | ||
//! Most recent version of the library can be found at my GitHub: https://github.com/DhrBaksteen/ArduinoOPL2 | ||
//! Hacked for a OPL2LPT test program by [email protected]. | ||
//! Original code (C) Maarten Janssen ([email protected]) 2016-04-13 | ||
//! https://github.com/DhrBaksteen/ArduinoOPL2 | ||
//! Hacked for a OPL2LPT test program Peter De Wachter ([email protected]). | ||
//! https://github.com/pdewacht/adlipt/issues | ||
//! Rewritten in Rust by Daniel Balsom for opl3-rs | ||
//! | ||
//! Permission is hereby granted, free of charge, to any person obtaining a copy of this software | ||
//! and associated documentation files (the “Software”), to deal in the Software without | ||
//! restriction, including without limitation the rights to use, copy, modify, merge, publish, | ||
//! distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the | ||
//! Software is furnished to do so, subject to the following conditions: | ||
// | ||
// The above copyright notice and this permission notice shall be included in all copies or | ||
// substantial portions of the Software. | ||
// | ||
// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING | ||
// BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND | ||
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, | ||
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE | ||
|
||
use crossbeam_channel::Sender; | ||
|
||
|
@@ -171,7 +185,7 @@ impl MusicPlayer { | |
self.tempo = 120; | ||
self.opl3.reset(None); | ||
|
||
self.opl3.write_register(0x01, 0x20); // Set WSE=1 | ||
self.opl3.write_register(0x01, 0x20, true); // Set WSE=1 | ||
|
||
set_instrument(&mut self.opl3, 0, &OPL_INSTRUMENT_PIANO1); | ||
set_block(&mut self.opl3, 0, 5); | ||
|
@@ -260,46 +274,47 @@ impl MusicPlayer { | |
} | ||
|
||
fn parse_tune(&mut self, t: usize) { | ||
// Read and process tune data until we find a note or pause command. | ||
while self.tunes[t].data.peek() != 0 { | ||
if self.tunes[t].data.peek() == b'<' && self.tunes[t].octave > 1 { | ||
// Decrease octave if greater than 1. | ||
// '<': Decrease octave if greater than 1. | ||
self.tunes[t].octave -= 1; | ||
} else if self.tunes[t].data.peek() == b'>' && self.tunes[t].octave < 7 { | ||
// Increase octave if less than 7. | ||
// '>': Increase octave if less than 7. | ||
self.tunes[t].octave += 1; | ||
} else if self.tunes[t].data.peek() == b'o' | ||
&& self.tunes[t].data.peek_next() >= b'1' | ||
&& self.tunes[t].data.peek_next() <= b'7' | ||
{ | ||
// Set octave. | ||
self.tunes[t].octave = self.tunes[t].data.peek_next() as i32 - 48; | ||
// 'o': Set octave. | ||
self.tunes[t].octave = (self.tunes[t].data.peek_next() - b'0') as i32; | ||
self.tunes[t].data.next(); | ||
} else if self.tunes[t].data.peek() == b'l' { | ||
// Set default note duration. | ||
// 'l': Set default note duration. | ||
self.tunes[t].data.next(); | ||
let duration = self.parse_number(t); | ||
if duration != 0 { | ||
self.tunes[t].note_duration = duration as i32; | ||
} | ||
} else if self.tunes[t].data.peek() == b'm' { | ||
// Set note length in percent. | ||
// 'm': Set note length in percent. | ||
self.tunes[t].data.next(); | ||
self.tunes[t].note_length = self.parse_number(t) as u32; | ||
} else if self.tunes[t].data.peek() == b't' { | ||
// Set song tempo. | ||
// 't': Set song tempo. | ||
self.tunes[t].data.next(); | ||
self.tempo = self.parse_number(t) as u32; | ||
if self.tempo == 0 { | ||
// Tempo cannot be 0 | ||
self.tempo = 1; | ||
} | ||
} else if self.tunes[t].data.peek() == b'p' || self.tunes[t].data.peek() == b'r' { | ||
// Pause. | ||
// 'p' or 'r': Pause. | ||
self.tunes[t].data.next(); | ||
self.tunes[t].next_note_time = self.get_timer() + self.parse_duration(t); | ||
break; | ||
} else if self.tunes[t].data.peek() >= b'a' && self.tunes[t].data.peek() <= b'g' { | ||
// Next character is a note A..G so play it. | ||
// 'a'-'g': Play note. | ||
self.parse_note(t); | ||
break; | ||
} | ||
|
@@ -312,15 +327,18 @@ impl MusicPlayer { | |
// Get index of note in base frequency table. | ||
let note_char = self.tunes[t].data.peek() as char; | ||
let note_idx = self.tunes[t].data.index(); | ||
let mut note: u8 = (self.tunes[t].data.peek() - 97) * 3; | ||
self.tunes[t].data.next(); | ||
|
||
// Get relative note index, and adjust times 3 for sharp/flat notes. | ||
let mut note: u8 = (self.tunes[t].data.get() - b'a') * 3; | ||
|
||
if self.tunes[t].data.peek() == b'-' { | ||
note += 1; | ||
self.tunes[t].data.next(); | ||
// Flat note. | ||
note += 1; | ||
} else if self.tunes[t].data.peek() == b'+' { | ||
note += 2; | ||
self.tunes[t].data.next(); | ||
// Sharp note. | ||
note += 2; | ||
} | ||
|
||
// Get duration, set delay and play note. | ||
|
@@ -370,9 +388,6 @@ impl MusicPlayer { | |
base = 4; | ||
} | ||
|
||
if duration == 0 { | ||
duration = 1; | ||
} | ||
// Calculate note duration in timer ticks (0.01s) | ||
let ticks = 6000u32 * base / duration / self.tempo; | ||
return ticks; | ||
|
@@ -384,13 +399,16 @@ impl MusicPlayer { | |
&& self.tunes[t].data.peek() >= b'0' | ||
&& self.tunes[t].data.peek() <= b'9' | ||
{ | ||
// Data is number. Parse it... | ||
while self.tunes[t].data.peek() != 0 | ||
&& self.tunes[t].data.peek() >= b'0' | ||
&& self.tunes[t].data.peek() <= b'9' | ||
{ | ||
number = number * 10 + (self.tunes[t].data.get() - 48); | ||
// Keep multiplying by 10 as long as we have additional digits. | ||
number = number * 10 + (self.tunes[t].data.get() - b'0'); | ||
} | ||
self.tunes[t].data.prev() | ||
// Last character wasn't a digit, so go back one step. | ||
self.tunes[t].data.prev(); | ||
} | ||
return number; | ||
} | ||
|
Oops, something went wrong.