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
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
//! Meter ballistics for realistic meter behavior.
//!
//! Implements various ballistic standards for audio meters including
//! VU meters, PPM meters (EBU, BBC, DIN), and custom attack/release characteristics.
/// Ballistic type for meter movement.
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum BallisticType {
/// VU meter: 300ms integration time to reach 99% of full scale.
Vu,
/// PPM EBU: 10ms integration, 20dB/1.5s return.
PpmEbu,
/// PPM BBC: 10ms integration, 24dB/2.8s return.
PpmBbc,
/// PPM DIN: 10ms integration, 20dB/1.5s return (similar to EBU).
PpmDin,
/// Fast peak: 1-2 sample attack, fast release.
FastPeak,
/// Custom ballistics.
Custom {
/// Attack time constant in seconds.
attack_time: f64,
/// Release time constant in seconds.
release_time: f64,
},
}
impl BallisticType {
/// Get attack time constant in seconds.
pub fn attack_time(&self) -> f64 {
match self {
Self::Vu => 0.3, // 300ms integration
Self::PpmEbu | Self::PpmDin => 0.01, // 10ms integration
Self::PpmBbc => 0.01, // 10ms integration
Self::FastPeak => 0.000_02, // ~1 sample at 48kHz
Self::Custom { attack_time, .. } => *attack_time,
}
}
/// Get release time constant in seconds.
pub fn release_time(&self) -> f64 {
match self {
Self::Vu => 0.3, // Same as attack for VU
Self::PpmEbu | Self::PpmDin => 1.5, // 20dB/1.5s
Self::PpmBbc => 2.8, // 24dB/2.8s
Self::FastPeak => 0.1, // Fast release
Self::Custom { release_time, .. } => *release_time,
}
}
}
/// Ballistic processor for applying attack/release characteristics to meter values.
pub struct BallisticProcessor {
ballistic_type: BallisticType,
sample_rate: f64,
attack_coeff: f64,
release_coeff: f64,
current_value: f64,
peak_hold_time: f64,
peak_hold_samples: usize,
peak_hold_counter: usize,
peak_hold_value: f64,
}
impl BallisticProcessor {
/// Create a new ballistic processor.
///
/// # Arguments
///
/// * `ballistic_type` - Type of ballistics to apply
/// * `sample_rate` - Sample rate in Hz
/// * `peak_hold_time` - Peak hold time in seconds (0.0 for no hold)
pub fn new(ballistic_type: BallisticType, sample_rate: f64, peak_hold_time: f64) -> Self {
let attack_time = ballistic_type.attack_time();
let release_time = ballistic_type.release_time();
// Calculate attack/release coefficients for exponential smoothing
// coeff = exp(-1 / (time_constant * sample_rate))
let attack_coeff = (-1.0 / (attack_time * sample_rate)).exp();
let release_coeff = (-1.0 / (release_time * sample_rate)).exp();
let peak_hold_samples = (peak_hold_time * sample_rate) as usize;
Self {
ballistic_type,
sample_rate,
attack_coeff,
release_coeff,
current_value: 0.0,
peak_hold_time,
peak_hold_samples,
peak_hold_counter: 0,
peak_hold_value: 0.0,
}
}
/// Process a single input value and apply ballistics.
///
/// # Arguments
///
/// * `input` - Input value (linear scale)
///
/// # Returns
///
/// Ballistically filtered value
pub fn process(&mut self, input: f64) -> f64 {
// Apply attack or release based on whether input is rising or falling
if input > self.current_value {
// Attack: input is rising
self.current_value = input + self.attack_coeff * (self.current_value - input);
} else {
// Release: input is falling
self.current_value = input + self.release_coeff * (self.current_value - input);
}
// Update peak hold
if input > self.peak_hold_value {
self.peak_hold_value = input;
self.peak_hold_counter = self.peak_hold_samples;
} else if self.peak_hold_counter > 0 {
self.peak_hold_counter -= 1;
} else {
self.peak_hold_value = self.current_value;
}
self.current_value
}
/// Get the current ballistically filtered value.
pub fn current_value(&self) -> f64 {
self.current_value
}
/// Get the peak hold value.
pub fn peak_hold_value(&self) -> f64 {
self.peak_hold_value
}
/// Reset the processor to initial state.
pub fn reset(&mut self) {
self.current_value = 0.0;
self.peak_hold_counter = 0;
self.peak_hold_value = 0.0;
}
/// Get the ballistic type.
pub fn ballistic_type(&self) -> BallisticType {
self.ballistic_type
}
}
/// Multi-channel ballistic processor.
pub struct MultiChannelBallistics {
channels: Vec<BallisticProcessor>,
}
impl MultiChannelBallistics {
/// Create a new multi-channel ballistic processor.
///
/// # Arguments
///
/// * `num_channels` - Number of channels
/// * `ballistic_type` - Type of ballistics to apply
/// * `sample_rate` - Sample rate in Hz
/// * `peak_hold_time` - Peak hold time in seconds
pub fn new(
num_channels: usize,
ballistic_type: BallisticType,
sample_rate: f64,
peak_hold_time: f64,
) -> Self {
let channels = (0..num_channels)
.map(|_| BallisticProcessor::new(ballistic_type, sample_rate, peak_hold_time))
.collect();
Self { channels }
}
/// Process samples for all channels.
///
/// # Arguments
///
/// * `inputs` - Input values for each channel (linear scale)
///
/// # Returns
///
/// Ballistically filtered values for each channel
pub fn process(&mut self, inputs: &[f64]) -> Vec<f64> {
inputs
.iter()
.zip(&mut self.channels)
.map(|(&input, processor)| processor.process(input))
.collect()
}
/// Get current values for all channels.
pub fn current_values(&self) -> Vec<f64> {
self.channels
.iter()
.map(BallisticProcessor::current_value)
.collect()
}
/// Get peak hold values for all channels.
pub fn peak_hold_values(&self) -> Vec<f64> {
self.channels
.iter()
.map(BallisticProcessor::peak_hold_value)
.collect()
}
/// Reset all channels.
pub fn reset(&mut self) {
for channel in &mut self.channels {
channel.reset();
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_ballistic_attack() {
let mut ballistics = BallisticProcessor::new(BallisticType::FastPeak, 48000.0, 0.0);
// Apply step input
let result = ballistics.process(1.0);
// Should rise quickly but not instantly
assert!(result > 0.5);
assert!(result < 1.0);
}
#[test]
fn test_ballistic_release() {
let mut ballistics = BallisticProcessor::new(BallisticType::FastPeak, 48000.0, 0.0);
// Rise to 1.0
for _ in 0..1000 {
ballistics.process(1.0);
}
// Then fall
let result = ballistics.process(0.0);
// Should fall but not instantly
assert!(result > 0.0);
assert!(result < 1.0);
}
#[test]
fn test_peak_hold() {
let mut ballistics = BallisticProcessor::new(
BallisticType::FastPeak,
48000.0,
1.0, // 1 second hold
);
ballistics.process(1.0);
// Peak hold should maintain value
for _ in 0..100 {
ballistics.process(0.0);
}
assert_eq!(ballistics.peak_hold_value(), 1.0);
}
#[test]
fn test_vu_ballistics() {
let ballistics = BallisticProcessor::new(BallisticType::Vu, 48000.0, 0.0);
assert_eq!(ballistics.ballistic_type().attack_time(), 0.3);
assert_eq!(ballistics.ballistic_type().release_time(), 0.3);
}
#[test]
fn test_ppm_ebu_ballistics() {
let ballistics = BallisticProcessor::new(BallisticType::PpmEbu, 48000.0, 0.0);
assert_eq!(ballistics.ballistic_type().attack_time(), 0.01);
assert_eq!(ballistics.ballistic_type().release_time(), 1.5);
}
#[test]
fn test_multi_channel_ballistics() {
let mut ballistics = MultiChannelBallistics::new(2, BallisticType::FastPeak, 48000.0, 0.0);
let inputs = vec![0.5, 1.0];
let outputs = ballistics.process(&inputs);
assert_eq!(outputs.len(), 2);
assert!(outputs[0] > 0.0);
assert!(outputs[1] > 0.0);
}
}