Skip to content

fix: enable TTS audio playback on iOS when silent mode is on#1151

Open
NJX-njx wants to merge 1 commit intomoeru-ai:mainfrom
NJX-njx:NJX-njx/fix/ios-silent-mode-tts
Open

fix: enable TTS audio playback on iOS when silent mode is on#1151
NJX-njx wants to merge 1 commit intomoeru-ai:mainfrom
NJX-njx:NJX-njx/fix/ios-silent-mode-tts

Conversation

@NJX-njx
Copy link
Contributor

@NJX-njx NJX-njx commented Mar 5, 2026

Summary

Fixes TTS audio not playing on iOS when the mute/silent switch is on. Addresses #894.

Root Cause

On iOS, the hardware mute switch causes all Web Audio API output to be silent because AudioContext is treated as the "ambient" audio session category. This is a known WebKit limitation:

Solution

The standard workaround (used by YouTube, Spotify Web, and other production apps) is to play a silent HTML5 <audio> element alongside the Web Audio API. This forces iOS to assign the "playback" audio session category, which is not affected by the mute switch.

Changes

  • packages/stage-ui/src/utils/ios-audio-unmute.ts (new):

    • enableIOSPlayback(): Creates and loops a tiny silent WAV via HTML5 <audio>, forcing iOS into "playback" mode
    • disposeIOSPlayback(): Cleanup function
    • isIOS(): Platform detection (includes iPad with desktop UA)
    • No-ops safely on non-iOS platforms
  • packages/stage-ui/src/stores/audio.ts:

    • Registers one-time touchend/click document listener that calls enableIOSPlayback() on first user interaction
    • User gesture is required for iOS to allow <audio>.play()

Testing Notes

  • Safe no-op on non-iOS platforms (Android, desktop browsers)
  • The silent WAV is a data URI (44 bytes), no network request needed
  • Only one silent <audio> element is created (singleton pattern)

Closes #894

- Add ios-audio-unmute utility that plays a silent HTML5 <audio> element
  to force iOS into 'playback' audio session (bypasses mute switch)
- Register one-time touchend/click listener in audio context store
  to activate the workaround on first user interaction
- Safe no-op on non-iOS platforms

Closes moeru-ai#894
Copilot AI review requested due to automatic review settings March 5, 2026 16:03
@chatgpt-codex-connector
Copy link

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request resolves an issue where Text-to-Speech (TTS) audio would not play on iOS devices when the mute switch was engaged. The solution involves a common web development pattern for iOS, which plays a silent HTML5 audio element to force the browser into a "playback" audio session category, bypassing the mute switch's effect on Web Audio API output. This ensures a consistent audio experience for users regardless of their device's silent mode setting.

Highlights

  • iOS Silent Mode Fix: Implemented a workaround to enable Text-to-Speech (TTS) audio playback on iOS devices even when silent mode is active, addressing a known WebKit limitation.
  • New iOS Audio Unmute Utility: Introduced a new utility file (ios-audio-unmute.ts) containing functions to manage the silent HTML5 audio element workaround, including enableIOSPlayback() and disposeIOSPlayback().
  • User Interaction Integration: Integrated the enableIOSPlayback() function into the audio store, ensuring it is triggered on the first user interaction (touchend or click) to comply with iOS playback policies.
Changelog
  • packages/stage-ui/src/stores/audio.ts
    • Imported enableIOSPlayback from the new utility file.
    • Added a one-time event listener for touchend and click events on the document to call enableIOSPlayback() upon the first user interaction, unlocking audio playback.
  • packages/stage-ui/src/utils/ios-audio-unmute.ts
    • Created a new file to encapsulate the iOS audio unmute logic.
    • Defined enableIOSPlayback() to create and loop a silent HTML5 audio element with a data URI, forcing iOS into "playback" mode.
    • Included disposeIOSPlayback() for cleaning up the silent audio element.
    • Added an isIOS() utility function for platform detection.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds an iOS-specific workaround so Web Audio (TTS) playback isn’t muted by the hardware silent switch by forcing iOS into a “playback” audio session via a looping silent <audio> element.

Changes:

  • Added an iOS “audio unmute” utility that plays a silent looping WAV via HTML5 audio.
  • Registered one-time touchend/click listeners in the audio store to trigger the workaround on first user interaction.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

File Description
packages/stage-ui/src/utils/ios-audio-unmute.ts Introduces the silent-audio playback workaround + helpers.
packages/stage-ui/src/stores/audio.ts Hooks the workaround into first user interaction so play() is gesture-initiated.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +34 to +36
export function enableIOSPlayback(): void {
if (disposed || silentAudio || !isIOS())
return
Copy link

Copilot AI Mar 5, 2026

Choose a reason for hiding this comment

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

enableIOSPlayback() sets silentAudio before attempting play(), and the guard if (disposed || silentAudio || !isIOS()) return prevents any future call from retrying. If play() rejects (your comment says the next interaction will trigger it), the code will currently never re-attempt playback; consider retrying play() on subsequent calls or clearing silentAudio in the catch so a later user gesture can try again.

Copilot uses AI. Check for mistakes.
Comment on lines +19 to +20
const SILENT_WAV_DATA_URI
= 'data:audio/wav;base64,UklGRiQAAABXQVZFZm10IBAAAAABAAEARKwAAIhYAQACABAAZGF0YQAAAAA='
Copy link

Copilot AI Mar 5, 2026

Choose a reason for hiding this comment

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

The SILENT_WAV_DATA_URI assignment formatting (const ... on one line and = on the next) appears inconsistent with the surrounding codebase style and is likely to be reformatted/flagged by formatters/linters. Consider using the repository’s typical const X = ... layout (or the standard Prettier split with = at end of the first line).

Suggested change
const SILENT_WAV_DATA_URI
= 'data:audio/wav;base64,UklGRiQAAABXQVZFZm10IBAAAAABAAEARKwAAIhYAQACABAAZGF0YQAAAAA='
const SILENT_WAV_DATA_URI =
'data:audio/wav;base64,UklGRiQAAABXQVZFZm10IBAAAAABAAEARKwAAIhYAQACABAAZGF0YQAAAAA='

Copilot uses AI. Check for mistakes.
Comment on lines +51 to +56
export function disposeIOSPlayback(): void {
disposed = true
if (silentAudio) {
silentAudio.pause()
silentAudio.src = ''
silentAudio = undefined
Copy link

Copilot AI Mar 5, 2026

Choose a reason for hiding this comment

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

disposeIOSPlayback() sets a module-level disposed = true, which permanently disables enableIOSPlayback() for the rest of the session (even if called again after dispose). If disposeIOSPlayback() is intended as a temporary cleanup (e.g., on component unmount), consider allowing re-enable by resetting disposed (or removing the disposed flag and relying on silentAudio state).

Copilot uses AI. Check for mistakes.
Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request effectively addresses the issue of TTS audio not playing on iOS when silent mode is enabled by implementing the standard workaround of playing a silent HTML5 <audio> element. The new ios-audio-unmute.ts utility provides a clean encapsulation of the iOS detection and audio playback logic, and its integration into the useAudioContext store ensures it's triggered by a user gesture, as required by iOS. The solution is well-documented and considers non-iOS platforms and performance aspects like using a data URI for the silent WAV. There are a couple of minor improvements related to code clarity and adherence to the provided style guide for code suggestions.

Comment on lines +77 to +78
document.removeEventListener('touchend', unlockOnInteraction, true)
document.removeEventListener('click', unlockOnInteraction, true)
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

The once: true option in addEventListener automatically removes the listener after it has been invoked once. Therefore, explicitly calling document.removeEventListener inside unlockOnInteraction is redundant and can be removed for cleaner code.

Suggested change
document.removeEventListener('touchend', unlockOnInteraction, true)
document.removeEventListener('click', unlockOnInteraction, true)
document.addEventListener('touchend', unlockOnInteraction, { capture: true, once: true })
document.addEventListener('click', unlockOnInteraction, { capture: true, once: true })

Comment on lines +19 to +20
const SILENT_WAV_DATA_URI
= 'data:audio/wav;base64,UklGRiQAAABXQVZFZm10IBAAAAABAAEARKwAAIhYAQACABAAZGF0YQAAAAA='
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

For better readability and conciseness, especially for short constant definitions, consider placing the entire definition on a single line if it fits within typical line length limits. This also demonstrates adherence to the style guide's suggestion format (Repository Style Guide, lines 9-15).

Suggested change
const SILENT_WAV_DATA_URI
= 'data:audio/wav;base64,UklGRiQAAABXQVZFZm10IBAAAAABAAEARKwAAIhYAQACABAAZGF0YQAAAAA='
const SILENT_WAV_DATA_URI = 'data:audio/wav;base64,UklGRiQAAABXQVZFZm10IBAAAAABAAEARKwAAIhYAQACABAAZGF0YQAAAAA='
References
  1. Always wrap code suggestions in GitHub suggestion blocks to enable one-click application by maintainers. Use GitHub's suggestion syntax for all code recommendations. (link)

@github-actions
Copy link
Contributor

github-actions bot commented Mar 6, 2026

⏳ Approval required for deploying to Cloudflare Workers (Preview) for stage-web.

Name Link
🔭 Waiting for approval For maintainers, approve here

Hey, @nekomeowww, @sumimakito, @luoling8192, @LemonNekoGH, kindly take some time to review and approve this deployment when you are available. Thank you! 🙏

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug: cannot hear playback of text-to-speech audio on PWA

2 participants