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

document.addEventListener('visibilitychange', async () => {
  if (document.visibilityState === 'visible' && window.isPlaying && !wakeLock) {
    await requestWakeLock();
  }
});

Browser Support

Wake Lock API is supported in:


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

Two-Button Unlock

The lock overlay provides two buttons:

  1. Unlock Audio - Resumes audio context, keeps screen locked

    • Button turns green when audio is unlocked
    • Starts Bluetooth keep-alive (see below)
  2. 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:

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

Why 19kHz?


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:

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:


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

Tip: use ← → or ↑ ↓ to navigate the docs