Spaces:
Running
Running
Update src/containers/sound-editor.jsx
Browse files- src/containers/sound-editor.jsx +93 -14
src/containers/sound-editor.jsx
CHANGED
@@ -48,11 +48,12 @@ class SoundEditor extends React.Component {
|
|
48 |
'setRef',
|
49 |
'resampleBufferToRate',
|
50 |
'handleModifyMenu',
|
|
|
51 |
'getSelectionBuffer'
|
52 |
]);
|
53 |
this.state = {
|
54 |
copyBuffer: null,
|
55 |
-
chunkLevels: computeChunkedRMS(this.props.samples),
|
56 |
playhead: null, // null is not playing, [0 -> 1] is playing percent
|
57 |
trimStart: null,
|
58 |
trimEnd: null
|
@@ -69,6 +70,11 @@ class SoundEditor extends React.Component {
|
|
69 |
document.addEventListener('keydown', this.handleKeyPress);
|
70 |
}
|
71 |
componentWillReceiveProps(newProps) {
|
|
|
|
|
|
|
|
|
|
|
72 |
if (newProps.soundId !== this.props.soundId) { // A different sound has been selected
|
73 |
this.redoStack = [];
|
74 |
this.undoStack = [];
|
@@ -140,7 +146,7 @@ class SoundEditor extends React.Component {
|
|
140 |
this.audioBufferPlayer.stop();
|
141 |
this.audioBufferPlayer = new AudioBufferPlayer(samples, sampleRate);
|
142 |
this.setState({
|
143 |
-
chunkLevels: computeChunkedRMS(samples),
|
144 |
playhead: null
|
145 |
});
|
146 |
}
|
@@ -240,7 +246,9 @@ class SoundEditor extends React.Component {
|
|
240 |
this.handleStopPlaying();
|
241 |
}
|
242 |
effectFactory(name) {
|
243 |
-
return () => this.handleEffect(
|
|
|
|
|
244 |
}
|
245 |
copyCurrentBuffer() {
|
246 |
// Cannot reliably use props.samples because it gets detached by Firefox
|
@@ -249,7 +257,7 @@ class SoundEditor extends React.Component {
|
|
249 |
sampleRate: this.audioBufferPlayer.buffer.sampleRate
|
250 |
};
|
251 |
}
|
252 |
-
handleEffect(
|
253 |
const trimStart = this.state.trimStart === null ? 0.0 : this.state.trimStart;
|
254 |
const trimEnd = this.state.trimEnd === null ? 1.0 : this.state.trimEnd;
|
255 |
|
@@ -258,7 +266,7 @@ class SoundEditor extends React.Component {
|
|
258 |
return;
|
259 |
}
|
260 |
|
261 |
-
const effects = new AudioEffects(this.audioBufferPlayer.buffer,
|
262 |
effects.process((renderedBuffer, adjustedTrimStart, adjustedTrimEnd) => {
|
263 |
const samples = renderedBuffer.getChannelData(0);
|
264 |
const sampleRate = renderedBuffer.sampleRate;
|
@@ -448,16 +456,12 @@ class SoundEditor extends React.Component {
|
|
448 |
this.handleUpdateTrim(null, null);
|
449 |
}
|
450 |
}
|
|
|
451 |
handleModifyMenu() {
|
452 |
// get selected audio
|
453 |
const bufferSelection = this.getSelectionBuffer();
|
454 |
// for preview
|
455 |
const audio = new AudioContext();
|
456 |
-
// const testNode = audio.createBiquadFilter();
|
457 |
-
// testNode.type = "lowpass";
|
458 |
-
// testNode.frequency.value = 880;
|
459 |
-
// testNode.Q.value = 0.7;
|
460 |
-
// testNode.connect(audio.destination);
|
461 |
const gainNode = audio.createGain();
|
462 |
gainNode.gain.value = 1;
|
463 |
gainNode.connect(audio.destination);
|
@@ -470,7 +474,6 @@ class SoundEditor extends React.Component {
|
|
470 |
const truePitch = isNaN(Number(pitch.value)) ? 0 : Number(pitch.value);
|
471 |
const trueVolume = isNaN(Number(volume.value)) ? 0 : Number(volume.value);
|
472 |
this.handleEffect({
|
473 |
-
special: true,
|
474 |
pitch: truePitch * 10,
|
475 |
volume: trueVolume
|
476 |
});
|
@@ -584,6 +587,79 @@ class SoundEditor extends React.Component {
|
|
584 |
};
|
585 |
valueVolume.oninput = valueVolume.onchange;
|
586 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
587 |
displayPopup(title, width, height, okname, denyname, accepted, cancelled) {
|
588 |
const div = document.createElement("div");
|
589 |
document.body.append(div);
|
@@ -660,6 +736,7 @@ class SoundEditor extends React.Component {
|
|
660 |
onFaster={this.effectFactory(effectTypes.FASTER)}
|
661 |
onLouder={this.effectFactory(effectTypes.LOUDER)}
|
662 |
onModifySound={this.handleModifyMenu}
|
|
|
663 |
onMute={this.effectFactory(effectTypes.MUTE)}
|
664 |
onPaste={this.handlePaste}
|
665 |
onPlay={this.handlePlay}
|
@@ -689,7 +766,8 @@ SoundEditor.propTypes = {
|
|
689 |
samples: PropTypes.instanceOf(Float32Array),
|
690 |
soundId: PropTypes.string,
|
691 |
soundIndex: PropTypes.number,
|
692 |
-
vm: PropTypes.instanceOf(VM).isRequired
|
|
|
693 |
};
|
694 |
|
695 |
const mapStateToProps = (state, { soundIndex }) => {
|
@@ -708,10 +786,11 @@ const mapStateToProps = (state, { soundIndex }) => {
|
|
708 |
samples: audioBuffer.getChannelData(0),
|
709 |
isFullScreen: state.scratchGui.mode.isFullScreen,
|
710 |
name: sound.name,
|
711 |
-
vm: state.scratchGui.vm
|
|
|
712 |
};
|
713 |
};
|
714 |
|
715 |
export default connect(
|
716 |
mapStateToProps
|
717 |
-
)(SoundEditor);
|
|
|
48 |
'setRef',
|
49 |
'resampleBufferToRate',
|
50 |
'handleModifyMenu',
|
51 |
+
'handleFormatMenu',
|
52 |
'getSelectionBuffer'
|
53 |
]);
|
54 |
this.state = {
|
55 |
copyBuffer: null,
|
56 |
+
chunkLevels: computeChunkedRMS(this.props.samples, this.props.waveformChunkSize),
|
57 |
playhead: null, // null is not playing, [0 -> 1] is playing percent
|
58 |
trimStart: null,
|
59 |
trimEnd: null
|
|
|
70 |
document.addEventListener('keydown', this.handleKeyPress);
|
71 |
}
|
72 |
componentWillReceiveProps(newProps) {
|
73 |
+
if (newProps.waveformChunkSize !== this.props.waveformChunkSize) {
|
74 |
+
this.setState({
|
75 |
+
chunkLevels: computeChunkedRMS(newProps.samples, newProps.waveformChunkSize),
|
76 |
+
});
|
77 |
+
}
|
78 |
if (newProps.soundId !== this.props.soundId) { // A different sound has been selected
|
79 |
this.redoStack = [];
|
80 |
this.undoStack = [];
|
|
|
146 |
this.audioBufferPlayer.stop();
|
147 |
this.audioBufferPlayer = new AudioBufferPlayer(samples, sampleRate);
|
148 |
this.setState({
|
149 |
+
chunkLevels: computeChunkedRMS(samples, this.props.waveformChunkSize),
|
150 |
playhead: null
|
151 |
});
|
152 |
}
|
|
|
246 |
this.handleStopPlaying();
|
247 |
}
|
248 |
effectFactory(name) {
|
249 |
+
return () => this.handleEffect({
|
250 |
+
preset: name,
|
251 |
+
});
|
252 |
}
|
253 |
copyCurrentBuffer() {
|
254 |
// Cannot reliably use props.samples because it gets detached by Firefox
|
|
|
257 |
sampleRate: this.audioBufferPlayer.buffer.sampleRate
|
258 |
};
|
259 |
}
|
260 |
+
handleEffect(options) {
|
261 |
const trimStart = this.state.trimStart === null ? 0.0 : this.state.trimStart;
|
262 |
const trimEnd = this.state.trimEnd === null ? 1.0 : this.state.trimEnd;
|
263 |
|
|
|
266 |
return;
|
267 |
}
|
268 |
|
269 |
+
const effects = new AudioEffects(this.audioBufferPlayer.buffer, options, trimStart, trimEnd);
|
270 |
effects.process((renderedBuffer, adjustedTrimStart, adjustedTrimEnd) => {
|
271 |
const samples = renderedBuffer.getChannelData(0);
|
272 |
const sampleRate = renderedBuffer.sampleRate;
|
|
|
456 |
this.handleUpdateTrim(null, null);
|
457 |
}
|
458 |
}
|
459 |
+
|
460 |
handleModifyMenu() {
|
461 |
// get selected audio
|
462 |
const bufferSelection = this.getSelectionBuffer();
|
463 |
// for preview
|
464 |
const audio = new AudioContext();
|
|
|
|
|
|
|
|
|
|
|
465 |
const gainNode = audio.createGain();
|
466 |
gainNode.gain.value = 1;
|
467 |
gainNode.connect(audio.destination);
|
|
|
474 |
const truePitch = isNaN(Number(pitch.value)) ? 0 : Number(pitch.value);
|
475 |
const trueVolume = isNaN(Number(volume.value)) ? 0 : Number(volume.value);
|
476 |
this.handleEffect({
|
|
|
477 |
pitch: truePitch * 10,
|
478 |
volume: trueVolume
|
479 |
});
|
|
|
587 |
};
|
588 |
valueVolume.oninput = valueVolume.onchange;
|
589 |
}
|
590 |
+
handleFormatMenu() {
|
591 |
+
const sampleRates = [
|
592 |
+
3000, 4000, 8000, 11025, 16000, 22050, 32000, 44100,
|
593 |
+
48000, 88200, 96000, 176400, 192000, 352800, 384000,
|
594 |
+
];
|
595 |
+
let selectedSampleRate = this.props.sampleRate;
|
596 |
+
let selectedForceRate = false;
|
597 |
+
const menu = this.displayPopup("Format Sound", 580, 300, "Apply", "Cancel", () => {
|
598 |
+
// accepted
|
599 |
+
const edits = {
|
600 |
+
sampleRate: selectedSampleRate,
|
601 |
+
};
|
602 |
+
if (selectedForceRate) {
|
603 |
+
edits.sampleRateEnforced = selectedSampleRate;
|
604 |
+
}
|
605 |
+
this.handleEffect(edits);
|
606 |
+
});
|
607 |
+
|
608 |
+
menu.textarea.style = "padding:8px;";
|
609 |
+
|
610 |
+
const labelSampleRate = document.createElement("p");
|
611 |
+
labelSampleRate.innerHTML = "Sample Rate";
|
612 |
+
labelSampleRate.style = "font-size:14px;";
|
613 |
+
menu.textarea.append(labelSampleRate);
|
614 |
+
const inputSampleRate = document.createElement("select");
|
615 |
+
inputSampleRate.style = "width:50%;"
|
616 |
+
menu.textarea.append(inputSampleRate);
|
617 |
+
for (const rate of sampleRates) {
|
618 |
+
const option = document.createElement("option");
|
619 |
+
option.value = rate;
|
620 |
+
option.innerHTML = `${rate}`;
|
621 |
+
inputSampleRate.append(option);
|
622 |
+
}
|
623 |
+
inputSampleRate.selectedIndex = sampleRates.indexOf(this.props.sampleRate);
|
624 |
+
const labelSampleRateWarning = document.createElement("p");
|
625 |
+
labelSampleRateWarning.innerHTML = "Choosing a higher sample rate than the current rate will not make the existing audio higher quality.";
|
626 |
+
labelSampleRateWarning.style = "font-size:13px;opacity:0.5;";
|
627 |
+
menu.textarea.append(labelSampleRateWarning);
|
628 |
+
inputSampleRate.onchange = () => {
|
629 |
+
selectedSampleRate = inputSampleRate.value;
|
630 |
+
};
|
631 |
+
|
632 |
+
const labelResampleAudio = document.createElement("label");
|
633 |
+
labelResampleAudio.innerHTML = "Enforce New Sample Rate";
|
634 |
+
menu.textarea.append(labelResampleAudio);
|
635 |
+
const inputResampleAudio = document.createElement("input");
|
636 |
+
inputResampleAudio.type = "checkbox";
|
637 |
+
inputResampleAudio.style = "margin-right:8px;";
|
638 |
+
labelResampleAudio.prepend(inputResampleAudio);
|
639 |
+
const labelResampleAudioWarning = document.createElement("p");
|
640 |
+
labelResampleAudioWarning.innerHTML = "This changes the properties of the entire sound, "
|
641 |
+
+ "making lower sample rates use less file size. "
|
642 |
+
+ "However, audio added to this sound will only be able to use the new sample rate.";
|
643 |
+
labelResampleAudioWarning.style = "font-size:13px;opacity:0.5;";
|
644 |
+
menu.textarea.append(labelResampleAudioWarning);
|
645 |
+
|
646 |
+
const warning = document.createElement("p");
|
647 |
+
warning.innerHTML = "Applying these changes will cause the entire sound to change, not just the selected area.";
|
648 |
+
warning.style = "font-size:14px;";
|
649 |
+
warning.style.display = "none";
|
650 |
+
menu.textarea.append(warning);
|
651 |
+
|
652 |
+
inputResampleAudio.onchange = () => {
|
653 |
+
selectedForceRate = inputResampleAudio.checked;
|
654 |
+
if (selectedForceRate) {
|
655 |
+
warning.style.display = "";
|
656 |
+
} else {
|
657 |
+
warning.style.display = "none";
|
658 |
+
}
|
659 |
+
};
|
660 |
+
}
|
661 |
+
|
662 |
+
// TODO: use actual scratch-gui menus instead of this
|
663 |
displayPopup(title, width, height, okname, denyname, accepted, cancelled) {
|
664 |
const div = document.createElement("div");
|
665 |
document.body.append(div);
|
|
|
736 |
onFaster={this.effectFactory(effectTypes.FASTER)}
|
737 |
onLouder={this.effectFactory(effectTypes.LOUDER)}
|
738 |
onModifySound={this.handleModifyMenu}
|
739 |
+
onFormatSound={this.handleFormatMenu}
|
740 |
onMute={this.effectFactory(effectTypes.MUTE)}
|
741 |
onPaste={this.handlePaste}
|
742 |
onPlay={this.handlePlay}
|
|
|
766 |
samples: PropTypes.instanceOf(Float32Array),
|
767 |
soundId: PropTypes.string,
|
768 |
soundIndex: PropTypes.number,
|
769 |
+
vm: PropTypes.instanceOf(VM).isRequired,
|
770 |
+
waveformChunkSize: PropTypes.number,
|
771 |
};
|
772 |
|
773 |
const mapStateToProps = (state, { soundIndex }) => {
|
|
|
786 |
samples: audioBuffer.getChannelData(0),
|
787 |
isFullScreen: state.scratchGui.mode.isFullScreen,
|
788 |
name: sound.name,
|
789 |
+
vm: state.scratchGui.vm,
|
790 |
+
waveformChunkSize: state.scratchGui.addonUtil.soundEditorWaveformChunkSize,
|
791 |
};
|
792 |
};
|
793 |
|
794 |
export default connect(
|
795 |
mapStateToProps
|
796 |
+
)(SoundEditor);
|