Raw
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;