Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/vgmparse/__pycache__
17 changes: 17 additions & 0 deletions .project
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>vgmparse</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.python.pydev.PyDevBuilder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.python.pydev.pythonNature</nature>
</natures>
</projectDescription>
8 changes: 8 additions & 0 deletions .pydevproject
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<?eclipse-pydev version="1.0"?><pydev_project>
<pydev_property name="org.python.pydev.PYTHON_PROJECT_INTERPRETER">Default</pydev_property>
<pydev_property name="org.python.pydev.PYTHON_PROJECT_VERSION">python 2.7</pydev_property>
<pydev_pathproperty name="org.python.pydev.PROJECT_SOURCE_PATH">
<path>/${PROJECT_DIR_NAME}</path>
</pydev_pathproperty>
</pydev_project>
11 changes: 10 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
A Python module for parsing [VGM (Video Game Music)](https://en.wikipedia.org/wiki/VGM_(file_format))
files. `.vgm` and `.vgz` (Gzip compressed) files are supported.

Currently, only version 1.50 of the VGM specification is supported.
Currently, only versions 1.01 and 1.50 of the VGM specification are supported.

## Installation
The `vgmparse` module can be installed directly from GitHub using `pip`:
Expand Down Expand Up @@ -79,3 +79,12 @@ then seek and read like a file:
@>>> vgm_data.data_block.read(5)
b'\x82\x88\x8a\x8a\x88'
```

### Saving a VGM file

You can save the VGM back into a file by using the `.save()` method:

```
@>>> vgm_out = open('newfile.vgm', 'wb')
@>>> vgm_data.save(vgm_out)
```
Binary file not shown.
29 changes: 29 additions & 0 deletions test/test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import unittest
import vgmparse
import tempfile, time

class TestParsing(unittest.TestCase):

def test_vgm_1_01(self):
with open('Alex Kidd in Miracle World - 01 - Title Screen.vgm', 'rb') as f:
file_data = f.read()
parser = vgmparse.Parser(file_data)

# The first command is a GG Stereo command
self.assertEqual(0x4F, ord(parser.command_list[0]['command']))
self.assertEqual(0xFF, ord(parser.command_list[0]['data']))

# The second command is a SN76496 Latch/Data command
self.assertEqual(0x50, ord(parser.command_list[1]['command']))
self.assertEqual(0x80, ord(parser.command_list[1]['data']))

def test_save(self):
with open('Alex Kidd in Miracle World - 01 - Title Screen.vgm', 'rb') as f:
file_data = f.read()
parser = vgmparse.Parser(file_data)

with open('{0}/test-save-{1}.vgm'.format(tempfile.gettempdir(), time.time()), 'wb') as o:
parser.save(o)

if __name__ == '__main__':
unittest.main()
125 changes: 77 additions & 48 deletions vgmparse/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,41 +18,39 @@ class Parser:

# Supported VGM versions
supported_ver_list = [
0x00000101,
0x00000150,
]

# VGM metadata offsets
metadata_offsets = {
# Version 1.50
0x00000150: {
'vgm_ident': {'offset': 0x00, 'size': 4, 'type_format': None},
'eof_offset': {'offset': 0x04, 'size': 4, 'type_format': '<I'},
'version': {'offset': 0x08, 'size': 4, 'type_format': '<I'},
'sn76489_clock': {'offset': 0x0c, 'size': 4, 'type_format': '<I'},
'ym2413_clock': {'offset': 0x10, 'size': 4, 'type_format': '<I'},
'gd3_offset': {'offset': 0x14, 'size': 4, 'type_format': '<I'},
'total_samples': {'offset': 0x18, 'size': 4, 'type_format': '<I'},
'loop_offset': {'offset': 0x1c, 'size': 4, 'type_format': '<I'},
'loop_samples': {'offset': 0x20, 'size': 4, 'type_format': '<I'},
'rate': {'offset': 0x24, 'size': 4, 'type_format': '<I'},
'sn76489_feedback': {
'offset': 0x28,
'size': 2,
'type_format': '<H',
},
'sn76489_shift_register_width': {
'offset': 0x2a,
'size': 1,
'type_format': 'B',
},
'ym2612_clock': {'offset': 0x2c, 'size': 4, 'type_format': '<I'},
'ym2151_clock': {'offset': 0x30, 'size': 4, 'type_format': '<I'},
'vgm_data_offset': {
'offset': 0x34,
'size': 4,
'type_format': '<I',
},
}
'vgm_ident': {'offset': 0x00, 'size': 4, 'type_format': None},
'eof_offset': {'offset': 0x04, 'size': 4, 'type_format': '<I'},
'version': {'offset': 0x08, 'size': 4, 'type_format': '<I'},
'sn76489_clock': {'offset': 0x0c, 'size': 4, 'type_format': '<I'},
'ym2413_clock': {'offset': 0x10, 'size': 4, 'type_format': '<I'},
'gd3_offset': {'offset': 0x14, 'size': 4, 'type_format': '<I'},
'total_samples': {'offset': 0x18, 'size': 4, 'type_format': '<I'},
'loop_offset': {'offset': 0x1c, 'size': 4, 'type_format': '<I'},
'loop_samples': {'offset': 0x20, 'size': 4, 'type_format': '<I'},
'rate': {'offset': 0x24, 'size': 4, 'type_format': '<I'},
'sn76489_feedback': {
'offset': 0x28,
'size': 2,
'type_format': '<H',
},
'sn76489_shift_register_width': {
'offset': 0x2a,
'size': 1,
'type_format': 'B',
},
'ym2612_clock': {'offset': 0x2c, 'size': 4, 'type_format': '<I'},
'ym2151_clock': {'offset': 0x30, 'size': 4, 'type_format': '<I'},
'vgm_data_offset': {
'offset': 0x34,
'size': 4,
'type_format': '<I',
},
}

def __init__(self, vgm_data):
Expand Down Expand Up @@ -81,7 +79,7 @@ def parse_commands(self):
# Seek to the start of the VGM data
self.data.seek(
self.metadata['vgm_data_offset'] +
self.metadata_offsets[self.metadata['version']]['vgm_data_offset']['offset']
self.metadata_offsets['vgm_data_offset']['offset']
)

while True:
Expand Down Expand Up @@ -164,7 +162,7 @@ def parse_gd3(self):
# Seek to the start of the GD3 data
self.data.seek(
self.metadata['gd3_offset'] +
self.metadata_offsets[self.metadata['version']]['gd3_offset']['offset']
self.metadata_offsets['gd3_offset']['offset']
)

# Skip 8 bytes ('Gd3 ' string and 4 byte version identifier)
Expand Down Expand Up @@ -220,21 +218,20 @@ def parse_metadata(self):
self.metadata = {}

# Iterate over the offsets and parse the metadata
for version, offsets in self.metadata_offsets.items():
for value, offset_data in offsets.items():

# Seek to the data location and read the data
self.data.seek(offset_data['offset'])
data = self.data.read(offset_data['size'])

# Unpack the data if required
if offset_data['type_format'] is not None:
self.metadata[value] = struct.unpack(
offset_data['type_format'],
data,
)[0]
else:
self.metadata[value] = data
for value, offset_data in self.metadata_offsets.items():

# Seek to the data location and read the data
self.data.seek(offset_data['offset'])
data = self.data.read(offset_data['size'])

# Unpack the data if required
if offset_data['type_format'] is not None:
self.metadata[value] = struct.unpack(
offset_data['type_format'],
data,
)[0]
else:
self.metadata[value] = data

# Seek back to the original position in the VGM data
self.data.seek(original_pos)
Expand Down Expand Up @@ -266,4 +263,36 @@ def validate_vgm_data(self):

def validate_vgm_version(self):
if self.metadata['version'] not in self.supported_ver_list:
raise VersionError('VGM version is not supported')
raise VersionError('VGM version %s is not supported' % self.version_str())

def version_str(self):
version = self.metadata['version']
return '%01x.%02x' % (version >> 8, version & 0xFF)

def save(self, buffer):
# Pre-fill the header with zeroes
buffer.seek(0)
buffer.write(b'\0' * 0x40)

# Iterate over the offsets and write the metadata
for value, offset_data in self.metadata_offsets.items():

# Seek to the data location and read the data
buffer.seek(offset_data['offset'])

# Unpack the data if required
if offset_data['type_format'] is not None:
data = struct.pack(
offset_data['type_format'],
self.metadata[value],
)
buffer.write(data)
else:
buffer.write(self.metadata[value])

# Write out the commands
for cmd in self.command_list:
buffer.write(cmd['command'])
if cmd['data']:
buffer.write(cmd['data'])