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(absl::StrFormat("Line %d: Unrecognized: %s",
144 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())
243 continue;
244
245 // Check for note name
246 auto note_result = ParseNoteName(value);
247 if (note_result.ok()) {
248 if (state.current_channel >= 0 && state.current_channel < 8) {
249 auto& track = song.segments[0].tracks[state.current_channel];
250 track.is_empty = false;
251
252 TrackEvent event;
253 if (*note_result == kTrackEnd) {
255 } else {
256 event.type = TrackEvent::Type::Note;
257 event.note.pitch = *note_result;
258 event.note.duration = state.current_duration;
259 event.note.has_duration_prefix = false;
260 }
261 track.events.push_back(event);
262 state.bytes_generated++;
263 }
264 continue;
265 }
266
267 // Check for duration constant
268 auto duration_result = ParseDurationConstant(value);
269 if (duration_result.ok()) {
270 state.current_duration = *duration_result;
271 state.bytes_generated++;
272 continue;
273 }
274
275 // Check for hex value
276 auto hex_result = ParseHexValue(value);
277 if (hex_result.ok()) {
278 uint8_t byte_val = *hex_result;
279
280 // Determine what this byte represents
281 if (state.current_channel >= 0 && state.current_channel < 8) {
282 auto& track = song.segments[0].tracks[state.current_channel];
283 track.is_empty = false;
284
285 // Is it a note?
286 if (byte_val >= kNoteMinPitch && byte_val <= kNoteMaxPitch) {
287 TrackEvent event;
289 event.note.pitch = byte_val;
290 event.note.duration = state.current_duration;
291 event.note.has_duration_prefix = false;
292 track.events.push_back(event);
293 } else if (byte_val == kNoteTie || byte_val == kNoteRest) {
294 TrackEvent event;
296 event.note.pitch = byte_val;
297 event.note.duration = state.current_duration;
298 track.events.push_back(event);
299 } else if (byte_val == kTrackEnd) {
300 TrackEvent event;
302 track.events.push_back(event);
303 } else if (byte_val >= 0xE0 && byte_val <= 0xFF) {
304 // Command byte - we'd need to read following bytes for params
305 // For now, just record as raw command
306 TrackEvent event;
308 event.command.opcode = byte_val;
309 track.events.push_back(event);
310 } else if (byte_val < 0x80) {
311 // Duration byte
312 state.current_duration = byte_val;
313 }
314 state.bytes_generated++;
315 }
316 continue;
317 }
318
319 // Unknown value
320 if (options.verbose_errors) {
321 state.warnings.push_back(absl::StrFormat("Line %d: Unknown value: %s",
322 state.line_number, value));
323 }
324 }
325
326 return absl::OkStatus();
327}
328
329absl::StatusOr<std::vector<TrackEvent>> AsmImporter::ParseMacro(
330 const std::string& macro_call, ParseState& state,
331 const AsmImportOptions& options) {
332 std::vector<TrackEvent> events;
333
334 std::string macro_name;
335 std::vector<std::string> params;
336
337 if (!ParseMacroCall(macro_call, macro_name, params)) {
338 return absl::InvalidArgumentError(
339 absl::StrFormat("Invalid macro call: %s", macro_call));
340 }
341
342 // Check for instrument macro
343 auto inst_result = ResolveInstrumentMacro(macro_name);
344 if (inst_result.ok()) {
345 TrackEvent event;
347 event.command = *inst_result;
348 events.push_back(event);
349 state.bytes_generated += 2; // opcode + param
350 return events;
351 }
352
353 // Check for SetDuration macro
354 if (macro_name == "SetDuration" && params.size() >= 1) {
355 auto duration = ParseDurationConstant(params[0]);
356 if (duration.ok()) {
357 state.current_duration = *duration;
358 state.bytes_generated++;
359 return events; // No event, just state change
360 }
361 }
362
363 // Check for SetDurationN macro (with velocity)
364 if (macro_name == "SetDurationN" && params.size() >= 2) {
365 auto duration = ParseDurationConstant(params[0]);
366 if (duration.ok()) {
367 state.current_duration = *duration;
368 state.bytes_generated += 2;
369 return events;
370 }
371 }
372
373 // Check for command macro
374 std::vector<uint8_t> byte_params;
375 for (const auto& p : params) {
376 auto hex = ParseHexValue(p);
377 if (hex.ok()) {
378 byte_params.push_back(*hex);
379 }
380 }
381
382 auto cmd_result = ResolveCommandMacro(macro_name, byte_params);
383 if (cmd_result.ok()) {
384 TrackEvent event;
386 event.command = *cmd_result;
387 events.push_back(event);
388 state.bytes_generated += 1 + byte_params.size();
389 return events;
390 }
391
392 if (options.strict_mode) {
393 return absl::InvalidArgumentError(
394 absl::StrFormat("Unknown macro: %s", macro_name));
395 }
396
397 state.warnings.push_back(absl::StrFormat("Line %d: Unknown macro: %s",
398 state.line_number, macro_name));
399 return events;
400}
401
402absl::StatusOr<uint8_t> AsmImporter::ParseNoteName(
403 const std::string& note_name) {
404 for (const auto& mapping : kAsmNoteNames) {
405 if (note_name == mapping.name) {
406 return mapping.pitch;
407 }
408 }
409 return absl::NotFoundError("Unknown note name");
410}
411
413 const std::string& duration) {
414 // Check if it's a constant name (with or without !)
415 std::string name = duration;
416 if (!name.empty() && name[0] == '!') {
417 name = name.substr(1);
418 }
419
420 // Add back the ! for comparison
421 std::string full_name = "!" + name;
422
423 for (const auto& dc : kDurationConstants) {
424 if (full_name == dc.name) {
425 return dc.value;
426 }
427 }
428
429 return absl::NotFoundError("Unknown duration constant");
430}
431
432absl::StatusOr<MusicCommand> AsmImporter::ResolveInstrumentMacro(
433 const std::string& macro_name) {
434 for (const auto& mapping : kInstrumentMacroImport) {
435 if (macro_name == mapping.macro) {
436 MusicCommand cmd;
437 cmd.opcode = 0xE0; // SetInstrument
438 cmd.params[0] = mapping.id;
439 return cmd;
440 }
441 }
442 return absl::NotFoundError("Unknown instrument macro");
443}
444
445absl::StatusOr<MusicCommand> AsmImporter::ResolveCommandMacro(
446 const std::string& macro_name, const std::vector<uint8_t>& params) {
447 for (const auto& mapping : kCommandMacros) {
448 if (macro_name == mapping.name) {
449 MusicCommand cmd;
450 cmd.opcode = mapping.opcode;
451 for (size_t i = 0; i < params.size() && i < 3; ++i) {
452 cmd.params[i] = params[i];
453 }
454 return cmd;
455 }
456 }
457 return absl::NotFoundError("Unknown command macro");
458}
459
460bool AsmImporter::ParseMacroCall(const std::string& call,
461 std::string& macro_name,
462 std::vector<std::string>& params) {
463 // Format: %MacroName(param1, param2, ...)
464 if (call.empty() || call[0] != '%') {
465 return false;
466 }
467
468 size_t paren_start = call.find('(');
469 if (paren_start == std::string::npos) {
470 // Macro without parameters
471 macro_name = call.substr(1);
472 return true;
473 }
474
475 macro_name = call.substr(1, paren_start - 1);
476
477 size_t paren_end = call.find(')', paren_start);
478 if (paren_end == std::string::npos) {
479 return false;
480 }
481
482 std::string params_str =
483 call.substr(paren_start + 1, paren_end - paren_start - 1);
484 if (!params_str.empty()) {
485 std::vector<std::string> parts = absl::StrSplit(params_str, ',');
486 for (const auto& p : parts) {
487 params.push_back(Trim(p));
488 }
489 }
490
491 return true;
492}
493
494absl::StatusOr<uint8_t> AsmImporter::ParseHexValue(const std::string& value) {
495 if (value.empty()) {
496 return absl::InvalidArgumentError("Empty value");
497 }
498
499 try {
500 if (value[0] == '$') {
501 return static_cast<uint8_t>(std::stoul(value.substr(1), nullptr, 16));
502 } else if (value.length() >= 2 && value.substr(0, 2) == "0x") {
503 return static_cast<uint8_t>(std::stoul(value.substr(2), nullptr, 16));
504 } else if (std::isdigit(value[0])) {
505 return static_cast<uint8_t>(std::stoul(value, nullptr, 10));
506 }
507 } catch (...) {
508 return absl::InvalidArgumentError(
509 absl::StrFormat("Invalid hex value: %s", value));
510 }
511
512 return absl::InvalidArgumentError(
513 absl::StrFormat("Invalid hex value: %s", value));
514}
515
516std::string AsmImporter::Trim(const std::string& s) {
517 size_t start = s.find_first_not_of(" \t\r\n");
518 if (start == std::string::npos)
519 return "";
520 size_t end = s.find_last_not_of(" \t\r\n");
521 return s.substr(start, end - start + 1);
522}
523
524} // namespace music
525} // namespace zelda3
526} // 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