«Granular display» by tommaisey

on 14 Oct'12 22:13 in animationgranulardisplay

My first post to sccode! A display which visualises a granularised sound file. Each grain is represented by a dot which moves across the waveform of the sound file at its real speed (i.e. pitch) The dots fade in and out to show the effects of each grain's envelope, and amplitude is shown by position on the y-axis.

Partly inspired by the Borderlands granular synth (https://ccrma.stanford.edu/~carlsonc/256b/final/).

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
// Granular display by T.Maisey.
// Position in the sound file is (obviously) on the x-axis, amplitude on the y-axis.
// The grain's transparency shows its position in its envelope (window).
// The speed of the grain shows pitch.
// ----------------------------------------------

// Requires use of Qt gui kit.:
GUI.qt;

(
var soundFile, buffer, window, fileView, grainView, granary, displayGrain;

soundFile = SoundFile.openRead(Platform.resourceDir +/+ "sounds/a11wlk01.wav");
buffer = Buffer.readChannel(s, soundFile.path, channels: [0]);

{
SynthDef(\grain, {
		|bufNum = 0, pos, dur, env = 0.05, pitch = 1, pan = 0, amp = 0.5|
		var envl, sig;
		envl = EnvGen.kr(Env.linen(env, dur, env, amp, -3), doneAction: 2);
		sig = PlayBuf.ar(1, bufNum, pitch, 0, pos.linlin(0,1,0,BufFrames.kr(bufNum))) * envl;

		Out.ar(0, Pan2.ar(sig, pan));
}).add;

	s.sync;
}.fork;

~pos = { rrand(0.2, 0.6) };
~dur = { rrand(0.1, 0.5) };
~env = { rrand(0.3, 0.5) };
~amp = { rrand(0.1, 0.7) };
~pitch = { rrand(0.5, 2) }; // pitch is a ratio
~pan = { rrand(-0.8, 0.8) };
~wait = { 0.1 }; // time between grains

~playTask = Task({ loop {
	var pos, pitch, amp, dur, env;
	Synth(\grain,
		[
			\bufNum, buffer,
			\pos, pos = ~pos.value,
			\pitch, pitch = ~pitch.value,
			\amp, amp = ~amp.value,
			\dur, dur = ~dur.value,
			\env, env = ~env.value,
			\pan, ~pan.value,
		]);
	displayGrain.value(pos, pitch, amp, dur, env);

	~wait.value.wait;
}});


// Grain display

window = Window.new("Grain display", Rect(200, 300, 740, 300));


fileView = SoundFileView().readFile(soundFile).gridOn_(false);
grainView = UserView().resize_(5);

window.layout = StackLayout(fileView, grainView).mode_(\stackAll).index_(1);
window.onClose = {p.stop};
window.front;

// Where information about currently playing grains is stored for drawFunc to access:
granary = ();

// Add a grain to the granary:
displayGrain = {|pos, pitchRatio, amp, dur, env|
	var time, routine;

// I've limited it to displaying 30 grains, but I don't see why it couldn't do more.
	if (granary.size < 30, {

		time = Date.getDate().bootSeconds;
		routine = Routine {
			granary.put(time.asSymbol, [pos, pitchRatio, amp, dur, env]);
			(dur + (env * 2)).wait;
			granary.removeAt(time.asSymbol);
		}.play
	});
};

grainView.animate = true;

grainView.drawFunc = {
	var width, height, now, fileDur;

	width = grainView.bounds.width;
	height = grainView.bounds.height;
	now = Date.getDate().bootSeconds;
	fileDur = soundFile.duration;

	Pen.fillColor = Color.white;
	Pen.strokeColor = Color.black;

	granary.keysValuesDo {|k, v|
		var xP, yP, delta, alpha;

		delta = now - k.asFloat;
		xP = (width * v[0]) + (width * ((delta * v[1]) / fileDur));
		yP = (height * (1 - v[2]));

		if( delta < (v[3] + v[4]),
			{ alpha = (delta/v[4]).clip(0,1) },
			{ alpha = 1 - ((delta - (v[3] + v[4])) / v[4]) }
		);

		Pen.translate(xP, yP); // moveTo doesn't seem to work in this context, so translate...

		Pen.addOval(Rect(0,0,10,10));
		Pen.alpha = alpha;
		Pen.fillStroke;

		Pen.translate(xP.neg, yP.neg); // ...and translate back.
	};
};

// Start the grains:
p = ~playTask.play;

)

// Try some different granular parameters:
(
~pos = { rrand(0.3, 0.8) };
~dur = { rrand(0.01, 0.05) };
~env = { 0.02 };
~amp = { rrand(0.2, 0.6) };
~pitch = { rrand(2, 4.0) };
~pan = { rrand(-0.8, 0.8) };
~wait = { 0.01 }; // time between grains
)

// Or how about these:
(
~pos = { rrand(0, 0.5) };
~dur = { rrand(0.1, 0.2) };
~env = { rrand(0.3, 0.5) };
~amp = { rrand(0.2, 0.8) };
~pitch = { rrand(0.2, 0.7) };
~pan = { rrand(-0.8, 0.8) };
~wait = { 0.1 };
)

// To stop the grains, close the grain display window or evaluate:
p.stop;
descendants
«Re: Granular display» by julian.rohrhuber (private)
full graph
raw 3806 chars (focus & ctrl+a+c to copy)
comments
chrisdheij user 09 Nov'12 23:34

I'm really having fun with this, seeing the grains makes these things so much cooler

tommaisey user 16 Nov'12 20:08

Yeah, it makes the process much more intuitive. I'm working on a version that allows you to 'paint' onto the waveform, it's really fun.