1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
use crate::{audio_platform_cpal::AudioPlatformCpal, input_thread::UpdateSessionState};
use cpal::Stream;
use rusty_link::{AblLink, HostTimeFilter, SessionState};
use std::{
cmp::Ordering,
f32::consts::TAU,
sync::{Arc, Mutex, mpsc::Receiver},
time::Duration,
};
const LOW_TONE: f32 = 1108.73; // equals 'C#'
const HIGH_TONE: f32 = 1567.98; // equals 'G'
const CLICK_DURATION: u64 = 100; // In Milliseconds
/// Handles the SessionState in the Audio thread and the Metronome Sound Synth.
pub struct AudioEngine {
pub stream: Option<Stream>,
}
impl AudioEngine {
pub fn new(
link: Arc<AblLink>,
audio_cpal: AudioPlatformCpal,
input: Receiver<UpdateSessionState>,
quantum: Arc<Mutex<f64>>,
) -> Self {
// Introduce callback working variables:
let mut host_time_filter = HostTimeFilter::new();
let mut audio_session_state = SessionState::new();
let mut synth_clock: u64 = 0;
let mut time_at_last_click = Duration::from_secs(0);
let mut last_known_quantum = *quantum.lock().unwrap();
// Define Callback:
let engine_callback = move |buffer_size: usize,
sample_rate: u64,
output_latency: Duration,
sample_time: Duration,
sample_clock: u64| {
// Update time and other variables:
let invoke_time =
host_time_filter.sample_time_to_host_time(link.clock_micros(), sample_clock);
let invoke_time_as_duration = Duration::from_micros(invoke_time.try_into().unwrap());
let latency_compensated_time = invoke_time_as_duration + output_latency;
if let Ok(q) = quantum.try_lock() {
last_known_quantum = *q;
};
link.capture_audio_session_state(&mut audio_session_state);
// Commit SessionState changes sent by the Input Thread
if let Ok(command) = input.try_recv() {
match command {
UpdateSessionState::TempoPlus => {
audio_session_state
.set_tempo((audio_session_state.tempo() + 1.).min(999.), invoke_time);
link.commit_audio_session_state(&audio_session_state);
}
UpdateSessionState::TempoMinus => {
audio_session_state
.set_tempo((audio_session_state.tempo() - 1.).max(20.), invoke_time);
link.commit_audio_session_state(&audio_session_state);
}
UpdateSessionState::TogglePlaying => {
if audio_session_state.is_playing() {
audio_session_state.set_is_playing(false, invoke_time as i64);
} else {
audio_session_state.set_is_playing_and_request_beat_at_time(
true,
invoke_time as i64,
0.,
last_known_quantum,
);
}
link.commit_audio_session_state(&audio_session_state);
}
}
}
// Build latency compensated Sound Buffer
let mut buffer: Vec<f32> = Vec::with_capacity(buffer_size);
for sample in 0..buffer_size {
let mut y_amplitude: f32 = 0.; // A default sample is silent
if !audio_session_state.is_playing() {
buffer.push(y_amplitude);
continue;
}
// Compute the host time for this sample and the last.
let sample_host_time = latency_compensated_time + (sample_time * sample as u32);
let last_sample_host_time = sample_host_time - sample_time;
// Only make sound for positive beat magnitudes. Negative beat
// magnitudes are count-in beats.
if audio_session_state
.beat_at_time(sample_host_time.as_micros() as i64, last_known_quantum)
>= 0.
{
// If the phase wraps around between the last sample and the
// current one with respect to a 1 beat quantum, then a click
// should occur.
if audio_session_state.phase_at_time(sample_host_time.as_micros() as i64, 1.0)
< audio_session_state
.phase_at_time(last_sample_host_time.as_micros() as i64, 1.0)
{
time_at_last_click = sample_host_time; // reset last click trigger time
synth_clock = 0; // reset synth clock
}
let time_after_click = sample_host_time - time_at_last_click;
// If we're within the click duration of the last beat, render
// the click tone into this sample
if let Ordering::Less =
time_after_click.cmp(&Duration::from_millis(CLICK_DURATION))
{
// If the phase of the last beat with respect to the current
// quantum was zero, then it was at a quantum boundary and we
// want to use the high tone. For other beats within the
// quantum, use the low tone.
let freq = match audio_session_state
.phase_at_time(sample_host_time.as_micros() as i64, last_known_quantum)
.floor() as usize
{
0 => HIGH_TONE,
_ => LOW_TONE,
};
let x_time = synth_clock as f32 / sample_rate as f32;
// Simple cosine synth
y_amplitude =
(x_time * freq * TAU).cos() * (1. - (x_time * 2.5 * TAU).sin());
// Alternatively for fun, a simple sine synth ;)
// y_amplitude = (x_time * freq * TAU).sin();
synth_clock = (synth_clock + 1) % sample_rate;
}
}
buffer.push(y_amplitude);
}
buffer
};
// Build audio stream and start playback
let stream = audio_cpal.build_stream::<f32>(engine_callback);
Self {
stream: Some(stream),
}
}
}