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
//! One-pass validation for song compilation: the tempo/meter maps, pickup,
//! grid placement, and sections/markers checks (T1003–T1006). Every problem
//! is collected, never first-error — see [`crate::diag`].
use super::Song;
use crate::diag::{CompileError, Diagnostic};
use crate::units::Beat;
impl Song {
/// Validate the tempo/meter maps, pickup, grid placement, and
/// sections/markers — one pass, every problem collected (T1003–T1006).
pub(crate) fn validate_maps(&self, diags: &mut CompileError) {
// The tempo map (T1003): first at beat 0, strictly ascending, sane
// tempos, bounded (mirrors the document-level seq validation).
let map = &self.tempo_map;
if map.len() > 1024 {
diags.push(
Diagnostic::error(
"T1003",
"tempo_map",
format!("tempo_map is capped at 1024 points, got {}", map.len()),
)
.with_remediation("split the piece or thin the tempo changes"),
);
}
if let Some(first) = map.first()
&& first.at != Beat::zero()
{
diags.push(
Diagnostic::error("T1003", "tempo_map[0].at", "tempo_map's first point must be at beat 0")
.with_remediation("add a point at beat 0 (the song's bpm applies before the first change otherwise)"),
);
}
for (i, p) in map.iter().enumerate() {
if !(p.bpm.is_finite() && p.bpm > 0.0) {
diags.push(
Diagnostic::error(
"T1003",
format!("tempo_map[{i}].bpm"),
format!("tempo must be positive and finite, got {}", p.bpm),
)
.with_remediation("set a tempo above 0 BPM"),
);
}
if i > 0 && p.at <= map[i - 1].at {
diags.push(
Diagnostic::error(
"T1003",
format!("tempo_map[{i}].at"),
"tempo_map must be strictly ascending by beat",
)
.with_remediation("sort the changes and merge any duplicates"),
);
}
}
// The meter map (T1004): first at bar 0, ascending bars, numerator ≥ 1,
// power-of-two denominator, bounded.
let meter = &self.meter_map;
if meter.len() > 256 {
diags.push(
Diagnostic::error(
"T1004",
"meter_map",
format!("meter_map is capped at 256 points, got {}", meter.len()),
)
.with_remediation("consolidate repeated time-signature changes"),
);
}
if let Some(first) = meter.first()
&& first.bar != 0
{
diags.push(
Diagnostic::error(
"T1004",
"meter_map[0].bar",
"meter_map's first point must be at bar 0",
)
.with_remediation("add the opening time signature at bar 0"),
);
}
for (i, p) in meter.iter().enumerate() {
if p.numerator < 1 {
diags.push(
Diagnostic::error(
"T1004",
format!("meter_map[{i}].numerator"),
"time-signature numerator must be ≥ 1",
)
.with_remediation("use a numerator like 3 (3/4) or 6 (6/8)"),
);
}
if !p.denominator.is_power_of_two() || p.denominator > 64 {
diags.push(
Diagnostic::error(
"T1004",
format!("meter_map[{i}].denominator"),
format!(
"time-signature denominator must be a power of two ≤ 64, got {}",
p.denominator
),
)
.with_remediation("use 2, 4, 8, 16, 32, or 64"),
);
}
if i > 0 && p.bar <= meter[i - 1].bar {
diags.push(
Diagnostic::error(
"T1004",
format!("meter_map[{i}].bar"),
"meter_map must be strictly ascending by bar",
)
.with_remediation("sort the changes and merge any duplicates"),
);
}
}
if let Some(pickup) = self.pickup
&& pickup < Beat::zero()
{
diags.push(
Diagnostic::error("T1004", "pickup", "the pickup bar can't be negative")
.with_remediation("use zero (no pickup) or a positive beat length"),
);
}
// Grid placement (T1005): every placement must land on a step.
if !self.plain_meter() {
for (i, pl) in self.arrangement.iter().enumerate() {
if self.beat_to_step(self.beat_at_bar(pl.bar)).is_err() {
diags.push(
Diagnostic::error("T1005", format!("arrangement[{i}].bar"), format!("bar {} lands between grid steps", pl.bar))
.with_remediation("raise steps_per_beat, or move the placement/meter change onto the grid"),
);
}
}
}
// Sections and markers (T1006).
for (i, s) in self.sections.iter().enumerate() {
if s.name.is_empty() {
diags.push(
Diagnostic::error(
"T1006",
format!("sections[{i}].name"),
"a section needs a name",
)
.with_remediation("name it (e.g. \"verse\", \"chorus\")"),
);
}
if s.bars < 1 {
diags.push(
Diagnostic::error(
"T1006",
format!("sections[{i}].bars"),
"a section must be at least one bar",
)
.with_remediation("set bars ≥ 1"),
);
}
}
for (i, m) in self.markers.iter().enumerate() {
if m.name.is_empty() {
diags.push(
Diagnostic::error(
"T1006",
format!("markers[{i}].name"),
"a marker needs a name",
)
.with_remediation("name it (e.g. \"drop\", \"cue\")"),
);
}
}
}
}