1function gui()
2	pkg load signal;
3	clear;
4	EQ = [];
5	Fs = get_config().Fs;
6	nyquist_f = Fs/2;
7	fn = 1;
8	% create figure and panel on it
9	fn = figure(fn, "toolbar", "none", "menubar", "none", 'Position', [200 200 1500 700]); fn = fn + 1;
10	EQ_right = uipanel ("title", "EQ Right", "position", [0, 0, 1, 0.5]);
11	EQ_left = uipanel ("title", "EQ Left", "position", [0, 0.5, 1, 0.5]);
12	main_panes = [struct("pane", EQ_left); struct("pane", EQ_right)];
13
14	% UI panel relative position table
15	positions = [
16		[0    0   0.25 0.5 ]
17		[0.25 0   0.5  0.5 ]
18		[0.5  0   0.75 0.5 ]
19		[0.75 0   1    0.5 ]
20		[0    0.5 0.25 1   ]
21		[0.25 0.5 0.5  1   ]
22		[0.5  0.5 0.75 1   ]
23		[0.75 0.5 1    1   ]
24	];
25
26	fn = figure(fn); fn = fn + 1;
27	h = [0 0];
28	w = [1 nyquist_f];
29	n_octaves = 10;
30	subplot(2, 1, 1);
31	main_panes(1).plot = plot(log2(w), h, "linewidth", 4);
32	% Range set to match http://audio-tuning.appspot.com/
33	axis([log2(nyquist_f / (2 ^ n_octaves)), log2(nyquist_f), -24, 18]);
34	grid on;
35	subplot(2, 1, 2);
36	main_panes(2).plot = plot(log2(w), h, "linewidth", 4);
37	% Range set to match http://audio-tuning.appspot.com/
38	axis([log2(nyquist_f / (2 ^ n_octaves)), log2(nyquist_f), -24, 18]);
39	grid on;
40	% TODO add ability to switch between octaves and log(Hz)
41	for c = 1:2
42		for i = 1:length(positions)
43			EQ = initialize_biquad(i, c, positions(i, 1:end), main_panes, Fs, EQ);
44		endfor
45	endfor
46	% octave is silly and doesn't support references so we have to re-write the
47	% control matrix so the callbacks have access without using globals
48	for c = 1:2
49		for i = 1:length(positions)
50			update_control_elements(EQ(i, c), EQ);
51		endfor
52	endfor
53	fn = figure(fn, "toolbar", "none", "menubar", "none", 'Position', [200 200 300 220]); fn = fn + 1;
54	menu_window = struct();
55	menu_window.ip_box = uicontrol( ...
56		"style", "edit", ...
57		"position", [120 190 180 30]);
58	menu_window.ip_label = uicontrol( ...
59		"style", "text",
60		"string", "DUT IP:", ...
61		"position", [0 190 120 30]);
62	menu_window.user_box = uicontrol( ...
63		"style", "edit", ...
64		"position", [120 160 180 30]);
65	menu_window.user_label = uicontrol( ...
66		"style", "text",
67		"string", "DUT User:", ...
68		"position", [0 160 120 30]);
69	menu_window.device_box = uicontrol( ...
70		"style", "edit", ...
71		"position", [120 130 180 30]);
72	menu_window.device_label = uicontrol( ...
73		"style", "text",
74		"string", "Device Num:", ...
75		"position", [0 130 120 30]);
76	menu_window.alsa_box = uicontrol( ...
77		"style", "edit", ...
78		"position", [120 100 180 30]);
79	menu_window.alsa_label = uicontrol( ...
80		"style", "text",
81		"string", "ALSA Ctl Num:", ...
82		"position", [0 100 120 30]);
83	menu_window.upload = uicontrol( ...
84		"style", "pushbutton", ...
85		'string', 'Push config to DUT', ...
86		"callback", @push_config, ...
87		"position", [0 50 300 50], ...
88		"userdata", struct("menu_window", menu_window, "EQ", EQ));
89	menu_window.load_dsp = uicontrol( ...
90		"style", "pushbutton", ...
91		'string', 'Load dsp.ini', ...
92		"callback", @eq_load_dsp_ini, ...
93		"position", [0 0 300 50], ...
94		"userdata", struct("EQ", EQ));
95end
96
97function conf = get_config()
98  % since octave has no definitive way to have constants we are going to hack a
99  % struct into behaving like one
100  conf = struct( ...
101      "Fs", 48000 ...
102      );
103end
104
105function set_udata_field(h, field, value)
106	udata = get(h, "userdata");
107	udata = setfield(udata, field, value);
108	set(h, "userdata", udata);
109end
110
111function peq = build_peq(channel, EQ)
112	peq = [];
113	for i = 1:8
114		biquad = EQ(i, channel);
115		if get(biquad.enable_switch, "value") == 1
116			type = get(biquad.type.dropdown, "value");
117			freq = str2num(get(biquad.freq.edit, "string"));
118			Q = str2double(get(biquad.Q.edit, "string"));
119			gain = str2double(get(biquad.gain.edit, "string"));
120			% dropdown maps straight to filter index
121			% filter only uses needed arguments, all others will be ignored
122			peq = [peq; type, freq, gain, Q];
123		endif
124	endfor
125end
126
127function update_plot(channel, main_panes, EQ)
128	Fs = get_config().Fs;
129	nyquist_f = Fs / 2;
130	pl = main_panes(channel).plot;
131	spectrum_size = 1000;
132	n = logspace(log10(1), log10(nyquist_f), spectrum_size);
133	peq = build_peq(channel, EQ);
134	[z p k] = eq_define_parametric_eq(peq, Fs);
135	% octave gets angry when you pile up poles and zeros
136	[num den] = zp2tf(z, p, k);
137	[h, w] = freqz(num,den, n, Fs);
138	h = 20*log10(h);
139	set(pl, "YData", h, "XData", log2(w));
140end
141
142function set_section_visibility(section, visible)
143	for [control, control_name] = section
144		set(control, "visible", visible);
145	endfor
146end
147
148function dropdown_callback(h, event)
149	udata = get(h, "userdata");
150	EQ = udata.EQ;
151	biquad  = EQ(udata.index, udata.channel);
152	eq = eq_defaults();
153	switch (get(h, "value"))
154		case eq.PEQ_HP1
155			% highpass 1st order
156			set_section_visibility(biquad.Q, "off");
157			set_section_visibility(biquad.gain, "off");
158		case eq.PEQ_HP2
159			% highpass 2nd order
160			set_section_visibility(biquad.Q, "off");
161			set_section_visibility(biquad.gain, "off");
162		case eq.PEQ_LP1
163			% lowpass 1st order
164			set_section_visibility(biquad.Q, "off");
165			set_section_visibility(biquad.gain, "off");
166		case eq.PEQ_LP2
167			% lowpass 2nd order
168			set_section_visibility(biquad.Q, "off");
169			set_section_visibility(biquad.gain, "off");
170		case eq.PEQ_LS1
171			% lowshelf 1st order
172			set_section_visibility(biquad.Q, "off");
173			set_section_visibility(biquad.gain, "on");
174		case eq.PEQ_LS2
175			% lowhelf 2nd order
176			set_section_visibility(biquad.Q, "off");
177			set_section_visibility(biquad.gain, "on");
178		case eq.PEQ_HS1
179			% highshelf 1st order
180			set_section_visibility(biquad.Q, "off");
181			set_section_visibility(biquad.gain, "on");
182		case eq.PEQ_HS2
183			% highshelf 2nd order
184			set_section_visibility(biquad.Q, "off");
185			set_section_visibility(biquad.gain, "on");
186		case eq.PEQ_PN2
187			% peaking
188			set_section_visibility(biquad.Q, "on");
189			set_section_visibility(biquad.gain, "on");
190		case eq.PEQ_LP4
191			% lowpass 4th order
192			set_section_visibility(biquad.Q, "off");
193			set_section_visibility(biquad.gain, "off");
194		case eq.PEQ_HP4
195			% highpass 4th order
196			set_section_visibility(biquad.Q, "off");
197			set_section_visibility(biquad.gain, "off");
198		case eq.PEQ_LP2G
199			% lowpass 2nd order with resonnance
200			set_section_visibility(biquad.Q, "on");
201			set_section_visibility(biquad.gain, "off");
202		case eq.PEQ_HP2G
203			% highpass 2nd order with resonnance
204			set_section_visibility(biquad.Q, "on");
205			set_section_visibility(biquad.gain, "off");
206		case eq.PEQ_BP2
207			% bandpass
208			set_section_visibility(biquad.Q, "on");
209			set_section_visibility(biquad.gain, "off");
210		case eq.PEQ_NC2
211			% notch
212			set_section_visibility(biquad.Q, "on");
213			set_section_visibility(biquad.gain, "off");
214		case eq.PEQ_LS2G
215			% lowshelf, CRAS implementation
216			set_section_visibility(biquad.Q, "off");
217			set_section_visibility(biquad.gain, "on");
218		case eq.PEQ_HS2G
219			% highshelf, CRAS implementation
220			set_section_visibility(biquad.Q, "off");
221			set_section_visibility(biquad.gain, "on");
222	endswitch
223	update_plot(udata.channel, udata.main_panes, EQ);
224end
225
226function enable_callback(h, event)
227	off = get(h, "value") == 0;
228	if off
229		visible = "off";
230	else
231		visible = "on";
232	endif
233	udata = get(h, "userdata");
234	EQ = udata.EQ;
235	biquad  = EQ(udata.index, udata.channel);
236	for [section, section_name] = biquad
237		if strcmp(section_name, "enable_switch") || strcmp(section_name, "pane")
238			continue
239		endif
240		set_section_visibility(section, visible);
241	endfor
242	if !off
243		dropdown_callback(biquad.type.dropdown,0);
244	endif
245	update_plot(udata.channel, udata.main_panes, EQ);
246end
247
248function slider_callback(h, event)
249	udata = get(h, "userdata");
250	EQ = udata.EQ;
251	editbox  = getfield(EQ(udata.index, udata.channel), udata.prefix).edit;
252	val = get(h, "value");
253	if udata.is_log
254		val = 0.1*2^val;
255	endif
256	set(editbox, "string", num2str(val));
257	update_plot(udata.channel, udata.main_panes, EQ);
258end
259
260function edit_callback(h, event)
261	udata = get(h, "userdata");
262	EQ = udata.EQ;
263	slider  = getfield(EQ(udata.index, udata.channel), udata.prefix).slider;
264	val = str2num(get(h, "string"));
265	if udata.is_log && val != 0
266		val = log2(val/0.1);
267	end
268	set(slider, "value", val);
269	update_plot(udata.channel, udata.main_panes, EQ);
270end
271
272function EQ = initialize_biquad(index, channel, position, main_panes, Fs, EQ)
273	nyquist_f = Fs / 2;
274	% this string matches the order defined in eq.PEQ_*, do not change the order
275	eq_strings = {"highpass 1st order", "highpass 2nd order", ...
276		"lowpass 1st order", "lowpass 2nd order", "lowshelf 1st order", ...
277		"lowshelf 2nd order", "highshelf 1st order", "highshelf 2nd order", ...
278		"peaking 2nd order", "lowpass 4th order", "highpass 4th order", ...
279		"lowpass 2nd order (Google)", "highpass 2nd order (Google)", ...
280		"bandpass 2nd order", "notch 2nd order", "lowshelf 2nd order (Google)", ...
281		"highshelf 2nd order (Google)"};
282	eq = eq_defaults();
283	% UI panels, we have to use absolute positioning
284	%
285	% +-------+-----------+-------+--+--+--+--+--+----------+
286	% | Label | Dropdown  |       |  |  |  |  |  | checkbox |
287	% +-------+-----------+-------+--+--+--+--+--+----------+
288	% | Label | input box | label | slider                  |
289	% +-------+-----------+-------+-------------------------+
290	% | Label | input box | label | slider                  |
291	% +-------+-----------+-------+-------------------------+
292	% | Label | input box | label | slider                  |
293	% +-------+-----------+-------+-------------------------+
294
295	EQ(index, channel).pane = uipanel ("parent", main_panes(channel).pane, "position", position);
296	p = EQ(index, channel).pane;
297	% octave is silly and doesn't let one position controls relatively inside a uipanel
298	EQ(index, channel).gain.label = uicontrol( ...
299		"style", "text", ...
300		"parent", p, ...
301		"position", [0 10 40 30], ...
302		"visible", "off", ...
303		"string", "Gain");
304	EQ(index, channel).gain.edit = uicontrol( ...
305		"style", "edit", ...
306		"parent", p, ...
307		"position", [40 10 90 30], ...
308		"string", "0", ...
309		"userdata", struct( ...
310			"index", index, ...
311			"channel", channel, ...
312			"prefix", "gain", ...
313			"is_log", false, ...
314			"EQ", EQ, ...
315			"main_panes", main_panes), ...
316		"visible", "off", ...
317		"callback", @edit_callback);
318	EQ(index, channel).gain.units = uicontrol( ...
319		"style", "text", ...
320		"parent", p, ...
321		"string", "dB", ...
322		"visible", "off", ...
323		"position", [140 10 17 30]);
324	EQ(index, channel).gain.slider = uicontrol( ...
325		"style", "slider", ...
326		"parent", p, ...
327		"position", [160 10 200 30], ...
328		"min", -40, ...
329		"max", 40, ...
330		"userdata", struct(
331			"index", index, ...
332			"channel", channel, ...
333			"prefix", "gain", ...
334			"is_log", false, ...
335			"EQ", EQ, ...
336			"main_panes", main_panes), ...
337		"visible", "off", ...
338		"callback", @slider_callback);
339
340	EQ(index, channel).Q.label = uicontrol( ...
341		"style", "text", ...
342		"parent", p, ...
343		"position", [0 50 50 30], ...
344		"visible", "off", ...
345		"string", "Q");
346	EQ(index, channel).Q.edit = uicontrol( ...
347		"style", "edit", ...
348		"parent", p, ...
349		"position", [40 50 90 30], ...
350		"string", "1", ...
351		"userdata", struct( ...
352			"index", index, ...
353			"channel", channel, ...
354			"prefix", "Q", ...
355			"is_log", true, ...
356			"EQ", EQ, ...
357			"main_panes", main_panes), ...
358		"visible", "off", ...
359		"callback", @edit_callback);
360	EQ(index, channel).Q.slider = uicontrol( ...
361		"style", "slider",                 ...
362		"parent", p,                       ...
363		"position", [160 50 200 30],       ...
364		"min", 0,                          ...
365		"max", log2(1000/0.1),             ...
366		"value", log2(1/0.1),              ...
367		"userdata", struct(
368			"index", index, ...
369			"channel", channel, ...
370			"prefix", "Q", ...
371			"is_log", true, ...
372			"EQ", EQ, ...
373			"main_panes", main_panes), ...
374		"visible", "off", ...
375		"callback", @slider_callback);
376
377	EQ(index, channel).freq.label = uicontrol( ...
378		"style", "text", ...
379		"parent", p, ...
380		"position", [0 90 40 30], ...
381		"visible", "off", ...
382		"string", "Freq");
383	EQ(index, channel).freq.edit = uicontrol( ...
384		"style", "edit", ...
385		"parent", p, ...
386		"position", [40 90 90 30], ...
387		"string", "350", ...
388		"userdata", struct( ...
389			"index", index, ...
390			"channel", channel, ...
391			"prefix", "freq", ...
392			"is_log", true, ...
393			"EQ", EQ, ...
394			"main_panes", main_panes), ...
395		"visible", "off", ...
396		"callback", @edit_callback);
397	EQ(index, channel).freq.units = uicontrol( ...
398		"style", "text", ...
399		"parent", p, ...
400		"position", [140 90 17 30], ...
401		"visible", "off", ...
402		"string", "Hz");
403	EQ(index, channel).freq.slider   = uicontrol( ...
404		"style", "slider", ...
405		"parent", p, ...
406		"position", [160 90 200 30], ...
407		"min", 1, ...
408		"max", log2(nyquist_f/0.1), ...
409		"value", log2(350/0.1), ...
410		"userdata", struct(
411			"index", index, ...
412			"channel", channel, ...
413			"prefix", "freq", ...
414			"is_log", true, ...
415			"EQ", EQ, ...,
416			"main_panes", main_panes), ...
417		"visible", "off", ...
418		"callback", @slider_callback);
419
420	EQ(index, channel).type.label = uicontrol( ...
421		"style", "text", ...
422		"parent", p, ...
423		"position", [0 130 40 30], ...
424		"visible", "off", ...
425		"string", "Type");
426	EQ(index, channel).type.dropdown = uicontrol( ...
427		"style", "popupmenu", ...
428		"parent", p, ...
429		"position", [40 130 240 30], ...
430		"string", eq_strings, ...
431		"value", eq.PEQ_PN2, ...
432		"userdata", struct( ...
433			"index", index, ...
434			"channel", channel, ...
435			"EQ", EQ, ...
436			"main_panes", main_panes), ...
437		"visible", "off", ...
438		"callback", @dropdown_callback);
439	EQ(index, channel).enable_switch = uicontrol( ...
440		"style", "checkbox", ...
441		"parent", p, ...
442		"position", [350 130 30 30], ...
443		"value", 0, ...
444		"userdata", struct(
445			"index", index, ...
446			"channel", channel, ...
447			"EQ", EQ, ...
448			"main_panes", main_panes), ...
449		"callback", @enable_callback);
450end
451
452function push_config(h, evnt)
453	udata = get(h, "userdata");
454	window = udata.menu_window;
455	EQ = udata.EQ;
456	ip = get(window.ip_box, "string");
457	user = get(window.user_box, "string");
458	device = str2num(get(window.device_box, "string"));
459	alsa_num = str2num(get(window.alsa_box, "string"));
460	target = struct("ip", ip, "user", user, "device", device, "control", alsa_num);
461
462	temp_file = tempname();
463
464	eq_left = eq_defaults();
465	eq_left.enable_iir = 1;
466	eq_left.peq = build_peq(1, EQ);
467	eq_left.norm_type = 'peak';
468	eq_left.norm_offs_db = 0;
469	eq_left.fs = get_config().Fs;
470
471	eq_right = eq_defaults();
472	eq_right.enable_iir = 1;
473	eq_right.peq = build_peq(2, EQ);
474	eq_right.norm_type = 'peak';
475	eq_right.norm_offs_db = 0;
476	eq_right.fs = get_config().Fs;
477
478	eq_left  = eq_compute(eq_left);
479	eq_right = eq_compute(eq_right);
480
481	bq_left  = eq_iir_blob_quant(eq_left.p_z,  eq_left.p_p,  eq_left.p_k);
482	bq_right = eq_iir_blob_quant(eq_right.p_z, eq_right.p_p, eq_right.p_k);
483
484	channels_in_config = 2;
485	assign_response = [0 1];
486	num_responses = 2;
487	bm = eq_iir_blob_merge(channels_in_config, ...
488						   num_responses, ...
489						   assign_response, ...
490						   [bq_left bq_right]);
491
492	bp = eq_iir_blob_pack(bm);
493	eq_alsactl_write(temp_file, bp);
494	eq_deploy_to_dut(target, temp_file);
495end
496
497function update_control_elements(section, EQ)
498	set_udata_field(section.gain.edit, "EQ", EQ);
499	set_udata_field(section.gain.slider, "EQ", EQ);
500	set_udata_field(section.Q.edit, "EQ", EQ);
501	set_udata_field(section.Q.slider, "EQ", EQ);
502	set_udata_field(section.freq.edit, "EQ", EQ);
503	set_udata_field(section.freq.slider, "EQ", EQ);
504	set_udata_field(section.type.dropdown, "EQ", EQ);
505	set_udata_field(section.enable_switch, "EQ", EQ);
506end
507
508