Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix: Implemented STOP button behavior to toggle audio playback #353

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
2 changes: 1 addition & 1 deletion src/_data/build_info.json
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"version":"3.1.0","revision":"7201485","lastUpdated":"2023-11-02","copyrightYear":2023}
{"version":"3.1.0","revision":"0643084","lastUpdated":"2024-03-07","copyrightYear":2024}
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ to_root_dir: ../../../
<input type="number" name="frequency" id="demo-input-frequency"
class="p-1 rounded w-1/2 font-semibold" min="1" max="20000" value=440>
</div>
<button id="button-start" disabled>START</button>
<button id="button-start" class="start-button">START</button>
{% include "../../../_includes/example-source-code.njk" %}
</div>

Expand Down
40 changes: 27 additions & 13 deletions src/audio-worklet/basic/audio-worklet-node-options/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,30 +3,44 @@
// found in the LICENSE file.

const audioContext = new AudioContext();
let oscillatorProcessor;

const startAudio = async (context, options) => {
await context.audioWorklet.addModule('oscillator-processor.js');
const oscillatorProcessor =
new AudioWorkletNode(context, 'oscillator-processor', {
processorOptions: {
waveformType: options.waveformType,
frequency: options.frequency,
}});
oscillatorProcessor =
new AudioWorkletNode(context, 'oscillator-processor', {
processorOptions: {
waveformType: options.waveformType,
frequency: options.frequency,
}
});
oscillatorProcessor.connect(context.destination);
};

// A simplem onLoad handler. It also handles user gesture to unlock the audio
const stopAudio = () => {
if (oscillatorProcessor) {
oscillatorProcessor.disconnect();
oscillatorProcessor = null;
}
};

// A simple onLoad handler. It also handles user gesture to unlock the audio
// playback.
window.addEventListener('load', async () => {
const buttonEl = document.getElementById('button-start');
buttonEl.disabled = false;

buttonEl.addEventListener('click', async () => {
const waveformType =
if (!oscillatorProcessor) { // If audio is not playing, start audio
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Let's move the comment below the if().

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

const waveformType =
document.querySelector('#demo-select-waveform-type').value;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: 4-space indent

const frequency = document.querySelector('#demo-input-frequency').value;
await startAudio(audioContext, {waveformType, frequency});
audioContext.resume();
buttonEl.disabled = true;
buttonEl.textContent = 'Playing...';
const frequency = document.querySelector('#demo-input-frequency').value;
await startAudio(audioContext, { waveformType, frequency });
hoch marked this conversation as resolved.
Show resolved Hide resolved
audioContext.resume();
buttonEl.textContent = 'STOP';
} else { // If audio is playing, stop audio
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Ditto on comments.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

stopAudio();
buttonEl.textContent = 'START';
}
}, false);
});
2 changes: 1 addition & 1 deletion src/audio-worklet/basic/hello-audio-worklet/index.njk
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ to_root_dir: ../../../
Chrome Developers Article: Enter Audio Worklet</a> for more information.</p>

<div class="demo-box">
<button id="button-start" disabled>START</button>
<button id="button-start" class="start-button">START</button>
{% include "../../../_includes/example-source-code.njk" %}
</div>

Expand Down
42 changes: 36 additions & 6 deletions src/audio-worklet/basic/hello-audio-worklet/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,54 @@
// found in the LICENSE file.

const audioContext = new AudioContext();
let oscillator;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To make it clear, how about oscillatorNode?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for the suggestion. I've updated the variable name to oscillatorNode for better clarity. Please let me know if you have any further suggestions.

let isPlaying = false;

const startAudio = async (context) => {
await context.audioWorklet.addModule('bypass-processor.js');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto here; calling addModule every call of startAudio is not recommended.

Copy link
Author

@tarunsinghofficial tarunsinghofficial Mar 14, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done the requested changes as per the feedback.

const oscillator = new OscillatorNode(context);
oscillator = new OscillatorNode(context);
const bypasser = new AudioWorkletNode(context, 'bypass-processor');
oscillator.connect(bypasser).connect(context.destination);
oscillator.start();
};

const stopAudio = () => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should leave the rest and simply suspend the context.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the suggestion. I've modified the stopAudio() function to suspend the audio context instead of stopping and disconnecting the oscillator node. Please review the changes and let me know if they meet the requirements.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This also has the same problem with src/audio-worklet/basic/audio-worklet-node-options/main.js above. See my comments for the explanation.

if (oscillator) {
oscillator.stop();
oscillator.disconnect();
}
};

// A simplem onLoad handler. It also handles user gesture to unlock the audio
// playback.
window.addEventListener('load', async () => {
const buttonEl = document.getElementById('button-start');
buttonEl.disabled = false;

buttonEl.addEventListener('click', async () => {
await startAudio(audioContext);
audioContext.resume();
buttonEl.disabled = true;
buttonEl.textContent = 'Playing...';
}, false);
if (!isPlaying) {
await startAudio(audioContext);
audioContext.resume();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Once oscillatorNode is valid, resume() doesn't get called from anywhere. How does this recover from suspended status?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I modified the button click event handler to check if the AudioContext is suspended and call resume() accordingly.

isPlaying = true;
buttonEl.textContent = 'Playing...';
buttonEl.classList.remove('start-button');
} else {
stopAudio();
isPlaying = false;
buttonEl.textContent = 'START';
buttonEl.classList.add('start-button');
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similarly to the previous comment, the logic between L24~L40 is slightly confusing.

I think it's possible to split the check of oscillatorNode out of this if/else block, so the block only handles the context state.

});

buttonEl.addEventListener('mouseenter', () => {
if (isPlaying) {
buttonEl.textContent = 'STOP';
}
});

buttonEl.addEventListener('mouseleave', () => {
if (isPlaying) {
buttonEl.textContent = 'Playing...';
}
});
});
2 changes: 1 addition & 1 deletion src/audio-worklet/basic/noise-generator/index.njk
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ to_root_dir: ../../../
Chrome Developers Article: Enter Audio Worklet</a> for more information.</p>

<div class="demo-box">
<button id="button-start" disabled>START</button>
<button id="button-start" class="start-button">START</button>
{% include "../../../_includes/example-source-code.njk" %}
</div>

Expand Down
29 changes: 22 additions & 7 deletions src/audio-worklet/basic/noise-generator/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@
// found in the LICENSE file.

const audioContext = new AudioContext();
let isPlaying = false;
let noiseGenerator;

const startAudio = async (context) => {
await context.audioWorklet.addModule('noise-generator.js');
const modulator = new OscillatorNode(context);
const modGain = new GainNode(context);
const noiseGenerator = new AudioWorkletNode(context, 'noise-generator');
noiseGenerator = new AudioWorkletNode(context, 'noise-generator');
noiseGenerator.connect(context.destination);

// Connect the oscillator to 'amplitude' AudioParam.
Expand All @@ -20,15 +22,28 @@ const startAudio = async (context) => {
modulator.start();
};

// A simplem onLoad handler. It also handles user gesture to unlock the audio
const stopAudio = () => {
if (noiseGenerator) {
noiseGenerator.disconnect();
isPlaying = false;
}
};

// A simple onLoad handler. It also handles user gesture to unlock the audio
// playback.
window.addEventListener('load', async () => {
const buttonEl = document.getElementById('button-start');
buttonEl.disabled = false;

buttonEl.addEventListener('click', async () => {
await startAudio(audioContext);
audioContext.resume();
buttonEl.disabled = true;
buttonEl.textContent = 'Playing...';
}, false);
if (!isPlaying) { // If not playing, start audio
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Google's JS style doesn't use this type of inline comments.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

moved the comments to next line.

await startAudio(audioContext);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will call L10 (addModule) multiple times depending on isPlaying. Have you tested locally? Loading the same script twice should throw an exception.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have updated the code to ensure that the module is added only once by using a flag (moduleAdded) to track whether the module has already been loaded.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe this example is pretty much the same with the other oscillator example. Why do we need a different flag (moduleAdded) only for this example?

audioContext.resume();
isPlaying = true;
buttonEl.textContent = 'Playing...';
} else { // If playing, stop audio
stopAudio();
buttonEl.textContent = 'START';
}
});
});
2 changes: 1 addition & 1 deletion src/audio-worklet/basic/one-pole-filter/index.njk
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ to_root_dir: ../../../
Chrome Developers Article: Enter Audio Worklet</a> for more information.</p>

<div class="demo-box">
<button id="button-start" disabled>START</button>
<button id="button-start" class="start-button">START</button>
{% include "../../../_includes/example-source-code.njk" %}
</div>

Expand Down
44 changes: 38 additions & 6 deletions src/audio-worklet/basic/one-pole-filter/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@
// found in the LICENSE file.

const audioContext = new AudioContext();
let oscillator;
let isPlaying = false;

const startAudio = async (context) => {
await context.audioWorklet.addModule('one-pole-processor.js');
const oscillator = new OscillatorNode(context);
oscillator = new OscillatorNode(context);
const filter = new AudioWorkletNode(context, 'one-pole-processor');
const frequencyParam = filter.parameters.get('frequency');

Expand All @@ -19,15 +21,45 @@ const startAudio = async (context) => {
.exponentialRampToValueAtTime(0.01, 8.0);
};

const stopAudio = () => {
if (oscillator) {
oscillator.stop();
oscillator.disconnect();
}
};


// A simplem onLoad handler. It also handles user gesture to unlock the audio
// playback.
window.addEventListener('load', async () => {
const buttonEl = document.getElementById('button-start');
buttonEl.disabled = false;

buttonEl.addEventListener('click', async () => {
await startAudio(audioContext);
audioContext.resume();
buttonEl.disabled = true;
buttonEl.textContent = 'Playing...';
}, false);
if (!isPlaying) {
await startAudio(audioContext);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto here.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added the flag check here same as above.

audioContext.resume();
isPlaying = true;
buttonEl.textContent = 'Playing...';
buttonEl.classList.remove('start-button');
} else {
stopAudio();
isPlaying = false;
buttonEl.textContent = 'START';
buttonEl.classList.add('start-button');
}
});

buttonEl.addEventListener('mouseenter', () => {
if (isPlaying) {
buttonEl.textContent = 'STOP';
}
});

buttonEl.addEventListener('mouseleave', () => {
if (isPlaying) {
buttonEl.textContent = 'Playing...';
}
});
});

2 changes: 1 addition & 1 deletion src/audio-worklet/basic/volume-meter/index.njk
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ to_root_dir: ../../../
<input id="volume-meter" class="w-full bg-white"
type="range" min="0" max="100" value="0" step="1" disabled>
</div>
<button id="button-start" disabled>START</button>
<button id="button-start" class="start-button">START</button>
{% include "../../../_includes/example-source-code.njk" %}
</div>

Expand Down
34 changes: 26 additions & 8 deletions src/audio-worklet/basic/volume-meter/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,29 +3,47 @@
// found in the LICENSE file.

const audioContext = new AudioContext();
let mediaStream;
let volumeMeterNode;

const startAudio = async (context, meterElement) => {
await context.audioWorklet.addModule('volume-meter-processor.js');
const mediaStream = await navigator.mediaDevices.getUserMedia({audio: true});
mediaStream = await navigator.mediaDevices.getUserMedia({ audio: true });
const micNode = context.createMediaStreamSource(mediaStream);
const volumeMeterNode = new AudioWorkletNode(context, 'volume-meter');
volumeMeterNode.port.onmessage = ({data}) => {
volumeMeterNode = new AudioWorkletNode(context, 'volume-meter');
volumeMeterNode.port.onmessage = ({ data }) => {
meterElement.value = data * 500;
};
micNode.connect(volumeMeterNode).connect(context.destination);
};

// A simplem onLoad handler. It also handles user gesture to unlock the audio
const stopAudio = () => {
if (mediaStream) {
mediaStream.getTracks().forEach(track => track.stop());
mediaStream = null;
}
if (volumeMeterNode) {
volumeMeterNode.disconnect();
volumeMeterNode = null;
}
};

// A simple onLoad handler. It also handles user gesture to unlock the audio
// playback.
window.addEventListener('load', async () => {
const buttonEl = document.getElementById('button-start');
const meterEl = document.getElementById('volume-meter');
buttonEl.disabled = false;
meterEl.disabled = false;

buttonEl.addEventListener('click', async () => {
await startAudio(audioContext, meterEl);
audioContext.resume();
buttonEl.disabled = true;
buttonEl.textContent = 'Playing...';
if (!mediaStream) { // If audio is not playing, start audio
await startAudio(audioContext, meterEl);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I made the requested changes here:

#362

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added the flag check here too.

audioContext.resume();
buttonEl.textContent = 'STOP';
} else { // If audio is playing, stop audio
stopAudio();
buttonEl.textContent = 'START';
}
}, false);
});
5 changes: 5 additions & 0 deletions src/styles/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ button[disabled] {
@apply cursor-not-allowed opacity-50;
}

/* CSS for stop button */
#button-start:hover {
@apply bg-blue-600 opacity-50;
}

.inactive {
@apply fill-blue-400;
}
Expand Down