This doesn't work with the factory sounds as I don't understand its audio format. I purchased a Ninevolt bundle years back, such as Action Drums. This is my current attempt at a Python script that converts it to a `.sfz` file compatible at least with Renoise Redux. Apologies if the code isn't pretty. At present it outputs `.flac` files (which is what Renoise uses, and Renoise's sfz output was the template I used for the sfz files generated here), though by changing the "flac" to "wav" it could just as easily output wav files, and possibly with a couple more lines store it at a chosen bit depth. This code doesn't change the data type of the data read by `soundfile`, nor does it modify the sound data in any way, except taking slices of it to produce the individual slice files. This is sufficient that I can use the resulting `.sfz` files in Renoise Redux, which is a sampler/renoise instrument player that runs on all OS's I use. This works in Falcon3 and sforzando and Bitwig (Ableton still doesn't open `.sfz` in its sampler, and NI with their usual degree of suckage haven't implemented, or removed, sfz import in Kontakt). It doesn't extract midi correctly for non 4/4 files. I'll investigate why at some point. # Extractor ```py #!/usr/bin/env python import os import re import json import soundfile import numpy import math import jdamidi import rmxseq from collections import defaultdict from subprocess import run, DEVNULL import xml.etree.ElementTree as ET # later we want to point to a root of an rmx pack # and have it create folders elsewhere # though we can do this and move with a bash script tree = ET.parse('data.xml') root = tree.getroot() for child in root: print(child.tag, child.attrib) root_note = 48 odr = "out" os.makedirs(odr,exist_ok=True) seqs = {} for loop in root: lattr = loop.attrib loop_dirname = lattr['AUDIOFILENAME'] audio_input_filename = os.path.join(loop_dirname,"Audio") loop_outname = loop_dirname.replace(" ","_") sounddata, samplerate = soundfile.read(audio_input_filename) sfz_filename = f"{loop_outname}.sfz" samples_dirname = f"{loop_outname}_samples" os.makedirs(os.path.join(odr,samples_dirname),exist_ok=True) note = root_note l = len(loop) n = math.ceil(math.log(l+1,10)) # how many digits in sample_00.flac sfz_regions = [] # extract slice audio for i,sl in enumerate(loop): if sl.tag != "SLICE": continue attr = sl.attrib a = attr['BEGIN'] e = attr['END'] a = int(a) e = int(e) sample_fn = f"{loop_outname}_sample_{i:0{n}}.flac" sample_path = os.path.join(samples_dirname,sample_fn) sample_data = sounddata[a:e,:] ofn = os.path.join(odr,sample_path) if not os.path.exists(ofn): soundfile.write(ofn,sample_data,samplerate) print(f"written sample {ofn}") sfz_regions.append(f""" pitch_keycenter={note} lokey={note} hikey={note} sample={sample_path}""") note += 1 sfz = f"""// {loop_dirname} {"\n\n".join(sfz_regions)} """ ofn = os.path.join(odr,sfz_filename) with open(ofn,"wt") as f: f.write(sfz) print(f"Written {ofn}") # extract midi seq info for elt in loop: if elt.tag.lower() == "sliceseq": break else: print("no sliceseq") continue attr = elt.attrib ppqn = int(attr["TICKSPERQUARTER"]) timesignum = int(attr["TIMESIGNUM"]) timesigdenom = int(attr["TIMESIGDENOM"]) tempo = attr["TEMPO"] seq = [] for step in elt: print(" ",step) attr = step.attrib print(">>",attr) b = int(attr["BEGIN"]) e = int(attr["END"]) si = int(attr["SLICEINDEX"]) if si == -1: seqlen = b break else: seq.append([b,e,si]) sseq = { "seqlen": seqlen, "seq": seq, "ppqn": ppqn, "tempo": tempo, "timesig": [timesignum,timesigdenom] } seqs[loop_dirname] = sseq so = rmxseq.Seq(data=sseq) midi_data = so.makemidi() mid_filename = f"{loop_outname}.mid" ofn = os.path.join(odr,mid_filename) with open(ofn,"wb") as f: f.write(midi_data) print(f"Written {ofn}") ofn = os.path.join(odr,"seqs.json") with open(ofn,"wt") as f: json.dump(seqs,f) print(f"Written {ofn}") ``` # rmxseq.py This is a quick and dirty module to handle the slice sequence data from a stylus rmx `data.xml` file. ```py #!/usr/bin/env python import jdamidi import json def fread(fn,mode="rt"): with open(fn,mode) as f: return f.read() def jload(fn): with open(fn) as f: return json.load(f) class Seq: def __init__(self,fn=None,data=None,root_note=48): self.root_note = root_note self.fn = fn if fn is not None: data = jload(fn) self.__dict__.update(data) elif data is not None: self.__dict__.update(data) else: self.seq = [] self.ppqn = 960 self.seqlen = int(4.0*self.ppqn) self.tempo = "42c80001" self.timesig = [ 4, 4 ] def __repr__(self): seqe = f"{len(self.seq)} events" return f"Seq" def makemidi(self,channel=0): root_note = self.root_note events = [] for i,note in enumerate(self.seq): b,e,sidx = note events.append([b,[0x90+channel,i+root_note,100]]) events.append([e,[0x80+channel,i+root_note,100]]) events = list(sorted(events,key=lambda t: t[0])) mtrk = jdamidi.event_list_to_midi(events,self.seqlen) midi =jdamidi.make_smf([mtrk],ppqn=self.ppqn) return midi ``` # jdamidi.py This is a quick and dirty module to generate `.mid` files. It doesn't do everything yet, such as tempo. It has the beginnings of a more general idea I'm playing with, that of an event list of the form: ``` [ [40,["midi",0x90,0x60,0x70]] [event_time,["eventtype",...event params]] ] ``` inspired by the Csound score language. The idea is that if you want midi, the non-midi events are either dropped or converted to midi. Then you can have an arbitrary bit of code that expands non-midi events to midi events, or influences the state of the process converting the event list. And this is by no means restricted to generating midi: if you have some DSP code, your event list could be a recipe for generating a `.wav` file, much as Csound does. ```py import struct def encode_length(x): xs = [] y = x while x >= 0x80: xs.append(x&0x7f) x >>= 7 xs.append(x) for i in range(1,len(xs)): xs[i] += 0x80 xs = list(reversed(xs)) return bytes(xs) def parse_length(b,di): dt = 0 while b & 0x80: dt |= (b&0x7f) dt <<= 7 b = next(di) dt |= (b&0x7f) return dt def make_mthd(smf_format=1,num_tracks=1,ppqn=960): if smf_format < 0 or smf_format > 2: raise ValueError("smf_format must be 0..2") if num_tracks < 1: raise ValueError("number of tracks must be positive") return b"MThd"+struct.pack(">IHHH",6,smf_format,num_tracks,ppqn) def make_trk(midi_data): # takes raw midi data and wraps in a MTrk o = b"MTrk" o += struct.pack(">I",len(midi_data)) o += midi_data return o def make_smf(tracks,smf_format=None, ppqn=960): if smf_format is None: if len(tracks) == 1: smf_format = 0 else: smf_format = 1 mthd = make_mthd(smf_format=smf_format,num_tracks=len(tracks),ppqn=ppqn) mtracks = [ make_trk(track) for track in tracks ] mid = mthd + b"".join(mtracks) return mid def fevent_list_to_event_list(fevents,multiplier=1.0,ppqn=960): # t, data # [ 1.0, [ 0x90, 0x60, 0x61 ] ] # or # [ 2.0, b"\x90\x60\x61 ] # where 1.0, 2.0 is position in quater notes # times are multiplied by multiplier (e.g. multiplier=0.25 will turn quarters into sixteenths) events = [] for event in fevents: t,d = event t *= multiplier t = int(t*ppqn) events.append([t,d]) return events def event_list_to_midi(events,length): # t,data # [ 45, [ 0x90, 0x60, 0x61 ] ] # or # [ 45, b"\x90\x60\x61 ] # sort by time events = list(sorted(events,key = lambda t: t[0])) eventsd = [] data = b"" t0 = 0 for event in events: t,d = event dt = t - t0 # delta time from previous event t0 = t data += encode_length(dt) data += bytes(d) if length < t0: raise ValueError(f"Events exceed given length {length}<{t0}") # append all notes off and end track meta event dt = length - t0 data += encode_length(dt) data += b"\xb0\x7b\x00\x00\xff\x2f\x00" return data ```