multisamples in SuperDirt
this is a hack to play multisamples in SuperDirt, using what i am calling "note map functions". maybe it should become a PR for a proper built-in feature, but i might not get around to it in the near future. feel free to adapt it.
the problem: you have a SuperDirt sample folder, where each sample is the same instrument sampled at a different pitch. you would like to play these samples such that the one closest to your target pitch is automatically selected as the source material, to minimize artifacts from pitch shifting.
what follows is some code that uses SuperDirt's diversion feature to "monkey-patch" this functionality into the event processing pipeline. the underlying idea of note map functions is generic and could be used to implement various kinds of mapping logic, but i havent thought of any other useful ones.
my main motivation for implementing this is to get the lovely General MIDI soundfonts from Strudel into Tidal. more on that in the future, perhaps.
SuperDirt.start;
// Dirt-Samples must be installed
~dirt.soundLibrary.loadSoundFiles;
(
// define a pseudo-object to keep state and handle note mapping
~noteMapper = (
// dictionary of sound name -> note map function
maps: IdentityDictionary(),
// add a note map function for a given sound name
putMap: { |self, sound, noteMap|
self.maps.put(sound, noteMap);
},
// diversion for SuperDirt to run for each event. applies the current
// sound's note map function, if one exists
apply: { |self|
var noteMap, midinote, mapped;
if(~n == \none) {
noteMap = self.maps[~s];
if(noteMap.notNil) {
midinote = ~midinote.();
// call the note map function. it will inspect the current event
// (passed environmentally by SuperDirt) and may return an event
// with `n` and `midinote` keys
mapped = noteMap.();
if(mapped.notNil) {
// if the note map function returned something, adjust the
// current event's `n` and `midinote` accordingly
~n = mapped.n;
~midinote = { midinote - mapped.midinote + 60 };
// this is just to confirm that mapping is happening
"s: %, midinote: % -> n: %, n's midinote: %, adjusted midinote: %\n".postf(
~s, midinote,
~n, mapped.midinote, ~midinote.()
);
}
}
};
// return nil so we end up in the default SuperDirt playback path
nil
},
// given a SuperDirt instance, enable the note mapper by registering `apply`
// as the default diversion for each orbit
enable: { |self, dirt|
~dirt.orbits.do { |o|
o.defaultParentEvent.diversion = { self.apply };
}
}
);
// enable it
~noteMapper.enable(~dirt);
// example of a higher-order function to build a note map function - possibly
// the most useful one: given an array of notes, where the nth element specifies
// the midi note of the nth sample for a particular sound, return a note map
// function. the note map function will then, given a midi note, return the
// closest
~makeClosestNoteMap = { |midinotes|
midinotes = midinotes.collect { |midinote, n|
(midinote: midinote, n: n)
}.sort { |a, b| a.midinote < b.midinote };
{
var midinote = ~midinote.();
var lastDist = inf;
var dist;
block { |ret|
midinotes.do { |candidate, i|
dist = (midinote - candidate.midinote).abs;
if(dist >= lastDist) {
ret.(midinotes[i-1]);
};
lastDist = dist;
};
ret.(midinotes[midinotes.size - 1]);
}
}
};
// the moog directory in Dirt-Samples contains eight samples, pitched at C2, C3,
// C4, G1, G2, G3 and G4. we can create the corresponding note map function:
~noteMapper.putMap(\moog, ~makeClosestNoteMap.([36,48,60,31,43,55,67]));
)
// now in Tidal, you can do something like:
//
// d1 $ note "-12 -10 -9 -7 -5 -4 -2 0" # s "moog" # legato 1
//
// and watch/listen to the magic happen!
(
// but it doesnt have to be so manual. the moog samples are all tagged with
// their notes in the filename, e.g. "000_Mighty Moog C2.wav", so lets use that!
// helper function to parse a e.g. note like "C#4" into 61
// TODO: is there something built-in for this?
// TODO: error handling
~parseNote = { |noteName|
var parts = noteName.toLower.findRegexp("^([a-g])([#b]?)(\\d+)\$");
var baseNote = [0,2,4,5,7,9,11][['c','d','e','f','g','a','b'].indexOf(parts[1][1].asSymbol)];
var sharpFlatOffset = ['b','','#'].indexOf(parts[2][1].asSymbol) - 1;
var octaveOffset = (parts[3][1].asInteger + 1) * 12;
baseNote + sharpFlatOffset + octaveOffset;
};
// helper function to iterate through the samples for a sound and parse the note
// from each filename. `fileNameNoteRegexp` should be a regexp to extract the
// note name from an extension-less sample filename, by capturing it in the
// first subgroup
// TODO: error handling
~notesFromFileNames = { |sound, fileNameNoteRegexp|
~dirt.soundLibrary[sound].collect { |event|
var parts = PathName(event.bufferObject.path).fileNameWithoutExtension.findRegexp(fileNameNoteRegexp);
~parseNote.(parts[1][1]);
}
};
// and finally, the automatic version of building a note map for moog. since the
// filenames follow the form "000_Mighty Moog C2", we extract the note name
// as the last sequence of non-space characters
~noteMapper.putMap(\moog, ~makeClosestNoteMap.(~notesFromFileNames.(\moog, " ([^ ]+)\$")));
)