Skip to content

Commit

Permalink
Merge pull request #75 from davehorner/master
Browse files Browse the repository at this point in the history
example: filter_play_sound - example of using cpal and tokio-serial
  • Loading branch information
estokes authored Dec 31, 2024
2 parents 5f39e7d + c85816b commit 2a2c77c
Show file tree
Hide file tree
Showing 2 changed files with 326 additions and 2 deletions.
26 changes: 24 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,15 @@ default-features = false
features = ["net"]

[dependencies.tokio-util]
version = "0.7"
version = "0.7.12"
default-features = false
optional = true

[dev-dependencies.tokio-util]
version = "0.7.12"
default-features = false
features = ["codec"]

[dependencies.mio-serial]
version = "5.0.3"
default-features = false
Expand All @@ -43,15 +48,32 @@ version = "1"
default-features = false
optional = true

[dev-dependencies.bytes]
version = "1"

[dev-dependencies.cpal]
version = "0.15.3"

[dependencies.log]
version = "0.4"

[dependencies.cfg-if]
version = "1"

[dev-dependencies]
anyhow = "1.0.91"

[dev-dependencies.tokio]
version = "^1.8"
features = ["macros", "rt", "process", "time", "fs", "io-util"]
features = [
"macros",
"rt",
"process",
"time",
"fs",
"io-util",
"rt-multi-thread",
]
default-features = false

[dev-dependencies.env_logger]
Expand Down
302 changes: 302 additions & 0 deletions examples/filter_play_sound.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,302 @@
/// filter terms from the serial port and make a sound when found.
///
/// dave horner 10/24
///
/// Default settings for Nordic Thingy53, nrf5340dk, and other nordic devices (baud/com).
use bytes::BytesMut;
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
use futures::stream::StreamExt;
use std::sync::Mutex;
use std::sync::Arc;
use std::{env, io, str};
use tokio::time::Duration;
use tokio_serial::SerialPortBuilderExt;
use tokio_util::codec::{Decoder, Encoder};
extern crate anyhow;

#[cfg(unix)]
const DEFAULT_TTY: &str = "/dev/ttyACM1";
#[cfg(windows)]
const DEFAULT_TTY: &str = "COM8";

// Create the table of findable strings and their sound parameters
fn create_find_text_map() -> HashMap<&'static str, SoundParams> {
let mut map = HashMap::new();
map.insert("Using Zephyr OS", SoundParams {
waveform: Waveform::Sine,
frequency: 500.0,
duration: 150,
});
map.insert("Error", SoundParams {
waveform: Waveform::Square,
frequency: 800.0,
duration: 150,
});
map.insert("Warning", SoundParams {
waveform: Waveform::Triangle,
frequency: 300.0,
duration: 150,
});
map.insert("DK handling", SoundParams {
waveform: Waveform::Triangle,
frequency: 600.0,
duration: 150,
});
map
}

#[tokio::main]
async fn main() -> tokio_serial::Result<()> {
let mut args = env::args();
let tty_path = args.nth(1).unwrap_or_else(|| DEFAULT_TTY.into());


#[cfg(unix)]
let mut port = tokio_serial::new(tty_path, 115200).open_native_async()?; // Mutable on Unix
#[cfg(windows)]
let port = tokio_serial::new(tty_path, 115200).open_native_async()?; // Immutable on Windows
#[cfg(unix)]
port.set_exclusive(false)
.expect("Unable to set serial port exclusive to false");
let mut reader = LineCodec.framed(port);

let find_text_map = create_find_text_map();
while let Some(line_result) = reader.next().await {
let line = line_result.expect("Failed to read line");
print!("{}", line);

for (phrase, params) in &find_text_map {
if line.contains(phrase) {
let params_clone = params.clone();
tokio::spawn(async move {
let _ = play_sound(params_clone).await;
});
break;
}
}
}
Ok(())
}


///////////////////////////////////
/// Codec
/// ///////////////////////////////
struct LineCodec;

impl Decoder for LineCodec {
type Item = String;
type Error = io::Error;

fn decode(&mut self, src: &mut BytesMut) -> Result<Option<Self::Item>, Self::Error> {
let newline = src.as_ref().iter().position(|b| *b == b'\n');
if let Some(n) = newline {
let line = src.split_to(n + 1);
return match str::from_utf8(line.as_ref()) {
Ok(s) => Ok(Some(s.to_string())),
Err(_) => Err(io::Error::new(io::ErrorKind::Other, "Invalid String")),
};
}
Ok(None)
}
}

impl Encoder<String> for LineCodec {
type Error = io::Error;

fn encode(&mut self, _item: String, _dst: &mut BytesMut) -> Result<(), Self::Error> {
Ok(())
}
}


///////////////////////////////////
/// All this code to make noise.
/// ///////////////////////////////
use std::error::Error;
use std::f32::consts::PI;
use std::thread;
use std::collections::HashMap;

#[derive(Clone)]
struct SoundParams {
waveform: Waveform,
frequency: f32,
duration: u64,
}

async fn play_sound(params: SoundParams) -> Result<(), Box<dyn Error + Send + Sync>> {
let oscillator = Arc::new(Mutex::new(Oscillator::new(44100.0, params.frequency, params.waveform)));
let oscillator_clone = Arc::clone(&oscillator);

let play_handle = thread::spawn(move || {
let stream = start_audio_stream_arc(oscillator_clone).expect("Failed to start audio stream");
stream.play().expect("Failed to play audio stream");
std::thread::sleep(Duration::from_millis(params.duration));
});

play_handle.join().expect("Play thread panicked");
Ok(())
}

#[derive(Clone, Copy)]
pub enum Waveform {
Sine,
Square,
Saw,
Triangle,
}

pub struct Oscillator {
pub sample_rate: f32,
pub waveform: Waveform,
pub current_sample_index: f32,
pub frequency_hz: f32,
}

impl Oscillator {
pub fn new(sample_rate: f32, frequency_hz: f32, waveform: Waveform) -> Self {
Self {
sample_rate,
waveform,
current_sample_index: 0.0,
frequency_hz,
}
}

pub fn set_waveform(&mut self, waveform: Waveform) {
self.waveform = waveform;
}

pub fn tick(&mut self) -> f32 {
match self.waveform {
Waveform::Sine => self.sine_wave(),
Waveform::Square => self.square_wave(),
Waveform::Saw => self.saw_wave(),
Waveform::Triangle => self.triangle_wave(),
}
}

fn advance_sample(&mut self) {
self.current_sample_index = (self.current_sample_index + 1.0) % self.sample_rate;
}

fn calculate_sine_output(&self) -> f32 {
(self.current_sample_index * self.frequency_hz * 2.0 * PI / self.sample_rate).sin()
}

fn sine_wave(&mut self) -> f32 {
self.advance_sample();
self.calculate_sine_output()
}

fn square_wave(&mut self) -> f32 {
self.generative_waveform(2, 1.0)
}

fn saw_wave(&mut self) -> f32 {
self.generative_waveform(1, 1.0)
}

fn triangle_wave(&mut self) -> f32 {
self.generative_waveform(2, 2.0)
}

fn generative_waveform(&mut self, harmonic_step: i32, gain_factor: f32) -> f32 {
self.advance_sample();
let mut output = 0.0;
let mut harmonic = 1;
while self.frequency_hz * harmonic as f32 <= self.sample_rate / 2.0 {
let gain = 1.0 / (harmonic as f32).powf(gain_factor);
output += gain * self.calculate_sine_output();
harmonic += harmonic_step;
}
output
}
}

use cpal::{Sample, SampleFormat, SizedSample};

pub fn start_audio_stream(waveform: Waveform, frequency: f32) -> anyhow::Result<cpal::Stream> {
let (_host, device, config) = host_device_setup()?;
match config.sample_format() {
SampleFormat::F32 => create_stream::<f32>(&device, &config.into(), waveform, frequency),
_ => Err(anyhow::Error::msg("Unsupported sample format")),
}
}

pub fn start_audio_stream_arc(oscillator: Arc<Mutex<Oscillator>>) -> anyhow::Result<cpal::Stream> {
let (_host, device, config) = host_device_setup()?;
match config.sample_format() {
SampleFormat::F32 => create_stream_arc::<f32>(&device, &config.into(), oscillator),
_ => Err(anyhow::Error::msg("Unsupported sample format")),
}
}

fn host_device_setup(
) -> Result<(cpal::Host, cpal::Device, cpal::SupportedStreamConfig), anyhow::Error> {
let host = cpal::default_host();
let device = host
.default_output_device()
.ok_or_else(|| anyhow::Error::msg("No output device available"))?;
let config = device.default_output_config()?;
Ok((host, device, config))
}

pub fn create_stream_arc<T>(
device: &cpal::Device,
config: &cpal::StreamConfig,
oscillator: Arc<Mutex<Oscillator>>,
) -> anyhow::Result<cpal::Stream>
where
T: Sample + SizedSample + cpal::FromSample<f32>,
{
let num_channels = config.channels as usize;

let stream = device.build_output_stream(
config,
move |output: &mut [T], _| {
let mut osc = oscillator.lock().unwrap();
for frame in output.chunks_mut(num_channels) {
let sample_value: T = T::from_sample(osc.tick());
for sample in frame.iter_mut() {
*sample = sample_value;
}
}
},
|err| eprintln!("Error: {}", err),
None,
)?;

Ok(stream)
}

fn create_stream<T>(
device: &cpal::Device,
config: &cpal::StreamConfig,
waveform: Waveform,
frequency: f32,
) -> anyhow::Result<cpal::Stream>
where
T: Sample + SizedSample + cpal::FromSample<f32>,
{
let mut oscillator = Oscillator::new(config.sample_rate.0 as f32, frequency, waveform);
let num_channels = config.channels as usize;

let stream = device.build_output_stream(
config,
move |output: &mut [T], _| {
for frame in output.chunks_mut(num_channels) {
let sample_value: T = T::from_sample(oscillator.tick());
for sample in frame.iter_mut() {
*sample = sample_value;
}
}
},
|err| eprintln!("Error: {}", err),
None,
)?;

Ok(stream)
}

0 comments on commit 2a2c77c

Please sign in to comment.