Skip to content

Commit

Permalink
Add cmdline options to example, wav writing
Browse files Browse the repository at this point in the history
  • Loading branch information
dbalsom committed Jul 3, 2024
1 parent a34a8a4 commit 560ed4a
Show file tree
Hide file tree
Showing 5 changed files with 143 additions and 46 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/target
*.iml
.idea
*.wav

# Added by cargo
#
Expand Down
13 changes: 10 additions & 3 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 4 additions & 3 deletions examples/play_tune/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
[package]
name = "play_tune"
version = "0.1.2"
version = "0.2.0"
edition = "2021"
publish = false

[dependencies]
"opl3-rs" = { path = "../.." }
"rodio" = "0.18"
"rodio" = "0.19"
"chrono" = "0.4"
"timer" = "0.2"
"crossbeam-channel" = "0.5"
"hound" = "3.1"
"hound" = "3.5"
"bpaf" = { version = "0.9", features = ["autocomplete"] }
121 changes: 108 additions & 13 deletions examples/play_tune/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,12 @@
// 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::fs::File;
use std::io::{BufWriter, Write};
use std::path::PathBuf;
use std::sync::{Arc, Mutex};

use bpaf::*;
use chrono::Duration;
use crossbeam_channel::unbounded;
use rodio::cpal::traits::HostTrait;
Expand All @@ -44,6 +47,44 @@ mod opl_instruments;

const TIMER_FREQ: i64 = 100; // We will set a timer callback at 100Hz

#[derive(Debug, Clone)]
struct Out {
debug: Option<bool>,
test_note: Option<bool>,
output_wav: Option<PathBuf>,
}

fn opts() -> OptionParser<Out> {
// Set up bpaf argument parsing.
let debug = short('d')
.long("debug")
.help("Activate debug mode")
.switch()
.fallback(false)
.optional();

let test_note = short('t')
.long("test_note")
.help("Play a single test note for 1 second.")
.switch()

This comment has been minimized.

Copy link
@pacak

pacak Jul 7, 2024

This .switch() makes a parser that returns true if -t or --test_note is present or false otherwise.

.fallback(false) makes it so if parser fails with "missing value" type of error - it returns specified value, here - false. Problem is .switch() never fails in this way. You can safely drop this method without changing the behavior for the test_note field.

.optional() does something similar to .fallback(foo), but instead of replacing the result with what you specified - it makes parser that returns None if value is absent and Some(val) - if it is present. As fallback it relies on parser failing with "value not present" type of error - this never happens for switch() but even if you had some other parser - fallback by itself handles this error. I'd drop .optional() part and change the field to test_note: bool - should make consuming it a bit easier.

So overall I'd have something like this:

    let test_note = short('t')
        .long("test-note")
        .help("Play a single test note for 1 second.")
        .switch();

Same goes for output_wav.

This comment has been minimized.

Copy link
@dbalsom

dbalsom Jul 10, 2024

Author Owner

Thanks for the tips.

.fallback(false)
.optional();

let output_wav = short('w')
.long("wav_file")
.help("WAV File to write")
.argument::<PathBuf>("FILE")
.optional();

construct!(Out {
debug,
test_note,
output_wav
})
.to_options()
.descr("opl3-rs: play_tune example")
}

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
Expand All @@ -54,6 +95,9 @@ fn main() {
// will open the default device via cpal first, query the sample rate, then open the
// rodio output stream from the cpal device.

// Get the command line options.
let opts = opts().run();

// 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()
Expand All @@ -80,27 +124,42 @@ fn main() {
device_name, sample_rate, channels, sample_format
);

// 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);
let mut wav_out = None;

if let Some(filename) = opts.output_wav {
// If we've specified a wave file for output, open it now and create a BufWriter for it.
let file = File::create(filename).expect("Couldn't create output file.");
wav_out = Some(BufWriter::new(file));
}

play_music(sample_rate, stream_handle, false);
// Play the test note if option -t was specified, otherwise play music.
if opts.test_note.is_some_and(|b| b) {
play_note::<BufWriter<File>>(sample_rate, stream_handle, wav_out.as_mut());
} else {
play_music::<BufWriter<File>>(
sample_rate,
stream_handle,
wav_out.as_mut(),
opts.debug.unwrap_or(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.
/// The generated samples are saved to the specified writer Option, if provided.
#[allow(dead_code)]
fn play_note(sample_rate: u32, stream_handle: rodio::OutputStreamHandle, save_wav: bool) {
fn play_note<W: Write>(
sample_rate: u32,
stream_handle: rodio::OutputStreamHandle,
_wav_out: Option<&mut W>,
) {
// Create a stereo buffer one second long. (Length = Sample rate * 2 channels)
let mut samples = vec![0; 2 * sample_rate as usize];

// Create the music player. We don't use this channel in this example.
let (s, _r) = unbounded();
let mut player = MusicPlayer::new(sample_rate, s);
let mut player = MusicPlayer::new(sample_rate, s, false);

// Start the player and play a single note, leaving it sustained.
player.setup();
Expand Down Expand Up @@ -140,14 +199,19 @@ fn play_note(sample_rate: u32, stream_handle: rodio::OutputStreamHandle, save_wa
/// 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) {
fn play_music<W: Write + std::io::Seek>(
sample_rate: u32,
stream_handle: rodio::OutputStreamHandle,
wav_out: Option<&mut W>,
debug: 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 and initialize the music player.
let mut player = MusicPlayer::new(sample_rate, s);
let mut player = MusicPlayer::new(sample_rate, s, debug);
player.setup();

// Wrap the player in an Arc<Mutex<>> so we can share it with the timer callback.
Expand Down Expand Up @@ -175,6 +239,25 @@ fn play_music(sample_rate: u32, stream_handle: rodio::OutputStreamHandle, save_w
})
};

// If a writer was provided, create a Hound wav writer wrapped in Some, otherwise None.
let mut wav_writer = if let Some(w) = wav_out {
// Use our converted format, using the specified sample rate and 32-bit float samples.
let wav_writer = hound::WavWriter::new(
w,
hound::WavSpec {
channels: 2,
sample_rate,
bits_per_sample: 32,
sample_format: hound::SampleFormat::Float,
},
)
.expect("Couldn't create wav writer.");

Some(wav_writer)
} else {
None
};

// Start playing the rodio audio sink. This may not be strictly necessary as we never paused it.
sink.play();

Expand Down Expand Up @@ -202,11 +285,23 @@ fn play_music(sample_rate: u32, stream_handle: rodio::OutputStreamHandle, save_w
.map(|c| *c as f32 / i16::MAX as f32)
.collect();

// If we have a wav writer, write the samples to the wav file.
if let Some(wav_writer) = &mut wav_writer {
for sample in &channel_samples {
wav_writer.write_sample(*sample).unwrap();
}
}

// 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);
}
}
}

// If we have a wav writer, finalize the wav file.
if let Some(wav_writer) = wav_writer {
wav_writer.finalize().expect("Couldn't finalize wav file.");
}
}
47 changes: 20 additions & 27 deletions examples/play_tune/src/music_player.rs
Original file line number Diff line number Diff line change
Expand Up @@ -164,10 +164,11 @@ pub struct MusicPlayer {
opl3: Opl3Device,
sender: Sender<CallbackMessage>,
playing: bool,
debug: bool,
}

impl MusicPlayer {
pub fn new(sample_rate: u32, sender: Sender<CallbackMessage>) -> MusicPlayer {
pub fn new(sample_rate: u32, sender: Sender<CallbackMessage>, debug: bool) -> MusicPlayer {
let samples_per_interval = (sample_rate / TIMER_FREQ as u32) as usize;
println!("Samples per interval: {}", samples_per_interval);
MusicPlayer {
Expand All @@ -178,6 +179,7 @@ impl MusicPlayer {
opl3: Opl3Device::new(sample_rate),
sender,
playing: false,
debug,
}
}

Expand Down Expand Up @@ -223,18 +225,6 @@ impl MusicPlayer {
self.main_loop();
self.opl3.generate_samples(&mut self.sample_buf);

/*
let min = self.sample_buf.iter().cloned().fold(i16::MAX, i16::min);
let max = self.sample_buf.iter().cloned().fold(i16::MIN, i16::max);
println!(
"Sending {} samples, min: {}, max: {}, Example: {:?}",
self.sample_buf.len(),
min,
max,
&self.sample_buf[0..16]
);
*/

self.sender
.send(CallbackMessage::HaveSamples(self.sample_buf.clone()))
.unwrap();
Expand Down Expand Up @@ -356,20 +346,23 @@ impl MusicPlayer {
);
set_frequency(&mut self.opl3, self.tunes[t].channel, f);

println!(
"Index: {:05} Next: {:05} Char: {} Note: {:02}, channel: {} octave: {} freq: {:04} timer: {:05} duration: {} length: {} release_time: {}",
note_idx,
self.tunes[t].data.index(),
note_char,
note,
self.tunes[t].channel,
self.tunes[t].octave,
f,
self.get_timer(),
duration,
self.tunes[t].note_length,
self.tunes[t].release_time
);
if self.debug {
println!(
"Index: {:05} Next: {:05} Char: {} Note: {:02}, channel: {} octave: {} freq: {:04} timer: {:05} duration: {} length: {} release_time: {}",
note_idx,
self.tunes[t].data.index(),
note_char,
note,
self.tunes[t].channel,
self.tunes[t].octave,
f,
self.get_timer(),
duration,
self.tunes[t].note_length,
self.tunes[t].release_time
);
}

set_key_on(&mut self.opl3, self.tunes[t].channel, true);
}

Expand Down

0 comments on commit 560ed4a

Please sign in to comment.