///|
/// Parsed fields of a fixed-section miniSEED data header.
pub struct MiniSeedHeader {
sequence : String
station : String
location : String
channel : String
network : String
year : Int
julian_day : Int
hour : Int
minute : Int
second : Int
fractional_seconds : Int
sample_count : Int
sample_rate : Double
data_offset : Int
}
///|
/// Convert miniSEED factor/multiplier pairs to a positive sample rate.
pub fn decode_miniseed_rate(factor : Int, multiplier : Int) -> Double {
if factor > 0 && multiplier > 0 {
factor.to_double() * multiplier.to_double()
} else if factor > 0 && multiplier < 0 {
factor.to_double() / (-multiplier).to_double()
} else if factor < 0 && multiplier > 0 {
(-factor).to_double() / multiplier.to_double()
} else if factor < 0 && multiplier < 0 {
1.0 / ((-factor).to_double() * (-multiplier).to_double())
} else {
0.0
}
}
///|
/// Encode a positive sample rate as a compact factor/multiplier pair.
pub fn encode_miniseed_rate(rate : Double) -> (Int, Int) {
if rate <= 0.0 {
return (0, 0)
}
let rounded = rate.round()
if (rounded - rate).abs() < 0.0000001 && rounded <= 32767.0 {
(rounded.to_int(), 1)
} else {
let factor = (rate * 10000.0).round().to_int()
(factor, -10000)
}
}
///|
/// Parse a miniSEED fixed-section header without decoding samples.
pub fn parse_miniseed_header(bytes : Bytes) -> Result[MiniSeedHeader, String] {
if bytes.length() < 48 {
return Err("miniSEED header requires at least 48 bytes")
}
let factor = read_int16_be(bytes, 32)
let multiplier = read_int16_be(bytes, 34)
let data_offset = read_uint16_be(bytes, 44)
let sample_count = read_uint16_be(bytes, 30)
if data_offset < 48 || data_offset > bytes.length() {
return Err("miniSEED data offset is outside the record")
}
Ok({
sequence: read_string(bytes, 0, 6),
station: read_string(bytes, 8, 5),
location: read_string(bytes, 13, 2),
channel: read_string(bytes, 15, 3),
network: read_string(bytes, 18, 2),
year: read_uint16_be(bytes, 20),
julian_day: read_uint16_be(bytes, 22),
hour: bytes[24].to_int(),
minute: bytes[25].to_int(),
second: bytes[26].to_int(),
fractional_seconds: read_uint16_be(bytes, 28),
sample_count,
sample_rate: decode_miniseed_rate(factor, multiplier),
data_offset,
})
}
///|
/// Validate a fixed-width uncompressed miniSEED record.
pub fn validate_miniseed_record(
bytes : Bytes,
) -> Result[MiniSeedHeader, String] {
let header = match parse_miniseed_header(bytes) {
Ok(value) => value
Err(message) => return Err(message)
}
let expected = header.data_offset + header.sample_count * 4
if expected > bytes.length() {
Err("miniSEED record is truncated for its sample count")
} else {
Ok(header)
}
}
///|
/// Decode all uncompressed float or integer samples from a miniSEED record.
pub fn miniseed_samples(
bytes : Bytes,
is_float : Bool,
) -> Result[Array[Double], String] {
let header = match validate_miniseed_record(bytes) {
Ok(value) => value
Err(message) => return Err(message)
}
let result = Array::new(capacity=header.sample_count)
for index = 0; index < header.sample_count; index = index + 1 {
let offset = header.data_offset + index * 4
let value = if is_float {
read_float32(bytes, offset, true).to_double()
} else {
read_int32(bytes, offset, true).to_double()
}
result.push(value)
}
Ok(result)
}
///|
/// Return the nominal record size rounded to a power-of-two frame.
pub fn miniseed_record_size(sample_count : Int, data_offset : Int) -> Int {
let count = if sample_count < 0 { 0 } else { sample_count }
let offset = if data_offset < 48 { 48 } else { data_offset }
let needed = offset + count * 4
if needed % 256 == 0 {
needed
} else {
(needed / 256 + 1) * 256
}
}
///|
/// Convert a Julian day and clock fields to seconds from the year start.
pub fn miniseed_seconds_from_year(
julian_day : Int,
hour : Int,
minute : Int,
second : Int,
fractional_100us : Int,
) -> Double {
let day = if julian_day < 1 { 1 } else { julian_day }
(day - 1).to_double() * 86400.0 +
hour.to_double() * 3600.0 +
minute.to_double() * 60.0 +
second.to_double() +
fractional_100us.to_double() / 10000.0
}
///|
/// Convert a trace start time in seconds to miniSEED clock fields.
pub fn miniseed_clock_from_seconds(
seconds : Double,
) -> (Int, Int, Int, Int, Int) {
let safe = if seconds < 0.0 { 0.0 } else { seconds }
let julian_day = (safe / 86400.0).to_int() + 1
let remainder = safe % 86400.0
let hour = (remainder / 3600.0).to_int()
let minute = ((remainder - hour.to_double() * 3600.0) / 60.0).to_int()
let second = (remainder -
hour.to_double() * 3600.0 -
minute.to_double() * 60.0).to_int()
let fractional = ((remainder - remainder.to_int().to_double()) * 10000.0)
.round()
.to_int()
(julian_day, hour, minute, second, fractional)
}
///|
/// Return the duration represented by a miniSEED sample count.
pub fn miniseed_duration(sample_count : Int, sample_rate : Double) -> Double {
if sample_count <= 0 || sample_rate <= 0.0 {
0.0
} else {
(sample_count - 1).to_double() / sample_rate
}
}