Skip to content

Commit 0f5f2fa

Browse files
author
Dario Segura
committed
[SPEAKER] Added speaker image and analyser
1 parent 2dbf036 commit 0f5f2fa

File tree

3 files changed

+322
-19
lines changed

3 files changed

+322
-19
lines changed

src/frontend/routes/Speaker.js

Lines changed: 307 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,28 +6,80 @@ import {Buttons, GeneralEvents} from '../Events';
66
import {MediaManager} from '../../service/MediaManager';
77

88
export class Speaker {
9-
oninit() {
9+
oninit(vnode) {
1010
if (!(Service.activeService() instanceof Service) || !Service.activeService().isSpeaker) {
1111
m.route.set('/Splash');
1212
}
13+
14+
vnode.state.visualization = {};
15+
}
16+
17+
// oncreate(vnode) {
18+
// const canvas = document.getElementById('speaker-visualization-canvas');
19+
// if (canvas) {
20+
// // const analyser = Service.activeService().audioAnalyser;
21+
//
22+
// canvas.width = canvas.offsetWidth * window.devicePixelRatio;
23+
// canvas.height = canvas.offsetHeight * window.devicePixelRatio;
24+
//
25+
// vnode.state.visualization.canvas = canvas;
26+
// vnode.state.visualization.context = canvas.getContext('2d');
27+
// // vnode.state.visualization.analyser = analyser;
28+
// // vnode.state.visualization.bufferLength = analyser.frequencyBinCount;
29+
// // vnode.state.visualization.buffer = new Uint8Array(vnode.state.visualization.bufferLength);
30+
//
31+
// this._renderLoop(vnode);
32+
// }
33+
// }
34+
35+
onupdate(vnode) {
36+
const canvas = document.getElementById('speaker-visualization-canvas');
37+
if (canvas && vnode.state.visualization.canvas !== canvas) {
38+
if (vnode.state.visualization.animation) {
39+
cancelAnimationFrame(vnode.state.visualization.animation);
40+
}
41+
42+
const analyser = Service.activeService().audioAnalyser;
43+
44+
canvas.width = canvas.offsetWidth * window.devicePixelRatio;
45+
canvas.height = canvas.offsetHeight * window.devicePixelRatio;
46+
47+
vnode.state.visualization.canvas = canvas;
48+
vnode.state.visualization.context = canvas.getContext('2d');
49+
vnode.state.visualization.analyser = analyser;
50+
vnode.state.visualization.bufferLength = analyser.frequencyBinCount;
51+
vnode.state.visualization.buffer = new Uint8Array(vnode.state.visualization.bufferLength);
52+
vnode.state.visualization.sampleRate = Service.activeService().audioContext.sampleRate;
53+
54+
this._renderLoop(vnode);
55+
}
56+
}
57+
58+
onremove(vnode) {
59+
if (vnode.state.visualization.animation) {
60+
cancelAnimationFrame(vnode.state.visualization.animation);
61+
}
62+
vnode.state.visualization = null;
1363
}
1464

1565
view() {
1666
if (!Service.activeService() || !Service.activeService().isSpeakerPlaying) {
17-
return m('.speaker-message-container', [
18-
m('.speaker-message-title', 'Speaker'),
19-
m('.speaker-message-content', 'This device is ready to be used as a speaker, please make sure to adjust the delay and that your device is not muted.'),
20-
m('.speaker-start-button', m(Button, {
21-
raised: true,
22-
label: 'START',
23-
events: {
24-
onclick: () => {
25-
EventCenter.emit(GeneralEvents.BUTTON_PRESS, Buttons.SPEAKER_START_STREAMING, this);
26-
m.redraw();
67+
return [
68+
m('.speaker-message-container', [
69+
m('.speaker-message-title', 'Speaker'),
70+
m('.speaker-message-content', 'This device is ready to be used as a speaker, please make sure to adjust the delay and that your device is not muted.'),
71+
m('.speaker-start-button', m(Button, {
72+
raised: true,
73+
label: 'START',
74+
events: {
75+
onclick: () => {
76+
EventCenter.emit(GeneralEvents.BUTTON_PRESS, Buttons.SPEAKER_START_STREAMING, this);
77+
m.redraw();
78+
},
2779
},
28-
},
29-
})),
30-
]);
80+
})),
81+
]),
82+
];
3183
}
3284

3385
const currentSong = MediaManager.currentSong;
@@ -43,9 +95,246 @@ export class Speaker {
4395
currentSong.formattedDuration ? m('.song-list-view-duration', currentSong.formattedDuration) : null,
4496
]) : m('.now-playing-song-info-container', m('.now-playing-list-title', 'Not Playing'));
4597

46-
return m('.now-playing-song-container', [
47-
// artwork,
48-
info,
49-
]);
98+
return [
99+
m('.now-playing-song-container', [
100+
// artwork,
101+
info,
102+
]),
103+
m('canvas', { class: 'speaker-visualization-canvas', id: 'speaker-visualization-canvas' }),
104+
];
105+
}
106+
107+
_renderLoop(vnode) {
108+
if (vnode.state.visualization) {
109+
vnode.state.visualization.animation = requestAnimationFrame(() => this._renderLoop(vnode));
110+
}
111+
112+
this._drawSpeaker(vnode, vnode.state.visualization.canvas.width);
113+
}
114+
115+
_drawSpeaker(vnode, size) {
116+
const visualization = vnode.state.visualization;
117+
const canvas = visualization.canvas;
118+
const context = visualization.context;
119+
const buffer = visualization.buffer;
120+
const bufferLength = visualization.bufferLength;
121+
const hzPerBin = (visualization.sampleRate * 0.5) / bufferLength;
122+
vnode.state.visualization.analyser.getByteFrequencyData(buffer);
123+
124+
const bassStart = 0;
125+
const bassEnd = Math.ceil(120 / hzPerBin);
126+
127+
const midStart = bassEnd + 1;
128+
const midEnd = Math.ceil(600 / hzPerBin);
129+
130+
const highStart = midEnd + 1;
131+
const highEnd = bufferLength; // Math.ceil(200/ hzPerBin);
132+
133+
context.clearRect(0, 0, canvas.width, canvas.height);
134+
135+
context.beginPath();
136+
context.rect(0.2 * size, 0.025 * size, 0.6 * size, 0.95 * size);
137+
context.closePath();
138+
139+
// cabinet
140+
this._drawCircle(context,
141+
0.25 * size,
142+
0.075 * size,
143+
0.01 * size,
144+
true);
145+
context.closePath();
146+
this._drawCircle(context,
147+
0.75 * size,
148+
0.075 * size,
149+
0.01 * size,
150+
true);
151+
context.closePath();
152+
this._drawCircle(context,
153+
0.25 * size,
154+
0.925 * size,
155+
0.01 * size,
156+
true);
157+
context.closePath();
158+
this._drawCircle(context,
159+
0.75 * size,
160+
0.925 * size,
161+
0.01 * size,
162+
true);
163+
context.closePath();
164+
165+
this._drawCircle(context,
166+
0.675 * size,
167+
0.875 * size,
168+
0.01 * size,
169+
true);
170+
context.closePath();
171+
this._drawCircle(context,
172+
0.325 * size,
173+
0.525 * size,
174+
0.01 * size,
175+
true);
176+
context.closePath();
177+
this._drawCircle(context,
178+
0.675 * size,
179+
0.525 * size,
180+
0.01 * size,
181+
true);
182+
context.closePath();
183+
this._drawCircle(context,
184+
0.325 * size,
185+
0.875 * size,
186+
0.01 * size,
187+
true);
188+
context.closePath();
189+
190+
this._drawCircle(context,
191+
0.4 * size,
192+
0.1 * size,
193+
0.0075 * size,
194+
true);
195+
context.closePath();
196+
this._drawCircle(context,
197+
0.4 * size,
198+
0.3 * size,
199+
0.0075 * size,
200+
true);
201+
context.closePath();
202+
this._drawCircle(context,
203+
0.6 * size,
204+
0.1 * size,
205+
0.0075 * size,
206+
true);
207+
context.closePath();
208+
this._drawCircle(context,
209+
0.6 * size,
210+
0.3 * size,
211+
0.0075 * size,
212+
true);
213+
context.closePath();
214+
215+
this._drawCircle(context,
216+
0.35 * size,
217+
0.4375 * size,
218+
0.005 * size,
219+
true);
220+
context.closePath();
221+
this._drawCircle(context,
222+
0.25 * size,
223+
0.3375 * size,
224+
0.005 * size,
225+
true);
226+
context.closePath();
227+
228+
this._drawCircle(context,
229+
0.65 * size,
230+
0.4375 * size,
231+
0.005 * size,
232+
true);
233+
context.closePath();
234+
this._drawCircle(context,
235+
0.75 * size,
236+
0.3375 * size,
237+
0.005 * size,
238+
true);
239+
context.closePath();
240+
241+
// speakers
242+
this._drawCircle(context,
243+
0.5 * size,
244+
0.7 * size,
245+
0.2 * size,
246+
true);
247+
context.closePath();
248+
this._drawCircle(context,
249+
0.5 * size,
250+
0.7 * size,
251+
this._radiusFromSpectrum(buffer, bassStart, bassEnd, 0.1625 * size, 0.19 * size),
252+
false);
253+
context.closePath();
254+
this._drawCircle(context,
255+
0.5 * size,
256+
0.7 * size,
257+
this._radiusFromSpectrum(buffer, bassStart, bassEnd, 0.05 * size, 0.075 * size),
258+
true);
259+
context.closePath();
260+
261+
this._drawCircle(context,
262+
0.5 * size,
263+
0.2 * size,
264+
0.1125 * size,
265+
true);
266+
context.closePath();
267+
this._drawCircle(context,
268+
0.5 * size,
269+
0.2 * size,
270+
this._radiusFromSpectrum(buffer, midStart, midEnd, 0.0875 * size, 0.1025 * size),
271+
false);
272+
context.closePath();
273+
this._drawCircle(context,
274+
0.5 * size,
275+
0.2 * size,
276+
this._radiusFromSpectrum(buffer, midStart, midEnd, 0.025 * size, 0.05 * size),
277+
true);
278+
context.closePath();
279+
280+
this._drawCircle(context,
281+
0.3 * size,
282+
0.3875 * size,
283+
0.05 * size,
284+
true);
285+
context.closePath();
286+
this._drawCircle(context,
287+
0.3 * size,
288+
0.3875 * size,
289+
this._radiusFromSpectrum(buffer, highStart, highEnd, 0.03 * size, 0.0475 * size, this._easeInQuad),
290+
false);
291+
context.closePath();
292+
293+
this._drawCircle(context,
294+
0.7 * size,
295+
0.3875 * size,
296+
0.05 * size,
297+
true);
298+
context.closePath();
299+
this._drawCircle(context,
300+
0.7 * size,
301+
0.3875 * size,
302+
this._radiusFromSpectrum(buffer, highStart, highEnd, 0.03 * size, 0.0475 * size, this._easeInQuad),
303+
true);
304+
context.closePath();
305+
306+
context.fillStyle = 'rgba(0, 0, 0, 0.7)';
307+
context.fill();
308+
}
309+
310+
_drawCircle(context, x, y, radius, anticlockwise = false) {
311+
context.arc(x, y, radius, 0, 2 * Math.PI, anticlockwise);
312+
}
313+
314+
_radiusFromSpectrum(spectrum, start, end, min, max, easeFunc = this._easeIn) {
315+
let value = 0;
316+
let count = 0;
317+
318+
for (let i = start; i < end; ++i) {
319+
if (spectrum[i] !== -Infinity && spectrum[i] > 32) {
320+
count += 1;
321+
value += spectrum[i];
322+
}
323+
}
324+
325+
if (count) {
326+
value /= count;
327+
value = easeFunc(value / 255);
328+
}
329+
330+
return min + ((max - min) * value);
331+
}
332+
333+
_easeIn(value) {
334+
return Math.pow(value, 5);
335+
}
336+
337+
_easeInQuad(value) {
338+
return value * value;
50339
}
51340
}

src/service/jukebox/JukeboxService.js

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export class JukeboxService extends Service {
1717
this.mAudioContext = null;
1818
this.mAudioSource = null;
1919
this.mAudioDelay = null;
20+
this.mAudioAnalyser = null;
2021

2122
this.mBufferingProgress = 0;
2223
this.mPlaybackProgress = 0;
@@ -89,6 +90,10 @@ export class JukeboxService extends Service {
8990
return this.mAudioDelay;
9091
}
9192

93+
get audioAnalyser() {
94+
return this.mAudioAnalyser;
95+
}
96+
9297
async configureAsServer(service) {
9398
if (service.canServeJukebox) {
9499
if (await this.mConnection.initAsServer()) {
@@ -208,7 +213,11 @@ export class JukeboxService extends Service {
208213
this.mAudioDelay = this.mAudioContext.createDelay(5.0);
209214
this.mAudioDelay.delayTime.value = 0.1; // just for giggles
210215
this.mAudioSource.connect(this.mAudioDelay);
211-
this.mAudioDelay.connect(this.mAudioContext.destination);
216+
// this.mAudioDelay.connect(this.mAudioContext.destination);
217+
218+
this.mAudioAnalyser = this.mAudioContext.createAnalyser();
219+
this.mAudioDelay.connect(this.mAudioAnalyser);
220+
this.mAudioAnalyser.connect(this.mAudioContext.destination);
212221
}
213222
}
214223

src/style/speaker.scss

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,8 @@
3030
width: 100%;
3131
text-align: right;
3232
}
33+
34+
.speaker-visualization-canvas {
35+
width: 250px;
36+
height: 250px;
37+
}

0 commit comments

Comments
 (0)