binaural_beat_generator_cli/modules/
bb_generator.rs

1//! A module that contains the bulk of the code that allows the program to run.
2
3use anyhow::Error;
4use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
5use std::sync::{Arc, Mutex};
6use std::thread;
7use std::time::{Duration as StdDuration, Instant}; // Alias to avoid conflict with enum variant
8
9//Cancellation support
10use std::sync::atomic::{AtomicBool, Ordering};
11
12use crate::modules::duration::duration_common::ToMinutes;
13use crate::modules::frequency::frequency_common::ToFrequency;
14use crate::modules::preset::BinauralPresetGroup;
15
16/// A function that wats for the chosen time limit to end before exiting.
17/// The function will constantly check if the user wants to stop running of the program.
18///
19fn wait_until_end(cancel_token: Arc<AtomicBool>, duration_minutes: u32) {
20    let total_duration = StdDuration::from_secs((duration_minutes * 60) as u64);
21    let start_time = Instant::now();
22
23    while start_time.elapsed() < total_duration {
24        // Break the loop immediately if the user requested cancellation
25        if cancel_token.load(Ordering::Relaxed) {
26            println!("Playback cancelled by user.");
27            break;
28        }
29        // Sleep for a short period to avoid high CPU usage
30        thread::sleep(StdDuration::from_millis(500));
31    }
32}
33
34/// Generates and plays binaural beat tones based on specified carrier frequency,
35/// beat frequency, and duration.
36///
37/// # Arguments
38/// - `preset_options`: Specifies the binaural beat options choosen by the user to execute.
39/// - `cancel_token`: An atomic instance of a boolean that controls the stopping of the program before the timelimit.
40///
41/// # Returns
42/// `Result<(), anyhow::Error>` indicating success or failure.
43pub fn generate_binaural_beats(
44    preset_options: BinauralPresetGroup,
45    cancel_token: Arc<AtomicBool>,
46) -> Result<(), Error> {
47    // Extract concrete values from generic parameters
48    let carrier_hz = preset_options.carrier.to_hz();
49    let beat_hz = preset_options.beat.to_hz();
50    let duration_minutes = preset_options.duration.to_minutes();
51
52    // Calculate left and right ear frequencies
53    let f_left = carrier_hz - (beat_hz / 2.0);
54    let f_right = carrier_hz + (beat_hz / 2.0);
55
56    // Basic validation for frequencies
57    if f_left <= 0.0 || f_right <= 0.0 {
58        return Err(anyhow::anyhow!(
59            "Calculated frequency for one ear is zero or negative. Adjust carrier or beat frequency."
60        ));
61    }
62    if duration_minutes == 0 {
63        return Err(anyhow::anyhow!(
64            "Duration must be greater than zero minutes."
65        ));
66    }
67
68    println!("--- Binaural Beat Settings ---");
69    println!("Preset {}", preset_options.preset);
70    println!("Carrier Frequency: {:.2} Hz", carrier_hz);
71    println!("Beat Frequency: {:.2} Hz", beat_hz);
72    println!("Left Ear Frequency: {:.2} Hz", f_left);
73    println!("Right Ear Frequency: {:.2} Hz", f_right);
74    println!("Duration: {} minutes", duration_minutes);
75    println!("----------------------------");
76
77    let host = cpal::default_host();
78
79    let device = host
80        .default_output_device()
81        .ok_or_else(|| anyhow::anyhow!("No output device available."))?;
82
83    let config = device.default_output_config()?;
84
85    let sample_rate_val = config.sample_rate().0 as f64;
86    let channels_val = config.channels() as usize;
87
88    let sample_clock_left = Arc::new(Mutex::new(0f64));
89    let sample_clock_right = Arc::new(Mutex::new(0f64));
90
91    let sample_clock_left_for_closure = Arc::clone(&sample_clock_left);
92    let sample_clock_right_for_closure = Arc::clone(&sample_clock_right);
93    let stream_cancel_token = Arc::clone(&cancel_token); // Clone for the stream closure
94
95    let stream = device.build_output_stream(
96        &config.clone().into(), // Clone config for the stream builder
97        move |data: &mut [f32], _: &cpal::OutputCallbackInfo| {
98            // Check the token's state inside the audio loop
99            if stream_cancel_token.load(Ordering::Relaxed) {
100                // If the token is true, fill the buffer with silence and return
101                for frame in data.chunks_mut(channels_val) {
102                    if channels_val == 2 {
103                        frame[0] = 0.0;
104                        frame[1] = 0.0;
105                    } else {
106                        frame[0] = 0.0;
107                    }
108                }
109                return;
110            }
111
112            let mut current_sample_clock_left = sample_clock_left_for_closure.lock().unwrap();
113            let mut current_sample_clock_right = sample_clock_right_for_closure.lock().unwrap();
114
115            for frame in data.chunks_mut(channels_val) {
116                //Always keep the final sample outputs as f32 but make the calculations using f64 so that we don't lose the signal.
117                let left_sample =
118                    ((2.0 * std::f64::consts::PI * f_left as f64 * *current_sample_clock_left
119                        / sample_rate_val)
120                        .sin()) as f32;
121                *current_sample_clock_left += 1.0;
122
123                let right_sample =
124                    ((2.0 * std::f64::consts::PI * f_right as f64 * *current_sample_clock_right
125                        / sample_rate_val)
126                        .sin()) as f32;
127                *current_sample_clock_right += 1.0;
128
129                if channels_val == 2 {
130                    frame[0] = left_sample * 0.5; // Reduce amplitude to avoid clipping
131                    frame[1] = right_sample * 0.5;
132                } else {
133                    frame[0] = (left_sample + right_sample) * 0.25; // For mono, sum and reduce further
134                }
135            }
136        },
137        |err| eprintln!("An error occurred on stream: {}", err),
138        None,
139    )?;
140
141    stream.play()?;
142
143    // The main thread now waits for EITHER the timer to expire OR the cancel token to be set.
144    wait_until_end(cancel_token, duration_minutes);
145
146    Ok(())
147}