Skip to content
This repository was archived by the owner on Feb 18, 2022. It is now read-only.

Commit fba5f31

Browse files
committed
Add MVP recorder functionality
1 parent 54d2978 commit fba5f31

File tree

4 files changed

+169
-3
lines changed

4 files changed

+169
-3
lines changed

demo/demo.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export default class Demo extends Component {
1919

2020
this.state = {
2121
playing: true,
22+
downloadLinkVisible: false,
2223
};
2324

2425
this.handleAudioProcess = this.handleAudioProcess.bind(this);
@@ -32,12 +33,34 @@ export default class Demo extends Component {
3233
playing: !this.state.playing,
3334
});
3435
}
36+
37+
handleRecordStop(blob, fileName) {
38+
this.setState({
39+
downloadLinkVisible: true,
40+
}, () => {
41+
const url = URL.createObjectURL(blob);
42+
const anchor = this.refs.downloadLink;
43+
anchor.href = url;
44+
anchor.download = new Date().toISOString() + '.wav';
45+
});
46+
}
47+
renderDownloadLink() {
48+
if (!this.state.downloadLinkVisible) {
49+
return null;
50+
}
51+
52+
return (
53+
<a ref="downloadLink" className="react-music-download-link">Download</a>
54+
);
55+
}
3556
render() {
3657
return (
3758
<div>
3859
<Song
3960
playing={this.state.playing}
4061
tempo={90}
62+
record
63+
onRecordStop={this.handleRecordStop.bind(this)}
4164
>
4265
<Analyser onAudioProcess={this.handleAudioProcess}>
4366
<Sequencer
@@ -101,6 +124,7 @@ export default class Demo extends Component {
101124
>
102125
{this.state.playing ? 'Stop' : 'Play'}
103126
</button>
127+
{this.state.downloadLinkVisible && this.renderDownloadLink()}
104128
</div>
105129
);
106130
}

demo/index.css

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,4 +47,4 @@
4747
.react-music-button:active {
4848
box-shadow: none;
4949
border: solid 1px #a7a7a7;
50-
}
50+
}

src/components/song.js

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,14 @@
22
/* eslint-disable no-loop-func, react/no-did-mount-set-state */
33
import React, { Component, PropTypes } from 'react';
44
import Scheduler from '../utils/scheduler';
5+
import Recorder from '../utils/recorder';
56

67
type Props = {
78
children?: any;
89
playing?: boolean;
10+
record?: boolean;
911
tempo: number;
12+
onRecordStop: func;
1013
};
1114

1215
export default class Song extends Component {
@@ -27,10 +30,13 @@ export default class Song extends Component {
2730
children: PropTypes.node,
2831
playing: PropTypes.bool,
2932
tempo: PropTypes.number,
33+
record: PropTypes.bool,
34+
onRecordStop: PropTypes.func,
3035
};
3136
static defaultProps = {
3237
playing: false,
3338
tempo: 90,
39+
record: false,
3440
};
3541
static childContextTypes = {
3642
audioContext: PropTypes.object,
@@ -43,7 +49,6 @@ export default class Song extends Component {
4349
};
4450
constructor(props: Props) {
4551
super(props);
46-
4752
this.state = {
4853
buffersLoaded: false,
4954
};
@@ -61,22 +66,30 @@ export default class Song extends Component {
6166

6267
window.AudioContext = window.AudioContext || window.webkitAudioContext;
6368
this.audioContext = new AudioContext();
69+
this.destination = this.audioContext.destination;
6470

6571
this.scheduler = new Scheduler({
6672
context: this.audioContext,
6773
});
74+
75+
if (props.record) {
76+
this.recorder = new Recorder(this.audioContext, props.onRecordStop);
77+
this.recorder.processor.connect(this.destination);
78+
this.destination = this.recorder.processor;
79+
}
6880
}
6981
getChildContext(): Object {
7082
return {
7183
tempo: this.props.tempo,
7284
audioContext: this.audioContext,
7385
barInterval: this.barInterval,
7486
bufferLoaded: this.bufferLoaded,
75-
connectNode: this.audioContext.destination,
87+
connectNode: this.destination,
7688
getMaster: this.getMaster,
7789
scheduler: this.scheduler,
7890
};
7991
}
92+
8093
componentDidMount() {
8194
if (Object.keys(this.buffers).length === 0) {
8295
this.setState({
@@ -85,6 +98,9 @@ export default class Song extends Component {
8598
}
8699
}
87100
componentWillReceiveProps(nextProps: Props) {
101+
if (this.recorder.recording && !nextProps.playing) {
102+
this.recorder.stop();
103+
}
88104
this.barInterval = (60000 / nextProps.tempo) * 4;
89105
}
90106
componentDidUpdate(prevProps: Object, prevState: Object) {

src/utils/recorder.js

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
// Much inspired by http://typedarray.org/from-microphone-to-wav-with-getusermedia-and-web-audio
2+
3+
// @flow
4+
export default class Recorder {
5+
constructor(context, onRecordStop) {
6+
const bufferSize = 2048;
7+
this.recording = true;
8+
this.processor = context.createScriptProcessor(bufferSize, 2, 2);
9+
this.processor.recordingLength = 0;
10+
this.processor.leftChannel = [];
11+
this.processor.rightChannel = [];
12+
this.onRecordStop = onRecordStop;
13+
14+
this.processor.onaudioprocess = function (e) {
15+
const left = e.inputBuffer.getChannelData(0);
16+
const right = e.inputBuffer.getChannelData(1);
17+
// we clone the samples
18+
this.leftChannel.push(new Float32Array(left));
19+
this.rightChannel.push(new Float32Array(right));
20+
this.recordingLength += bufferSize;
21+
}.bind(this.processor);
22+
23+
this.processor.stop = function () {
24+
this.recording = false;
25+
const left = mergeBuffers(this.processor.leftChannel, this.processor.recordingLength);
26+
const right = mergeBuffers(this.processor.rightChannel, this.processor.recordingLength);
27+
const interleavedChannels = interleave(left, right);
28+
const blob = getWAV(interleavedChannels, context.sampleRate);
29+
30+
this.processor.disconnect();
31+
this.onRecordStop(blob);
32+
// blobToBase64(blob, (base64) => {
33+
// cb(base64);
34+
// });
35+
}.bind(this);
36+
}
37+
38+
stop() {
39+
this.processor.stop();
40+
}
41+
42+
connect(node) {
43+
this.processor.connect(node);
44+
}
45+
}
46+
47+
function mergeBuffers(channelBuffer, recordingLength) {
48+
const result = new Float32Array(recordingLength);
49+
let offset = 0;
50+
const lng = channelBuffer.length;
51+
for (let i = 0; i < lng; i++) {
52+
const buffer = channelBuffer[i];
53+
result.set(buffer, offset);
54+
offset += buffer.length;
55+
}
56+
return result;
57+
}
58+
59+
function interleave(leftChannel, rightChannel) {
60+
const length = leftChannel.length + rightChannel.length;
61+
const result = new Float32Array(length);
62+
63+
let inputIndex = 0;
64+
65+
for (let index = 0; index < length;) {
66+
result[index++] = leftChannel[inputIndex];
67+
result[index++] = rightChannel[inputIndex];
68+
inputIndex++;
69+
}
70+
return result;
71+
}
72+
73+
function writeUTFBytes(view, offset, string) {
74+
const lng = string.length;
75+
for (let i = 0; i < lng; i++) {
76+
view.setUint8(offset + i, string.charCodeAt(i));
77+
}
78+
}
79+
80+
function getWAV(interleaved, sampleRate) {
81+
// create the buffer and view to create the .WAV file
82+
const buffer = new ArrayBuffer(44 + interleaved.length * 2);
83+
const view = new DataView(buffer);
84+
85+
// write the WAV container, check spec at: https://ccrma.stanford.edu/courses/422/projects/WaveFormat/
86+
// RIFF chunk descriptor
87+
writeUTFBytes(view, 0, 'RIFF');
88+
view.setUint32(4, 44 + interleaved.length * 2, true);
89+
writeUTFBytes(view, 8, 'WAVE');
90+
// FMT sub-chunk
91+
writeUTFBytes(view, 12, 'fmt ');
92+
view.setUint32(16, 16, true);
93+
view.setUint16(20, 1, true);
94+
// stereo (2 channels)
95+
view.setUint16(22, 2, true);
96+
view.setUint32(24, sampleRate, true);
97+
view.setUint32(28, sampleRate * 4, true);
98+
view.setUint16(32, 4, true);
99+
view.setUint16(34, 16, true);
100+
// data sub-chunk
101+
writeUTFBytes(view, 36, 'data');
102+
view.setUint32(40, interleaved.length * 2, true);
103+
104+
// write the PCM samples
105+
const lng = interleaved.length;
106+
const volume = 1;
107+
let index = 44;
108+
for (let i = 0; i < lng; i++) {
109+
view.setInt16(index, interleaved[i] * (0x7FFF * volume), true);
110+
index += 2;
111+
}
112+
113+
// our final binary blob that we can hand off
114+
return new Blob([view], { type: 'audio/wav' });
115+
}
116+
117+
// found on stackoverflow
118+
function blobToBase64(blob, cb) {
119+
const reader = new FileReader();
120+
reader.onload = function () {
121+
const dataUrl = reader.result;
122+
const base64 = dataUrl.split(',')[1];
123+
cb(base64);
124+
};
125+
reader.readAsDataURL(blob);
126+
}

0 commit comments

Comments
 (0)