Skip to content

Commit

Permalink
feat: add live waveform visualization to sampler (#4040)
Browse files Browse the repository at this point in the history
* replace web audio api with Tone.js

* implement live waveform feature

* update sampler.js and synthutils.js
  • Loading branch information
haroon10725 authored Dec 19, 2024
1 parent cf031bd commit 7057e8b
Show file tree
Hide file tree
Showing 2 changed files with 102 additions and 81 deletions.
63 changes: 63 additions & 0 deletions js/utils/synthutils.js
Original file line number Diff line number Diff line change
Expand Up @@ -2000,5 +2000,68 @@ function Synth() {
Tone.Destination.volume.rampTo(db, 0.01);
};

/**
* Starts Recording
* @function
* @memberof Synth
*/
this.startRecording = async () => {
await Tone.start();
this.mic = new Tone.UserMedia();
this.recorder = new Tone.Recorder();
await this.mic.open()
.then(() => {
console.log("Mic opened");
this.mic.connect(this.recorder);
this.recorder.start();
})
.catch((error) => {
console.log(error);
});
}

/**
* Stops Recording
* @function
* @memberof Synth
*/
this.stopRecording = async () => {
this.recording = await this.recorder.stop();
this.mic.close();
this.audioURL = URL.createObjectURL(this.recording);
return this.audioURL;
}

/**
* Plays Recording
* @function
* @memberof Synth
*/
this.playRecording = async () => {
const player = new Tone.Player().toDestination();
await player.load(this.audioURL)
player.start();
}

/**
* Analyzing the audio
* @function
* @memberof Synth
*/
this.LiveWaveForm = () => {
this.analyser = new Tone.Analyser('waveform', 8192);
this.mic.connect(this.analyser);
}

/**
* Gets real-time waveform values
* @function
* @memberof Synth
*/
this.getWaveFormValues = () => {
const values = this.analyser.getValue();
return values;
};

return this;
}
120 changes: 39 additions & 81 deletions js/widgets/sampler.js
Original file line number Diff line number Diff line change
Expand Up @@ -233,16 +233,16 @@ function SampleWidget() {
* Displays a message indicating that recording has started.
* @returns {void}
*/
function displayRecordingStartMessage() {
this.activity.textMsg(_("Recording started..."));
this.displayRecordingStartMessage = function () {
this.activity.textMsg(_("Recording started"));
}

/**
* Displays a message indicating that recording has stopped.
* @returns {void}
*/
function displayRecordingStopMessage() {
this.activity.textMsg(_("Recording complete..."));
this.displayRecordingStopMessage = function () {
this.activity.textMsg(_("Recording complete"));
}


Expand Down Expand Up @@ -448,92 +448,41 @@ function SampleWidget() {
_("Toggle Mic"),
""
);
this._recordBtn.onclick = function() {
ToggleMic(this);
}.bind(this._recordBtn);


this._playbackBtn= widgetWindow.addButton(
"playback.svg",
ICONSIZE,
_("Playback"),
"");
this._playbackBtn.id="playbackBtn"
this._playbackBtn.classList.add("disabled");


const togglePlaybackButtonState = () => {
if (!audioURL) {
this._playbackBtn.classList.add("disabled");
this._playbackBtn.id="playbackBtn";
this._playbackBtn.classList.add("disabled");

this.is_recording = false;

this._recordBtn.onclick = async () => {
if (!this.is_recording) {
await this.activity.logo.synth.startRecording();
this.is_recording = true;
this._recordBtn.getElementsByTagName('img')[0].src = "header-icons/record.svg";
this.displayRecordingStartMessage();
this.activity.logo.synth.LiveWaveForm();
} else {
this.recordingURL = await this.activity.logo.synth.stopRecording();
this.is_recording = false;
this._recordBtn.getElementsByTagName('img')[0].src = "header-icons/mic.svg";
this.displayRecordingStopMessage();
this._playbackBtn.classList.remove("disabled");
}
};

this._playbackBtn.onclick = () => {
playAudio();
this.sampleData = this.recordingURL;
this.sampleName = `Recorded Audio ${this.recordingURL}`;
this._addSample();
this.activity.logo.synth.playRecording();
};

let can_record = false;
let is_recording = false;
let recorder = null;
let chunks = [];
let audioURL = null;

async function setUpAudio() {
if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
try {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
recorder = new MediaRecorder(stream);
recorder.ondataavailable = e => {
chunks.push(e.data);
};
recorder.onstop = async e => {
let blob = new Blob(chunks, { type: 'audio/webm' });
chunks = [];
audioURL = URL.createObjectURL(blob);
displayRecordingStopMessage.call(that);
togglePlaybackButtonState();

const module = await import("https://cdn.jsdelivr.net/npm/[email protected]/+esm");
const getWaveBlob = module.getWaveBlob;
const wavBlob = await getWaveBlob(blob);
const wavAudioURL = URL.createObjectURL(wavBlob);
that.sampleData = wavAudioURL;
that.sampleName = `Recorded Audio ${audioURL}`;
that._addSample();
};
can_record = true;
} catch (err) {
console.log("The following error occurred: " + err);
}
}
}
function ToggleMic(buttonElement) {
if (!can_record) return;

is_recording = !is_recording;
if (is_recording) {
recorder.start();
buttonElement.getElementsByTagName('img')[0].src = "header-icons/record.svg";
displayRecordingStartMessage.call(that);
} else {
recorder.stop();
buttonElement.getElementsByTagName('img')[0].src = "header-icons/mic.svg";
}
}

function playAudio() {
if (audioURL) {
const audio = new Audio(audioURL);
audio.play();
console.log("Playing audio.");
} else {
console.error("No recorded audio available.");
}
}

setUpAudio();

widgetWindow.sendToCenter();
this.widgetWindow = widgetWindow;

Expand Down Expand Up @@ -962,13 +911,13 @@ function SampleWidget() {

const draw = () => {
this.drawVisualIDs[turtleIdx] = requestAnimationFrame(draw);
if (this.pitchAnalysers[turtleIdx] && (this.running || resized)) {
if (this.is_recording || (this.pitchAnalysers[turtleIdx] && (this.running || resized))) {
canvasCtx.fillStyle = "#FFFFFF";
canvasCtx.font = "10px Verdana";
this.verticalOffset = -canvas.height / 4;
this.zoomFactor = 40.0;
canvasCtx.fillRect(0, 0, width, height);

let oscText;
if (turtleIdx >= 0) {
//.TRANS: The sound sample that the user uploads.
Expand All @@ -980,16 +929,25 @@ function SampleWidget() {
canvasCtx.fillText(oscText, 10, canvas.height / 2 + 10);

for (let turtleIdx = 0; turtleIdx < 2; turtleIdx += 1) {
const dataArray = this.pitchAnalysers[turtleIdx].getValue();
let dataArray;
if (this.is_recording) {
dataArray = turtleIdx === 0
? this.pitchAnalysers[0].getValue()
: this.activity.logo.synth.getWaveFormValues();
console.log(dataArray);
} else {
dataArray = this.pitchAnalysers[turtleIdx].getValue();
}

const bufferLength = dataArray.length;
const rbga = SAMPLEOSCCOLORS[turtleIdx];
const sliceWidth = (width * this.zoomFactor) / bufferLength;
canvasCtx.lineWidth = 2;
canvasCtx.strokeStyle = rbga;
canvasCtx.beginPath();

let x = 0;

for (let i = 0; i < bufferLength; i++) {
const y = (height / 2) * (1 - dataArray[i]) + this.verticalOffset;
if (i === 0) {
Expand Down

0 comments on commit 7057e8b

Please sign in to comment.