|
1
|
/* eslint-disable no-script-url */ |
|
2
|
/* eslint-disable jsx-a11y/anchor-is-valid */ |
|
3
|
import axios from 'axios'; |
|
4
|
import React, { CSSProperties } from 'react'; |
|
5
|
import { Component } from 'react'; |
|
6
|
import { Col, Row, Spinner } from 'react-bootstrap'; |
|
7
|
import YouTube, { Options } from 'react-youtube'; |
|
8
|
import { GiMicrophone, GiDrumKit, GiGuitarHead, GiGuitarBassHead, GiPianoKeys, GiPlayButton, GiPauseButton } from 'react-icons/gi'; |
|
9
|
import { IconContext } from 'react-icons'; |
|
10
|
import { db, AudioFiles } from './db'; |
|
11
|
import { FormattedMessage } from 'react-intl'; |
|
12
|
import { Channel, Player} from 'tone'; |
|
13
|
|
|
14
|
const buttonStyle: CSSProperties = { |
|
15
|
|
|
16
|
} |
|
17
|
|
|
18
|
interface Result { |
|
19
|
source_file: string, |
|
20
|
filename: string |
|
21
|
} |
|
22
|
|
|
23
|
interface AudioFile { |
|
24
|
youtube_video_id: string, |
|
25
|
results: Result[] |
|
26
|
} |
|
27
|
|
|
28
|
interface YouTubeEventTarget { |
|
29
|
pauseVideo: () => void, |
|
30
|
playVideo: () => void, |
|
31
|
seekTo: (arg0: number) => void, |
|
32
|
getPlayerState: () => YouTubePlayerState, |
|
33
|
getCurrentTime: () => number |
|
34
|
|
|
35
|
} |
|
36
|
|
|
37
|
// https://developers.google.com/youtube/iframe_api_reference#Playback_status |
|
38
|
enum YouTubePlayerState { |
|
39
|
unstarted = -1, ended, playing, paused, buffering, videoCued |
|
40
|
} |
|
41
|
|
|
42
|
type AudioProps = { |
|
43
|
audioFileId: string |
|
44
|
} |
|
45
|
|
|
46
|
type AudioState = { |
|
47
|
audioFile: AudioFile | null, |
|
48
|
playerTime: number, |
|
49
|
isPlayerReady: boolean, |
|
50
|
isYoutubePlayerReady: boolean, |
|
51
|
isPlaying: boolean |
|
52
|
vocalAudioOn: boolean |
|
53
|
guitarAudioOn: boolean |
|
54
|
bassAudioOn: boolean |
|
55
|
drumsAudioOn: boolean |
|
56
|
pianoAudioOn: boolean |
|
57
|
} |
|
58
|
|
|
59
|
export enum Mode { |
|
60
|
vocalist, bassist, guitarist, drummer, keyboardist |
|
61
|
} |
|
62
|
|
|
63
|
class AudioPlayer extends Component<AudioProps, AudioState> { |
|
64
|
|
|
65
|
vocalPlayer: Player | null = null; |
|
66
|
otherPlayer: Player | null = null; |
|
67
|
bassPlayer: Player | null = null; |
|
68
|
drumsPlayer: Player | null = null; |
|
69
|
pianoPlayer: Player | null = null; |
|
70
|
|
|
71
|
youTubeEventTarget: YouTubeEventTarget | null = null |
|
72
|
updateYouTubePlayerStatusIntervalId: any | null = null |
|
73
|
|
|
74
|
constructor(props: AudioProps) { |
|
75
|
super(props); |
|
76
|
this.state = { |
|
77
|
audioFile: null, |
|
78
|
playerTime: 0, |
|
79
|
isPlayerReady: false, |
|
80
|
isYoutubePlayerReady: false, |
|
81
|
isPlaying: false, |
|
82
|
vocalAudioOn: true, |
|
83
|
guitarAudioOn: true, |
|
84
|
bassAudioOn: true, |
|
85
|
drumsAudioOn: true, |
|
86
|
pianoAudioOn: true, |
|
87
|
} |
|
88
|
} |
|
89
|
|
|
90
|
componentDidMount() { |
|
91
|
axios.get(`/splitfire/${this.props.audioFileId}`) |
|
92
|
.then(res => { |
|
93
|
// console.log("Get results", res); |
|
94
|
const audioFile = res.data.audio_file; |
|
95
|
this.setState({ audioFile }); |
|
96
|
// console.log(this.state); |
|
97
|
this.downloadAudioFilesIfNeeded(); |
|
98
|
}); |
|
99
|
} |
|
100
|
|
|
101
|
componentWillUnmount() { |
|
102
|
this.setState({isPlaying: false}) |
|
103
|
this.vocalPlayer?.stop() |
|
104
|
this.bassPlayer?.stop() |
|
105
|
this.drumsPlayer?.stop() |
|
106
|
this.pianoPlayer?.stop() |
|
107
|
this.otherPlayer?.stop() |
|
108
|
} |
|
109
|
|
|
110
|
downloadAudioFilesIfNeeded() { |
|
111
|
this._prepareAudio(Mode.vocalist); |
|
112
|
this._prepareAudio(Mode.bassist); |
|
113
|
this._prepareAudio(Mode.drummer); |
|
114
|
this._prepareAudio(Mode.guitarist); |
|
115
|
this._prepareAudio(Mode.keyboardist); |
|
116
|
} |
|
117
|
|
|
118
|
async _downloadFile(type: Mode) { |
|
119
|
// console.log('_downloadFile', type); |
|
120
|
let audioFile: Result | undefined; |
|
121
|
switch (type) { |
|
122
|
case Mode.vocalist: |
|
123
|
audioFile = this.state.audioFile?.results.find(obj => { return obj.filename.startsWith('vocals') }); |
|
124
|
break; |
|
125
|
case Mode.bassist: |
|
126
|
audioFile = this.state.audioFile?.results.find(obj => { return obj.filename.startsWith('bass') }); |
|
127
|
break; |
|
128
|
case Mode.drummer: |
|
129
|
audioFile = this.state.audioFile?.results.find(obj => { return obj.filename.startsWith('drums') }); |
|
130
|
break; |
|
131
|
case Mode.guitarist: |
|
132
|
audioFile = this.state.audioFile?.results.find(obj => { return obj.filename.startsWith('other') }); |
|
133
|
break; |
|
134
|
case Mode.keyboardist: |
|
135
|
audioFile = this.state.audioFile?.results.find(obj => { return obj.filename.startsWith('piano') }); |
|
136
|
break; |
|
137
|
} |
|
138
|
|
|
139
|
const audioFileId = this.props.audioFileId; |
|
140
|
// console.log("Download url", audioFile?.source_file); |
|
141
|
axios({ |
|
142
|
url: audioFile?.source_file, |
|
143
|
method: 'GET', |
|
144
|
responseType: 'blob', |
|
145
|
}).then((response) => { |
|
146
|
const file = new Blob([response.data], { type: 'audio/mp3' }); |
|
147
|
const item: AudioFiles = { |
|
148
|
audioFileId, type: type, file |
|
149
|
} |
|
150
|
db.audioFiles.add(item); |
|
151
|
// console.log('File Downloaded', file); |
|
152
|
this._setAudioSrc(type, file); |
|
153
|
}).catch(error => { |
|
154
|
// console.log(error); |
|
155
|
}); |
|
156
|
} |
|
157
|
|
|
158
|
async _prepareAudio(type: Mode) { |
|
159
|
const audioFileId = this.props.audioFileId; |
|
160
|
db.audioFiles |
|
161
|
.get({ audioFileId: audioFileId, type: type }) |
|
162
|
.then(record => { |
|
163
|
if (record?.file === undefined) { |
|
164
|
this._downloadFile(type); |
|
165
|
return |
|
166
|
} |
|
167
|
this._setAudioSrc(type, record.file) |
|
168
|
}) |
|
169
|
.catch(onrejected => { |
|
170
|
// console.log("Rejected", onrejected); |
|
171
|
}); |
|
172
|
} |
|
173
|
|
|
174
|
_setAudioSrc(type: Mode, file: Blob) { |
|
175
|
// Get window.URL object |
|
176
|
const URL = window.URL || window.webkitURL; |
|
177
|
// Create and revoke ObjectURL |
|
178
|
const audioFileURL = URL.createObjectURL(file); |
|
179
|
const channel = new Channel().toDestination(); |
|
180
|
|
|
181
|
switch (type) { |
|
182
|
case Mode.vocalist: |
|
183
|
this.vocalPlayer = new Player({ url: audioFileURL, onload: this._updatePlayerStatus }) |
|
184
|
this.vocalPlayer.connect(channel); |
|
185
|
break; |
|
186
|
case Mode.bassist: |
|
187
|
this.bassPlayer = new Player({ url: audioFileURL, onload: this._updatePlayerStatus }) |
|
188
|
this.bassPlayer.connect(channel); |
|
189
|
break; |
|
190
|
case Mode.drummer: |
|
191
|
this.drumsPlayer = new Player({ url: audioFileURL, onload: this._updatePlayerStatus }) |
|
192
|
this.drumsPlayer.connect(channel); |
|
193
|
break; |
|
194
|
case Mode.guitarist: |
|
195
|
this.otherPlayer = new Player({ url: audioFileURL, onload: this._updatePlayerStatus }) |
|
196
|
this.otherPlayer.connect(channel); |
|
197
|
break; |
|
198
|
case Mode.keyboardist: |
|
199
|
this.pianoPlayer = new Player({ url: audioFileURL, onload: this._updatePlayerStatus }) |
|
200
|
this.pianoPlayer.connect(channel); |
|
201
|
break; |
|
202
|
} |
|
203
|
} |
|
204
|
|
|
205
|
_updatePlayerStatus: (() => void) = () => { |
|
206
|
if ( |
|
207
|
this.vocalPlayer?.loaded && |
|
208
|
this.drumsPlayer?.loaded && |
|
209
|
this.otherPlayer?.loaded && |
|
210
|
this.bassPlayer?.loaded && |
|
211
|
this.pianoPlayer?.loaded |
|
212
|
) { |
|
213
|
this.setState({ isPlayerReady: true }); |
|
214
|
} |
|
215
|
} |
|
216
|
|
|
217
|
togglePlayAudio() { |
|
218
|
this.setState({isPlaying: !this.state.isPlaying}) |
|
219
|
if (this.state.isPlaying) { |
|
220
|
this.vocalPlayer?.stop() |
|
221
|
this.bassPlayer?.stop() |
|
222
|
this.drumsPlayer?.stop() |
|
223
|
this.pianoPlayer?.stop() |
|
224
|
this.otherPlayer?.stop() |
|
225
|
this.youTubeEventTarget?.pauseVideo() |
|
226
|
} else { |
|
227
|
this.youTubeEventTarget?.seekTo(0) |
|
228
|
this.youTubeEventTarget?.playVideo() |
|
229
|
this.vocalPlayer?.start(); |
|
230
|
this.bassPlayer?.start(); |
|
231
|
this.drumsPlayer?.start(); |
|
232
|
this.pianoPlayer?.start(); |
|
233
|
this.otherPlayer?.start(); |
|
234
|
} |
|
235
|
} |
|
236
|
|
|
237
|
setToggleInstrument(mode: Mode) { |
|
238
|
switch (mode) { |
|
239
|
case Mode.vocalist: |
|
240
|
if (!this.vocalPlayer) return; |
|
241
|
this.setState({ vocalAudioOn: !this.state.vocalAudioOn }); |
|
242
|
this.vocalPlayer.mute = this.state.vocalAudioOn; |
|
243
|
break; |
|
244
|
case Mode.bassist: |
|
245
|
if (!this.bassPlayer) return; |
|
246
|
this.setState({ bassAudioOn: !this.state.bassAudioOn }); |
|
247
|
this.bassPlayer.mute = this.state.bassAudioOn; |
|
248
|
break; |
|
249
|
case Mode.guitarist: |
|
250
|
if (!this.otherPlayer) return; |
|
251
|
this.setState({ guitarAudioOn: !this.state.guitarAudioOn }); |
|
252
|
this.otherPlayer.mute = this.state.guitarAudioOn; |
|
253
|
break; |
|
254
|
case Mode.keyboardist: |
|
255
|
if (!this.pianoPlayer) return; |
|
256
|
this.setState({ pianoAudioOn: !this.state.pianoAudioOn }); |
|
257
|
this.pianoPlayer.mute = this.state.pianoAudioOn;; |
|
258
|
break; |
|
259
|
case Mode.drummer: |
|
260
|
if (!this.drumsPlayer) return; |
|
261
|
this.setState({ drumsAudioOn: !this.state.drumsAudioOn }); |
|
262
|
this.drumsPlayer.mute = this.state.drumsAudioOn; |
|
263
|
break; |
|
264
|
} |
|
265
|
} |
|
266
|
|
|
267
|
render() { |
|
268
|
if (this.state.isPlayerReady) { |
|
269
|
const opts: Options = { |
|
270
|
width: '100%', |
|
271
|
playerVars: { |
|
272
|
// https://developers.google.com/youtube/player_parameters |
|
273
|
autoplay: 0, |
|
274
|
mute: 1, |
|
275
|
controls: 0, |
|
276
|
rel: 0, |
|
277
|
showinfo: 0 |
|
278
|
}, |
|
279
|
}; |
|
280
|
let togglePlayButton = <div> |
|
281
|
<p> |
|
282
|
<FormattedMessage id="player.loadingYoutube" |
|
283
|
defaultMessage="Waiting for YouTube..." |
|
284
|
description="Loading message"/> |
|
285
|
</p> |
|
286
|
<Spinner animation='grow' variant="danger"></Spinner> |
|
287
|
</div> |
|
288
|
if (this.state.isYoutubePlayerReady) { |
|
289
|
let button |
|
290
|
if (this.state.isPlaying) { |
|
291
|
button = <Col> |
|
292
|
<GiPauseButton |
|
293
|
color={this.state.isPlaying ? `white` : `red`} |
|
294
|
onClick={() => this.togglePlayAudio() } |
|
295
|
style={buttonStyle} /> |
|
296
|
</Col> |
|
297
|
} else { |
|
298
|
button = <Col> |
|
299
|
<GiPlayButton |
|
300
|
color={this.state.isPlaying ? `white` : `red`} |
|
301
|
onClick={() => this.togglePlayAudio() } |
|
302
|
style={buttonStyle} /> |
|
303
|
</Col> |
|
304
|
} |
|
305
|
togglePlayButton = <Col> |
|
306
|
<p> |
|
307
|
<FormattedMessage id="player.instruction" |
|
308
|
defaultMessage="Click isntrument icon to mute part of the song." |
|
309
|
description="Instruction message"/> |
|
310
|
</p> |
|
311
|
{button} |
|
312
|
</Col> |
|
313
|
} |
|
314
|
|
|
315
|
return ( |
|
316
|
<IconContext.Provider value={{ size: "2em", color: "white", className: "global-class-name" }}> |
|
317
|
<Header/> |
|
318
|
<Row className="mb-3 mt-3"> |
|
319
|
{togglePlayButton} |
|
320
|
</Row> |
|
321
|
<Row className="mb-3 mt-3"> |
|
322
|
<Col> |
|
323
|
<GiMicrophone |
|
324
|
color={this.state.vocalAudioOn ? `white` : `red`} |
|
325
|
onClick={() => this.setToggleInstrument(Mode.vocalist)} |
|
326
|
style={buttonStyle} /> |
|
327
|
</Col> |
|
328
|
<Col> |
|
329
|
<GiDrumKit |
|
330
|
color={this.state.drumsAudioOn ? `white` : `red`} |
|
331
|
onClick={() => this.setToggleInstrument(Mode.drummer)} |
|
332
|
style={buttonStyle} /> |
|
333
|
</Col> |
|
334
|
<Col> |
|
335
|
<GiGuitarBassHead |
|
336
|
color={this.state.bassAudioOn ? `white` : `red`} |
|
337
|
onClick={() => this.setToggleInstrument(Mode.bassist)} |
|
338
|
style={buttonStyle} /> |
|
339
|
</Col> |
|
340
|
<Col> |
|
341
|
<GiGuitarHead |
|
342
|
color={this.state.guitarAudioOn ? `white` : `red`} |
|
343
|
onClick={() => this.setToggleInstrument(Mode.guitarist)} |
|
344
|
style={buttonStyle} /> |
|
345
|
</Col> |
|
346
|
<Col> |
|
347
|
<GiPianoKeys |
|
348
|
color={this.state.pianoAudioOn ? `white` : `red`} |
|
349
|
onClick={() => this.setToggleInstrument(Mode.keyboardist)} |
|
350
|
style={buttonStyle} /> |
|
351
|
</Col> |
|
352
|
</Row> |
|
353
|
<Row> |
|
354
|
<Col style={{display: this.state.isYoutubePlayerReady ? 'block' : 'none', pointerEvents: 'none'}}> |
|
355
|
<YouTube |
|
356
|
videoId={this.state.audioFile?.youtube_video_id} |
|
357
|
opts={opts} |
|
358
|
onReady={this._onReady.bind(this)} |
|
359
|
onStateChange={this._onStateChange.bind(this)} |
|
360
|
/> |
|
361
|
</Col> |
|
362
|
</Row> |
|
363
|
</IconContext.Provider> |
|
364
|
) |
|
365
|
} else { |
|
366
|
return ( |
|
367
|
<Row> |
|
368
|
<Header/> |
|
369
|
<Col className="mb-3 mt-3"> |
|
370
|
<p> |
|
371
|
<FormattedMessage id="player.loading" |
|
372
|
defaultMessage="🤖 I'm doing my business..." |
|
373
|
description="Loading message"/> |
|
374
|
</p> |
|
375
|
<Spinner animation='grow' variant="danger"></Spinner> |
|
376
|
</Col> |
|
377
|
</Row> |
|
378
|
) |
|
379
|
} |
|
380
|
} |
|
381
|
|
|
382
|
_onReady(event: any) { |
|
383
|
// console.log('YouTube ready: ', event) |
|
384
|
this.youTubeEventTarget = event.target |
|
385
|
this.youTubeEventTarget?.playVideo() |
|
386
|
this.updateYouTubePlayerStatusIntervalId = setInterval(() => { |
|
387
|
if (!this.state.isYoutubePlayerReady) { |
|
388
|
this._updateYouTubePlayerStatusIfNeeded() |
|
389
|
} |
|
390
|
}, 2000) |
|
391
|
} |
|
392
|
|
|
393
|
_onStateChange(event: any) { |
|
394
|
// console.log('State changed: ', event) |
|
395
|
// console.log('getCurrentTime: ', event.target.getCurrentTime()) |
|
396
|
// console.log('getPlayerState: ', event.target.getPlayerState()) |
|
397
|
if (!this.youTubeEventTarget) return |
|
398
|
|
|
399
|
if (this.youTubeEventTarget.getPlayerState() === YouTubePlayerState.ended) { |
|
400
|
this.setState({ isPlaying: false }) |
|
401
|
} |
|
402
|
} |
|
403
|
|
|
404
|
_updateYouTubePlayerStatusIfNeeded() { |
|
405
|
|
|
406
|
if (this.state.isYoutubePlayerReady) return |
|
407
|
if (!this.youTubeEventTarget) return |
|
408
|
|
|
409
|
// Let's say 5 seconds buffer is enough. |
|
410
|
if (this.youTubeEventTarget?.getCurrentTime() > 5) { |
|
411
|
this.setState({ isYoutubePlayerReady: true }) |
|
412
|
this.youTubeEventTarget?.pauseVideo() |
|
413
|
|
|
414
|
if (this.updateYouTubePlayerStatusIntervalId) |
|
415
|
clearInterval(this.updateYouTubePlayerStatusIntervalId) |
|
416
|
} |
|
417
|
} |
|
418
|
|
|
419
|
} |
|
420
|
|
|
421
|
const Header = () => ( |
|
422
|
<Row className="mb-3 mt-3"> |
|
423
|
<Col> |
|
424
|
<h1>SplitFire AI</h1> |
|
425
|
<p> |
|
426
|
<FormattedMessage id="splitfire.whatIs" |
|
427
|
defaultMessage="I'm an artificial intelligence software who will split your favorite music to its separate components." |
|
428
|
description="Explanation message"/> |
|
429
|
</p> |
|
430
|
<a href="https://testflight.apple.com/join/4cb0rDIo" target={`__blank`}> |
|
431
|
<img src='https://splitfire.ai/packs/media/images/Pre-order_on_the_App_Store_Badge_US-UK_RGB_blk_121217-5fb138d3f649c68f7b3250e3887dbef5.svg' /> |
|
432
|
</a> |
|
433
|
</Col> |
|
434
|
</Row> |
|
435
|
) |
|
436
|
|
|
437
|
export default AudioPlayer; |