Skip to content

Instantly share code, notes, and snippets.

@McMartin
Last active July 10, 2024 00:36
Show Gist options
  • Save McMartin/07f02c287a40ed317df7570071cc1eb7 to your computer and use it in GitHub Desktop.
Save McMartin/07f02c287a40ed317df7570071cc1eb7 to your computer and use it in GitHub Desktop.
Read DLS file using Python
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