Skip to content

Commit 7057e8b

Browse files
authored
feat: add live waveform visualization to sampler (#4040)
* replace web audio api with Tone.js * implement live waveform feature * update sampler.js and synthutils.js
1 parent cf031bd commit 7057e8b

File tree

2 files changed

+102
-81
lines changed

2 files changed

+102
-81
lines changed

js/utils/synthutils.js

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2000,5 +2000,68 @@ function Synth() {
20002000
Tone.Destination.volume.rampTo(db, 0.01);
20012001
};
20022002

2003+
/**
2004+
* Starts Recording
2005+
* @function
2006+
* @memberof Synth
2007+
*/
2008+
this.startRecording = async () => {
2009+
await Tone.start();
2010+
this.mic = new Tone.UserMedia();
2011+
this.recorder = new Tone.Recorder();
2012+
await this.mic.open()
2013+
.then(() => {
2014+
console.log("Mic opened");
2015+
this.mic.connect(this.recorder);
2016+
this.recorder.start();
2017+
})
2018+
.catch((error) => {
2019+
console.log(error);
2020+
});
2021+
}
2022+
2023+
/**
2024+
* Stops Recording
2025+
* @function
2026+
* @memberof Synth
2027+
*/
2028+
this.stopRecording = async () => {
2029+
this.recording = await this.recorder.stop();
2030+
this.mic.close();
2031+
this.audioURL = URL.createObjectURL(this.recording);
2032+
return this.audioURL;
2033+
}
2034+
2035+
/**
2036+
* Plays Recording
2037+
* @function
2038+
* @memberof Synth
2039+
*/
2040+
this.playRecording = async () => {
2041+
const player = new Tone.Player().toDestination();
2042+
await player.load(this.audioURL)
2043+
player.start();
2044+
}
2045+
2046+
/**
2047+
* Analyzing the audio
2048+
* @function
2049+
* @memberof Synth
2050+
*/
2051+
this.LiveWaveForm = () => {
2052+
this.analyser = new Tone.Analyser('waveform', 8192);
2053+
this.mic.connect(this.analyser);
2054+
}
2055+
2056+
/**
2057+
* Gets real-time waveform values
2058+
* @function
2059+
* @memberof Synth
2060+
*/
2061+
this.getWaveFormValues = () => {
2062+
const values = this.analyser.getValue();
2063+
return values;
2064+
};
2065+
20032066
return this;
20042067
}

js/widgets/sampler.js

Lines changed: 39 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -233,16 +233,16 @@ function SampleWidget() {
233233
* Displays a message indicating that recording has started.
234234
* @returns {void}
235235
*/
236-
function displayRecordingStartMessage() {
237-
this.activity.textMsg(_("Recording started..."));
236+
this.displayRecordingStartMessage = function () {
237+
this.activity.textMsg(_("Recording started"));
238238
}
239239

240240
/**
241241
* Displays a message indicating that recording has stopped.
242242
* @returns {void}
243243
*/
244-
function displayRecordingStopMessage() {
245-
this.activity.textMsg(_("Recording complete..."));
244+
this.displayRecordingStopMessage = function () {
245+
this.activity.textMsg(_("Recording complete"));
246246
}
247247

248248

@@ -448,92 +448,41 @@ function SampleWidget() {
448448
_("Toggle Mic"),
449449
""
450450
);
451-
this._recordBtn.onclick = function() {
452-
ToggleMic(this);
453-
}.bind(this._recordBtn);
454-
451+
455452
this._playbackBtn= widgetWindow.addButton(
456453
"playback.svg",
457454
ICONSIZE,
458455
_("Playback"),
459456
"");
460-
this._playbackBtn.id="playbackBtn"
461-
this._playbackBtn.classList.add("disabled");
462-
463457

464-
const togglePlaybackButtonState = () => {
465-
if (!audioURL) {
466-
this._playbackBtn.classList.add("disabled");
458+
this._playbackBtn.id="playbackBtn";
459+
this._playbackBtn.classList.add("disabled");
460+
461+
this.is_recording = false;
462+
463+
this._recordBtn.onclick = async () => {
464+
if (!this.is_recording) {
465+
await this.activity.logo.synth.startRecording();
466+
this.is_recording = true;
467+
this._recordBtn.getElementsByTagName('img')[0].src = "header-icons/record.svg";
468+
this.displayRecordingStartMessage();
469+
this.activity.logo.synth.LiveWaveForm();
467470
} else {
471+
this.recordingURL = await this.activity.logo.synth.stopRecording();
472+
this.is_recording = false;
473+
this._recordBtn.getElementsByTagName('img')[0].src = "header-icons/mic.svg";
474+
this.displayRecordingStopMessage();
468475
this._playbackBtn.classList.remove("disabled");
469476
}
470477
};
471478

472479
this._playbackBtn.onclick = () => {
473-
playAudio();
480+
this.sampleData = this.recordingURL;
481+
this.sampleName = `Recorded Audio ${this.recordingURL}`;
482+
this._addSample();
483+
this.activity.logo.synth.playRecording();
474484
};
475485

476-
let can_record = false;
477-
let is_recording = false;
478-
let recorder = null;
479-
let chunks = [];
480-
let audioURL = null;
481-
482-
async function setUpAudio() {
483-
if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
484-
try {
485-
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
486-
recorder = new MediaRecorder(stream);
487-
recorder.ondataavailable = e => {
488-
chunks.push(e.data);
489-
};
490-
recorder.onstop = async e => {
491-
let blob = new Blob(chunks, { type: 'audio/webm' });
492-
chunks = [];
493-
audioURL = URL.createObjectURL(blob);
494-
displayRecordingStopMessage.call(that);
495-
togglePlaybackButtonState();
496-
497-
const module = await import("https://cdn.jsdelivr.net/npm/[email protected]/+esm");
498-
const getWaveBlob = module.getWaveBlob;
499-
const wavBlob = await getWaveBlob(blob);
500-
const wavAudioURL = URL.createObjectURL(wavBlob);
501-
that.sampleData = wavAudioURL;
502-
that.sampleName = `Recorded Audio ${audioURL}`;
503-
that._addSample();
504-
};
505-
can_record = true;
506-
} catch (err) {
507-
console.log("The following error occurred: " + err);
508-
}
509-
}
510-
}
511-
function ToggleMic(buttonElement) {
512-
if (!can_record) return;
513-
514-
is_recording = !is_recording;
515-
if (is_recording) {
516-
recorder.start();
517-
buttonElement.getElementsByTagName('img')[0].src = "header-icons/record.svg";
518-
displayRecordingStartMessage.call(that);
519-
} else {
520-
recorder.stop();
521-
buttonElement.getElementsByTagName('img')[0].src = "header-icons/mic.svg";
522-
}
523-
}
524-
525-
function playAudio() {
526-
if (audioURL) {
527-
const audio = new Audio(audioURL);
528-
audio.play();
529-
console.log("Playing audio.");
530-
} else {
531-
console.error("No recorded audio available.");
532-
}
533-
}
534-
535-
setUpAudio();
536-
537486
widgetWindow.sendToCenter();
538487
this.widgetWindow = widgetWindow;
539488

@@ -962,13 +911,13 @@ function SampleWidget() {
962911

963912
const draw = () => {
964913
this.drawVisualIDs[turtleIdx] = requestAnimationFrame(draw);
965-
if (this.pitchAnalysers[turtleIdx] && (this.running || resized)) {
914+
if (this.is_recording || (this.pitchAnalysers[turtleIdx] && (this.running || resized))) {
966915
canvasCtx.fillStyle = "#FFFFFF";
967916
canvasCtx.font = "10px Verdana";
968917
this.verticalOffset = -canvas.height / 4;
969918
this.zoomFactor = 40.0;
970919
canvasCtx.fillRect(0, 0, width, height);
971-
920+
972921
let oscText;
973922
if (turtleIdx >= 0) {
974923
//.TRANS: The sound sample that the user uploads.
@@ -980,16 +929,25 @@ function SampleWidget() {
980929
canvasCtx.fillText(oscText, 10, canvas.height / 2 + 10);
981930

982931
for (let turtleIdx = 0; turtleIdx < 2; turtleIdx += 1) {
983-
const dataArray = this.pitchAnalysers[turtleIdx].getValue();
932+
let dataArray;
933+
if (this.is_recording) {
934+
dataArray = turtleIdx === 0
935+
? this.pitchAnalysers[0].getValue()
936+
: this.activity.logo.synth.getWaveFormValues();
937+
console.log(dataArray);
938+
} else {
939+
dataArray = this.pitchAnalysers[turtleIdx].getValue();
940+
}
941+
984942
const bufferLength = dataArray.length;
985943
const rbga = SAMPLEOSCCOLORS[turtleIdx];
986944
const sliceWidth = (width * this.zoomFactor) / bufferLength;
987945
canvasCtx.lineWidth = 2;
988946
canvasCtx.strokeStyle = rbga;
989947
canvasCtx.beginPath();
990-
948+
991949
let x = 0;
992-
950+
993951
for (let i = 0; i < bufferLength; i++) {
994952
const y = (height / 2) * (1 - dataArray[i]) + this.verticalOffset;
995953
if (i === 0) {

0 commit comments

Comments
 (0)