aboutsummaryrefslogtreecommitdiffstats
path: root/SNPSonificator.sc
blob: a14dde108af8fc056b0ceb6f7b85325d01a4dcf8 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
SNPSonificator {
	var <>numChannels;//number of channels to use
	var <snpSynths;
	var <>snpDict;//the SNPDict to use
	var <>playTime;//calculated play time of a single SNP, defined by playTime setup on init
	var <>includeUnknown;//wether to include SNPs with unknown resolvers (aka. SNPS with only bases set)
	var <>ignoreSNPs = #[];//Array of Symbols of chromosomes to ignore (1-22,X,Y,MT)
	var <keys;//Array storing normalized settings for each chromosome
	var <keyToFrq;//Dictionary with normalized frequency ranges for 8 different chromosome lengths
	var <play = false;//boolean indicating the state of playing
	var <currentStep = 0.0;//storage for the position up to which sonification should take place
	var <lastStep = 0.0;//storage for the last played step (SNP position) aka. current position playing
	var <lengthOfSNP = 1.0;//distance between two SNPs in ms
	var <speakerSetup;//Array holding the chromosome numbers for each speaker
	var <synthGroup;//Group for the Synths
	var <fxGroup;//Group for effects
	var <compressionGroup;//Group for compression Synths
	var <recordingGroup;//Group for recording Synths
	var <sonBus;//Bus for Synth->compression
	var <fxBus;//Bus for chromosome based effects
	var <recBus;//Bus for compression->recording/out
	var <record =false;//boolean indicating wether to record or not (default off)
	var <>recPath;//String representing directory to use for recording
	var <recordingBuffer;//the recording buffer to use, wehen recording
	var <baseTone = #[110.00, 65.4, 98.00, 73.42];//the fundamentals from which 8 octaves will be calculated: A (A2), C (C2), G (G2), T (D2)

	*new{
		arg snpDict, numChannels = 2, playTime, includeUnknown = false, ignoreSNPs, setRecord = false, recordPath;
		^super.new.sonificatorInit(snpDict, numChannels, playTime, includeUnknown, ignoreSNPs, setRecord, recordPath);
	}

	sonificatorInit{
		arg snpDict, numChannels, playTime, includeUnknown, ignoreSNPs, setRecord, recordPath;
		this.snpDict = snpDict;
		this.numChannels_(numChannels);
		this.playTime = playTime;
		lengthOfSNP = this.calcSNPTime(playTime);
		this.includeUnknown = includeUnknown;
		if(ignoreSNPs.notNil && ignoreSNPs.isArray,{
			this.ignoreSNPs = ignoreSNPs;
		});
		this.initKeyRange.value;
		this.setupSpeakers(numChannels, true);
		this.setupGroups(numChannels);
		this.setupBusses(numChannels);
		this.addSNPSynths.value;
		//TODO: add effects Synths based on chromosome areas
		if(setRecord,{//if recording should be set up
			if(recordPath.notNil && recordPath.isString,{//first check if recordingPath is set
				if(recordPath[recordPath.size-1]!="/",{//compensate for missing trailing slash
					recPath = (recordPath++"/");
				},{
					recPath = recordPath;
				});
				if(File.exists(recPath),{//if path exists, init recording to file
					recPath = recordPath;
					record = setRecord;
					this.setupRecording(numChannels);	
				},{
					("Path does not exist: "++recPath.asString).postln;
					("Update recPath!")
				});
			},{
				("Recording path invalid: "++recPath.asString).postln;
			});
		});
	}

	setupBusses{//setup the busses for compression and recording
		arg channels;
		sonBus = Bus.audio(Server.local, channels);
		recBus = Bus.audio(Server.local, channels);
	}
	
	setupGroups{//setup Groups (for Synths, compression and recording)
		arg channels;
		synthGroup = Group.new;
//		fxGroup = Group.after(synthGroup);
		compressionGroup = Group.after(synthGroup);
		recordingGroup = Group.after(compressionGroup);
		NodeWatcher.register(synthGroup);
//		NodeWatcher.register(fxGroup);
		NodeWatcher.register(compressionGroup);
		NodeWatcher.register(recordingGroup);
	}
	
	setupRecording{//create a buffer and a recording SynthDef, leaving file open for writing
		arg channels;
		("Setting up recording to file.").postln;
		recordingBuffer = Buffer.alloc(Server.local, 262144, channels);
		recordingBuffer.write(recPath++"SNPSonification-"++(Date.seed).asString++"_("++channels.asString++"_chan).aiff".standardizePath, "aiff", "float32", 0, 0, true);
		SynthDef(\recordSNP,{
			arg buffer;
			DiskOut.ar(buffer, In.ar(recBus, channels));
		}).add;
	}
	
	setupSpeakers{//setup speakers for stereo and circular multichannel
		arg channels, printOuts = false;
		switch(channels.asInt, 
			2,{speakerSetup = [[8,16,24,6,14,22,4,12,20,2,10,18],[7,15,23,1,9,17,3,11,19,5,11,21]];},
			4,{speakerSetup = [[3,7,11,15,19,23],[4,8,12,16,20,24],[2,6,10,14,18,22],[1,5,9,13,17,21]];},
			6,{speakerSetup = [[5,11,17,23],[6,12,18,24],[4,10,16,12],[2,8,14,20],[1,7,13,19],[3,9,15,21]];},
			8,{speakerSetup = [[7,15,23],[8,16,24],[6,14,22],[4,12,20],[2,10,18],[1,9,17],[3,11,19],[5,13,21]];},
			{"Invalid number of speakers set! Fix this by calling setupSpeakers(nSpeakers) on this object or this will break. Seriously!".postln;}
		);
		if(printOuts,{//print the speaker setup
			speakerSetup.do({
				arg item, i;
				("Speaker "++(i+1).asString++" has chromosomes: ").post; 
				item.do({
					arg chromosome;
					(chromosome.asString++" ("++keys[chromosome-1].asString++") ").post;
				});
				"".postln;
			});
		});
	}

	initKeyRange{//init 8 octave spanning frequency set, map SNPs to frequencies
		keys = round(SNPInfo.chromosomesFactor[0..SNPInfo.chromosomesFactor.size-2].normalizeSum*10, 0.125);
		keys.add(0.0);
		keyToFrq = Dictionary.new(8);
		8.do({//setup for 8 octaves
			arg item, i;
			switch(i,
				0, {keyToFrq.put(0.000, this.calcFrqCombination(baseTone));},
				1, {keyToFrq.put(0.125, this.calcFrqCombination(baseTone*2.pow(i)));},
				2, {keyToFrq.put(0.250, this.calcFrqCombination(baseTone*2.pow(i)));},
				3, {keyToFrq.put(0.375, this.calcFrqCombination(baseTone*2.pow(i)));},
				4, {keyToFrq.put(0.500, this.calcFrqCombination(baseTone*2.pow(i)));},
				5, {keyToFrq.put(0.625, this.calcFrqCombination(baseTone*2.pow(i)));},
				6, {keyToFrq.put(0.750, this.calcFrqCombination(baseTone*2.pow(i)));},
				7, {keyToFrq.put(0.875, this.calcFrqCombination(baseTone*2.pow(i)));}
			);
		});
	}
	
	calcFrqCombination{//calculate tones for bases and their combinations for a given set of notes from the same octave
		arg baseTones;
		^Dictionary.newFrom(List[
			SNPInfo.a, baseTones[0], 
			SNPInfo.c, baseTones[1], 
			SNPInfo.g, baseTones[2], 
			SNPInfo.t, baseTones[3], 
			SNPInfo.a-SNPInfo.c, (baseTones[0]+baseTones[1])/2, 
			SNPInfo.a-SNPInfo.g, (baseTones[0]+baseTones[2])/2, 
			SNPInfo.a-SNPInfo.t, (baseTones[0]+baseTones[3])/2, 
			SNPInfo.c-SNPInfo.g, (baseTones[1]+baseTones[2])/2, 
			SNPInfo.c-SNPInfo.t, (baseTones[1]+baseTones[3])/2, 
			SNPInfo.g-SNPInfo.t, (baseTones[2]+baseTones[3])/2]
		);
	}
	
	addSNPSynths{//Synthdefs for base pairs, single bases and the MT SNPs
		"Adding SynthDef for base pairs -> \base_pair".postln;
		SynthDef(\base_pair, {|out=0, freq=#[0.0,0.0,0.0] ,amplitude=0.6, attackTime = 0.01, releaseTime = 0.3, curve = -4|	
		var env = EnvGen.kr(
				Env.perc(attackTime,releaseTime,amplitude,curve),doneAction:2//free the enclosing Synth when done
			);
		Out.ar(
			sonBus.index+out,//speaker to play on
			SinOsc.ar(//sine tone for the base pair
				freq[0],0,env,
				SinOsc.ar(//sine tone for the first (lower) resolver
					freq[1],0,env*0.5
				)+
				SinOsc.ar(//sine tone for the second (upper) resolver
					freq[2],0,env * 0.5
				)
			)
		);}, variants: (//variants for 23 first chromosomes
			1: [amplitude: 0.10, attackTime:0.01, releaseTime:0.7, curve:-4],//0.125
			2: [amplitude: 0.10, attackTime:0.01, releaseTime:0.7, curve:-4],//0.125
			3: [amplitude: 0.08, attackTime:0.01, releaseTime:0.6, curve:-4],//0.250
			4: [amplitude: 0.08, attackTime:0.01, releaseTime:0.6, curve:-4],//0.250
			5: [amplitude: 0.08, attackTime:0.01, releaseTime:0.6, curve:-4],//0.250
			6: [amplitude: 0.08, attackTime:0.01, releaseTime:0.6, curve:-4],//0.250
			7: [amplitude: 0.08, attackTime:0.01, releaseTime:0.6, curve:-4],//0.250
			8: [amplitude: 0.08, attackTime:0.01, releaseTime:0.6, curve:-4],//0.250
			9: [amplitude: 0.08, attackTime:0.01, releaseTime:0.6, curve:-4],//0.250
			10: [amplitude: 0.06, attackTime:0.01, releaseTime:0.5, curve:-2],//0.375
			11: [amplitude: 0.06, attackTime:0.01, releaseTime:0.5, curve:-2],//0.375
			12: [amplitude: 0.06, attackTime:0.01, releaseTime:0.5, curve:-2],//0.375
			13: [amplitude: 0.06, attackTime:0.01, releaseTime:0.5, curve:-2],//0.375
			14: [amplitude: 0.06, attackTime:0.01, releaseTime:0.5, curve:-2],//0.375
			15: [amplitude: 0.06, attackTime:0.01, releaseTime:0.5, curve:-2],//0.375
			16: [amplitude: 0.04, attackTime:0.01, releaseTime:0.5, curve:-2],//0.500
			17: [amplitude: 0.04, attackTime:0.01, releaseTime:0.5, curve:-2],//0.500
			18: [amplitude: 0.04, attackTime:0.01, releaseTime:0.5, curve:-2],//0.500
			19: [amplitude: 0.04, attackTime:0.01, releaseTime:0.5, curve:-2],//0.750
			20: [amplitude: 0.05, attackTime:0.01, releaseTime:0.5, curve:-2],//0.625
			21: [amplitude: 0.05, attackTime:0.01, releaseTime:0.5, curve:-2],//0.875
			22: [amplitude: 0.05, attackTime:0.01, releaseTime:0.5, curve:-2],//0.875
			23: [amplitude: 0.05, attackTime:0.01, releaseTime:0.5, curve:-2]//0.875
		)).add;
		"Adding SynthDef for single bases -> \base_single".postln;
		SynthDef(\base_single, {|out=0, freq=#[0.0,0.0] ,amplitude=0.6, attackTime = 0.01, releaseTime = 0.3, curve = -4|
			var env = EnvGen.kr(
				Env.perc(attackTime,releaseTime,amplitude,curve),doneAction:2//free the enclosing Synth when done
			);
			Out.ar(
				sonBus.index+out,
				Saw.ar(//sine tone for the base
					freq[0],env,SinOsc.ar(//sine tone for the resolver
						freq[1],0,env*0.5
					)
				)
			);},variants: (//variants for X and Y chromosomes
				23:[amplitude: 0.07, attackTime: 0.01, releaseTime: 0.5, curve: -4],//0.250 -> X
				24:[amplitude: 0.10, attackTime: 0.01, releaseTime: 0.7, curve: -4]//0.75 ->Y
		)).add;
		"Adding SynthDef for MT bases -> \base_mt".postln;
		SynthDef(\base_mt, {|startPosition = 0, freq = #[0.0,0.0], attackTime = 0.01, releaseTime = 1.5, amplitude = 0.4, curve = -4 |
			var mtSynth, panAround; 
			var env = EnvGen.kr(
				Env.perc(attackTime,releaseTime,amplitude,curve),doneAction:2//free the enclosing Synth when done
			);
			mtSynth = Saw.ar(//sine tone for the base
				freq[0],env,SinOsc.ar(//sine tone for the resolver
					freq[1],0,env*0.5
				)
			);
			panAround = PanAz.ar(
				numChannels,//span the whole number of channels available
				mtSynth,//use the MT Synth as input
				LFSaw.kr(2,0),//pan around TODO: + startPosition
				0.5,//control level
				2,//width of sound
				startPosition//random starting point
			);
			Out.ar(sonBus, panAround);
		}).add;
		"Adding SynthDef for compression -> \compressor".postln;
		SynthDef(\compressor, {|inBus|
			var in,compressed;
			in = In.ar(inBus,numChannels);
			compressed = Compander.ar(
				in,//signal in
				in,//control signal
				0.5,//threshold
				1,//slopeBelow
				0.3,//slopeAbove
				0.002,//clampTime
				0.01//relaxTime
			);
			ReplaceOut.ar(0, compressed);
			Out.ar(recBus, compressed);
		}).add;
	}
	
	startDSPSynths{//start compression and possibly recording Synths
		("Starting compression Synth.").postln;
		Synth(\compressor, [inBus:sonBus], compressionGroup);//play compression Synth
		if(record,{//if recording, play recording Synth
			("Starting recording Synth for recording to file.").postln;
			Synth(\recordSNP, [buffer: this.recordingBuffer], recordingGroup);
		});
	}

	stopDSPSynths{//stop compression and possibly recording Synths
		Routine{
			2.wait;
			if(record,{
				("Stopping recording to file.").postln;
				recordingBuffer.close;
				recordingBuffer.free;
				if(recordingGroup.isRunning,{
					recordingGroup.run(false);
				});
			});
			1.wait;
			if(compressionGroup.isRunning,{
				("Stopping compressor Synth.").postln;
				compressionGroup.run(false);
			});
		}.play;
	}
	
	playFromTo{//start from a position (if none set, start from the beginning), play to a position (or end, if none set)
		arg fromPosition = 0.0, toPosition = snpDict.positions;//start from position, play to position
		var startTime=0.0;//time to first position (if not playing from the beginning, this stays the same)
		this.startDSPSynths;//start Synths for compression and recording
		play = true; //set play parameter
		if((fromPosition == 0.0) || (snpDict.positions<fromPosition),{//if playing from start (or out of bounds), calculate start time for the first SNP
			startTime = lengthOfSNP*snpDict.distanceToNextPosition(fromPosition.asFloat);
			currentStep = fromPosition;//set start position
		},{//else start at once 
			currentStep = fromPosition;
		});
		if((toPosition>snpDict.positions) || (toPosition < fromPosition) || (toPosition.isNil),{//if to position is out of bounds or smaller then from, set to total length
			lastStep = snpDict.positions;
		},{//else play up to that position
			lastStep = toPosition;
		});
		("Starting to play positions: "++currentStep++" - "++lastStep).postln;
		SystemClock.sched(startTime,{//start a SystemClock immediately or with calculated startTime to read from the SNPDict
			arg time;
			var resched = 0.0;
			if((play) && (snpDict.positions>currentStep) && (currentStep <= lastStep),{//if playing and positions are left, play the position and reschedule SystemClock to the next SNP
//				("Playing SNP #"++currentStep++" of "++snpDict.positions++".").postln;
				resched = lengthOfSNP*this.playPosition(snpDict.snpAtPosition(snpDict.snpArr[currentStep]), snpDict.snpArr[currentStep]);
				currentStep = currentStep+1.0;
//				("Rescheduling in "++resched++"s for step #"++currentStep++" of "++snpDict.positions).postln;
				resched;
			},{//end the SystemClock and print out last position
				("Done playing. Last position: "++(currentStep-1).asString).postln;
				this.stopDSPSynths;
				nil;
			});
		});
	}
	
	pausePlay{//pause the sonification
		play = false;
	}
	
	resumePlay{//resume the sonification from last position played
		this.playSon(currentStep, lastStep);
	}
	
	playPosition{//play the SNP(s) at a position and return the distance to the next
		arg posDict, position;
		posDict.keysValuesDo{
			arg key, value;
			if(ignoreSNPs.includes(key).not,{//ignore chromosomes if that is set up
				if(includeUnknown,{//if SNPs without resolvers should be ignored, do that
					this.playBase(value);
				},{
					if(value.hasResolver,{
						this.playBase(value);
					});
				});
			});
		};
		^snpDict.distanceToNextPosition(position);
	}

	playBase{//play the actual base/base pair
		arg snp;
		var baseKey = keyToFrq.at(keys[snp.vecChromosome-1]);
		if(SNPInfo.isBasePair(snp.base),{//play base pairs
			Synth('base_pair.'++snp.vecChromosome.asSymbol, [out: this.findSpeaker(snp.vecChromosome), freq: [baseKey.at(snp.vecBase), baseKey.at(snp.vecResolver[0]), baseKey.at(snp.vecResolver[1])]], synthGroup);
		},{
			if(SNPInfo.isBase(snp.base), {//play single bases
				if(snp.chromosome==\MT, {//play the MT chromosome
					Synth('base_mt', [startPosition: rrand(numChannels,0), freq: [baseKey.at(snp.vecBase), baseKey.at(snp.vecResolver[0])]], synthGroup);
				},{//play parts of the X and the Y chromosome
					Synth('base_single.'++snp.vecChromosome.asSymbol, [out: this.findSpeaker(snp.vecChromosome), freq: [baseKey.at(snp.vecBase), baseKey.at(snp.vecResolver[0])]], synthGroup);
				});
			});
		});
		baseKey.free;
	}

	findSpeaker{//find the speaker for the chromosome (defined in speakerSetup)
		arg chromosome;
		var speaker = 0;
		speakerSetup.do({
			arg item, i;
			if(item.includes(chromosome),{
				speaker = i;
			});
		});
		^speaker;
	}

	calcSNPTime{//calculate the clock time for a single SNP
		arg playTime;
		var snpTime;
		snpTime = ((1/SNPInfo.lengthAtOneMs)*playTime)/1000;
		^snpTime;
	}

	queryTimeAndPosition{//query current time and position
		("Positions: "++currentStep.asString++" - "++lastStep.asString++".").postln;
		("Play time: "++(currentStep*lengthOfSNP*1000).asString++"s - "++(lastStep*lengthOfSNP*1000).asString++"s.").postln;
	}
}