Compare commits
8 Commits
main
...
monolith-a
| Author | SHA1 | Date | |
|---|---|---|---|
|
824939672b
|
|||
|
2ebf5d1ea2
|
|||
|
a827afd872
|
|||
|
535727268d
|
|||
|
29181f9c76
|
|||
|
0a043e10f1
|
|||
|
6bdb38bcee
|
|||
|
|
49e4f05b5e |
1400
Cargo.lock
generated
1400
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
17
Cargo.toml
17
Cargo.toml
@@ -4,3 +4,20 @@ version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
clap = { version = "4.5.53", features = ["derive"] }
|
||||
jack = { version = "0.13.3" }
|
||||
pulseaudio = { version = "0.3.1", optional = true}
|
||||
alsa = { version = "0.10.0", optional = true}
|
||||
symphonia = { version = "0.5.5", features = ["mp3"] }
|
||||
coreaudio-rs = { version = "0.13.0", optional = true }
|
||||
cpal = { version = "0.16.0", optional = true }
|
||||
ringbuf = "0.4.8"
|
||||
crossbeam = { version = "0.8.4", features = ["crossbeam-channel"] }
|
||||
crossbeam-channel = "0.5.15"
|
||||
anyhow = "1.0.100"
|
||||
|
||||
[features]
|
||||
pulseaudio = ["dep:pulseaudio"]
|
||||
alsa = ["dep:alsa"]
|
||||
coreaudio = ["dep:coreaudio-rs"]
|
||||
cpal = ["dep:cpal"]
|
||||
|
||||
214
src/main.rs
214
src/main.rs
@@ -1,3 +1,213 @@
|
||||
fn main() {
|
||||
println!("Hello, world!");
|
||||
//! Sine wave generator with frequency configuration exposed through standard
|
||||
//! input.
|
||||
|
||||
use crossbeam_channel::bounded;
|
||||
use jack::{AsyncClient, AudioOut, Client, ProcessHandler, ProcessScope};
|
||||
use ringbuf::traits::{Consumer, Producer, Split};
|
||||
use ringbuf::HeapRb;
|
||||
use std::fs::File;
|
||||
use std::str::FromStr;
|
||||
use std::thread::{self, sleep};
|
||||
use std::time::Duration;
|
||||
use std::{env, io};
|
||||
use symphonia::core::audio::{AudioBufferRef, SampleBuffer, Signal};
|
||||
use symphonia::core::codecs::DecoderOptions;
|
||||
use symphonia::core::formats::FormatOptions;
|
||||
use symphonia::core::io::MediaSourceStream;
|
||||
use symphonia::core::meta::MetadataOptions;
|
||||
use symphonia::core::probe::Hint;
|
||||
use symphonia::core::units::Time;
|
||||
const RB_SIZE: usize = 48000 * 2; // ~1 second mono
|
||||
//
|
||||
//
|
||||
fn start_decoder_thread(
|
||||
mut looper: AudioFileLooper,
|
||||
mut producer: impl ringbuf::traits::Producer<Item = f32> + std::marker::Send + 'static,
|
||||
) {
|
||||
thread::spawn(move || {
|
||||
loop {
|
||||
let sample = looper.next_sample();
|
||||
let _ = producer.try_push(sample);
|
||||
// Drop if buffer is full (never block)
|
||||
}
|
||||
});
|
||||
}
|
||||
//
|
||||
//
|
||||
|
||||
struct JackHandler<C>
|
||||
where
|
||||
C: Consumer<Item = f32>,
|
||||
{
|
||||
out: jack::Port<jack::AudioOut>,
|
||||
consumer: C,
|
||||
}
|
||||
|
||||
impl<C> ProcessHandler for JackHandler<C>
|
||||
where
|
||||
C: Consumer<Item = f32> + std::marker::Send,
|
||||
{
|
||||
fn process(&mut self, _: &Client, ps: &ProcessScope) -> jack::Control {
|
||||
let buffer = self.out.as_mut_slice(ps);
|
||||
|
||||
for sample in buffer.iter_mut() {
|
||||
*sample = self.consumer.try_pop().unwrap_or(0.0);
|
||||
}
|
||||
|
||||
jack::Control::Continue
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let path = env::args()
|
||||
.nth(1)
|
||||
.expect("Usage: cargo run -- <audio_file>");
|
||||
let mut looper = AudioFileLooper::open(&path).expect("Brah");
|
||||
// Ring buffer
|
||||
let rb = HeapRb::<f32>::new(RB_SIZE);
|
||||
let (producer, consumer) = rb.split();
|
||||
|
||||
// Start decoder thread
|
||||
start_decoder_thread(looper, producer);
|
||||
// 1. open a client
|
||||
let (client, _status) =
|
||||
jack::Client::new("rust_jack_sine", jack::ClientOptions::default()).unwrap();
|
||||
|
||||
// 2. register port
|
||||
let out = client
|
||||
.register_port("sine_out", jack::AudioOut::default())
|
||||
.unwrap();
|
||||
|
||||
let handler = JackHandler { out, consumer };
|
||||
let active_client = {
|
||||
let notification_handler = ();
|
||||
AsyncClient::new(client, notification_handler, handler)
|
||||
}
|
||||
.expect("bvrah");
|
||||
|
||||
println!("Running… press Enter to quit");
|
||||
let mut input = String::new();
|
||||
std::io::stdin().read_line(&mut input).unwrap();
|
||||
// 4. Activate the client. Also connect the ports to the system audio.
|
||||
// 6. Optional deactivate. Not required since active_client will deactivate on
|
||||
// drop, though explicit deactivate may help you identify errors in
|
||||
// deactivate.
|
||||
if let Err(err) = active_client.deactivate() {
|
||||
eprintln!("JACK exited with error: {err}");
|
||||
};
|
||||
}
|
||||
|
||||
/// Attempt to read a frequency from standard in. Will block until there is
|
||||
/// user input. `None` is returned if there was an error reading from standard
|
||||
/// in, or the retrieved string wasn't a compatible u16 integer.
|
||||
fn read_freq() -> Option<f64> {
|
||||
let mut user_input = String::new();
|
||||
match io::stdin().read_line(&mut user_input) {
|
||||
Ok(_) => u16::from_str(user_input.trim()).ok().map(|n| n as f64),
|
||||
Err(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub struct AudioFileLooper {
|
||||
format: Box<dyn symphonia::core::formats::FormatReader>,
|
||||
decoder: Box<dyn symphonia::core::codecs::Decoder>,
|
||||
track_id: u32,
|
||||
|
||||
sample_buf: Vec<f32>,
|
||||
sample_pos: usize,
|
||||
}
|
||||
impl AudioFileLooper {
|
||||
pub fn open(path: &str) -> Result<Self, symphonia::core::errors::Error> {
|
||||
let file = File::open(path)?;
|
||||
let mss = MediaSourceStream::new(Box::new(file), Default::default());
|
||||
|
||||
let mut hint = Hint::new();
|
||||
if let Some(ext) = path.split('.').last() {
|
||||
hint.with_extension(ext);
|
||||
}
|
||||
|
||||
let format = symphonia::default::get_probe()
|
||||
.format(
|
||||
&hint,
|
||||
mss,
|
||||
&FormatOptions::default(),
|
||||
&MetadataOptions::default(),
|
||||
)?
|
||||
.format;
|
||||
|
||||
let track = format
|
||||
.tracks()
|
||||
.iter()
|
||||
.find(|t| t.codec_params.sample_rate.is_some())
|
||||
.unwrap();
|
||||
let track_id = track.id;
|
||||
|
||||
let decoder = symphonia::default::get_codecs()
|
||||
.make(&track.codec_params, &DecoderOptions::default())?;
|
||||
|
||||
Ok(Self {
|
||||
format,
|
||||
decoder,
|
||||
track_id: track_id,
|
||||
sample_buf: Vec::new(),
|
||||
sample_pos: 0,
|
||||
})
|
||||
}
|
||||
fn refill_samples(&mut self) -> Result<(), symphonia::core::errors::Error> {
|
||||
self.sample_buf.clear();
|
||||
self.sample_pos = 0;
|
||||
|
||||
loop {
|
||||
match self.format.next_packet() {
|
||||
Ok(packet) => {
|
||||
if packet.track_id() != self.track_id {
|
||||
continue;
|
||||
}
|
||||
|
||||
let decoded = self.decoder.decode(&packet)?;
|
||||
|
||||
match decoded {
|
||||
AudioBufferRef::F32(buf) => {
|
||||
self.sample_buf.extend_from_slice(buf.chan(0));
|
||||
}
|
||||
|
||||
_ => {
|
||||
let mut sample_buffer = SampleBuffer::<f32>::new(
|
||||
decoded.capacity() as u64,
|
||||
*decoded.spec(),
|
||||
);
|
||||
sample_buffer.copy_interleaved_ref(decoded);
|
||||
self.sample_buf.extend_from_slice(sample_buffer.samples());
|
||||
}
|
||||
}
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
Err(symphonia::core::errors::Error::IoError(_)) => {
|
||||
// EOF → loop
|
||||
self.format.seek(
|
||||
symphonia::core::formats::SeekMode::Accurate,
|
||||
symphonia::core::formats::SeekTo::Time {
|
||||
time: Time::new(0, 0.0),
|
||||
track_id: Some(self.track_id),
|
||||
},
|
||||
)?;
|
||||
}
|
||||
|
||||
Err(e) => return Err(e),
|
||||
}
|
||||
}
|
||||
}
|
||||
pub fn next_sample(&mut self) -> f32 {
|
||||
if self.sample_pos >= self.sample_buf.len() {
|
||||
if self.refill_samples().is_err() {
|
||||
return 0.0;
|
||||
}
|
||||
}
|
||||
|
||||
let sample = self.sample_buf[self.sample_pos];
|
||||
self.sample_pos += 1;
|
||||
sample
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user