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":"894b234","lastUpdated":"2024-03-14","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
36 changes: 22 additions & 14 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,38 @@
// 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
// 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 =
document.querySelector('#demo-select-waveform-type').value;
const frequency = document.querySelector('#demo-input-frequency').value;
await startAudio(audioContext, {waveformType, frequency});
audioContext.resume();
buttonEl.disabled = true;
buttonEl.textContent = 'Playing...';
if (!oscillatorProcessor) {
// If audio is not playing, start the audio.
const waveformType = document.querySelector('#demo-select-waveform-type').value;
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 the audio.
audioContext.suspend();
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
39 changes: 31 additions & 8 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,47 @@
// found in the LICENSE file.

const audioContext = new AudioContext();
let oscillatorNode;
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);
oscillatorNode = new OscillatorNode(context);
const bypasser = new AudioWorkletNode(context, 'bypass-processor');
oscillator.connect(bypasser).connect(context.destination);
oscillator.start();
oscillatorNode.connect(bypasser).connect(context.destination);
oscillatorNode.start();
};

// 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 (!oscillatorNode) {
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 {
audioContext.suspend();
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
30 changes: 23 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,29 @@ 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 the audio.
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