«luce : standalone synth and custom slider GUI demo» by Dindoléon

on 18 Nov'18 11:54 in guibeginnerslidermodule

luce is a standalone synth module driven by a custom 2D slider GUI. It is easy to integrate and to modify. It also serves the purpose of understanding the bases of developping modules with SuperCollider for beginners.

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
(
/* luce module for SuperCollider, licensed under GNU GPL v3 (see end of file),

Luce is a surname, which means 'Light'; (latin origin: "Lux", see Genesis 1:3 for more informations).

This software was written by Simon Deplat (aka Dindoleon), but should be considered as a production of the whole SuperCollider community.

This demonstrates how to :
- Create a standalone module which opens inside a new window,
- Create a custom graphical 2d slider. Be careful, it only responds to mouse action, not to keyboard instructions,
- Write a simple SC code for beginners, as I tried to comment each step of the software. This covers creating variables, instanciating a SynthDef, creating a GUI, and creating functions which allows this GUI to interact with the playing synth,

This is a part of a music station I'm currently working on. The idea is that you can integrate this little module inside your own GUI, by passing a view as display argument to the slider, instead of the window (see line 59 - 60).
Also, you can easily create new modules by following those steps :
- Change the SynthDef by another SynthDef,
- Change the 'amount' mapping according to your needs,
- Finally (optionnal), change the name of the window and the colors of the displays.

See http://sccode.org/1-5aZ for an uncommented alternate version.

Special thanks to Bruno Ruviaro for his GUI examples which inspired (and allowed) this piece of code.
*/

// Variables we're going to use :
var synth, synth_amount_range, win, window_size, slider, margin, value, knob_size, knob_outline_size, stroke_size, frame_color, frame_border_color, gradient_color, diamond_color, diamond_outline_color;

// Size of items
window_size = [ 400, 300 ];

knob_size = 8;
knob_outline_size = 15;
stroke_size = 2;

margin = 3;

// Colors
frame_color = Color.new( 0.7, 0.4, 0 );
frame_border_color = Color.new( 1, 0.4, 0 );
gradient_color = Color.new( 0.9, 0.5, 0.3 );
diamond_color = Color.new( 1, 0.75, 0.25 );
diamond_outline_color = Color.new( 0.8, 0.55, 0.05 );

// Mapping ( Y axis doesn't require further variable mapping as amplitude goes from 0 to 1 )
value = [ 0, 1 ];
synth_amount_range = [ 24, 16000 ];

// Synthdef; this a really simple sound inspired by the amazing Tour of UGen documentation file.
synth = SynthDef(\luce, {
	|out = 0, freq = 440, amp = 0, amount = 24|
	var snd;

	snd = Resonz.ar( Saw.ar(freq), amount, 0.2 ); // Change this mul: variable if you want to increase the max amplitude of the synth, but go easy on it because you could easily harm your ears !

	Out.ar(out, Pan2.ar(snd, 0, amp));
}).play;

// GUI management:
win = Window("luce", Rect(0, 0, window_size[0], window_size[1]), false);
win.background_( frame_color );

// Change the 'win' argument for a View if you want to integrate it inside a custom GUI. You will also need to change the window_size variable according to the size you want the module to have.
slider = UserView( win, Rect( margin, margin, window_size[0] - ( margin * 2 ), window_size[1] - ( margin * 2 ) ));

// Here is the function used to draw the custom slider:
slider.drawFunc = {
	Pen.width = stroke_size;

	// First, draw the background frame:
	Pen.addRect( Rect(0,0, slider.bounds.width,slider.bounds.height) );
	Pen.fillAxialGradient( slider.bounds.leftBottom, slider.bounds.rightTop, Color.black, gradient_color );

	// Draw the diamond itself:
	Pen.moveTo( ( slider.bounds.width * value[0] - knob_size ) @ ( slider.bounds.height * value[1] ) );
	Pen.lineTo( ( slider.bounds.width * value[0] ) @ ( slider.bounds.height * value[1] - knob_size ) );
	Pen.lineTo( ( slider.bounds.width * value[0] + knob_size ) @ (( slider.bounds.height * value[1] ) ) );
	Pen.lineTo( ( slider.bounds.width * value[0] ) @ ( slider.bounds.height * value[1] + knob_size ) );
	// Fourth line isn't needed as we fill the shape.

	Pen.fillColor_( diamond_color );
	Pen.fill;

	// Draw the diamond outline:
	Pen.moveTo( 0 @ ( slider.bounds.height * value[1] ) );
	Pen.lineTo( ( slider.bounds.width * value[0] - knob_outline_size ) @ ( slider.bounds.height * value[1] ) );

	Pen.moveTo( ( slider.bounds.width * value[0] ) @ 0 );
	Pen.lineTo( ( slider.bounds.width * value[0] ) @ ( slider.bounds.height * value[1] - knob_outline_size ) );

	Pen.moveTo( ( slider.bounds.width * value[0] + knob_outline_size ) @ (( slider.bounds.height * value[1] ) ) );
	Pen.lineTo( slider.bounds.width @ (( slider.bounds.height * value[1] ) ) );
	Pen.moveTo( ( slider.bounds.width * value[0] ) @ (( slider.bounds.height * value[1] + knob_outline_size ) ) );
	Pen.lineTo( ( slider.bounds.width * value[0] ) @  slider.bounds.height );

	Pen.moveTo( ( slider.bounds.width * value[0] - knob_outline_size ) @ ( slider.bounds.height * value[1] ) );
	Pen.lineTo( ( slider.bounds.width * value[0] ) @ ( slider.bounds.height * value[1] - knob_outline_size ) );
	Pen.lineTo( ( slider.bounds.width * value[0] + knob_outline_size ) @ (( slider.bounds.height * value[1] ) ) );
	Pen.lineTo( ( slider.bounds.width * value[0] ) @ (( slider.bounds.height * value[1] + knob_outline_size ) ) );
	Pen.lineTo( ( slider.bounds.width * value[0] - knob_outline_size ) @ ( slider.bounds.height * value[1] ) );

	Pen.strokeColor_( diamond_outline_color );
	Pen.stroke;

	// Draw the frame border:
	Pen.addRect( Rect(0,0, slider.bounds.width,slider.bounds.height) );
	Pen.strokeColor_( frame_border_color );
	Pen.stroke;
};

// Set the default action
slider.action = {
	synth.set(\amp, 1 - value[1]); // Requires to invert the value, as the Y axis from GUI goes from top to bottom, and sliders usually goes from bottom to top.
	synth.set(\amount, linexp(value[0], 0, 1, synth_amount_range[0], synth_amount_range[1])); // Exponential mapping between the 0 -> 1 value and the amount range. Change this settings according to your needs.

	slider.refresh // Call the drawFunc of the slider to update graphics
};

// Define mouse actions
slider.mouseDownAction = { arg slider, x = 0.5, y = 0.5, m;
	([256, 0].includes(m)).if{ // restrict to no modifier
		value[0] = (x).linlin(0,slider.bounds.width,0,1); // Linear mapping between the slider size and 0 -> 1
		value[1] = (y).linlin(0,slider.bounds.height,0,1);
		slider.doAction};
};

slider.mouseMoveAction = slider.mouseDownAction; // Map the mouse action to its function

win.front; // Start the software by giving life to the window

/* LICENSE:

This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <https://www.gnu.org/licenses/>.

*/
)
raw 7141 chars (focus & ctrl+a+c to copy)
reception
comments
beryann.parker user 07 Oct'23 13:59

Merci pour ce morceau de code qui permet à la souris de transformer le son en fonction de sa spatialisation! la longeur du code est somme toute impressionnante! :-)

s.deplat user 01 Nov'23 11:01

Merci beaucoup ! C'est l'algorithme de dessin de l'interface qui prend beaucoup de place (tire un trait là, puis là, puis là...). J'ai commencé à développer un Quark qui donne accès à ce slider à deux dimensions en tant que Classe, de fait, il n'y a plus besoin que d'environ cinq lignes pour le créer et le paramétrer. En espérant le partager avec vous bientôt !