Spaces:
Runtime error
Runtime error
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);
|