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 >> f, ' <name>%s</name>' % self.__xml_escape (name)<br>
print >> f, ' <splitmap>'<br>
for n in split_list:<br>
print >>f, ' <split>%i</split>' % n<br>
print >> f, ' </splitmap>'<br>
print >> f, ' <copyintonewsamplenamecounter>' <br>
+ '0</copyintonewsamplenamecounter>'<br>
print >> f, ' <copyintonewinstrumentnamecounter>' <br>
+ '0</copyintonewinstrumentnamecounter>'<br>
print >> f, ' <samples>'<br>
for sd in group_list:<br>
print >> f, ' <sample>'<br>
print >> f, ' <name>%s%02i</name>' <br>
% (self.__xml_escape (name), sd.base_note)<br>
print >> f, ' <basenote>%i</basenote>' % sd.base_note<br>
print >> f, ' <newnoteaction>NoteOff</newnoteaction>'<br>
if sd.loop_info is not None:<br>
print >> f, ' <loopmode>%s</loopmode>' <br>
% self.__xml_escape (sd.loop_info[0])<br>
print >> f, ' <loopstart>%s</loopstart>' % sd.loop_info[1]<br>
print >> f, ' <loopend>%s</loopend>' % sd.loop_info[2]<br>
print >> f, ' </sample>'<br>
print >> f, ' </samples>'<br>
print >> 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.