UX Features — Developer Reference
Technical documentation for user experience features designed for mobile/tablet performance scenarios. Covers screen wake lock, pocket mode with audio unlock, and audio kill button.
Overview
These features address common issues when using web-based scores on mobile devices during live performance:
| Feature | Problem Solved |
|---|---|
| Wake Lock | Screen dimming/sleep during playback |
| Screen Lock (Pocket Mode) | Accidental touches when phone is in pocket |
| Require Audio Unlock | Ensures audio context is unlocked before performance |
| Audio Kill Button | Panic stop for all playing audio |
Wake Lock API
Purpose
Prevents the device screen from dimming or sleeping during score playback. Essential for performers who need continuous screen visibility without manual interaction.
Implementation
Located in transport/oscillaTransport.js:
let wakeLock = null;
async function requestWakeLock() {
if (!('wakeLock' in navigator)) {
console.log('[WakeLock] API not supported');
return;
}
try {
wakeLock = await navigator.wakeLock.request('screen');
console.log('[WakeLock] Screen wake lock acquired');
wakeLock.addEventListener('release', () => {
console.log('[WakeLock] Screen wake lock released');
wakeLock = null;
});
} catch (err) {
console.log(`[WakeLock] Failed: ${err.name}, ${err.message}`);
}
}
function releaseWakeLock() {
if (wakeLock) {
wakeLock.release();
wakeLock = null;
}
}
Lifecycle
- Acquired: When
startPlayback()is called - Released: When
pausePlayback()is called - Re-acquired: When page becomes visible again during active playback
document.addEventListener('visibilitychange', async () => {
if (document.visibilityState === 'visible' && window.isPlaying && !wakeLock) {
await requestWakeLock();
}
});
Browser Support
Wake Lock API is supported in:
- Chrome 84+
- Edge 84+
- Safari 16.4+ (iOS 16.4+)
- Firefox: Not supported (falls back gracefully)
Screen Lock (Pocket Mode)
Purpose
Allows performers to lock the screen to prevent accidental touches when the device is in a pocket or bag. Common scenario: phone in back pocket while playing an instrument.
File Location
system/screenLock.js
HTML Structure
<!-- Lock button in transport controls -->
<button id="screen-lock-button" class="gui-button side-button">
<svg><!-- lock icon --></svg>
</button>
<!-- Full-screen overlay when locked -->
<div id="screen-lock-overlay" class="screen-lock-overlay hidden">
<div class="lock-message">
<svg><!-- large lock icon --></svg>
<p>Screen Locked</p>
<p class="unlock-hint">Double-tap to unlock</p>
</div>
</div>
Unlock Mechanism
Double-tap detection is restricted to the center lock message area to prevent accidental unlocks. Unlocking also resumes the audio context:
async function unlockScreen() {
if (!screenLockOverlay) return;
isScreenLocked = false;
screenLockOverlay.classList.add('hidden');
screenLockButton?.classList.remove('active');
// Also unlock audio context
const ctx = getAudioContext();
if (ctx && ctx.state === 'suspended') {
try {
await ctx.resume();
console.log('[ScreenLock] Audio context resumed');
} catch (err) {
console.log('[ScreenLock] Failed to resume audio:', err);
}
}
}
Visual Design
- Semi-transparent overlay (40% opacity) to maintain score visibility
- Subtle backdrop blur for visual separation
- Dashed border on unlock target area
- Clear visual hierarchy for the unlock hint
Two-Button Unlock
The lock overlay provides two buttons:
-
Unlock Audio - Resumes audio context, keeps screen locked
- Button turns green when audio is unlocked
- Starts Bluetooth keep-alive (see below)
-
Unlock Screen - Unlocks screen and audio
- Stops Bluetooth keep-alive
- Returns to normal interaction
Exported Functions
window.lockScreen = lockScreen;
window.unlockScreen = unlockScreen;
window.unlockAudioOnly = unlockAudioOnly;
window.isScreenLocked = () => isScreenLocked;
Bluetooth Keep-Alive
Purpose
Prevents Bluetooth audio devices (earbuds, speakers) from disconnecting during idle periods when the screen is locked. This is critical for the time between rehearsal setup and performance start.
Implementation
Plays an inaudible 19kHz tone for 50ms every 3 minutes while:
- Screen is locked AND
- Audio context is running
const KEEP_ALIVE_FREQUENCY = 19000; // 19kHz - inaudible to most adults
const KEEP_ALIVE_DURATION = 0.05; // 50ms
const KEEP_ALIVE_INTERVAL = 180000; // 3 minutes
const KEEP_ALIVE_VOLUME = 0.01; // Very quiet
Lifecycle
- Starts: When "Unlock Audio" is tapped (screen still locked)
- Starts: When screen is re-locked and audio is already running
- Stops: When "Unlock Screen" is tapped
Why 19kHz?
- Inaudible to most adults (hearing typically drops off above 15-17kHz)
- Still activates the full audio pipeline
- Keeps Bluetooth connection alive
- Won't interfere with musical content
Require Audio Unlock Preference
Purpose
When enabled, the screen is automatically locked on page load. This ensures performers must deliberately unlock the screen (which also unlocks the audio context) before the performance begins.
Preference
In Preferences dialog under Playback tab:
- Key:
requireAudioUnlock - Label: "Require Audio Unlock"
- Default:
false
Implementation
function checkAutoLock() {
const checkPrefs = () => {
const prefs = window.oscillaPreferences || {};
if (prefs.requireAudioUnlock) {
lockScreen();
console.log('[ScreenLock] Auto-locked (requireAudioUnlock enabled)');
}
};
// Check after delay to ensure preferences are loaded
setTimeout(checkPrefs, 500);
}
Use Case
Projects that use audio cues should enable this preference to ensure all performers have unlocked their audio context before playback begins. This prevents the common issue of audio not playing on iOS devices that haven't had user interaction.
Audio Kill Button
Purpose
Emergency stop for all playing audio. Essential for live performance scenarios where immediate silence is needed.
Implementation
Located in transport controls with a speaker-with-X icon:
<button id="stop-audio-button" class="gui-button side-button">
<svg width="20" height="20" viewBox="0 0 24 24">
<!-- Speaker with X icon -->
<path d="M11 5L6 9H2v6h4l5 4V5z"/>
<line x1="18" y1="9" x2="22" y2="15"/>
<line x1="22" y1="9" x2="18" y2="15"/>
</svg>
</button>
Behavior
Triggers stopAllAudio() from cues/audio/index.js, which:
- Stops all active
<audio>elements - Disconnects audio nodes
- Clears the
activeAudioCuesmap
Transport Controls Layout
Left Controls Grid (2x3)
┌─────────┬─────────┬─────────┐
│ Rewind │ Notes │ Screen │
│ ↶ │ N │ Lock │
├─────────┼─────────┼─────────┤
│Rehearsal│ Audio │ │
│ R │ Master │ │
└─────────┴─────────┴─────────┘
Right Controls Grid
┌─────────┬─────────┬─────────┐
│ Settings│ Pointer │ Dark │
│ ⚙ │ Share │ Mode │
├─────────┼─────────┼─────────┤
│ Live │ Audio │ Project │
│ Console │ Kill │ Menu │
└─────────┴─────────┴─────────┘
Module Dependencies
app.js
└─ import './system/screenLock.js'
screenLock.js
├─ getAudioContext() → window.sharedAudioCtx
├─ initScreenLock()
├─ checkAutoLock() → reads window.oscillaPreferences
└─ exports: lockScreen, unlockScreen, isScreenLocked
oscillaTransport.js
├─ requestWakeLock()
├─ releaseWakeLock()
├─ startPlayback() → calls requestWakeLock()
└─ pausePlayback() → calls releaseWakeLock()
Testing Checklist
- [ ] Wake Lock: Screen stays on during playback
- [ ] Screen Lock: Overlay appears, blocks all touches except unlock area
- [ ] Screen Lock: Double-tap on center icon unlocks screen AND audio
- [ ] Require Audio Unlock: Screen auto-locks on load when preference enabled
- [ ] Audio Kill: Stops all playing audio immediately
Tip: use ← → or ↑ ↓ to navigate the docs