audio: Introduce audio subsystem.

Based on VGM-like playback with VGM files as input.

Including a "silence" track which doubles as a stress test, updating all chip
registers every other frame.
M openmsx.tcl +2 -2
@@ 45,12 45,12 @@ debug set_bp -once [symbol Scene_Run] {[
 	# profile::section_begin_bp vdprdy [expr [symbol VDPCommand_ExecuteS2HL.WaitReady] - 2] {[my_slot_pc]}
 	# profile::section_end_bp vdprdy [expr [symbol VDPCommand_ExecuteS2HL.WaitReady] + 5] {[my_slot_pc]}
 	profile::section_scope_bp trig [symbol Scene_UpdateTriggers] {[my_slot_pc]}
-	profile::section_scope_bp mus [symbol Scene_UpdateMusic] {[my_slot_pc]}
 	profile::section_scope_bp sync [symbol SplitHandler_WaitIY] {[my_slot_pc]}
 	profile::section_irq_bp int
+	profile::section_scope_bp mus [symbol Application_instance.engine.audio.Tick]
 	profile::section_vdp_bp vdp
 
-	profile::section_exclude int [profile::section_list {int vdp frame}]
+	profile::section_exclude int [profile::section_list {int vdp frame mus}]
 	profile::section_exclude sync frame
 }
 

          
A => res/audio/silence.vgm +0 -0

M res/resources.json +6 -0
@@ 18,5 18,11 @@ 
 			"name": "May",
 			"path": "sprites/may.json"
 		}
+	],
+	"audio": [
+		{
+			"name": "Silence",
+			"path": "audio/silence.vgm"
+		}
 	]
 }

          
M src/Application.asm +1 -1
@@ 7,7 7,7 @@ Application: MACRO
 	engine:
 		Engine
 	scene:
-		Scene FieldMap_instance, UIImage_instance
+		Scene FieldMap_instance, UIImage_instance, Silence_track
 	ENDM
 
 Application_Main:

          
M src/Engine.asm +28 -0
@@ 34,6 34,8 @@ Engine: MACRO
 		Interrupt
 	spriteColorCopy:
 		VRAMTableCopy 1EC00H, 1E800H, SpriteColorTable_SIZE
+	audio:
+		Audio
 	ENDM
 
 ; ix = this

          
@@ 44,6 46,11 @@ Engine_Construct:
 	call Input_Construct
 	pop ix
 
+	push ix
+	call Engine_GetAudio
+	call Audio_Construct
+	pop ix
+
 	ld de,Engine.patternNameTable
 	add ix,de
 	call PatternNameTable_Construct

          
@@ 85,6 92,15 @@ Engine_Construct:
 	call Engine_GetVideo
 	call Video_Construct
 	pop ix
+
+	push ix
+	call Engine_GetInterrupt_IY
+	ld e,ixl
+	ld d,ixh
+	ld hl,Engine.audio.Tick
+	add hl,de
+	call Interrupt_SetFrameHandlerIY
+	pop ix
 	ret
 
 ; ix = this

          
@@ 99,6 115,11 @@ Engine_Destruct:
 	call Engine_GetInterrupt
 	call Interrupt_Destruct
 	pop ix
+
+	push ix
+	call Engine_GetAudio
+	call Audio_Destruct
+	pop ix
 	ret
 
 ; iy = this

          
@@ 222,6 243,13 @@ Engine_GetVideoIY:
 	add iy,de
 	ret
 
+; ix = this
+; ix <- audio
+Engine_GetAudio:
+	ld de,Engine.audio
+	add ix,de
+	ret
+
 ; iy = this
 Engine_StartIY:
 	push iy

          
M src/ROM.asm +1 -0
@@ 44,6 44,7 @@ RAM_RESIDENT: equ RAM
 	INCLUDE "Math.asm"
 	INCLUDE "Vector.asm"
 	INCLUDE "Engine.asm"
+	INCLUDE "audio/Audio.asm"
 	INCLUDE "video/Video.asm"
 	INCLUDE "split/Split.asm"
 	INCLUDE "Interrupt.asm"

          
M src/Scene.asm +23 -10
@@ 1,11 1,13 @@ 
 ;
 ; A scene.
 ;
-Scene: MACRO ?tilemap, ?uiImage
+Scene: MACRO ?tilemap, ?uiImage, ?track
 	engine:
 		dw 0
 	tileMap:
 		dw ?tilemap
+	track:
+		dw ?track
 	exiting:
 		db 0
 	exit:

          
@@ 95,6 97,15 @@ Scene_Construct:
 	ret
 
 ; ix = this
+; ix <- engine
+Scene_GetEngine:
+	ld e,(ix + Scene.engine)
+	ld d,(ix + Scene.engine + 1)
+	ld ixl,e
+	ld ixh,d
+	ret
+
+; ix = this
 ; iy <- engine
 Scene_GetEngine_IY:
 	ld e,(ix + Scene.engine)

          
@@ 200,6 211,17 @@ Scene_Start:
 	call Scene_GetMapView
 	call MapView_Start
 	pop ix
+
+	push ix
+	ld e,(ix + Scene.track)
+	ld d,(ix + Scene.track + 1)
+	push de
+	call Scene_GetEngine
+	call Engine_GetAudio
+	pop de
+	call Audio_SetTrack
+	call Audio_Play
+	pop ix
 	ret
 
 ; ix = this

          
@@ 252,7 274,6 @@ Scene_Update:
 	call Scene_DrawBitmapPrimary
 	call Engine_WaitDisplayedIY  ; avoid having to triple-buffer the SAT
 	call Engine_CopySpriteColorToSecondary
-	call Scene_UpdateMusic
 	call Scene_UpdateGameFrame
 	call Scene_DrawSpriteAttributes
 	call Engine_SubmitSecondaryIY

          
@@ 508,11 529,3 @@ Scene_UpdateTriggers:
 	call Dialog_UpdateTriggers
 	pop ix
 	ret
-
-; ix = this
-; iy = engine
-Scene_UpdateMusic:
-	ld b,0
-	djnz $
-	djnz $
-	ret

          
A => src/audio/Audio.asm +151 -0
@@ 0,0 1,151 @@ 
+;
+; Audio subsystem
+;
+	INCLUDE "AudioTrack.asm"
+	INCLUDE "PSG.asm"
+	INCLUDE "MSXMusic.asm"
+
+Audio: MACRO
+	; Modifies: af, bc, hl
+	Tick:
+	playing: ret
+		ld a,0
+	bank: equ $ - 1
+		ld (ROMMapper_instance.page8000.BANK_SELECT),a
+		ld hl,0
+	address: equ $ - 2
+		ld a,1
+	wait: equ $ - 1
+		dec a
+		call z,Audio_Process
+		ld (wait),a
+		ld (address),hl
+		ld a,(ROMMapper_instance.page8000.bank)
+		ld (ROMMapper_instance.page8000.BANK_SELECT),a
+		ret
+
+	psg:
+		PSG
+	msxMusic:
+		MSXMusic
+	ENDM
+
+; ix = this
+Audio_Construct:
+	push ix
+	call Audio_GetPSG
+	call PSG_Construct
+	pop ix
+	push ix
+	call Audio_GetMSXMusic
+	call MSXMusic_Construct
+	pop ix
+	ret
+
+; ix = this
+Audio_Destruct:
+	call Audio_Stop
+	push ix
+	call Audio_GetPSG
+	call PSG_Destruct
+	pop ix
+	push ix
+	call Audio_GetMSXMusic
+	call MSXMusic_Destruct
+	pop ix
+	ret
+
+; ix = this
+Audio_GetPSG:
+	ld de,Audio.psg
+	add ix,de
+	ret
+
+; ix = this
+Audio_GetMSXMusic:
+	ld de,Audio.msxMusic
+	add ix,de
+	ret
+
+; de = track
+; ix = this
+Audio_SetTrack:
+	push iy
+	ld iyl,e
+	ld iyh,d
+	ld a,(iy + AudioTrack.commands.bank)
+	ld (ix + Audio.bank),a
+	ld a,(iy + AudioTrack.commands.address)
+	ld (ix + Audio.address),a
+	ld a,(iy + AudioTrack.commands.address + 1)
+	ld (ix + Audio.address + 1),a
+	pop iy
+	ld (ix + Audio.wait),1
+	ret
+
+; ix = this
+Audio_Play:
+	ld (ix + Audio.playing),00H  ; nop
+	ret
+
+; ix = this
+Audio_Stop:
+	ld (ix + Audio.playing),0C9H  ; ret
+	ret
+
+; ix = this
+Audio_Tick:
+	jp ix
+
+; hl = command list
+; a <- wait
+; hl <- next command
+Audio_Process:
+	ld a,(hl)
+	cp 40H
+	jr c,Audio_ProcessPSG
+	cp 80H
+	jr c,Audio_ProcessOPLL
+	cp 0FFH
+	jr c,Audio_ProcessWait
+	jr Audio_ProcessEnd
+
+; hl = command list
+; a <- wait
+; hl <- next command
+Audio_ProcessPSG:
+	ld c,PSG_DATA
+	inc hl
+	out (PSG_ADDRESS),a
+	outi
+	jr Audio_Process
+
+; hl = command list
+; a <- wait
+; hl <- next command
+Audio_ProcessOPLL:
+	sub 40H
+	inc hl
+	ld c,MSXMusic_DATA
+	out (MSXMusic_ADDRESS),a  ; wait 12 cycles
+	outi                      ; wait 84 cycles
+	jr Audio_Process
+
+; hl = command list
+; a <- wait
+; hl <- next command
+Audio_ProcessWait:
+	sub 80H
+	inc hl
+	ret
+
+; hl = command list
+; a <- wait
+; hl <- next command
+Audio_ProcessEnd:
+	inc hl
+	ld a,(hl)
+	inc hl
+	ld h,(hl)
+	ld l,a
+	jr Audio_Process

          
A => src/audio/AudioTrack.asm +38 -0
@@ 0,0 1,38 @@ 
+;
+; Audio track
+;
+AudioTrack: MACRO ?commands
+	commands:
+		ROMAddress ?commands, 8000H
+	ENDM
+
+AudioCommand: MACRO ?command
+	command:
+		db ?command
+	ENDM
+
+AudioCommandPSG: MACRO ?address, ?value
+	command:
+		AudioCommand ?address
+	value:
+		db ?value
+	ENDM
+
+AudioCommandOPLL: MACRO ?address, ?value
+	command:
+		AudioCommand 40H + ?address
+	value:
+		db ?value
+	ENDM
+
+AudioCommandWait: MACRO ?time
+	command:
+		AudioCommand 80H + ?time
+	ENDM
+
+AudioCommandEnd: MACRO ?loop
+	command:
+		AudioCommand 0FFH
+	loop:
+		dw ?loop & 1FFFH | 8000H
+	ENDM

          
A => src/audio/MSXMusic.asm +146 -0
@@ 0,0 1,146 @@ 
+;
+; MSX-MUSIC YM2413 OPLL driver
+;
+MSXMusic_ADDRESS: equ 7CH
+MSXMusic_DATA: equ 7DH
+MSXMusic_ID_ADDRESS: equ 4018H
+MSXMusic_ENABLE_ADDRESS: equ 7FF6H
+
+MSXMusic: MACRO
+	found:
+		db 0
+	slot:
+		db 0
+	internal:
+		db 0
+	ENDM
+
+; ix = this
+MSXMusic_Construct:
+	call MSXMusic_Detect
+	ret nc
+	ld (ix + MSXMusic.found),1
+	ld (ix + MSXMusic.slot),a
+	ld (ix + MSXMusic.internal),b
+	call MSXMusic_Enable
+	jr MSXMusic_Reset
+
+; ix = this
+MSXMusic_Destruct:
+	bit 0,(ix + MSXMusic.found)
+	ret z
+	call MSXMusic_Mute
+	jr MSXMusic_Disable
+
+; e = register
+; d = value
+; ix = this
+MSXMusic_WriteRegister:
+	ld a,e
+	out (MSXMusic_ADDRESS),a  ; wait 12 cycles
+	ld a,d
+	out (MSXMusic_DATA),a     ; wait 84 cycles
+	ex (sp),ix
+	ex (sp),ix
+	ret
+
+; ix = this
+MSXMusic_Enable:
+	bit 0,(ix + MSXMusic.internal)
+	ret nz
+	ld a,(ix + MSXMusic.slot)
+	ld hl,MSXMusic_ENABLE_ADDRESS
+	call Memory_ReadSlot
+	set 0,a
+	ld e,a
+	ld a,(ix + MSXMusic.slot)
+	ld hl,MSXMusic_ENABLE_ADDRESS
+	jp Memory_WriteSlot
+
+; ix = this
+MSXMusic_Disable:
+	bit 0,(ix + MSXMusic.internal)
+	ret nz
+	ld a,(ix + MSXMusic.slot)
+	ld hl,MSXMusic_ENABLE_ADDRESS
+	call Memory_ReadSlot
+	res 0,a
+	ld e,a
+	ld a,(ix + MSXMusic.slot)
+	ld hl,MSXMusic_ENABLE_ADDRESS
+	jp Memory_WriteSlot
+
+; b = count
+; e = register base
+; d = value
+; ix = this
+MSXMusic_FillRegisters:
+	push bc
+	push de
+	call MSXMusic_WriteRegister
+	in a,(09AH)  ; R800 wait: ~62 cycles - 5 (ret)
+	pop de
+	pop bc
+	inc e
+	djnz MSXMusic_FillRegisters
+	ret
+
+; ix = this
+MSXMusic_Mute:
+	ld de,000EH
+	call MSXMusic_WriteRegister  ; rhythm off
+	ld de,0F07H
+	call MSXMusic_WriteRegister  ; max carrier release rate
+	ld b,9
+	ld de,0F30H
+	call MSXMusic_FillRegisters  ; instrument 0, min volume
+	ld b,9
+	ld de,0010H
+	call MSXMusic_FillRegisters  ; frequency 0
+	ld b,9
+	ld de,0020H
+	jr MSXMusic_FillRegisters    ; key off
+
+; ix = this
+MSXMusic_Reset:
+	ld b,39H
+	ld de,0000H
+	jr MSXMusic_FillRegisters
+
+; ix = this
+; f <- c: found
+; a <- slot
+; b <- 0: external, -1: internal
+MSXMusic_Detect:
+	ld hl,MSXMusic_MatchInternalID
+	call Memory_SearchSlots
+	ld b,-1
+	ret c
+	ld hl,MSXMusic_MatchExternalID
+	call Memory_SearchSlots
+	ld b,0
+	ret
+
+; a = slot id
+; f <- c: found
+MSXMusic_MatchInternalID:
+	ld de,MSXMusic_internalId
+	ld hl,MSXMusic_ID_ADDRESS
+	ld bc,8
+	jp Memory_MatchSlotString
+
+; a = slot id
+; ix = this
+; f <- c: found
+MSXMusic_MatchExternalID:
+	ld de,MSXMusic_externalId
+	ld hl,MSXMusic_ID_ADDRESS + 4
+	ld bc,4
+	jp Memory_MatchSlotString
+
+;
+MSXMusic_internalId:
+	db "APRLOPLL"
+
+MSXMusic_externalId:
+	db "OPLL"

          
A => src/audio/PSG.asm +86 -0
@@ 0,0 1,86 @@ 
+;
+; AY-3-8910 PSG / YM2149 SSG driver
+;
+PSG_ADDRESS: equ 0A0H
+PSG_DATA: equ 0A1H
+PSG_READ: equ 0A2H
+
+PSG: MACRO
+	found:
+		db 0
+	ENDM
+
+; ix = this
+; iy = drivers
+PSG_Construct:
+	call PSG_Detect
+	ret nc
+	ld (ix + PSG.found),1
+	jr PSG_Reset
+
+; ix = this
+PSG_Destruct:
+	bit 0,(ix + PSG.found)
+	ret nc
+	jr PSG_Reset
+
+; e = register
+; d = value
+; ix = this
+PSG_WriteRegister:
+	ld a,e
+	di
+	out (PSG_ADDRESS),a
+	ld a,d
+	ei
+	out (PSG_DATA),a
+	ret
+
+; a = register
+; a = value
+; ix = this
+PSG_ReadRegister:
+	di
+	out (PSG_ADDRESS),a
+	ei
+	in a,(PSG_READ)
+	ret
+
+; b = count
+; e = register
+; d = value
+; ix = this
+PSG_FillRegisters:
+	call PSG_WriteRegister
+	inc e
+	djnz PSG_FillRegisters
+	ret
+
+; ix = this
+PSG_Reset:
+	ld b,6
+	ld de,0008H
+	call PSG_FillRegisters
+	ld de,8007H
+	call PSG_WriteRegister
+	ld b,7
+	ld de,0000H
+	jr PSG_FillRegisters
+
+; ix = this
+; f <- c: found
+PSG_Detect:
+	ld de,1200H
+	call PSG_WriteRegister
+	ld de,3402H
+	call PSG_WriteRegister
+	ld a,0
+	call PSG_ReadRegister
+	xor 12H
+	ret nz
+	ld a,2
+	call PSG_ReadRegister
+	xor 34H
+	ret nz
+	scf
+	ret

          
A => tools/audio.js +178 -0
@@ 0,0 1,178 @@ 
+"use strict"
+const fs = require('fs');
+const { checkTypes, toAsmHex } = require('./utils');
+
+class AudioTrack {
+	constructor(name) {
+		checkTypes(arguments, "string");
+		this.name = name;
+		this.commands = [];
+		this.loop = -1;
+	}
+
+	addCommand(command) {
+		checkTypes(arguments, AudioCommand);
+		if (command instanceof AudioCommandWait && this.commands.length > 0) {
+			const lastCommand = this.commands[this.commands.length - 1];
+			if (lastCommand instanceof AudioCommandWait) {
+				lastCommand.time += command.time;
+				return;
+			}
+		} else if (command instanceof AudioCommandLoop) {
+			this.loop = this.commands.length;
+		}
+		this.commands.push(command);
+	}
+
+	toAsm(frameRate) {
+		checkTypes(arguments, "number");
+		const lines = [];
+		lines.push(`${this.name}_track:`);
+		lines.push(`\tAudioTrack ${this.name}_commands`);
+		lines.push("");
+		lines.push("\tSECTION ROM_DATA");
+		lines.push("\tALIGN 2000H");
+		lines.push(`${this.name}_commands:`);
+		let time = 0.5 / 44100 * frameRate;  // slight offset to avoid rounding errors
+		for (const command of this.commands) {
+			if (command instanceof AudioCommandPSG) {
+				lines.push(`\tAudioCommandPSG ${toAsmHex(command.address)}, ${toAsmHex(command.value)}`);
+			} else if (command instanceof AudioCommandOPLL) {
+				lines.push(`\tAudioCommandOPLL ${toAsmHex(command.address)}, ${toAsmHex(command.value)}`);
+			} else if (command instanceof AudioCommandWait) {
+				const lastTime = time;
+				time += command.time * frameRate;
+				for (let frames = Math.floor(time) - Math.floor(lastTime); frames > 0; frames -= 0x7F) {
+					lines.push(`\tAudioCommandWait ${Math.min(frames, 0x7F)}`);
+				}
+			} else if (command instanceof AudioCommandLoop) {
+				lines.push(`${this.name}_commands_loop:`);
+			} else if (command instanceof AudioCommandEnd) {
+				if (this.loop < 0) {
+					lines.push(`${this.name}_commands_loop:`);
+					lines.push(`\tAudioCommandWait 1`);
+				}
+				lines.push(`\tAudioCommandEnd ${this.name}_commands_loop`);
+			} else {
+				throw new Error("Unrecognised command.");
+			}
+		}
+		lines.push("\tENDS");
+		return lines.join("\n");
+	}
+}
+
+class AudioCommand {
+	constructor() {
+	}
+}
+
+class AudioCommandPSG extends AudioCommand {
+	constructor(address, value) {
+		checkTypes(arguments, "number", "number");
+		if (address >= 0x0E)
+			throw new Error("Invalid PSG register.");
+		super();
+		this.address = address;
+		this.value = value;
+	}
+}
+
+class AudioCommandOPLL extends AudioCommand {
+	constructor(address, value) {
+		checkTypes(arguments, "number", "number");
+		if (address >= 0x40)
+			throw new Error("Invalid OPLL register.");
+		super();
+		this.address = address;
+		this.value = value;
+	}
+}
+
+class AudioCommandWait extends AudioCommand {
+	constructor(time) {
+		checkTypes(arguments, "number");
+		super();
+		this.time = time;
+	}
+}
+
+class AudioCommandLoop extends AudioCommand {
+	constructor() {
+		super();
+	}
+}
+
+class AudioCommandEnd extends AudioCommand {
+	constructor() {
+		super();
+	}
+}
+
+class VGMLoader {
+	constructor(name, path) {
+		checkTypes(arguments, "string", "string");
+		this.name = name;
+		this.path = path;
+	}
+
+	async loadTrack() {
+		const vgm = await fs.promises.readFile(this.path);
+		const getByte = address => vgm[address];
+		const getWord = address => getByte(address) | getByte(address + 1) << 8;
+		const getDoubleWord = address => getWord(address) | getWord(address + 2) << 16;
+		if (getDoubleWord(0x00) != 0x206d6756)
+			throw new Error("Not a VGM file.");
+		const version = getDoubleWord(0x08);
+		const loop = getDoubleWord(0x1C) ? getDoubleWord(0x1C) + 0x1C : 0;
+		const headerSize = version >= 0x150 ? getDoubleWord(0x34) + 0x34 : 0x40;
+		const frequency = 44100;
+		const track = new AudioTrack(this.name);
+		let address = headerSize;
+		while (address < vgm.length) {
+			if (address == loop)
+				track.addCommand(new AudioCommandLoop());
+			const type = getByte(address++);
+			if (type == 0x51) {
+				const register = getByte(address++);
+				const value = getByte(address++);
+				if (register < 0x3F) {
+					track.addCommand(new AudioCommandOPLL(register, value));
+				}
+			} else if (type == 0xA0) {
+				const register = getByte(address++);
+				let value = getByte(address++);
+				if (register < 0x0E) {
+					if (register == 0x07)
+						value = value & 0x3F | 0x80;
+					track.addCommand(new AudioCommandPSG(register, value));
+				}
+			} else if (type == 0x61) {
+				const time = getWord(address) / frequency;
+				address += 2;
+				track.addCommand(new AudioCommandWait(time));
+			} else if (type == 0x62) {
+				track.addCommand(new AudioCommandWait(735 / frequency));
+			} else if (type == 0x63) {
+				track.addCommand(new AudioCommandWait(882 / frequency));
+			} else if (type == 0x66) {
+				track.addCommand(new AudioCommandEnd());
+				break;
+			} else if (type >= 0x70 && type < 0x80) {
+				track.addCommand(new AudioCommandWait(((type & 15) + 1) / frequency));
+			} else {
+				throw new Error(`Unsupported command ${type.toString(16)} at address ${(address - 1).toString(16)}`);
+			}
+		}
+		return track;
+	}
+}
+
+exports.AudioTrack = AudioTrack;
+exports.AudioCommand = AudioCommand;
+exports.AudioCommandPSG = AudioCommandPSG;
+exports.AudioCommandOPLL = AudioCommandOPLL;
+exports.AudioCommandWait = AudioCommandWait;
+exports.AudioCommandLoop = AudioCommandLoop;
+exports.AudioCommandEnd = AudioCommandEnd;
+exports.VGMLoader = VGMLoader;

          
M tools/resources.js +18 -3
@@ 5,6 5,7 @@ const { checkTypes } = require('./utils'
 const { Tmx } = require('./tilemaps');
 const { PNGLoader } = require('./images');
 const { AsepriteSheet } = require('./sprites');
+const { VGMLoader } = require('./audio');
 
 async function main() {
 	if (process.argv.length < 3) {

          
@@ 20,13 21,14 @@ async function main() {
 }
 
 class Resources {
-	constructor(screenMode, frameRate, tileMaps, images, sprites) {
-		checkTypes(arguments, "number", "number", Array, Array, Array);
+	constructor(screenMode, frameRate, tileMaps, images, sprites, audio) {
+		checkTypes(arguments, "number", "number", Array, Array, Array, Array);
 		this.screenMode = screenMode;
 		this.frameRate = frameRate;
 		this.tileMaps = tileMaps;
 		this.images = images;
 		this.sprites = sprites;
+		this.audio = audio;
 	}
 
 	optimize() {

          
@@ 46,6 48,9 @@ class Resources {
 		for (const sprite of this.sprites) {
 			lines.push(sprite.toAsm(this.frameRate));
 		}
+		for (const track of this.audio) {
+			lines.push(track.toAsm(this.frameRate));
+		}
 		return lines.join("\n\n");
 	}
 }

          
@@ 64,7 69,8 @@ class ResourcesLoader {
 			...await Promise.all([
 				this.parseTileMaps(json.tilemaps),
 				this.parseImages(json.images),
-				this.parseSprites(json.sprites)
+				this.parseSprites(json.sprites),
+				this.parseAudio(json.audio)
 			])
 		);
 	}

          
@@ 94,6 100,15 @@ class ResourcesLoader {
 			return new AsepriteSheet(jsonSprite.name, spritePath).parse();
 		}));
 	}
+
+	async parseAudio(jsonAudio) {
+		checkTypes(arguments, Array);
+		return Promise.all(jsonAudio.map(async jsonTrack => {
+			const trackPath = path.resolve(path.dirname(this.path), jsonTrack.path);
+			const track = await new VGMLoader(jsonTrack.name, trackPath).loadTrack();
+			return track;
+		}));
+	}
 }
 
 exports.Resources = Resources;

          
M tools/utils.js +6 -0
@@ 1,5 1,10 @@ 
 "use strict"
 
+function toAsmHex(value) {
+	const hex = value.toString(16).toUpperCase();
+	return hex.charCodeAt(0) > 64 ? `0${hex}H` : `${hex}H`;
+}
+
 /**
  * Type checker, intended for use at the start of any function.
  * Primitive types are checked by a string, objects by their constructor.

          
@@ 27,3 32,4 @@ function checkTypes(args) {
 }
 
 exports.checkTypes = checkTypes;
+exports.toAsmHex = toAsmHex;