///|
/// A bounded sample buffer for online waveform algorithms.
pub struct StreamingBuffer {
capacity : Int
data : Array[Double]
total_samples : Int
}
///|
/// Online first and second moments using Welford's method.
pub struct RunningMoments {
count : Int
mean : Double
m2 : Double
minimum : Double
maximum : Double
}
///|
/// Stateful STA/LTA trigger values for a streaming detector.
pub struct StreamingTriggerState {
sta : Double
lta : Double
sample_index : Int
triggered : Bool
trigger_start : Int
peak_ratio : Double
}
///|
/// Summary produced for one streaming block.
pub struct StreamingBlockSummary {
start_index : Int
end_index : Int
sample_count : Int
mean : Double
rms : Double
peak : Double
ratio_peak : Double
triggered : Bool
}
///|
/// Create an empty bounded buffer.
pub fn StreamingBuffer::new(capacity : Int) -> StreamingBuffer {
{ capacity: capacity.max(1), data: [], total_samples: 0 }
}
///|
/// Append a sample and retain only the newest capacity values.
pub fn StreamingBuffer::push(
self : StreamingBuffer,
value : Double,
) -> StreamingBuffer {
let data = self.data.copy()
data.push(value)
if data.length() > self.capacity {
ignore(data.remove(0))
}
{ capacity: self.capacity, data, total_samples: self.total_samples + 1 }
}
///|
/// Append a block of samples to a bounded buffer.
pub fn StreamingBuffer::push_block(
self : StreamingBuffer,
values : Array[Double],
) -> StreamingBuffer {
let mut result = self
for value in values {
result = result.push(value)
}
result
}
///|
/// Return the latest buffer values.
pub fn StreamingBuffer::values(self : StreamingBuffer) -> Array[Double] {
self.data.copy()
}
///|
/// Return an empty online moment accumulator.
pub fn RunningMoments::new() -> RunningMoments {
{ count: 0, mean: 0.0, m2: 0.0, minimum: 0.0, maximum: 0.0 }
}
///|
/// Update moments with one value.
pub fn RunningMoments::update(
self : RunningMoments,
value : Double,
) -> RunningMoments {
let count = self.count + 1
let difference = value - self.mean
let mean = self.mean + difference / count.to_double()
let m2 = self.m2 + difference * (value - mean)
{
count,
mean,
m2,
minimum: if self.count == 0 {
value
} else {
self.minimum.min(value)
},
maximum: if self.count == 0 {
value
} else {
self.maximum.max(value)
},
}
}
///|
/// Update moments with a complete block.
pub fn RunningMoments::update_block(
self : RunningMoments,
values : Array[Double],
) -> RunningMoments {
let mut result = self
for value in values {
result = result.update(value)
}
result
}
///|
/// Return online population variance.
pub fn RunningMoments::variance(self : RunningMoments) -> Double {
if self.count == 0 {
0.0
} else {
self.m2 / self.count.to_double()
}
}
///|
/// Return online standard deviation.
pub fn RunningMoments::standard_deviation(self : RunningMoments) -> Double {
self.variance().sqrt()
}
///|
/// Initialize an online STA/LTA trigger.
pub fn StreamingTriggerState::new() -> StreamingTriggerState {
{
sta: 0.0,
lta: 0.0,
sample_index: 0,
triggered: false,
trigger_start: -1,
peak_ratio: 0.0,
}
}
///|
/// Advance a streaming STA/LTA state by one absolute sample.
pub fn StreamingTriggerState::update(
self : StreamingTriggerState,
value : Double,
sta_samples : Int,
lta_samples : Int,
onset_threshold : Double,
offset_threshold : Double,
) -> StreamingTriggerState {
let sta_decay = @math.exp(-1.0 / sta_samples.max(1).to_double())
let lta_decay = @math.exp(
-1.0 / lta_samples.max(sta_samples.max(1)).to_double(),
)
let amplitude = value.abs()
let sta = sta_decay * self.sta + (1.0 - sta_decay) * amplitude
let lta = lta_decay * self.lta + (1.0 - lta_decay) * amplitude
let ratio = if lta <= 0.0 { 1.0 } else { sta / lta }
let triggered = if self.triggered {
ratio >= offset_threshold
} else {
ratio >= onset_threshold
}
{
sta,
lta,
sample_index: self.sample_index + 1,
triggered,
trigger_start: if !self.triggered && triggered {
self.sample_index
} else {
self.trigger_start
},
peak_ratio: self.peak_ratio.max(ratio),
}
}
///|
/// Consume a block and return the final state plus a block summary.
pub fn process_streaming_block(
state : StreamingTriggerState,
values : Array[Double],
sta_samples : Int,
lta_samples : Int,
onset_threshold : Double,
offset_threshold : Double,
) -> (StreamingTriggerState, StreamingBlockSummary) {
let mut current = state
let start = state.sample_index
for value in values {
current = current.update(
value, sta_samples, lta_samples, onset_threshold, offset_threshold,
)
}
(
current,
{
start_index: start,
end_index: current.sample_index,
sample_count: values.length(),
mean: mean_samples(values),
rms: rms_samples(values),
peak: absolute_peak(values),
ratio_peak: current.peak_ratio,
triggered: current.triggered,
},
)
}
///|
/// Compute a rolling RMS stream with a bounded history.
pub fn rolling_rms_stream(
values : Array[Double],
window : Int,
) -> Array[Double] {
let result = Array::make(values.length(), 0.0)
let size = window.max(1)
let mut energy = 0.0
for index = 0; index < values.length(); index = index + 1 {
energy += values[index] * values[index]
if index >= size {
energy -= values[index - size] * values[index - size]
}
let count = (index + 1).min(size)
result[index] = (energy / count.to_double()).sqrt()
}
result
}
///|
/// Compute a rolling mean stream using a prefix accumulator.
pub fn rolling_mean_stream(
values : Array[Double],
window : Int,
) -> Array[Double] {
let result = Array::make(values.length(), 0.0)
let size = window.max(1)
let mut total = 0.0
for index = 0; index < values.length(); index = index + 1 {
total += values[index]
if index >= size {
total -= values[index - size]
}
result[index] = total / (index + 1).min(size).to_double()
}
result
}
///|
/// Return a block summary for every complete block.
pub fn summarize_stream_blocks(
values : Array[Double],
block_size : Int,
) -> Array[StreamingBlockSummary] {
let result = Array::new()
if block_size <= 0 {
return result
}
let mut start = 0
while start < values.length() {
let end = (start + block_size).min(values.length())
let block = slice_samples(values, start, end)
result.push({
start_index: start,
end_index: end,
sample_count: block.length(),
mean: mean_samples(block),
rms: rms_samples(block),
peak: absolute_peak(block),
ratio_peak: 0.0,
triggered: false,
})
start = end
}
result
}
///|
/// Calculate a block-level drift from online moments.
pub fn running_drift(values : Array[Double], block_size : Int) -> Array[Double] {
let result = Array::new()
if block_size <= 0 {
return result
}
let blocks = summarize_stream_blocks(values, block_size)
if blocks.length() < 2 {
return result
}
for index = 1; index < blocks.length(); index = index + 1 {
result.push(blocks[index].mean - blocks[index - 1].mean)
}
result
}
///|
/// Return the fraction of blocks whose RMS exceeds a threshold.
pub fn active_block_fraction(
values : Array[Double],
block_size : Int,
threshold : Double,
) -> Double {
let blocks = summarize_stream_blocks(values, block_size)
if blocks.is_empty() {
0.0
} else {
blocks.filter(block => block.rms >= threshold).length().to_double() /
blocks.length().to_double()
}
}
///|
/// Format an online moment record.
pub fn running_moments_text(moments : RunningMoments) -> String {
"count=" +
moments.count.to_string() +
" mean=" +
moments.mean.to_string() +
" variance=" +
moments.variance().to_string() +
" minimum=" +
moments.minimum.to_string() +
" maximum=" +
moments.maximum.to_string()
}
///|
/// Format a streaming trigger state.
pub fn streaming_trigger_text(state : StreamingTriggerState) -> String {
"index=" +
state.sample_index.to_string() +
" sta=" +
state.sta.to_string() +
" lta=" +
state.lta.to_string() +
" triggered=" +
state.triggered.to_string() +
" start=" +
state.trigger_start.to_string() +
" peak_ratio=" +
state.peak_ratio.to_string()
}