Skip to content

Commit

Permalink
refactor(geiger): improve audio configuration and volume control
Browse files Browse the repository at this point in the history
- Add volume normalization (0-1 range)
- Add localStorage volume persistence
- Extract browser-specific audio configs
  • Loading branch information
pivanov committed Dec 17, 2024
1 parent a1209e0 commit 2990525
Show file tree
Hide file tree
Showing 2 changed files with 62 additions and 23 deletions.
83 changes: 60 additions & 23 deletions packages/scan/src/core/web/utils/geiger.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
// MIT License

// Copyright (c) 2024 Kristian Dupont

// Permission is hereby granted, free of charge, to any person obtaining a copy
Expand All @@ -22,19 +21,52 @@

// Taken from: https://github.com/kristiandupont/react-geiger/blob/main/src/Geiger.tsx

import { isFirefox, readLocalStorage } from "@web-utils/helpers";

// Simple throttle for high-frequency calls
let lastPlayTime = 0;
const MIN_INTERVAL = 32; // ~30fps throttle

// Pre-calculate common values
const BASE_VOLUME = 0.5;
const FREQ_MULTIPLIER = 200;
const DEFAULT_VOLUME = 0.5;

// Ensure volume is between 0 and 1
const storedVolume = Math.max(0, Math.min(1, readLocalStorage<number>('react-scan-volume') ?? DEFAULT_VOLUME));

// Audio configurations for different browsers
const config = {
firefox: {
duration: 0.02,
oscillatorType: 'square' as const,
startFreq: 880,
endFreq: 220,
attack: 0.002,
volumeMultiplier: storedVolume,
},
default: {
duration: 0.001,
oscillatorType: 'sine' as const,
startFreq: 440,
endFreq: 220,
attack: 0.0005,
volumeMultiplier: storedVolume,
}
} as const; // Make entire config readonly

// Cache the selected config
const audioConfig = isFirefox ? config.firefox : config.default;

/**
* Plays a Geiger counter-like click sound
* Optimized for render tracking with minimal changes
* Cross-browser compatible version (Firefox, Chrome, Safari)
*/
export const playGeigerClickSound = (
audioContext: AudioContext,
amplitude: number,
) => {
// Simple throttle to prevent audio overlap

const now = performance.now();
if (now - lastPlayTime < MIN_INTERVAL) {
return;
Expand All @@ -43,25 +75,30 @@ export const playGeigerClickSound = (

// Cache currentTime for consistent timing
const currentTime = audioContext.currentTime;
const volume = Math.max(0.5, amplitude);
const duration = 0.001;
const startFrequency = 440 + amplitude * 200;

const oscillator = audioContext.createOscillator();
oscillator.type = 'sine';
oscillator.frequency.setValueAtTime(startFrequency, currentTime);
oscillator.frequency.exponentialRampToValueAtTime(
220,
currentTime + duration,
);

const gainNode = audioContext.createGain();
gainNode.gain.setValueAtTime(volume, currentTime);
gainNode.gain.exponentialRampToValueAtTime(0.01, duration / 2);

oscillator.connect(gainNode);
gainNode.connect(audioContext.destination);

oscillator.start();
const { duration, oscillatorType, startFreq, endFreq, attack } = audioConfig;

// Pre-calculate volume once
const volume = Math.max(BASE_VOLUME, amplitude) * audioConfig.volumeMultiplier;

// Create and configure nodes in one go
const oscillator = new OscillatorNode(audioContext, {
type: oscillatorType,
frequency: startFreq + amplitude * FREQ_MULTIPLIER
});

const gainNode = new GainNode(audioContext, {
gain: 0
});

// Schedule all parameters
oscillator.frequency.exponentialRampToValueAtTime(endFreq, currentTime + duration);
gainNode.gain.linearRampToValueAtTime(volume, currentTime + attack);

// Connect and schedule playback
oscillator
.connect(gainNode)
.connect(audioContext.destination);

oscillator.start(currentTime);
oscillator.stop(currentTime + duration);
};
2 changes: 2 additions & 0 deletions packages/scan/src/core/web/utils/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ export const cn = (...inputs: Array<ClassValue>): string => {
return twMerge(clsx(inputs));
};

export const isFirefox = typeof navigator !== 'undefined' && navigator.userAgent.includes('Firefox');

export const onIdle = (callback: () => void) => {
if ('scheduler' in globalThis) {
return globalThis.scheduler.postTask(callback, {
Expand Down

0 comments on commit 2990525

Please sign in to comment.