#!/usr/bin/python # # mkinstr # # Copyright 2009 Unavowed (unavowed at vexillium org) # License: GNU General Public License version 3, or any later version # # Takes a directory with samples of the form prefixN.wav, where N is the MIDI # note number, groups them according to prefix and uses them to creates Renoise # .xrni instrument files in the current directory. Requires the programs `zip' # and `flac' in the current PATH. Probably only runs on UNIX-like systems. from stat import S_ISDIR, S_ISREG import os import os.path import re import shutil import sys import tempfile if len (sys.argv) < 2: print >>sys.stderr, 'Usage: %s SAMPLE_PATH' % sys.argv[0] sys.exit (1) SAMPLE_RE = re.compile (r'^.*\.wav', re.IGNORECASE) SAMPLE_NUMBER_RE = re.compile (r'^(.*[^\d])(\d+)\s*\.wav$', re.IGNORECASE) class SampleDescriptor: file_name = '' loop_info = None base_note = 60 def __init__ (self, file_name, base_note): self.file_name = file_name self.base_note = base_note def __cmp__ (self, other): return cmp (self.base_note, other.base_note) class SmplChunk: manufacturer = 0 product = 0 sample_period = 0 unity_note = 0 pitch_fraction = 0 smpte_format = 0 smpte_offset = 0 loop_count = 0 sampler_data = 0 loops = [] class SmplLoop: cue_point_id = 0 type = 0 start = 0 end = 0 fraction = 0 play_count = 0 def read_u32_le (file): bytes = file.read (4) if len (bytes) == 0: raise IOError number = 0 for n, byte in enumerate (bytes): number |= ord (byte) << (n * 8) return number def read_smpl_loop (file): loop = SmplLoop () loop.cue_point_id = read_u32_le (file) loop.type = read_u32_le (file) loop.start = read_u32_le (file) loop.end = read_u32_le (file) loop.fraction = read_u32_le (file) loop.play_count = read_u32_le (file) return loop def read_smpl_chunk (file): smpl = SmplChunk () file.seek (4, 1) smpl.manufacturer = read_u32_le (file) smpl.product = read_u32_le (file) smpl.sample_period = read_u32_le (file) smpl.unity_note = read_u32_le (file) smpl.pitch_fraction = read_u32_le (file) smpl.smpte_format = read_u32_le (file) smpl.smpte_offset = read_u32_le (file) smpl.loop_count = read_u32_le (file) smpl.sampler_data = read_u32_le (file) smpl.loops = [] for n in xrange (smpl.loop_count): smpl.loops.append (read_smpl_loop (file)) return smpl class MKInstr: def __init__ (self, path): self.path = path self.samples = [] self.groups = {} def __shell_escape (self, str): str = str.replace ("'", "'\\''") return '\'%s\'' % str def __xml_escape (self, str): str = str.replace ('&', '&') str = str.replace ('<', '<') str = str.replace ('>', '>') str = str.replace ('"', '"') str = str.replace ("'", ''') return str def __gather_samples (self, path): entries = os.listdir (path) files = [] dirs = [] for e in entries: full = '%s/%s' % (path, e) st = os.stat (full) if S_ISDIR (st.st_mode): dirs.append (e) elif S_ISREG (st.st_mode): files.append (full) dirs.sort () files.sort () samples = [] for file in filter (SAMPLE_RE.match, files): samples.append (file) for dir in dirs: samples.extend (self.__gather_samples ('%s/%s' % (path, dir))) return samples def __read_loop_info (self, file_name): f = open (file_name, 'r') riff = f.read (4) if riff != 'RIFF': return None f.seek (8, 1) while True: chunk = f.read (4) if len (chunk) == 0: break if chunk == 'smpl': smpl = read_smpl_chunk (f) if smpl.loop_count == 0: return None loop = smpl.loops[0] if loop.type == 1: type = 'Alternating' elif loop.type == 2: type = 'Backward' else: type = 'Forward' return (type, loop.start, loop.end) size = read_u32_le (f) f.seek (size, 1) f.close () return None def __split_samples_into_groups (self, samples): groups = {} for sample in samples: m = SAMPLE_NUMBER_RE.match (sample) if m is None: continue number = int (m.group (2)) base = os.path.basename (m.group (1)) if len (base) == 0: continue base = base.replace (' - ', '-').replace (' ', ' ') base = base.replace (' ', '_').lower () while len (base) > 0 and base[-1] == '_': base = base[:-1] if len (base) == 0: continue sd = SampleDescriptor (sample, number) #print sample, base, number if base in groups: groups[base].append (sd) else: groups[base] = [sd] for list in groups.itervalues (): list.sort () return groups def __create_flac (self, src, dst): command = 'flac --best -o %s %s' \ % (self.__shell_escape (dst), self.__shell_escape (src)) return os.system (command) == 0 def __assign_samples_to_notes (self, group_list): samples = [0] * 120 prev = -1 for n, sd in enumerate (group_list): if prev < 0: left = 0 else: left = prev + 1 + (sd.base_note - prev) // 2 left = min (left, sd.base_note) assert left >= prev + 1 for x in xrange (prev + 1, left): samples[x] = n - 1 #print 'samples[%i] = %i' % (x, samples[x]) for x in xrange (left, sd.base_note + 1): samples[x] = n #print 'samples[%i] = %i' % (x, samples[x]) prev = sd.base_note #print file for x in xrange (prev + 1, len (samples)): samples[x] = n #print 'samples[%i] = %i' % (x, samples[x]) return samples def __create_instrument_xml (self, fname, name, group_list, split_list): f = open (fname, 'w') print >> f, '' print >> f, '' print >> f, ' %s' % self.__xml_escape (name) print >> f, ' ' for n in split_list: print >>f, ' %i' % n print >> f, ' ' print >> f, ' ' \ + '0' print >> f, ' ' \ + '0' print >> f, ' ' for sd in group_list: print >> f, ' ' print >> f, ' %s%02i' \ % (self.__xml_escape (name), sd.base_note) print >> f, ' %i' % sd.base_note print >> f, ' NoteOff' if sd.loop_info is not None: print >> f, ' %s' \ % self.__xml_escape (sd.loop_info[0]) print >> f, ' %s' % sd.loop_info[1] print >> f, ' %s' % sd.loop_info[2] print >> f, ' ' print >> f, ' ' print >> f, ''' true Curve true 1 0 71 Off 128 Curve 33 0.0 0,0.0 1,1.0 5,0.4 13,0.08 21,0.02 31,0.0 ''' return True def __create_zip (self, directory, output): command = '(cd %s && zip -r - *) > %s' \ % (self.__shell_escape (directory), self.__shell_escape (output)) return (os.system (command) == 0) def __create_flacs (self, dir, group_name, group_list): sddir = '%s/SampleData' % dir os.mkdir (sddir) for n, sd in enumerate (group_list): self.__create_flac (sd.file_name, '%s/Sample%02i (%s%02i).flac' \ % (sddir, n, group_name, sd.base_note)) def __read_all_loop_info (self, group_list): for sd in group_list: sd.loop_info = self.__read_loop_info (sd.file_name) def __make_instrument (self, group_name): dir = tempfile.mkdtemp () list = self.groups[group_name] split_list = self.__assign_samples_to_notes (list) self.__create_flacs (dir, group_name, list) self.__read_all_loop_info (list) self.__create_instrument_xml ('%s/Instrument.xml' % dir, group_name, list, split_list) self.__create_zip (dir, '%s.xrni' % group_name) shutil.rmtree (dir) def run (self): self.samples = self.__gather_samples (self.path) self.groups = self.__split_samples_into_groups (self.samples) for grp, lst in self.groups.iteritems (): self.__make_instrument (grp) return True mki = MKInstr (sys.argv[1]) mki.run () # vim:sw=4