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
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
use crate::track_event::*;
use crate::track_builder::TrackBuilder;
use crate::constants::{mml, midi, sysex, control_change, event_timing, timing};
use regex::Regex;
use std::collections::HashMap;
// Keep original constants for compatibility (marked as used to avoid dead code warnings)
#[allow(dead_code)]
const MIN_VOLUME: u8 = mml::MIN_VOLUME;
#[allow(dead_code)]
const MAX_VOLUME: u8 = mml::MAX_VOLUME;
#[allow(dead_code)]
const MAX_OCTAVE: u8 = mml::MAX_OCTAVE;
pub struct Mf2tt2mf {
channel: u8,
instrument: u8,
pan: u8,
reverb: u8,
min_note: u8,
max_note: u8,
sound_map: HashMap<char, i32>,
track_builders: Vec<TrackBuilder>,
}
impl Mf2tt2mf {
pub fn new(channel: u8, instrument: u8, pan: u8, reverb: u8) -> Self {
let mut sound_map = HashMap::new();
sound_map.insert('c', 0);
sound_map.insert('d', 2);
sound_map.insert('e', 4);
sound_map.insert('f', 5);
sound_map.insert('g', 7);
sound_map.insert('a', 9);
sound_map.insert('b', 11);
sound_map.insert('C', 0);
sound_map.insert('D', 2);
sound_map.insert('E', 4);
sound_map.insert('F', 5);
sound_map.insert('G', 7);
sound_map.insert('A', 9);
sound_map.insert('B', 11);
Mf2tt2mf {
channel,
instrument,
pan,
reverb,
min_note: 0,
max_note: 96,
sound_map,
track_builders: Vec::new(),
}
}
pub fn from_mml(&mut self, mml: &str) -> bool {
self.track_builders.clear();
let regex_pattern = r"(MML@)\s*([\s0-9a-glnortvA-GLNORTV#<>.&+-]*),\s*([\s0-9a-glnortvA-GLNORTV#<>.&+-]*),\s*([\s0-9a-glnortvA-GLNORTV#<>.&+-]*);";
let regex = match Regex::new(regex_pattern) {
Ok(r) => r,
Err(_) => {
eprintln!("Regex compile failed");
return false;
}
};
let captures = match regex.captures(mml) {
Some(caps) => caps,
None => {
eprintln!("Regex parse failed");
return false;
}
};
let mut track_list = Vec::new();
for i in 1..captures.len() {
if let Some(track_match) = captures.get(i) {
let track = track_match.as_str();
if !track.starts_with("MML@") {
track_list.push(track.to_string());
}
}
}
if track_list.is_empty() {
eprintln!("Track is empty");
return false;
}
let ch = self.channel;
let inst = self.instrument;
let pan = self.pan;
let reverb = self.reverb;
for (i, track) in track_list.iter().enumerate() {
let mut builder = TrackBuilder::new(ch);
if ch == 1 && i == 0 {
let meta_text = Box::new(MetaText::new(sysex::YOKOSO_META_TEXT.to_string()));
let tempo = Box::new(Tempo::new(midi::DEFAULT_TEMPO_MICROSECONDS));
let sys_ex = Box::new(SysEx::new(sysex::YOKOSO_SYSEX_DATA.to_vec()));
builder.put_event(meta_text);
builder.put_event(tempo);
builder.put_event(sys_ex);
}
let mut prog_change = Box::new(ProgramChange::new(ch, inst));
prog_change.set_lead_time(event_timing::PROGRAM_CHANGE_OFFSET);
builder.put_event(prog_change);
let mut pan_control = Box::new(ControlChange::new(ch, control_change::PAN, pan));
pan_control.set_lead_time(event_timing::PAN_CONTROL_OFFSET);
builder.put_event(pan_control);
let mut reverb_control = Box::new(ControlChange::new(ch, control_change::REVERB, reverb));
reverb_control.set_lead_time(event_timing::REVERB_CONTROL_OFFSET);
builder.put_event(reverb_control);
if !track.is_empty() {
let track_events = self.parse_track(track, event_timing::TRACK_START_TIME);
builder.put_events(track_events);
} else {
let mut end_track = Box::new(EndOfTrack::new());
end_track.set_lead_time(event_timing::EMPTY_TRACK_END_TIME);
builder.put_event(end_track);
}
self.track_builders.push(builder);
}
true
}
fn parse_track(&self, track: &str, lead_time: u32) -> Vec<Box<dyn TrackEvent>> {
use regex::Regex;
let mut events: Vec<Box<dyn TrackEvent>> = Vec::new();
let mut delta_time = lead_time;
// C++ algorithm state variables using constants
let mut note_time = timing::TICKS_PER_QUARTER_NOTE; // Current note duration (quarter note = 96 ticks)
let mut octave = mml::DEFAULT_OCTAVE; // Current octave
let mut volume = mml::DEFAULT_VOLUME; // Current volume (1-15)
let mut curr_note = 0i32; // For tie processing
let mut is_tied = false; // Tie state
// C++ time constants using defined constants
let semibreve = timing::TICKS_PER_WHOLE_NOTE; // Whole note = 384 ticks
let minim = timing::TICKS_PER_HALF_NOTE; // Half note = 192 ticks
// Step 1: Remove whitespace (C++ line 115-116)
let clean_track = track.chars().filter(|c| !c.is_whitespace()).collect::<String>();
// Step 2: Extract all MML tokens using exact C++ regex pattern (line 118)
let token_regex = Regex::new(r"[OTLVNRA-Gotlvnra-g<>][\+\-\#]?[0-9]*\.?&?").unwrap();
let tokens: Vec<&str> = token_regex.find_iter(&clean_track).map(|m| m.as_str()).collect();
// Step 3: Process tokens in order (C++ lines 130-259)
for token in tokens {
// Parse control tokens first (length, octave, tempo, volume, octave shift)
let control_regex = Regex::new(r"([lotvLOTV<>])([1-9][0-9]*|0?)(\.?)(&?)").unwrap();
if let Some(caps) = control_regex.captures(token) {
let op = caps.get(1).unwrap().as_str().to_lowercase();
let value_str = caps.get(2).map_or("", |m| m.as_str());
let dot = caps.get(3).map_or("", |m| m.as_str());
let _ampersand = caps.get(4).map_or("", |m| m.as_str()); // For ties
let value = if value_str.is_empty() { 0i32 } else { value_str.parse::<i32>().unwrap_or(0) };
match op.as_str() {
"l" => {
// Length token (C++ lines 142-147)
if value > 0 && value <= minim as i32 {
note_time = semibreve / value as u32; // C++ formula: floor(semibreve/value)
if dot == "." {
note_time = (note_time as f32 * 1.5) as u32; // Dotted notes
}
// Handle ties (&) - for now just mark the flag
if _ampersand == "&" {
is_tied = true;
}
}
}
"o" => {
// Octave token (C++ lines 148-149)
octave = value;
}
"t" => {
// Tempo token (C++ lines 164-167)
if value > 0 {
let tempo_microseconds = (60_000_000 / value) as u32; // Use integer division like C++
let mut tempo_event: Box<dyn TrackEvent> = Box::new(Tempo::new(tempo_microseconds));
tempo_event.set_lead_time(delta_time);
events.push(tempo_event);
}
}
"v" => {
// Volume token (C++ lines 168-174)
if value < 1 {
volume = 1;
} else if value > 15 {
volume = 15;
} else {
volume = value;
}
}
"<" => {
// Octave down (C++ lines 175-180)
if octave <= 0 {
octave = 0;
} else {
octave -= 1;
}
}
">" => {
// Octave up (C++ lines 181-186)
if octave >= 9 {
octave = 9;
} else {
octave += 1;
}
}
_ => {}
}
} else {
// Parse note/rest tokens (C++ lines 188-257)
let note_regex = Regex::new(r"([a-gnA-GN])([\+\#-]?)([0-9]*)(\.?)(&?)").unwrap();
if let Some(caps) = note_regex.captures(token) {
let note_char = caps.get(1).unwrap().as_str().to_lowercase();
let pitch = caps.get(2).map_or("", |m| m.as_str());
let length_str = caps.get(3).map_or("", |m| m.as_str());
let dot = caps.get(4).map_or("", |m| m.as_str());
let ampersand = caps.get(5).map_or("", |m| m.as_str());
let mut note = 0i32;
let mut tick = note_time;
// Handle 'n' notes differently (C++ lines 196-200)
if note_char == "n" {
if !length_str.is_empty() {
if let Ok(value) = length_str.parse::<i32>() {
if value >= 0 && value <= self.max_note as i32 {
note = value;
}
}
}
} else {
// Regular note processing (C++ lines 201-216)
if !length_str.is_empty() {
if let Ok(length_val) = length_str.parse::<i32>() {
if length_val >= 1 && length_val <= minim as i32 {
tick = semibreve / length_val as u32;
}
}
}
if dot == "." {
tick = (tick as f32 * 1.5) as u32;
}
// Calculate MIDI note number from sound map (C++ lines 208-210)
if let Some(&base_note) = self.sound_map.get(¬e_char.chars().next().unwrap()) {
note = (12 * octave) + base_note;
}
// Apply accidentals (C++ lines 211-215)
if pitch == "+" || pitch == "#" {
note += 1;
} else if pitch == "-" {
note -= 1;
}
}
// Clamp to valid range (C++ lines 218-224)
while note < self.min_note as i32 { note += 12; }
while note > self.max_note as i32 { note -= 12; }
note += 12; // Final offset (C++ line 224)
// Handle ties and note events (C++ lines 226-241)
if is_tied && note != curr_note {
is_tied = false;
// Generate Note Off for previous tied note
let mut note_off: Box<dyn TrackEvent> = Box::new(NoteOff::new(self.channel, curr_note as u8, 0));
note_off.set_lead_time(delta_time);
events.push(note_off);
}
if !is_tied {
// Generate Note On (C++ line 232)
let note_number = note as u8;
let velocity = (mml::VELOCITY_MULTIPLIER * volume) as u8;
let mut note_on: Box<dyn TrackEvent> = Box::new(NoteOn::new(self.channel, note_number, velocity));
note_on.set_lead_time(delta_time);
events.push(note_on);
}
delta_time += tick; // Advance time (C++ line 234)
if ampersand == "&" {
is_tied = true;
curr_note = note;
} else {
is_tied = false;
// Generate Note Off (C++ line 240)
let mut note_off: Box<dyn TrackEvent> = Box::new(NoteOff::new(self.channel, note as u8, 0));
note_off.set_lead_time(delta_time);
events.push(note_off);
}
} else {
// Handle rest tokens 'r' (C++ lines 246-257)
let rest_regex = Regex::new(r"[rR]([0-9]*)(\.?)").unwrap();
if let Some(caps) = rest_regex.captures(token) {
let mut tick = note_time;
let length_str = caps.get(1).map_or("", |m| m.as_str());
let dot = caps.get(2).map_or("", |m| m.as_str());
if let Ok(length_val) = length_str.parse::<i32>() {
if length_val >= 1 && length_val <= minim as i32 {
tick = semibreve / length_val as u32;
}
}
if dot == "." {
tick = (tick as f32 * 1.5) as u32;
}
delta_time += tick;
}
}
}
}
// Handle remaining tied notes (C++ lines 242-245)
if is_tied {
let mut note_off: Box<dyn TrackEvent> = Box::new(NoteOff::new(self.channel, curr_note as u8, 0));
note_off.set_lead_time(delta_time);
events.push(note_off);
}
// Add final note time like C++ (line 260)
delta_time += note_time;
let mut end_track: Box<dyn TrackEvent> = Box::new(EndOfTrack::new());
end_track.set_lead_time(delta_time);
events.push(end_track);
events
}
pub fn build_to_string(&self) -> Vec<String> {
let mut result = Vec::new();
result.push(format!("MFile 1 {} 96", self.channel));
for builder in &self.track_builders {
let build_result = builder.build();
result.extend(build_result);
}
result
}
pub fn build(&self) -> Vec<Vec<Box<dyn TrackEvent>>> {
let mut result: Vec<Vec<Box<dyn TrackEvent>>> = Vec::new();
for builder in &self.track_builders {
let mut track_events: Vec<Box<dyn TrackEvent>> = Vec::new();
for event in builder.event_list() {
// Now we can properly clone events using the clone_event method
track_events.push(event.clone_event());
}
result.push(track_events);
}
result
}
}