///|
/// A self-describing waveform frame for streaming transport.
pub struct WaveformFrame {
sequence : Int
station : String
channel : String
start_time : Double
sample_rate : Double
data : Array[Double]
checksum : String
}
///|
/// Index metadata for a framed waveform bundle.
pub struct FrameIndexEntry {
sequence : Int
start_time : Double
end_time : Double
sample_count : Int
checksum : String
}
///|
/// A set of frames and its reconstruction diagnostics.
pub struct WaveformFrameBundle {
station : String
channel : String
sample_rate : Double
frames : Array[WaveformFrame]
total_samples : Int
complete : Bool
missing_sequences : Array[Int]
}
///|
/// Compute a deterministic checksum over quantized sample values.
pub fn waveform_checksum(data : Array[Double]) -> String {
let modulus = 2147483647
let mut state = 17
for value in data {
let quantized = (value * 1000000.0).round().to_int()
state = (state * 65599 + quantized) % modulus
if state < 0 {
state += modulus
}
}
state.to_string()
}
///|
/// Construct one frame and calculate its checksum.
pub fn make_waveform_frame(
sequence : Int,
station : String,
channel : String,
start_time : Double,
sample_rate : Double,
data : Array[Double],
) -> WaveformFrame {
{
sequence,
station,
channel,
start_time,
sample_rate,
data: data.copy(),
checksum: waveform_checksum(data),
}
}
///|
/// Split a trace into non-overlapping transport frames.
pub fn frame_trace(trace : Trace, frame_size : Int) -> Array[WaveformFrame] {
let result = Array::new()
if frame_size <= 0 || trace.data.is_empty() {
return result
}
let mut sequence = 0
let mut start = 0
while start < trace.data.length() {
let end = (start + frame_size).min(trace.data.length())
result.push(
make_waveform_frame(
sequence,
trace.station,
trace.channel,
trace.start_time + start.to_double() * trace.delta(),
trace.sampling_rate,
slice_samples(trace.data, start, end),
),
)
sequence += 1
start = end
}
result
}
///|
/// Split a trace with a fixed overlap for analysis or retransmission.
pub fn frame_trace_overlapping(
trace : Trace,
frame_size : Int,
overlap : Int,
) -> Array[WaveformFrame] {
let result = Array::new()
if frame_size <= 0 || trace.data.is_empty() {
return result
}
let step = (frame_size - overlap).max(1)
let starts = frame_start_indexes(trace.data.length(), frame_size, step, true)
for sequence = 0; sequence < starts.length(); sequence = sequence + 1 {
let start = starts[sequence]
let end = (start + frame_size).min(trace.data.length())
result.push(
make_waveform_frame(
sequence,
trace.station,
trace.channel,
trace.start_time + start.to_double() * trace.delta(),
trace.sampling_rate,
slice_samples(trace.data, start, end),
),
)
}
result
}
///|
/// Verify the checksum and metadata of a frame.
pub fn validate_waveform_frame(frame : WaveformFrame) -> Result[Unit, String] {
if frame.sequence < 0 {
return Err("frame sequence cannot be negative")
}
if frame.sample_rate <= 0.0 {
return Err("frame sample rate must be positive")
}
if waveform_checksum(frame.data) != frame.checksum {
return Err("frame checksum mismatch")
}
Ok(())
}
///|
/// Sort frames by sequence without changing frame values.
pub fn sort_waveform_frames(
frames : Array[WaveformFrame],
) -> Array[WaveformFrame] {
let result = frames.copy()
result.sort_by((left, right) => left.sequence - right.sequence)
result
}
///|
/// Return missing sequence numbers in a frame set.
pub fn missing_frame_sequences(frames : Array[WaveformFrame]) -> Array[Int] {
let sorted = sort_waveform_frames(frames)
let result = Array::new()
if sorted.is_empty() {
return result
}
let mut expected = 0
for frame in sorted {
while expected < frame.sequence {
result.push(expected)
expected += 1
}
if frame.sequence == expected {
expected += 1
}
}
result
}
///|
/// Build bundle metadata and identify missing frames.
pub fn make_frame_bundle(frames : Array[WaveformFrame]) -> WaveformFrameBundle {
let sorted = sort_waveform_frames(frames)
let missing = missing_frame_sequences(sorted)
let first = if sorted.is_empty() { None } else { Some(sorted[0]) }
let station = match first {
Some(frame) => frame.station
None => ""
}
let channel = match first {
Some(frame) => frame.channel
None => ""
}
let rate = match first {
Some(frame) => frame.sample_rate
None => 0.0
}
{
station,
channel,
sample_rate: rate,
frames: sorted,
total_samples: frames
.map(frame => frame.data.length())
.fold(init=0, (total, count) => total + count),
complete: missing.is_empty(),
missing_sequences: missing,
}
}
///|
/// Reassemble contiguous frames into a trace, rejecting gaps or mismatched metadata.
pub fn reassemble_frame_bundle(
bundle : WaveformFrameBundle,
) -> Result[Trace, String] {
if bundle.frames.is_empty() {
return Err("cannot reassemble an empty frame bundle")
}
if !bundle.missing_sequences.is_empty() {
return Err("frame bundle contains missing sequences")
}
let first = bundle.frames[0]
let data = Array::new(capacity=bundle.total_samples)
let mut expected_start = first.start_time
for frame in bundle.frames {
match validate_waveform_frame(frame) {
Ok(_) => ()
Err(message) => return Err(message)
}
if frame.station != bundle.station || frame.channel != bundle.channel {
return Err("frame metadata does not agree with bundle")
}
if (frame.start_time - expected_start).abs() >
1.0 / bundle.sample_rate * 0.01 {
return Err("frame timing contains a discontinuity")
}
for value in frame.data {
data.push(value)
}
expected_start += frame.data.length().to_double() / bundle.sample_rate
}
Ok(
Trace::new(
bundle.station,
bundle.channel,
bundle.sample_rate,
first.start_time,
data,
),
)
}
///|
/// Build index entries for fast frame browsing.
pub fn frame_index(frames : Array[WaveformFrame]) -> Array[FrameIndexEntry] {
let result = Array::new(capacity=frames.length())
for frame in sort_waveform_frames(frames) {
let end_time = if frame.data.is_empty() {
frame.start_time
} else {
frame.start_time +
(frame.data.length() - 1).to_double() / frame.sample_rate
}
result.push({
sequence: frame.sequence,
start_time: frame.start_time,
end_time,
sample_count: frame.data.length(),
checksum: frame.checksum,
})
}
result
}
///|
/// Serialize a frame in a readable transport format.
pub fn waveform_frame_to_text(frame : WaveformFrame) -> String {
let mut output = "WAVEFORM_FRAME\n" +
frame.sequence.to_string() +
"\n" +
frame.station +
"\n" +
frame.channel +
"\n" +
frame.start_time.to_string() +
"\n" +
frame.sample_rate.to_string() +
"\n" +
frame.checksum +
"\n"
for value in frame.data {
output = output + value.to_string() + "\n"
}
output
}
///|
/// Parse the readable frame format emitted by `waveform_frame_to_text`.
pub fn waveform_frame_from_text(text : String) -> Result[WaveformFrame, String] {
let lines = Array::new()
for line_view in text.split("\n") {
let line = line_view.trim().to_owned()
if line != "" {
lines.push(line)
}
}
if lines.length() < 7 || lines[0] != "WAVEFORM_FRAME" {
return Err("invalid waveform frame header")
}
let sequence = match parse_text_number(lines[1], 2) {
Ok(value) => value.to_int()
Err(_) => return Err("invalid frame sequence")
}
let start_time = match parse_text_number(lines[4], 5) {
Ok(value) => value
Err(_) => return Err("invalid frame start time")
}
let sample_rate = match parse_text_number(lines[5], 6) {
Ok(value) => value
Err(_) => return Err("invalid frame sample rate")
}
let data = Array::new()
for index = 7; index < lines.length(); index = index + 1 {
match parse_text_number(lines[index], index + 1) {
Ok(value) => data.push(value)
Err(_) => return Err("invalid frame sample")
}
}
let frame = {
sequence,
station: lines[2],
channel: lines[3],
start_time,
sample_rate,
data,
checksum: lines[6],
}
match validate_waveform_frame(frame) {
Ok(_) => Ok(frame)
Err(message) => Err(message)
}
}
///|
/// Serialize a frame index as a CSV table.
pub fn frame_index_to_csv(index : Array[FrameIndexEntry]) -> String {
let mut output = "sequence,start_time,end_time,sample_count,checksum\n"
for entry in index {
output = output +
entry.sequence.to_string() +
"," +
entry.start_time.to_string() +
"," +
entry.end_time.to_string() +
"," +
entry.sample_count.to_string() +
"," +
entry.checksum +
"\n"
}
output
}
///|
/// Return the physical duration represented by a frame.
pub fn frame_duration(frame : WaveformFrame) -> Double {
if frame.sample_rate <= 0.0 || frame.data.length() < 2 {
0.0
} else {
(frame.data.length() - 1).to_double() / frame.sample_rate
}
}
///|
/// Format bundle status for the streaming monitor.
pub fn frame_bundle_status(bundle : WaveformFrameBundle) -> String {
"station=" +
bundle.station +
" channel=" +
bundle.channel +
" frames=" +
bundle.frames.length().to_string() +
" samples=" +
bundle.total_samples.to_string() +
" complete=" +
bundle.complete.to_string() +
" missing=" +
bundle.missing_sequences.length().to_string()
}