For Reaper users: 2 scripts as bridges to/from dictionaries

Dear all

I needed to get some dirty sequences in Max in a Reaper project and wanted to avoid recording the audio since I wanted to be able to edit/mix the playback windows. So I coded a little javascript utility that takes a simple dictionary with 5 variables per item (what file to play, when, its offset, how long for, and on what track) and converts that to a dirty reaper file that I can then plunder! Max for now, but I’ll port it to SuperCollider soon as I need it there too :slight_smile:

dict2reaper.zip (7.5 KB)

In the file, there is a ‘file granulator’ which is quite fun. It was meant as a test but I’ve used it already in my piece in progress!


Now you can imagine that I needed the other way around too, so I did a Python script for Reaper that will make the same format of dictionary with the selected items. It is quite fun to use for instance @jamesbradbury’s ReaCoMa to segment, then to export those to Max (or SC) to use in the next process…

Here we go.

reaper2dict.zip (2.8 KB)

Both are only tested by me, and have very little safeguarding, so use at your own risk (in other words, save before using :smiley: )

3 Likes

And now, for SuperCollider users (@spluta and @tedmoore and @groma but also myself at the moment)

The same idea, the script from line 61 is the magic, the rest is just a file-pointer-granulator as example. I will use the structure above for all my patches where I want the findings to be sent to a reaper file (the 5 items described above)

I hope you like it. The other way round already works, I’ll do the parsing example soon.

// loading a file of sounds to check their lenght
(
~sources = Dictionary();
~nbEntry = 0;

FileDialog.new({|selection|
	var fileNames = PathName.new(selection[0])
	.entries
	.select({|f|
		[\wav, \WAV, \mp3,\aif].includes(f.extension.asSymbol);});
	fileNames.do({arg fp;
		SoundFile.use(fp.asAbsolutePath , {
			arg file;
			~sources[("entry-"++~nbEntry)] = Dictionary.newFrom(["path",fp.asAbsolutePath, "length", file.numFrames]);
		});
		~nbEntry = ~nbEntry + 1;
	});
}, fileMode:2);
)

// check that source dictionary
~sources.postln
~nbEntry.postln

// granulate the file pointers in another dictionary
(
~numItems = 10;
~numtracks = 0;
~minLength = 0.01;
~maxLength = 0.1;
~minDelta = 0.01;
~maxDelta = 0.1;
)
// run the granular process
(
~time = 0;
~itemNb = 0;
~items = Dictionary();
~numItems.do{
	var dur, which, track, offset, sourceLen;
	which = ~nbEntry.rand;
	sourceLen = ~sources[("entry-"++which)]["length"];
	if (~numtracks !=0, {track = ~numtracks.rand},{track = which});
	dur =  ~minLength.rrand(~maxLength);
	if (dur > sourceLen, {offset = 0;dur=sourceLen},{offset = (sourceLen - dur).rand});
	~items[("item-"++~itemNb)] = Dictionary.newFrom([
		"file", ~sources[("entry-"++which)]["path"],
		"start", ~time,
		"offset", offset,
		"length", dur,
		"track", track]);
	~time = ~time + (~minDelta.rrand(~maxDelta));
	~itemNb = ~itemNb + 1;
};
)

//check the first item
~items["item-0"].postln


//this is making the reaper file from that dictionary structure
(
//first create a new file that ends with rpp - it will overwrite if the file exists
f = File.new("/tmp/testgranul.rpp","w+");

if (f.isOpen , {
	//sort the input dict per track in a temp dict
	var sortedpertrack = Dictionary();
	~items.keysValuesDo{|key,val|
		sortedpertrack.atFail(val["track"],{sortedpertrack[val["track"]] = Dictionary();});
		sortedpertrack[val["track"]].put(key, val);
	};

	//write the header
	f.write("<REAPER_PROJECT 0.1 \"5.99/OSX64\" 1603037150\n\n");

	//iterate through the tracks
	sortedpertrack.keysValuesDo{|trackname, trackitems|
		//write the track header
		f.write("<TRACK\nNAME \"" ++ trackname ++ "\"\n");
		// iterate through the items in the track
		trackitems.keysValuesDo{|itemname, itemdata|
			f.write("<ITEM\nPOSITION " ++ itemdata["start"] ++ "\nLENGTH " ++ itemdata["length"] ++ "\nNAME \"" ++ itemname ++ "\"\nSOFFS " ++ itemdata["offset"] ++ "\n<SOURCE WAVE\nFILE \"" ++ itemdata["file"] ++ "\"\n>\n>\n");
		};
		//write the track footer
		f.write(">\n");
	};
	//write the footer
	f.write(">\n");
	f.close;
});
)
1 Like

People of the world, I’d like to declare my love to SuperCollider:

//parses the file sent from Reaper
a = "/tmp/fromReaper.json".parseJSONFile

//plays a randomfile
b = a.keys.asList.scramble[0];
c = SoundFile.new(a[b]["file"]).sampleRate;
Buffer.read(s,a[b]["file"],(a[b]["offset"]).asFloat*c ,(a[b]["length"]).asFloat*c ,{|x|x.play;});

Just run the Python script in the first post in Reaper to generate the file, then use that dictionary in SuperCollider…

1 Like

one of us, one of us, one of us…

2 Likes

I found a ‘feature’ that I want to remove… items of length 0.0s that appeared because of other programmatic shenanigans should be removed to make a valid item… if you agree, use this revised version:

#make_dictionary_from_selection.py
#this script will make a basic json file where each selected item is an entry with basic info
#it was made possible through the FluCoMa project (European Union’s Horizon 2020 research and innovation programme, grant #725899)

import json

#get the number of selections
nb_of_sel = RPR_CountSelectedMediaItems(0)

thedict = {}

for item_number in range(nb_of_sel):
    #get the item's active take's source's filepath
    item = RPR_GetSelectedMediaItem( 0, item_number )
    active_take = RPR_GetActiveTake(item)
    dur = RPR_GetMediaItemInfo_Value( item, "D_LENGTH" )
    if (dur > 0):
        thedict["item" + str(item_number)] = {"track": RPR_GetTrackName(RPR_GetMediaItemTrack(item), "", 512)[2], "start": RPR_GetMediaItemInfo_Value( item, "D_POSITION" ), "file": RPR_GetMediaSourceFileName(RPR_GetMediaItemTake_Source(active_take), "", 512)[1], "offset": RPR_GetMediaItemTakeInfo_Value(active_take, "D_STARTOFFS"), "length": dur }

# RPR_ShowConsoleMsg(thedict + "\n")

f = open("/tmp/fromReaper.json",'w')
f.write(json.dumps(thedict))
f.close()