yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
asm_importer.cc
Go to the documentation of this file.
2
3#include <algorithm>
4#include <cctype>
5#include <fstream>
6#include <regex>
7#include <sstream>
8
9#include "absl/strings/str_format.h"
10#include "absl/strings/str_split.h"
11
12namespace yaze {
13namespace zelda3 {
14namespace music {
15
16absl::StatusOr<AsmParseResult> AsmImporter::ImportSong(
17 const std::string& asm_source, const AsmImportOptions& options) {
18 AsmParseResult result;
19 ParseState state;
20
21 // Initialize song with default segment
22 result.song.segments.push_back(MusicSegment{});
23 for (int i = 0; i < 8; ++i) {
24 result.song.segments[0].tracks[i].is_empty = true;
25 }
26
27 // Parse line by line
28 std::istringstream stream(asm_source);
29 std::string line;
30
31 while (std::getline(stream, line)) {
32 state.line_number++;
33 result.lines_parsed++;
34
35 auto status = ParseLine(line, result.song, state, options);
36 if (!status.ok()) {
37 if (options.strict_mode) {
38 return status;
39 }
40 state.errors.push_back(
41 absl::StrFormat("Line %d: %s", state.line_number, status.message()));
42 }
43 }
44
45 // Finalize song metadata
46 if (!state.song_label.empty()) {
47 result.song.name = state.song_label;
48 }
49
50 result.warnings = std::move(state.warnings);
51 result.errors = std::move(state.errors);
52 result.bytes_generated = state.bytes_generated;
53
54 return result;
55}
56
57absl::StatusOr<AsmParseResult> AsmImporter::ImportFromFile(
58 const std::string& path, const AsmImportOptions& options) {
59 std::ifstream file(path);
60 if (!file.is_open()) {
61 return absl::FailedPreconditionError(
62 absl::StrFormat("Failed to open file: %s", path));
63 }
64
65 std::ostringstream buffer;
66 buffer << file.rdbuf();
67 file.close();
68
69 auto result = ImportSong(buffer.str(), options);
70 if (result.ok()) {
71 // Use filename as song name if no label found
72 if (result->song.name.empty()) {
73 size_t pos = path.find_last_of("/\\");
74 std::string filename =
75 (pos == std::string::npos) ? path : path.substr(pos + 1);
76 // Remove extension
77 pos = filename.find_last_of('.');
78 if (pos != std::string::npos) {
79 filename = filename.substr(0, pos);
80 }
81 result->song.name = filename;
82 }
83 }
84
85 return result;
86}
87
88absl::Status AsmImporter::ParseLine(const std::string& line, MusicSong& song,
89 ParseState& state,
90 const AsmImportOptions& options) {
91 std::string trimmed = Trim(line);
92
93 // Skip empty lines and comments
94 if (trimmed.empty() || trimmed[0] == ';') {
95 return absl::OkStatus();
96 }
97
98 // Remove inline comments
99 size_t comment_pos = trimmed.find(';');
100 if (comment_pos != std::string::npos) {
101 trimmed = Trim(trimmed.substr(0, comment_pos));
102 }
103
104 if (trimmed.empty()) {
105 return absl::OkStatus();
106 }
107
108 // Check for label
109 if (ParseLabel(trimmed, state)) {
110 return absl::OkStatus();
111 }
112
113 // Check for directive (!ARAMAddr, etc.)
114 if (ParseDirective(trimmed, state)) {
115 return absl::OkStatus();
116 }
117
118 // Check for data bytes (db, dw)
119 if (trimmed.substr(0, 2) == "db" || trimmed.substr(0, 2) == "dw") {
120 return ParseDataBytes(trimmed, song, state, options);
121 }
122
123 // Check for macro call
124 if (trimmed[0] == '%') {
125 auto events_result = ParseMacro(trimmed, state, options);
126 if (!events_result.ok()) {
127 return events_result.status();
128 }
129
130 // Add events to current channel
131 if (state.current_channel >= 0 && state.current_channel < 8) {
132 auto& track = song.segments[0].tracks[state.current_channel];
133 track.is_empty = false;
134 for (const auto& event : *events_result) {
135 track.events.push_back(event);
136 }
137 }
138 return absl::OkStatus();
139 }
140
141 // Unknown line - just warn
142 if (options.verbose_errors) {
143 state.warnings.push_back(
144 absl::StrFormat("Line %d: Unrecognized: %s", state.line_number, trimmed));
145 }
146
147 return absl::OkStatus();
148}
149
150bool AsmImporter::ParseLabel(const std::string& line, ParseState& state) {
151 // Check for label definition (ends with : or starts with .)
152 if (line.back() == ':') {
153 std::string label = line.substr(0, line.length() - 1);
154
155 // Is this a channel label?
156 if (label.substr(0, 8) == ".Channel" && label.length() == 9) {
157 int ch = label[8] - '0';
158 if (ch >= 0 && ch < 8) {
159 state.current_channel = ch;
160 state.label_to_channel[label] = ch;
161 return true;
162 }
163 }
164
165 // Is this a subroutine label?
166 if (label[0] == '.') {
167 // Mark as subroutine location
168 state.label_to_channel[label] = -1; // Will be filled by data
169 return true;
170 }
171
172 // Must be the song label
173 if (state.song_label.empty()) {
174 state.song_label = label;
175 }
176 return true;
177 }
178
179 // Label without colon (some assemblers)
180 if (line[0] == '.') {
181 std::string label = line;
182 if (label.substr(0, 8) == ".Channel" && label.length() >= 9) {
183 int ch = label[8] - '0';
184 if (ch >= 0 && ch < 8) {
185 state.current_channel = ch;
186 state.label_to_channel[label] = ch;
187 return true;
188 }
189 }
190 return true; // Other dot labels
191 }
192
193 return false;
194}
195
196bool AsmImporter::ParseDirective(const std::string& line, ParseState& state) {
197 // !ARAMAddr = $XXXX
198 if (line[0] == '!') {
199 size_t eq_pos = line.find('=');
200 if (eq_pos != std::string::npos) {
201 std::string name = Trim(line.substr(1, eq_pos - 1));
202 std::string value = Trim(line.substr(eq_pos + 1));
203
204 if (name == "ARAMAddr") {
205 auto parsed = ParseHexValue(value);
206 if (parsed.ok()) {
207 // For 16-bit values, parse differently
208 if (value.length() > 3) {
209 uint16_t addr = 0;
210 if (value[0] == '$') {
211 addr = std::stoul(value.substr(1), nullptr, 16);
212 } else if (value.substr(0, 2) == "0x") {
213 addr = std::stoul(value.substr(2), nullptr, 16);
214 }
215 state.aram_address = addr;
216 }
217 }
218 return true;
219 }
220 // Other directives (!ARAMC, etc.) - just acknowledge
221 return true;
222 }
223 }
224
225 return false;
226}
227
228absl::Status AsmImporter::ParseDataBytes(const std::string& line,
229 MusicSong& song, ParseState& state,
230 const AsmImportOptions& options) {
231 // Skip "db " or "dw "
232 std::string data = Trim(line.substr(2));
233 if (data.empty()) {
234 return absl::OkStatus();
235 }
236
237 // Split by comma
238 std::vector<std::string> parts = absl::StrSplit(data, ',');
239
240 for (const auto& part : parts) {
241 std::string value = Trim(part);
242 if (value.empty()) continue;
243
244 // Check for note name
245 auto note_result = ParseNoteName(value);
246 if (note_result.ok()) {
247 if (state.current_channel >= 0 && state.current_channel < 8) {
248 auto& track = song.segments[0].tracks[state.current_channel];
249 track.is_empty = false;
250
251 TrackEvent event;
252 if (*note_result == kTrackEnd) {
254 } else {
255 event.type = TrackEvent::Type::Note;
256 event.note.pitch = *note_result;
257 event.note.duration = state.current_duration;
258 event.note.has_duration_prefix = false;
259 }
260 track.events.push_back(event);
261 state.bytes_generated++;
262 }
263 continue;
264 }
265
266 // Check for duration constant
267 auto duration_result = ParseDurationConstant(value);
268 if (duration_result.ok()) {
269 state.current_duration = *duration_result;
270 state.bytes_generated++;
271 continue;
272 }
273
274 // Check for hex value
275 auto hex_result = ParseHexValue(value);
276 if (hex_result.ok()) {
277 uint8_t byte_val = *hex_result;
278
279 // Determine what this byte represents
280 if (state.current_channel >= 0 && state.current_channel < 8) {
281 auto& track = song.segments[0].tracks[state.current_channel];
282 track.is_empty = false;
283
284 // Is it a note?
285 if (byte_val >= kNoteMinPitch && byte_val <= kNoteMaxPitch) {
286 TrackEvent event;
288 event.note.pitch = byte_val;
289 event.note.duration = state.current_duration;
290 event.note.has_duration_prefix = false;
291 track.events.push_back(event);
292 } else if (byte_val == kNoteTie || byte_val == kNoteRest) {
293 TrackEvent event;
295 event.note.pitch = byte_val;
296 event.note.duration = state.current_duration;
297 track.events.push_back(event);
298 } else if (byte_val == kTrackEnd) {
299 TrackEvent event;
301 track.events.push_back(event);
302 } else if (byte_val >= 0xE0 && byte_val <= 0xFF) {
303 // Command byte - we'd need to read following bytes for params
304 // For now, just record as raw command
305 TrackEvent event;
307 event.command.opcode = byte_val;
308 track.events.push_back(event);
309 } else if (byte_val < 0x80) {
310 // Duration byte
311 state.current_duration = byte_val;
312 }
313 state.bytes_generated++;
314 }
315 continue;
316 }
317
318 // Unknown value
319 if (options.verbose_errors) {
320 state.warnings.push_back(
321 absl::StrFormat("Line %d: Unknown value: %s", state.line_number, value));
322 }
323 }
324
325 return absl::OkStatus();
326}
327
328absl::StatusOr<std::vector<TrackEvent>> AsmImporter::ParseMacro(
329 const std::string& macro_call, ParseState& state,
330 const AsmImportOptions& options) {
331 std::vector<TrackEvent> events;
332
333 std::string macro_name;
334 std::vector<std::string> params;
335
336 if (!ParseMacroCall(macro_call, macro_name, params)) {
337 return absl::InvalidArgumentError(
338 absl::StrFormat("Invalid macro call: %s", macro_call));
339 }
340
341 // Check for instrument macro
342 auto inst_result = ResolveInstrumentMacro(macro_name);
343 if (inst_result.ok()) {
344 TrackEvent event;
346 event.command = *inst_result;
347 events.push_back(event);
348 state.bytes_generated += 2; // opcode + param
349 return events;
350 }
351
352 // Check for SetDuration macro
353 if (macro_name == "SetDuration" && params.size() >= 1) {
354 auto duration = ParseDurationConstant(params[0]);
355 if (duration.ok()) {
356 state.current_duration = *duration;
357 state.bytes_generated++;
358 return events; // No event, just state change
359 }
360 }
361
362 // Check for SetDurationN macro (with velocity)
363 if (macro_name == "SetDurationN" && params.size() >= 2) {
364 auto duration = ParseDurationConstant(params[0]);
365 if (duration.ok()) {
366 state.current_duration = *duration;
367 state.bytes_generated += 2;
368 return events;
369 }
370 }
371
372 // Check for command macro
373 std::vector<uint8_t> byte_params;
374 for (const auto& p : params) {
375 auto hex = ParseHexValue(p);
376 if (hex.ok()) {
377 byte_params.push_back(*hex);
378 }
379 }
380
381 auto cmd_result = ResolveCommandMacro(macro_name, byte_params);
382 if (cmd_result.ok()) {
383 TrackEvent event;
385 event.command = *cmd_result;
386 events.push_back(event);
387 state.bytes_generated += 1 + byte_params.size();
388 return events;
389 }
390
391 if (options.strict_mode) {
392 return absl::InvalidArgumentError(
393 absl::StrFormat("Unknown macro: %s", macro_name));
394 }
395
396 state.warnings.push_back(
397 absl::StrFormat("Line %d: Unknown macro: %s", state.line_number, macro_name));
398 return events;
399}
400
401absl::StatusOr<uint8_t> AsmImporter::ParseNoteName(const std::string& note_name) {
402 for (const auto& mapping : kAsmNoteNames) {
403 if (note_name == mapping.name) {
404 return mapping.pitch;
405 }
406 }
407 return absl::NotFoundError("Unknown note name");
408}
409
411 const std::string& duration) {
412 // Check if it's a constant name (with or without !)
413 std::string name = duration;
414 if (!name.empty() && name[0] == '!') {
415 name = name.substr(1);
416 }
417
418 // Add back the ! for comparison
419 std::string full_name = "!" + name;
420
421 for (const auto& dc : kDurationConstants) {
422 if (full_name == dc.name) {
423 return dc.value;
424 }
425 }
426
427 return absl::NotFoundError("Unknown duration constant");
428}
429
430absl::StatusOr<MusicCommand> AsmImporter::ResolveInstrumentMacro(
431 const std::string& macro_name) {
432 for (const auto& mapping : kInstrumentMacroImport) {
433 if (macro_name == mapping.macro) {
434 MusicCommand cmd;
435 cmd.opcode = 0xE0; // SetInstrument
436 cmd.params[0] = mapping.id;
437 return cmd;
438 }
439 }
440 return absl::NotFoundError("Unknown instrument macro");
441}
442
443absl::StatusOr<MusicCommand> AsmImporter::ResolveCommandMacro(
444 const std::string& macro_name, const std::vector<uint8_t>& params) {
445 for (const auto& mapping : kCommandMacros) {
446 if (macro_name == mapping.name) {
447 MusicCommand cmd;
448 cmd.opcode = mapping.opcode;
449 for (size_t i = 0; i < params.size() && i < 3; ++i) {
450 cmd.params[i] = params[i];
451 }
452 return cmd;
453 }
454 }
455 return absl::NotFoundError("Unknown command macro");
456}
457
458bool AsmImporter::ParseMacroCall(const std::string& call,
459 std::string& macro_name,
460 std::vector<std::string>& params) {
461 // Format: %MacroName(param1, param2, ...)
462 if (call.empty() || call[0] != '%') {
463 return false;
464 }
465
466 size_t paren_start = call.find('(');
467 if (paren_start == std::string::npos) {
468 // Macro without parameters
469 macro_name = call.substr(1);
470 return true;
471 }
472
473 macro_name = call.substr(1, paren_start - 1);
474
475 size_t paren_end = call.find(')', paren_start);
476 if (paren_end == std::string::npos) {
477 return false;
478 }
479
480 std::string params_str = call.substr(paren_start + 1, paren_end - paren_start - 1);
481 if (!params_str.empty()) {
482 std::vector<std::string> parts = absl::StrSplit(params_str, ',');
483 for (const auto& p : parts) {
484 params.push_back(Trim(p));
485 }
486 }
487
488 return true;
489}
490
491absl::StatusOr<uint8_t> AsmImporter::ParseHexValue(const std::string& value) {
492 if (value.empty()) {
493 return absl::InvalidArgumentError("Empty value");
494 }
495
496 try {
497 if (value[0] == '$') {
498 return static_cast<uint8_t>(std::stoul(value.substr(1), nullptr, 16));
499 } else if (value.length() >= 2 && value.substr(0, 2) == "0x") {
500 return static_cast<uint8_t>(std::stoul(value.substr(2), nullptr, 16));
501 } else if (std::isdigit(value[0])) {
502 return static_cast<uint8_t>(std::stoul(value, nullptr, 10));
503 }
504 } catch (...) {
505 return absl::InvalidArgumentError(
506 absl::StrFormat("Invalid hex value: %s", value));
507 }
508
509 return absl::InvalidArgumentError(
510 absl::StrFormat("Invalid hex value: %s", value));
511}
512
513std::string AsmImporter::Trim(const std::string& s) {
514 size_t start = s.find_first_not_of(" \t\r\n");
515 if (start == std::string::npos) return "";
516 size_t end = s.find_last_not_of(" \t\r\n");
517 return s.substr(start, end - start + 1);
518}
519
520} // namespace music
521} // namespace zelda3
522} // namespace yaze
absl::StatusOr< AsmParseResult > ImportSong(const std::string &asm_source, const AsmImportOptions &options)
Import a song from ASM string.
bool ParseLabel(const std::string &line, ParseState &state)
absl::StatusOr< uint8_t > ParseDurationConstant(const std::string &duration)
bool ParseDirective(const std::string &line, ParseState &state)
absl::StatusOr< MusicCommand > ResolveCommandMacro(const std::string &macro_name, const std::vector< uint8_t > &params)
absl::Status ParseLine(const std::string &line, MusicSong &song, ParseState &state, const AsmImportOptions &options)
absl::StatusOr< uint8_t > ParseNoteName(const std::string &note_name)
static std::string Trim(const std::string &s)
absl::Status ParseDataBytes(const std::string &line, MusicSong &song, ParseState &state, const AsmImportOptions &options)
bool ParseMacroCall(const std::string &call, std::string &macro_name, std::vector< std::string > &params)
absl::StatusOr< uint8_t > ParseHexValue(const std::string &value)
absl::StatusOr< AsmParseResult > ImportFromFile(const std::string &path, const AsmImportOptions &options)
Import a song from a file.
absl::StatusOr< MusicCommand > ResolveInstrumentMacro(const std::string &macro_name)
absl::StatusOr< std::vector< TrackEvent > > ParseMacro(const std::string &macro_call, ParseState &state, const AsmImportOptions &options)
constexpr uint8_t kNoteRest
Definition song_data.h:58
constexpr DurationConstant kDurationConstants[]
constexpr uint8_t kTrackEnd
Definition song_data.h:59
constexpr CommandMacroMapping kCommandMacros[]
constexpr NoteNameMapping kAsmNoteNames[]
constexpr InstrumentMacroMapping kInstrumentMacroImport[]
constexpr uint8_t kNoteMinPitch
Definition song_data.h:55
constexpr uint8_t kNoteTie
Definition song_data.h:57
constexpr uint8_t kNoteMaxPitch
Definition song_data.h:56
Options for ASM import from music_macros.asm format.
std::unordered_map< std::string, int > label_to_channel
Parse result with diagnostics.
std::vector< std::string > errors
std::vector< std::string > warnings
Represents an N-SPC command (opcodes 0xE0-0xFF).
Definition song_data.h:222
std::array< uint8_t, 3 > params
Definition song_data.h:224
A segment containing 8 parallel tracks.
Definition song_data.h:315
A complete song composed of segments.
Definition song_data.h:334
std::vector< MusicSegment > segments
Definition song_data.h:336
A single event in a music track (note, command, or control).
Definition song_data.h:247