Last active
July 10, 2024 00:36
-
-
Save McMartin/07f02c287a40ed317df7570071cc1eb7 to your computer and use it in GitHub Desktop.
Read DLS file using Python
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import struct | |
import sys | |
from chunk import Chunk | |
FOURCC_DLS = b'DLS ' | |
FOURCC_DLID = b'dlid' | |
FOURCC_COLH = b'colh' | |
FOURCC_WVPL = b'wvpl' | |
FOURCC_PTBL = b'ptbl' | |
FOURCC_PATH = b'path' | |
FOURCC_wave = b'wave' | |
FOURCC_LINS = b'lins' | |
FOURCC_INS = b'ins ' | |
FOURCC_INSH = b'insh' | |
FOURCC_LRGN = b'lrgn' | |
FOURCC_RGN = b'rgn ' | |
FOURCC_RGNH = b'rgnh' | |
FOURCC_LART = b'lart' | |
FOURCC_ART1 = b'art1' | |
FOURCC_WLNK = b'wlnk' | |
FOURCC_WSMP = b'wsmp' | |
FOURCC_VERS = b'vers' | |
# Generic Sources | |
SRC_NONE = 0x0000 | |
SRC_LFO = 0x0001 | |
SRC_KEYONVELOCITY = 0x0002 | |
SRC_KEYNUMBER = 0x0003 | |
SRC_EG1 = 0x0004 | |
SRC_EG2 = 0x0005 | |
SRC_PITCHWHEEL = 0x0006 | |
# Midi Sources | |
SRC_CC1 = 0x0081 | |
SRC_CC7 = 0x0087 | |
SRC_CC10 = 0x008a | |
SRC_CC11 = 0x008b | |
SRC_RPN0 = None | |
SRC_RPN1 = None | |
SRC_RPN2 = None | |
# Generic Destinations | |
DST_NONE = 0x0000 | |
DST_ATTENUATION = 0x0001 | |
DST_RESERVED = 0x0002 | |
DST_PITCH = 0x0003 | |
DST_PAN = 0x0004 | |
# LFO Destinations | |
DST_LFO_FREQUENCY = 0x0104 | |
DST_LFO_STARTDELAY = 0x0105 | |
# EG1 Destinations | |
DST_EG1_ATTACKTIME = 0x0206 | |
DST_EG1_DECAYTIME = 0x0207 | |
DST_EG1_RESERVED = 0x0208 | |
DST_EG1_RELEASETIME = 0x0209 | |
DST_EG1_SUSTAINLEVEL = 0x020a | |
# EG2 Destinations | |
DST_EG2_ATTACKTIME = 0x030a | |
DST_EG2_DECAYTIME = 0x030b | |
DST_EG2_RESERVED = 0x030c | |
DST_EG2_RELEASETIME = 0x030d | |
DST_EG2_SUSTAINLEVEL = 0x030e | |
TRN_NONE = 0x0000 | |
TRN_CONCAVE = 0x0001 | |
def exact_read(f, size): | |
data = f.read(size) | |
assert len(data) == size | |
return data | |
def read_struct(f, fmt): | |
return struct.unpack(fmt, f.read(struct.calcsize(fmt))) | |
def make_chunk(f): | |
return Chunk(f, bigendian=False) | |
def ignore_chunk(chunk, level): | |
print(' ' * level + '{} (unknown chunk, ignored)'.format(chunk.getname())) | |
chunk.skip() | |
def process_chunk_ICMT(chunk, level): # noqa | |
print(' ' * level + 'ICMT (comment: {})'.format(chunk.read()[:-1].decode('ASCII'))) | |
def process_chunk_ICOP(chunk, level): # noqa | |
print(' ' * level + 'ICOP (copyright: {})'.format(chunk.read()[:-1].decode('ASCII'))) | |
def process_chunk_IENG(chunk, level): # noqa | |
print(' ' * level + 'IENG (engineer: {})'.format(chunk.read()[:-1].decode('ASCII'))) | |
def process_chunk_INAM(chunk, level): # noqa | |
print(' ' * level + 'INAM (name: {})'.format(chunk.read()[:-1].decode('ASCII'))) | |
def process_chunk_ISFT(chunk, level): # noqa | |
print(' ' * level + 'ISFT (software: {})'.format(chunk.read()[:-1].decode('ASCII'))) | |
def process_chunk_ISBJ(chunk, level): # noqa | |
print(' ' * level + 'ISBJ (subject: {})'.format(chunk.read()[:-1].decode('ASCII'))) | |
def process_chunk_LIST(chunk, level): # noqa | |
list_type = chunk.read(4) | |
print(' ' * level + 'LIST - {}'.format(list_type.decode('ASCII'))) | |
list_chunk_size = chunk.getsize() | |
while chunk.tell() < list_chunk_size: | |
item_chunk = make_chunk(chunk) | |
process_chunk_fn = PROCESS_CHUNK_FNS[item_chunk.getname()] | |
process_chunk_fn(item_chunk, level + 2) | |
assert item_chunk.tell() >= item_chunk.getsize() | |
SOURCES = { | |
SRC_NONE: 'None', | |
SRC_LFO: 'LFO', | |
SRC_KEYONVELOCITY: 'KeyOnVelocity', | |
SRC_KEYNUMBER: 'KeyNumber', | |
SRC_EG1: 'EG1', | |
SRC_EG2: 'EG2', | |
SRC_PITCHWHEEL: 'PitchWheel', | |
} | |
CONTROLLERS = { | |
SRC_NONE: 'None', | |
SRC_CC1: 'CC1', | |
SRC_CC7: 'CC7', | |
SRC_CC10: 'CC10', | |
SRC_CC11: 'CC11', | |
} | |
DESTINATIONS = { | |
DST_NONE: 'None', | |
DST_ATTENUATION: 'Attenuation', | |
DST_RESERVED: 'Reserved', | |
DST_PITCH: 'Pitch', | |
DST_PAN: 'Pan', | |
DST_LFO_FREQUENCY: 'LFO_Frequency', | |
DST_LFO_STARTDELAY: 'LFO_StartDelay', | |
DST_EG1_ATTACKTIME: 'EG1_AttackTime', | |
DST_EG1_DECAYTIME: 'EG1_DecayTime', | |
DST_EG1_RESERVED: 'EG1_Reserved', | |
DST_EG1_RELEASETIME: 'EG1_ReleaseTime', | |
DST_EG1_SUSTAINLEVEL: 'EG1_SustainLevel', | |
DST_EG2_ATTACKTIME: 'EG2_AttackTime', | |
DST_EG2_DECAYTIME: 'EG2_DecayTime', | |
DST_EG2_RESERVED: 'EG2_Reserved', | |
DST_EG2_RELEASETIME: 'EG2_ReleaseTime', | |
DST_EG2_SUSTAINLEVEL: 'EG2_SustainLevel', | |
} | |
CONNECTIONS = { | |
# LFO Section | |
(SRC_NONE, SRC_NONE, DST_LFO_FREQUENCY, TRN_NONE): 'LFO Frequency', | |
(SRC_NONE, SRC_NONE, DST_LFO_STARTDELAY, TRN_NONE): 'LFO Start Delay', | |
(SRC_LFO, SRC_NONE, DST_ATTENUATION, TRN_NONE): 'LFO Attenuation Scale', | |
(SRC_LFO, SRC_NONE, DST_PITCH, TRN_NONE): 'LFO Pitch Scale', | |
(SRC_LFO, SRC_CC1, DST_ATTENUATION, TRN_NONE): 'LFO Modw to Attenuation', | |
(SRC_LFO, SRC_CC1, DST_PITCH, TRN_NONE): 'LFO Modw to Pitch', | |
# EG1 Section | |
(SRC_NONE, SRC_NONE, DST_EG1_ATTACKTIME, TRN_NONE): 'EG1 Attack Time', | |
(SRC_NONE, SRC_NONE, DST_EG1_DECAYTIME, TRN_NONE): 'EG1 Decay Time', | |
(SRC_NONE, SRC_NONE, DST_EG1_RESERVED, TRN_NONE): 'EG1 Reserved', | |
(SRC_NONE, SRC_NONE, DST_EG1_SUSTAINLEVEL, TRN_NONE): 'EG1 Sustain Level', | |
(SRC_NONE, SRC_NONE, DST_EG1_RELEASETIME, TRN_NONE): 'EG1 Release Time', | |
(SRC_KEYONVELOCITY, SRC_NONE, DST_EG1_ATTACKTIME, TRN_NONE): 'EG1 Velocity to Attack', | |
(SRC_KEYNUMBER, SRC_NONE, DST_EG1_DECAYTIME, TRN_NONE): 'EG1 Key to Decay', | |
# EG2 Section | |
(SRC_NONE, SRC_NONE, DST_EG2_ATTACKTIME, TRN_NONE): 'EG2 Attack Time', | |
(SRC_NONE, SRC_NONE, DST_EG2_DECAYTIME, TRN_NONE): 'EG2 Decay Time', | |
(SRC_NONE, SRC_NONE, DST_EG2_RESERVED, TRN_NONE): 'EG2 Reserved', | |
(SRC_NONE, SRC_NONE, DST_EG2_SUSTAINLEVEL, TRN_NONE): 'EG2 Sustain Level', | |
(SRC_NONE, SRC_NONE, DST_EG2_RELEASETIME, TRN_NONE): 'EG2 Release Time', | |
(SRC_KEYONVELOCITY, SRC_NONE, DST_EG2_ATTACKTIME, TRN_NONE): 'EG2 Velocity to Attack', | |
(SRC_KEYNUMBER, SRC_NONE, DST_EG2_DECAYTIME, TRN_NONE): 'EG2 Key to Decay', | |
# Miscellaneous Section | |
(SRC_NONE, SRC_NONE, DST_RESERVED, TRN_NONE): 'Reserved', | |
(SRC_NONE, SRC_NONE, DST_PAN, TRN_NONE): 'Initial Pan', | |
# Connections inferred by DLS1 Architecture | |
(SRC_EG1, SRC_NONE, DST_ATTENUATION, TRN_NONE): 'EG1 To Attenuation', | |
(SRC_EG2, SRC_NONE, DST_PITCH, TRN_NONE): 'EG2 To Pitch', | |
(SRC_KEYONVELOCITY, SRC_NONE, DST_ATTENUATION, TRN_CONCAVE): | |
'Key On Velocity to Attenuation', | |
(SRC_PITCHWHEEL, SRC_RPN0, DST_PITCH, TRN_NONE): 'Pitch Wheel to Pitch', | |
(SRC_KEYNUMBER, SRC_NONE, DST_PITCH, TRN_NONE): 'Key Number to Pitch', | |
(SRC_CC7, SRC_NONE, DST_ATTENUATION, TRN_CONCAVE): 'MIDI Controller 7 to Atten.', | |
(SRC_CC10, SRC_NONE, DST_PAN, TRN_NONE): 'MIDI Controller 10 to Pan', | |
(SRC_CC11, SRC_NONE, DST_ATTENUATION, TRN_CONCAVE): 'MIDI Controller 11 to Atten.', | |
(SRC_RPN1, SRC_NONE, DST_PITCH, TRN_NONE): 'RPN1 to Pitch', | |
(SRC_RPN2, SRC_NONE, DST_PITCH, TRN_NONE): 'RPN2 to Pitch', | |
} | |
def articulation_value(source, control, destination, transform, scale): | |
if destination == DST_LFO_FREQUENCY: | |
return '{:.2f} Hz'.format(2 ** ((scale / 65536 - 6900) / 1200) * 440) | |
if destination == DST_LFO_STARTDELAY: | |
return '{} time cents ({:.3f} secs)'.format(scale, 2 ** (scale / (1200 * 65536))) | |
if destination == DST_PITCH: | |
return '{} cents'.format(scale // 65536) | |
if destination in ( | |
DST_EG1_ATTACKTIME, DST_EG1_DECAYTIME, DST_EG1_RELEASETIME, | |
DST_EG2_ATTACKTIME, DST_EG2_DECAYTIME, DST_EG2_RELEASETIME, | |
): | |
if scale == -0x80000000: | |
return '0 secs (min)' | |
if destination in (DST_EG1_DECAYTIME, DST_EG2_DECAYTIME) and scale == 418578432: | |
return '40 secs (max)' | |
if source == SRC_KEYNUMBER: | |
return '{} time cents for Middle C'.format(scale * 60 // 128) | |
return '{} time cents ({:.3f} secs)'.format(scale, 2 ** (scale / (1200 * 65536))) | |
if destination in (DST_EG1_SUSTAINLEVEL, DST_EG2_SUSTAINLEVEL): | |
if scale == 0: | |
return '0% (-96 dB)' | |
return '{}% (-{:.3f} dB)'.format( | |
scale / (10 * 65536), (1 - scale / (10 * 65536) / 100) * 96) | |
return '0x{:08x}'.format(scale) | |
def process_chunk_art1(chunk, level): | |
size, connection_blocks = read_struct(chunk, '<LL') | |
assert chunk.tell() == size | |
print(' ' * level + 'art1 (connection_blocks: {})'.format(connection_blocks)) | |
print(' ' * (level + 2) + 'source control destination scale') | |
for i in range(0, connection_blocks): | |
source, control, destination, transform, scale = read_struct(chunk, '<HHHHl') | |
assert transform == TRN_NONE | |
connection = (source, control, destination, transform) | |
assert connection in CONNECTIONS, connection | |
print(' ' * (level + 2) + '{:<13} {:<7} {:<16} {}'.format( | |
SOURCES[source], | |
CONTROLLERS[control], | |
DESTINATIONS[destination], | |
articulation_value(source, control, destination, transform, scale), | |
)) | |
def process_chunk_colh(chunk, level): | |
instruments = read_struct(chunk, '<L') | |
print(' ' * level + 'colh (instruments: {})'.format(*instruments)) | |
def process_chunk_data(chunk, level): | |
print(' ' * level + 'data (...)') | |
chunk.skip() | |
def process_chunk_fmt_(chunk, level): | |
(format_tag, | |
channels, | |
samples_per_sec, | |
avg_bytes_per_sec, | |
block_align, | |
bits_per_sample, | |
undocumented_extra_data) = read_struct(chunk, '<HHLLHHH') | |
assert format_tag == 1 | |
assert channels == 1 | |
assert samples_per_sec == 22050 or samples_per_sec == 24000 | |
assert avg_bytes_per_sec == samples_per_sec * 2 | |
assert block_align == 2 | |
assert bits_per_sample == 16 | |
assert undocumented_extra_data == 18 | |
print(' ' * level + 'fmt (samples_per_sec: {})'.format(samples_per_sec)) | |
def process_chunk_insh(chunk, level): | |
regions, bank, instrument = read_struct(chunk, '<LLL') | |
bank_bits = bin(bank).lstrip('0b').zfill(32) | |
bank_select_msb = int(bank_bits[18:23], base=2) | |
bank_select_lsb = int(bank_bits[25:31], base=2) | |
assert bank_select_lsb == 0 | |
instrument_drums = bank_bits[0] | |
print( | |
' ' * level + 'insh (regions: {}, bank: {}, drum: {}, program_change: {})'.format( | |
regions, bank_select_msb, instrument_drums, instrument)) | |
def process_chunk_ptbl(chunk, level): | |
size, cues = read_struct(chunk, '<LL') | |
assert chunk.tell() == size | |
print(' ' * level + 'ptbl (cues: {})'.format(cues)) | |
for i in range(0, cues): | |
offset = read_struct(chunk, '<L') | |
print(' ' * (level + 2) + 'offset: {}'.format(*offset)) | |
def process_chunk_rgnh(chunk, level): | |
(range_key_low, | |
range_key_high, | |
range_velocity_low, | |
range_velocity_high, | |
options, | |
key_group) = read_struct(chunk, '<HHHHHH') | |
assert range_velocity_low == 0 | |
assert range_velocity_high == 127 | |
assert options == 0 or options == 1 | |
print(' ' * level + 'rgnh (key_range: {} - {}, key_group: {})'.format( | |
range_key_low, range_key_high, key_group)) | |
def process_chunk_vers(chunk, level): | |
version_ms, version_ls = read_struct(chunk, '<LL') | |
print(' ' * level + 'vers ({},{},{},{})'.format( | |
version_ms >> 16, version_ms & 0xff, version_ls >> 16, version_ls & 0xff | |
)) | |
def process_chunk_wlnk(chunk, level): | |
options, phase_group, channel, table_index = read_struct(chunk, '<HHLL') | |
assert options == 0 | |
assert phase_group == 0 | |
assert channel == 1 | |
print(' ' * level + 'wlnk (table index: {})'.format(table_index)) | |
def process_chunk_wsmp(chunk, level): | |
size, unity_note, fine_tune, attenuation, options, sample_loops = read_struct( | |
chunk, '<LHhlLL') | |
assert chunk.tell() == size | |
assert options == 0 | |
print(' ' * level + 'wsmp (root_note: {}, fine_tune: {}, volume: {:.2f}dB)'.format( | |
unity_note, fine_tune, attenuation / 655360)) | |
for i in range(0, sample_loops): | |
size, loop_type, loop_start, loop_length = read_struct(chunk, '<LLLL') | |
assert size == 16 | |
assert loop_type == 0 | |
print(' ' * (level + 2) + 'loop (start: {}, length: {})'.format( | |
loop_start, loop_length)) | |
PROCESS_CHUNK_FNS = { | |
FOURCC_ART1: process_chunk_art1, | |
FOURCC_COLH: process_chunk_colh, | |
FOURCC_INSH: process_chunk_insh, | |
FOURCC_PTBL: process_chunk_ptbl, | |
FOURCC_RGNH: process_chunk_rgnh, | |
FOURCC_VERS: process_chunk_vers, | |
FOURCC_WLNK: process_chunk_wlnk, | |
FOURCC_WSMP: process_chunk_wsmp, | |
b'ICMT': process_chunk_ICMT, | |
b'ICOP': process_chunk_ICOP, | |
b'IENG': process_chunk_IENG, | |
b'INAM': process_chunk_INAM, | |
b'ISBJ': process_chunk_ISBJ, | |
b'ISFT': process_chunk_ISFT, | |
b'LIST': process_chunk_LIST, | |
b'data': process_chunk_data, | |
b'fmt ': process_chunk_fmt_, | |
b'msyn': ignore_chunk, | |
} | |
def main(): | |
if len(sys.argv) < 2: | |
return 'You have to pass a file to read' | |
with open(sys.argv[1], 'rb') as fd: | |
main_chunk = make_chunk(fd) | |
if main_chunk.getname() != b'RIFF': | |
return 'This file is not a RIFF file' | |
dls_id = main_chunk.read(4) | |
if dls_id != FOURCC_DLS: | |
return 'This file is not a DLS file' | |
print('RIFF - DLS') | |
main_chunk_size = main_chunk.getsize() | |
while main_chunk.tell() < main_chunk_size: | |
next_chunk = make_chunk(main_chunk) | |
process_chunk_fn = PROCESS_CHUNK_FNS[next_chunk.getname()] | |
process_chunk_fn(next_chunk, 2) | |
assert next_chunk.tell() >= next_chunk.getsize() | |
return 0 | |
if __name__ == '__main__': | |
sys.exit(main()) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment