#!/usr/bin/env python # BB helpers # perl -pi -w -e 's/AIF/aif/g;' *.txt # find . -type f -name "*.fx[pb]" -exec rm {} \; # chmod -R u+w,-x,+X dir import SF2 import sys import os from optparse import OptionParser import subprocess import re import datetime version = '1.0' class KeyRange(SF2.u_key_range): def __init__(self, low, high): self.value = [low, high] class VelocityRange(SF2.u_vel_range): def __init__(self, low, high): self.value = [low, high] class LM4WaveAssign: def __init__(self, index, offset, length, note, name, filename, velocity_low, velocity_high, key_range_low, key_range_high, volume_left, volume_right, group_number, polyphony): self.index = int(index) self.offset = long(offset) self.length = long(length) self.note = int(note) self.name = name self.filename = filename self.velocity_low = int(velocity_low) self.velocity_high = int(velocity_high) self.key_range_low = int(key_range_low) self.key_range_high = int(key_range_high) self.volume_left = float(volume_left) self.volume_right = float(volume_right) self.group_number = int(group_number) self.polyphony = int(polyphony) if (not (self.group_number == 0 and self.polyphony == 0) and not (self.group_number != 0 and self.polyphony != 0)): raise "Invalid poliphony %u/%u" % (self.group_number, self.polyphony) def dump(self, s = ""): print s, "Index: %u" % self.index print s, "Name: \"%s\"" % self.name print s, "Filename: \"%s\"" % self.filename print s, "Offset: %lu" % self.offset print s, "Length: %lu" % self.length print s, "Note: %u" % self.note print s, "Velocity range: %u - %u" % (self.velocity_low, self.velocity_high) print s, "Key range: %u - %u" % (self.key_range_low, self.key_range_high) print s, "Volume (left/right): %f/%f" % (self.volume_left, self.volume_right) if self.group_number == 0 and self.polyphony == 0: print s, "Polyphony: full" elif self.group_number != 0 and self.polyphony != 0: print s, "Polyphony group/voices: %u/%u" % (self.group_number, self.polyphony) class SOX: def __init__(self, filename): #print "processing " + filename # get channels count re_input = re.compile((r"^sox: Input file %s: using sample rate (\d+)\n" r"\tsize ([^,]+), encoding ([^,]+), ([0-9]+) channels?$") % filename, re.MULTILINE) re_output = re.compile(r"sox: Output file -: using sample rate (\d+)\n" r"\tsize shorts, encoding signed \(2's complement\), 1 channel$", re.MULTILINE) sox = subprocess.Popen(["sox", "-V", filename, "-s", "-w", "-c", "1", "-t", "raw", "/dev/null"], stdout = subprocess.PIPE, stderr = subprocess.PIPE) (stdout, stderr) = sox.communicate() if sox.returncode != 0: raise ("Failed to get channel count from %s, sox returned %u, stderr:\n%s" % (filename, sox.returncode, stderr)) m = re.search(re_input, stderr) if m == None: raise "Failed to parse sox output\n" + stderr self.orig_sample_rate = m.group(1) self.orig_sample_size = m.group(2) self.orig_sample_encoding = m.group(3) self.orig_channels = int(m.group(4)) if self.orig_channels == 1: sox = subprocess.Popen(["sox", "-V", filename, "-s", "-w", "-c", "1", "-t", "raw", "-"], stdout = subprocess.PIPE, stderr = subprocess.PIPE) (stdout, stderr) = sox.communicate() if sox.returncode != 0: raise ("Failed to extract left channel data from %s, sox returned %u, stderr:\n%s" % (filename, sox.returncode, stderr)) m = re.search(re_output, stderr) if m == None: raise "Failed to parse sox output" self.sample_rate = m.group(1) self.left = stdout self.right = None elif self.orig_channels == 2: sox = subprocess.Popen(["sox", "-V", filename, "-s", "-w", "-c", "1", "-t", "raw", "-", "avg", "-l"], stdout = subprocess.PIPE, stderr = subprocess.PIPE) (stdout, stderr) = sox.communicate() if sox.returncode != 0: raise ("Failed to extract left channel data from %s, sox returned %u, stderr:\n%s" % (filename, sox.returncode, stderr)) m = re.search(re_output, stderr) if m == None: raise "Failed to parse sox output" self.sample_rate = m.group(1) self.left = stdout sox = subprocess.Popen(["sox", "-V", filename, "-s", "-w", "-c", "1", "-t", "raw", "-", "avg", "-r"], stdout = subprocess.PIPE, stderr = subprocess.PIPE) (stdout, stderr) = sox.communicate() if sox.returncode != 0: raise ("Failed to extract right channel data from %s, sox returned %u, stderr:\n%s" % (filename, sox.returncode, stderr)) m = re.search(re_output, stderr) if m == None: raise "Failed to parse sox output" assert self.sample_rate == m.group(1) self.right = stdout else: raise "No idea how to convert samples with %s channels" % self.orig_channels class LM4WaveSample: def __init__(self, offset, length, filename, velocity_low, velocity_high, key_range_low, key_range_high, volume_left, volume_right, group_number, polyphony): self.offset = long(offset) self.length = long(length) self.filename = filename self.velocity_low = int(velocity_low) self.velocity_high = int(velocity_high) self.key_range_low = int(key_range_low) self.key_range_high = int(key_range_high) self.volume_left = float(volume_left) self.volume_right = float(volume_right) self.group_number = int(group_number) self.polyphony = int(polyphony) if (not (self.group_number == 0 and self.polyphony == 0) and not (self.group_number != 0 and self.polyphony != 0)): raise "Invalid poliphony %u/%u" % (self.group_number, self.polyphony) def dump(self, s = ""): print s, "Filename: \"%s\"" % self.filename if self.left != None: print s, "Left sample real size is %lu bytes" % len(self.left) if self.right != None: print s, "Right sample real size is %lu bytes" % len(self.right) print s, "Offset: %lu" % self.offset print s, "Length: %lu" % self.length print s, "Velocity range: %u - %u" % (self.velocity_low, self.velocity_high) print s, "Key range: %u - %u" % (self.key_range_low, self.key_range_high) print s, "Volume (left/right): %f/%f" % (self.volume_left, self.volume_right) if self.group_number == 0 and self.polyphony == 0: print s, "Polyphony: full" elif self.group_number != 0 and self.polyphony != 0: print s, "Polyphony group/voices: %u/%u" % (self.group_number, self.polyphony) def read(self): if not os.path.isfile(self.filename): raise ("Invalid sample filename \"%s\"\n" + "Check if case of letters matches and correct it!") % self.filename sox = SOX(self.filename) self.channels = int(sox.orig_channels) self.rate = int(sox.sample_rate) self.left = sox.left self.right = sox.right class LM4Slot: def __init__(self, index, name, note): self.index = index self.name = name self.note = int(note) self.waves = [] def dump(self, s = ""): print s, "Index: %u" % self.index print s, "Name: %s" % self.name print s, "Note: %u" % self.note print s, "Wave samples:" for wave in self.waves: wave.dump(s + " ") print class LM4Parser(file): "LM-4 script file parser" def __init__(self, filename): file.__init__(self, filename, 'r') self.wave_assigns = [] self.slots = {} self.read_header() self.read_wave_assignements() self.comment = self.read().strip() self.create_slots() def read_header(self): line = self.read_header_line() if line != 'vst samples': raise "LM-4 Script signature mismatch" self.set_name = self.read_header_line() self.waves_directory = self.read_header_line() self.vendor = self.read_header_line() def read_wave_assignements(self): while True: line = self.readline() if line == None: raise "End of sets mark not found" if len(line) == 1 and line[0] == '-1': # End of sets break if len(line) != 14: raise "Wave file assignement must have 14 values" wave = LM4WaveAssign(line[0], line[1], line[2], line[3], line[4], line[5], line[6], line[7], line[8], line[9], line[10], line[11], line[12], line[13]) self.wave_assigns.append(wave) def read_header_line(self): line = self.readline() if line == None: raise "EOF while reading header" if len(line) != 1: raise "LM-4 Script header lines must contain only one entry, \"%s\"" % repr(line) return line[0] def readline(self): while True: line = file.readline(self) if line == '': return None # EOF line = line.rstrip('\r\n, ') if line != '': # not empty line line = line.split(',') print repr(line) return line def create_slots(self): for wave_assign in self.wave_assigns: if self.slots.has_key(wave_assign.index): if self.slots[wave_assign.index].name != wave_assign.name: raise "wave assings with same indexes but with different names detected" if self.slots[wave_assign.index].note != wave_assign.note: raise "wave assings with same indexes but with different notes detected" else: self.slots[wave_assign.index] = LM4Slot(wave_assign.index, wave_assign.name, wave_assign.note) wave_sample = LM4WaveSample( wave_assign.offset, wave_assign.length, os.path.abspath(os.path.join(os.path.split(self.name)[0], self.waves_directory, wave_assign.filename)), wave_assign.velocity_low, wave_assign.velocity_high, wave_assign.key_range_low, wave_assign.key_range_high, wave_assign.volume_left, wave_assign.volume_right, wave_assign.group_number, wave_assign.polyphony) self.slots[wave_assign.index].waves.append(wave_sample); def read_samples(self): print "Reading samples" for index, slot in self.slots.iteritems(): for wave_sample in slot.waves: sys.stdout.write('.') sys.stdout.flush() wave_sample.read() print "\ndone" def dump_header(self, s = ""): print s, "File \"%s\"" % self.name print s, "Set name \"%s\"" % self.set_name print s, "Waves directory \"%s\"" % self.waves_directory print s, "Vendor \"%s\"" % self.vendor print s, "Comment \"%s\"" % self.comment def dump_wave_assigns(self, s = ""): for wave_assign in self.wave_assigns: print s, "----" wave_assign.dump() def dump_slots(self, s = ""): print s, "Slots:" for index, slot in self.slots.iteritems(): slot.dump(s + " ") def dump(self, s = ""): self.dump_header() #self.dump_wave_assigns() self.dump_slots() def create_sample_bag(slot, wave, channel): bag = SF2.Bag() if wave.key_range_low != 0 or wave.key_range_high != 0: raise "key ranges not supported yet" key_range = KeyRange(slot.note, slot.note) generator_key_range = SF2.Generator(43, int(key_range)) bag.generators.append(generator_key_range) vel_range = VelocityRange(wave.velocity_low, wave.velocity_high) generator_vel_range = SF2.Generator(44, int(vel_range)) bag.generators.append(generator_vel_range) generator_overriding_root_key = SF2.Generator(58, slot.note) bag.generators.append(generator_overriding_root_key) if channel != 'mono': if channel == 'left': pan = -500 else: pan = +500 generator_pan = SF2.Generator(17, pan) bag.generators.append(generator_pan) if channel == 'left': data = wave.left else: data = wave.right else: data = wave.left if wave.group_number != 0: if (wave.polyphony != 1): raise "Don't know how to handle polyphony that is not full not 1 voice" generator_pan = SF2.Generator(57, wave.group_number) bag.generators.append(generator_pan) generator_sample = SF2.Generator(53, 0) name = os.path.basename(wave.filename) i = name.rfind('.') if i > 0: name = name[:i] if channel == 'left': name += "_L" elif channel == 'right': name += "_R" sample = SF2.Sample(name, wave.rate, data) generator_sample.sample = sample bag.generators.append(generator_sample) return bag def read_lm4(filename): print "Reading data from \"%s\"" % filename lm4 = LM4Parser(filename) lm4.read_samples() #lm4.dump() return lm4 def write_sf2(bank_name, sf2, lm4s): print "Writing data to \"%s\"" % sf2 presets = [] comment = '' midi_program = 0 for lm4 in lm4s: preset = SF2.Preset(lm4.set_name, midi_program, 128) for index, slot in lm4.slots.iteritems(): pbag = SF2.Bag() generator = SF2.Generator(41, index) instrument = SF2.Instrument(slot.name) bag = SF2.Bag() generator_release_vol_env = SF2.Generator(38, 8000) bag.generators.append(generator_release_vol_env) instrument.bags.append(bag) for wave in slot.waves: if wave.channels == 2: instrument.bags.append(create_sample_bag(slot, wave, 'left')) instrument.bags.append(create_sample_bag(slot, wave, 'right')) else: instrument.bags.append(create_sample_bag(slot, wave, 'mono')) generator.instrument = instrument pbag.generators.append(generator) preset.bags.append(pbag) presets.append(preset) if lm4.comment != '' and comment == '': globals()['comment'] = lm4.comment midi_program += 1 today = datetime.date.today() months = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"] info = {'isng': 'softsynth', 'INAM': bank_name, 'ISFT': '%s:' % tool_version, 'ICRD': "%s %u, %u" % (months[today.month-1], today.day, today.year), 'ICOP': lm4.vendor, 'IPRD': 'fluidsynth', } if comment != '': info['ICMT'] = comment SF2.write_sf2( sf2, presets, info) tool_version = os.path.basename(sys.argv[0]) + " " + version usage = "usage: %prog [options] [] ..." parser = OptionParser(usage=usage) (options, args) = parser.parse_args() if len(args) < 3: parser.print_help() sys.exit() lm4s = [] for lm4 in args[2:]: lm4s.append(read_lm4(lm4)) write_sf2(args[0], args[1], lm4s)