///|
pub(all) enum ValidationSeverity {
SeverityInfo
SeverityWarning
SeverityError
} derive(Eq, @debug.Debug)
///|
pub fn ValidationSeverity::label(self : ValidationSeverity) -> String {
match self {
SeverityInfo => "info"
SeverityWarning => "warning"
SeverityError => "error"
}
}
///|
pub(all) struct ValidationIssue {
severity : ValidationSeverity
code : String
message : String
offset : Int
} derive(Eq, @debug.Debug)
///|
pub fn ValidationIssue::new(
severity : ValidationSeverity,
code : String,
message : String,
offset? : Int = 0,
) -> ValidationIssue {
{ severity, code, message, offset }
}
///|
pub fn ValidationIssue::is_error(self : ValidationIssue) -> Bool {
self.severity == SeverityError
}
///|
pub(all) struct ChunkSummary {
id : String
offset : Int
data_offset : Int
size : Int
known : Bool
} derive(Eq, @debug.Debug)
///|
pub fn ChunkSummary::from_chunk(chunk : WaveChunk) -> ChunkSummary {
{
id: chunk.id,
offset: chunk.offset,
data_offset: chunk.data_offset,
size: chunk.size,
known: chunk.is_known(),
}
}
///|
pub(all) struct ValidationReport {
ok : Bool
error_count : Int
warning_count : Int
info_count : Int
format : WaveFormat
info : WaveInfo
issues : Array[ValidationIssue]
chunks : Array[ChunkSummary]
} derive(Eq, @debug.Debug)
///|
pub fn ValidationReport::empty() -> ValidationReport {
{
ok: false,
error_count: 0,
warning_count: 0,
info_count: 0,
format: WaveFormat::empty(),
info: WaveInfo::empty(),
issues: [],
chunks: [],
}
}
///|
pub fn ValidationReport::has_errors(self : ValidationReport) -> Bool {
self.error_count > 0
}
///|
pub fn ValidationReport::has_warnings(self : ValidationReport) -> Bool {
self.warning_count > 0
}
///|
fn count_severity(
issues : Array[ValidationIssue],
severity : ValidationSeverity,
) -> Int {
for issue in issues; count = 0 {
if issue.severity == severity {
continue count + 1
} else {
continue count
}
} nobreak {
count
}
}
///|
fn chunk_summaries(chunks : Array[WaveChunk]) -> Array[ChunkSummary] {
Array::makei(chunks.length(), i => ChunkSummary::from_chunk(chunks[i]))
}
///|
fn issue_from_error(error : WavError) -> ValidationIssue {
ValidationIssue::new(
SeverityError,
error.code(),
error.message,
offset=error.offset,
)
}
///|
fn add_format_warnings(
wav : ParsedWav,
issues : Array[ValidationIssue],
) -> Unit {
if wav.format.extra_size > 0 {
issues.push(
ValidationIssue::new(
SeverityInfo,
"fmt-extra",
"fmt chunk contains extension bytes that are preserved but not interpreted",
),
)
}
if wav.header.file_size_minus_8 + 8 != wav.source_length {
issues.push(
ValidationIssue::new(
SeverityWarning,
"riff-size-mismatch",
"RIFF declared size does not match the provided byte array length",
),
)
}
if wav.format.sample_rate < 8000 {
issues.push(
ValidationIssue::new(
SeverityWarning,
"low-sample-rate",
"sample rate is below common speech/audio interchange ranges",
),
)
}
if wav.format.channels > 8 {
issues.push(
ValidationIssue::new(
SeverityWarning,
"many-channels",
"file uses more than eight channels; downstream tools may need explicit layout handling",
),
)
}
if !wav.format.is_supported() {
issues.push(
ValidationIssue::new(
SeverityError,
"unsupported-format",
"MoonWavKit v1 supports PCM 8/16/24/32-bit and IEEE Float32 only",
),
)
}
if wav.format.block_align != wav.format.expected_block_align() {
issues.push(
ValidationIssue::new(
SeverityError,
"block-align-mismatch",
"fmt block_align does not match channels * bytes_per_sample",
),
)
}
if wav.format.byte_rate != wav.format.expected_byte_rate() {
issues.push(
ValidationIssue::new(
SeverityError,
"byte-rate-mismatch",
"fmt byte_rate does not match sample_rate * block_align",
),
)
}
if wav.format.block_align > 0 && wav.data_size % wav.format.block_align != 0 {
issues.push(
ValidationIssue::new(
SeverityError,
"partial-frame",
"data chunk ends in a partial audio frame",
offset=wav.data_offset + wav.data_size,
),
)
}
}
///|
fn add_chunk_warnings(wav : ParsedWav, issues : Array[ValidationIssue]) -> Unit {
let mut data_count = 0
let mut fmt_count = 0
for chunk in wav.chunks {
if chunk.id == "data" {
data_count = data_count + 1
} else if chunk.id == "fmt " {
fmt_count = fmt_count + 1
} else if !chunk.is_known() {
issues.push(
ValidationIssue::new(
SeverityInfo,
"unknown-chunk",
"unknown chunk is skipped and preserved in chunk metadata: \{chunk.id}",
offset=chunk.offset,
),
)
}
}
if fmt_count > 1 {
issues.push(
ValidationIssue::new(
SeverityWarning,
"multiple-fmt",
"multiple fmt chunks were found; parser uses the last one seen",
),
)
}
if data_count > 1 {
issues.push(
ValidationIssue::new(
SeverityWarning,
"multiple-data",
"multiple data chunks were found; parser uses the last one seen",
),
)
}
if wav.info_tags.length() > 0 {
issues.push(
ValidationIssue::new(
SeverityInfo,
"info-tags",
"LIST/INFO metadata tags were parsed",
),
)
}
if wav.cue_points.length() > 0 {
issues.push(
ValidationIssue::new(SeverityInfo, "cue-points", "cue points were parsed"),
)
}
}
///|
fn validation_report_from_wav(
wav : ParsedWav,
issues : Array[ValidationIssue],
) -> ValidationReport {
add_format_warnings(wav, issues)
add_chunk_warnings(wav, issues)
let errors = count_severity(issues, SeverityError)
let warnings = count_severity(issues, SeverityWarning)
let infos = count_severity(issues, SeverityInfo)
{
ok: errors == 0,
error_count: errors,
warning_count: warnings,
info_count: infos,
format: wav.format,
info: wav.info(),
issues,
chunks: chunk_summaries(wav.chunks),
}
}
///|
pub fn validate_wav_detailed(bytes : Array[Int]) -> ValidationReport {
let parsed = parse_wav(bytes)
let issues : Array[ValidationIssue] = []
if !parsed.ok {
issues.push(issue_from_error(parsed.error))
let errors = count_severity(issues, SeverityError)
{
ok: false,
error_count: errors,
warning_count: 0,
info_count: 0,
format: WaveFormat::empty(),
info: WaveInfo::empty(),
issues,
chunks: [],
}
} else {
validation_report_from_wav(parsed.wav, issues)
}
}
///|
pub fn validation_report_to_text(report : ValidationReport) -> String {
let header = if report.ok {
"MoonWavKit validation: PASS"
} else {
"MoonWavKit validation: FAIL"
}
let summary = "\{header}\nerrors: \{report.error_count}\nwarnings: \{report.warning_count}\ninfo: \{report.info_count}\nchunks: \{report.chunks.length()}"
if report.issues.length() == 0 {
summary
} else {
for issue in report.issues; text = summary {
continue "\{text}\n- \{issue.severity.label()} \{issue.code}: \{issue.message}"
} nobreak {
text
}
}
}