Autocreate Multisampled Instruments (python/bash Scripts)

I recently found a cool python script by Unavowed that takes multisamples and automatically converts them into an .xrni file. His original file was available at http://unavowed.vexillium.org/pub/util/scripts/mkinstr as of the date of this posting, but it had a problem (which I fixed) converting smaller .wav files into FLAC (it just needed --lax to work).

Here’s his description of the file: “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”

Since most samples have the format rootP.wav (i.e., Strings C4.wav) instead of Strings 36.wav, I created a bash script that will convert your samples in rootP.wav format to rootN.wav.

This only works for .wav, but I’m someone (perhaps Unavowed) could get this to work with any file format FLAC can handle.

I’ve tested this using terminal in OS X 10.5.6

Here’s my reversion of mkinstr.py:

#!/usr/bin/python  
#  
# mkinstr  
#  
# Copyright 2009 Unavowed (unavowed at vexillium org)  
# License: GNU General Public License version 3, or any later version  
#  
# Modified (additional FLAC flags added) by Steven  
# 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 --lax --keep-foreign-metadata --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, '<?xml version="1.0" encoding="UTF-8"?>'  
 print >> f, '<renoiseinstrument doc_version="7">'<br>
	print &gt;&gt; f, ' <name>%s</name>' % self.__xml_escape (name)<br>
	print &gt;&gt; f, ' <splitmap>'<br>
	for n in split_list:<br>
		print &gt;&gt;f, '	<split>%i</split>' % n<br>
	print &gt;&gt; f, ' </splitmap>'<br>
	print &gt;&gt; f, ' <copyintonewsamplenamecounter>' <br>
			+ '0</copyintonewsamplenamecounter>'<br>
		print &gt;&gt; f, ' <copyintonewinstrumentnamecounter>' <br>
			+ '0</copyintonewinstrumentnamecounter>'<br>
	print &gt;&gt; f, ' <samples>'<br>
	for sd in group_list:<br>
		print &gt;&gt; f, '	<sample>'<br>
		print &gt;&gt; f, ' <name>%s%02i</name>' <br>
				% (self.__xml_escape (name), sd.base_note)<br>
		print &gt;&gt; f, ' <basenote>%i</basenote>' % sd.base_note<br>
		print &gt;&gt; f, ' <newnoteaction>NoteOff</newnoteaction>'<br>
		if sd.loop_info is not None:<br>
		print &gt;&gt; f, ' <loopmode>%s</loopmode>' <br>
				% self.__xml_escape (sd.loop_info[0])<br>
		print &gt;&gt; f, ' <loopstart>%s</loopstart>' % sd.loop_info[1]<br>
		print &gt;&gt; f, ' <loopend>%s</loopend>' % sd.loop_info[2]<br>
		print &gt;&gt; f, '	</sample>'<br>
	print &gt;&gt; f, ' </samples>'<br>
	print &gt;&gt; f, ''' <envelopes><br>
	<volume><br>
	  <isactive>true</isactive><br>
	  <interpolationmode>Curve</interpolationmode><br>
	  <sustainisactive>true</sustainisactive><br>
	  <sustainpos>1</sustainpos><br>
	  <loopstart>0</loopstart><br>
	  <loopend>71</loopend><br>
	  <loopmode>Off</loopmode><br>
	  <decay>128</decay><br>
	  <nodes><br>
		<playmode>Curve</playmode><br>
		<length>33</length><br>
		<valuequantum>0.0</valuequantum><br>
		<points><br>
		  <point>0,0.0</point><br>
		  <point>1,1.0</point><br>
		  <point>5,0.4</point><br>
		  <point>13,0.08</point><br>
		  <point>21,0.02</point><br>
		  <point>31,0.0</point><br>
		</points><br>
	  </nodes><br>
	</volume><br>
  </envelopes><br>
</renoiseinstrument>'''  
  
 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  
  

Here’s my bash script to make multisamples in the usual format work for mkinstr.py . Note: I’m not a programmer and take no responsibility for any damage this may do to your system (I doubt it would).
Name this: notenames.sh (or whatever you want)

  
#!/bin/bash  
SEARCHDIR=.  
pitchclass=(C C# D D# E F F# G G# A A# B)  
  
 for ((oct=0;oct<=10;oct+=1))  
 do  
 for ((i=0;i<=11;i+=1))  
 do  
 fwildcard="*${pitchclass[i]}$oct.wav"  
 notevalue=$(((oct*12)+i))  
 findvar='s/'  
 find $SEARCHDIR -iname "$fwildcard" -exec rename "s/${pitchclass[i]}$oct.wav/_${notevalue}.wav/" {} \;  
 done  
 done  
  
pitchclass=(c c# d d# e f f# g g# a a# b)  
  
 for ((oct=0;oct<=10;oct+=1))  
 do  
 for ((i=0;i<=11;i+=1))  
 do  
 fwildcard="*${pitchclass[i]}$oct.wav"  
 notevalue=$(((oct*12)+i))  
 findvar='s/'  
 find $SEARCHDIR -iname "$fwildcard" -exec rename "s/${pitchclass[i]}$oct.wav/_${notevalue}.wav/" {} \;  
 done  
 done  
  
pitchclass=(c cx d dx e f fx g gx a ax b)  
  
 for ((oct=0;oct<=10;oct+=1))  
 do  
 for ((i=0;i<=11;i+=1))  
 do  
 fwildcard="*${pitchclass[i]}$oct.wav"  
 notevalue=$(((oct*12)+i))  
 findvar='s/'  
 find $SEARCHDIR -iname "$fwildcard" -exec rename "s/${pitchclass[i]}$oct.wav/_${notevalue}.wav/" {} \;  
 done  
 done  
  

If anybody would like to implement my note naming algorithm in some better way, please do! Let me know if you found this beneficial.