init project
This commit is contained in:
@@ -0,0 +1,232 @@
|
||||
import scanSuccessSoundUrl from "@/assets/sound/scan-success.mp3";
|
||||
import registrationSuccessSoundUrl from "@/assets/sound/registration-success.mp3";
|
||||
|
||||
export interface SoundSetting {
|
||||
scanSuccess: boolean;
|
||||
registerSuccess: boolean;
|
||||
volume: number;
|
||||
}
|
||||
|
||||
type SoundType = "scanSuccess" | "registerSuccess";
|
||||
|
||||
const SOUND_STORAGE_KEY = "gao-customer-h5:sound";
|
||||
const soundUrlMap: Record<SoundType, string> = {
|
||||
scanSuccess: scanSuccessSoundUrl,
|
||||
registerSuccess: registrationSuccessSoundUrl
|
||||
};
|
||||
// Windows 某些设备链路对极短提示音的起音处理不稳定,直接播放会偏小。
|
||||
// 这里先补一段极低音量的预热头,把输出链路“叫醒”后再接原音效。
|
||||
const SCAN_SUCCESS_WARMUP_HEAD_MS = 120;
|
||||
const SCAN_SUCCESS_WARMUP_GAIN = .08;
|
||||
|
||||
function createDefaultSoundSetting(): SoundSetting {
|
||||
return {
|
||||
scanSuccess: true,
|
||||
registerSuccess: true,
|
||||
volume: 70
|
||||
};
|
||||
}
|
||||
|
||||
function readStoredSoundSetting(): SoundSetting {
|
||||
const rawValue = localStorage.getItem(SOUND_STORAGE_KEY);
|
||||
if (!rawValue) {
|
||||
return createDefaultSoundSetting();
|
||||
}
|
||||
|
||||
try {
|
||||
const parsedValue = JSON.parse(rawValue) as Partial<SoundSetting>;
|
||||
const volume = Number(parsedValue.volume);
|
||||
return {
|
||||
scanSuccess: parsedValue.scanSuccess !== false,
|
||||
registerSuccess: parsedValue.registerSuccess !== false,
|
||||
volume: Number.isFinite(volume) ? Math.min(100, Math.max(0, volume)) : 70
|
||||
};
|
||||
} catch {
|
||||
return createDefaultSoundSetting();
|
||||
}
|
||||
}
|
||||
|
||||
export const useSettingStore = defineStore("setting", () => {
|
||||
const sound = ref<SoundSetting>(readStoredSoundSetting());
|
||||
const soundPlayerMap = shallowRef<Record<SoundType, HTMLAudioElement> | null>(null);
|
||||
const audioContext = shallowRef<AudioContext | null>(null);
|
||||
const scanSuccessBuffer = shallowRef<AudioBuffer | null>(null);
|
||||
const activeScanSuccessSource = shallowRef<AudioBufferSourceNode | null>(null);
|
||||
const soundUnlocked = ref(false);
|
||||
|
||||
function setSound(value: Partial<SoundSetting>): void {
|
||||
sound.value = {
|
||||
scanSuccess: value.scanSuccess ?? sound.value.scanSuccess,
|
||||
registerSuccess: value.registerSuccess ?? sound.value.registerSuccess,
|
||||
volume: Math.min(100, Math.max(0, value.volume ?? sound.value.volume))
|
||||
};
|
||||
localStorage.setItem(SOUND_STORAGE_KEY, JSON.stringify(sound.value));
|
||||
applySoundVolume();
|
||||
}
|
||||
|
||||
function loadSoundPlayers(): void {
|
||||
if (soundPlayerMap.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
soundPlayerMap.value = {
|
||||
scanSuccess: createSoundPlayer(soundUrlMap.scanSuccess),
|
||||
registerSuccess: createSoundPlayer(soundUrlMap.registerSuccess)
|
||||
};
|
||||
applySoundVolume();
|
||||
}
|
||||
|
||||
function getAudioContext(): AudioContext {
|
||||
if (audioContext.value) {
|
||||
return audioContext.value;
|
||||
}
|
||||
|
||||
audioContext.value = new window.AudioContext();
|
||||
return audioContext.value;
|
||||
}
|
||||
|
||||
function createSoundPlayer(url: string): HTMLAudioElement {
|
||||
const player = new Audio(url);
|
||||
player.preload = "auto";
|
||||
player.load();
|
||||
return player;
|
||||
}
|
||||
|
||||
function applySoundVolume(): void {
|
||||
if (!soundPlayerMap.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
const volume = sound.value.volume / 100;
|
||||
Object.values(soundPlayerMap.value).forEach((player) => {
|
||||
player.volume = volume;
|
||||
});
|
||||
}
|
||||
|
||||
async function unlockSoundPlayers(): Promise<boolean> {
|
||||
loadSoundPlayers();
|
||||
if (soundUnlocked.value || !soundPlayerMap.value) {
|
||||
return soundUnlocked.value;
|
||||
}
|
||||
|
||||
try {
|
||||
await getAudioContext().resume();
|
||||
for (const player of Object.values(soundPlayerMap.value)) {
|
||||
player.muted = true;
|
||||
player.volume = 0;
|
||||
player.currentTime = 0;
|
||||
await player.play();
|
||||
player.pause();
|
||||
player.currentTime = 0;
|
||||
player.muted = false;
|
||||
}
|
||||
applySoundVolume();
|
||||
soundUnlocked.value = true;
|
||||
return true;
|
||||
} catch {
|
||||
Object.values(soundPlayerMap.value).forEach((player) => {
|
||||
player.muted = false;
|
||||
});
|
||||
applySoundVolume();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadScanSuccessBuffer(): Promise<AudioBuffer> {
|
||||
if (scanSuccessBuffer.value) {
|
||||
return scanSuccessBuffer.value;
|
||||
}
|
||||
|
||||
const response = await fetch(soundUrlMap.scanSuccess);
|
||||
const arrayBuffer = await response.arrayBuffer();
|
||||
scanSuccessBuffer.value = await getAudioContext().decodeAudioData(arrayBuffer.slice(0));
|
||||
return scanSuccessBuffer.value;
|
||||
}
|
||||
|
||||
async function playScanSuccessBufferSound(): Promise<void> {
|
||||
const context = getAudioContext();
|
||||
if (context.state === "suspended") {
|
||||
await context.resume();
|
||||
}
|
||||
|
||||
activeScanSuccessSource.value?.stop();
|
||||
activeScanSuccessSource.value?.disconnect();
|
||||
|
||||
const source = context.createBufferSource();
|
||||
const gainNode = context.createGain();
|
||||
source.buffer = await buildScanSuccessPlaybackBuffer();
|
||||
gainNode.gain.value = sound.value.volume / 100;
|
||||
source.connect(gainNode);
|
||||
gainNode.connect(context.destination);
|
||||
source.onended = () => {
|
||||
if (activeScanSuccessSource.value === source) {
|
||||
activeScanSuccessSource.value = null;
|
||||
}
|
||||
source.disconnect();
|
||||
gainNode.disconnect();
|
||||
};
|
||||
activeScanSuccessSource.value = source;
|
||||
source.start(0);
|
||||
}
|
||||
|
||||
async function buildScanSuccessPlaybackBuffer(): Promise<AudioBuffer> {
|
||||
const sourceBuffer = await loadScanSuccessBuffer();
|
||||
const context = getAudioContext();
|
||||
const warmupFrameCount = Math.max(1, Math.round(context.sampleRate * SCAN_SUCCESS_WARMUP_HEAD_MS / 1000));
|
||||
const outputBuffer = context.createBuffer(
|
||||
sourceBuffer.numberOfChannels,
|
||||
sourceBuffer.length + warmupFrameCount,
|
||||
sourceBuffer.sampleRate
|
||||
);
|
||||
|
||||
for (let channelIndex = 0; channelIndex < sourceBuffer.numberOfChannels; channelIndex += 1) {
|
||||
const outputData = outputBuffer.getChannelData(channelIndex);
|
||||
const sourceData = sourceBuffer.getChannelData(channelIndex);
|
||||
const warmupSourceLength = Math.min(sourceData.length, warmupFrameCount);
|
||||
// 预热头不能是纯静音;纯静音对这类 Windows 问题无效。
|
||||
for (let frameIndex = 0; frameIndex < warmupFrameCount; frameIndex += 1) {
|
||||
const sourceIndex = frameIndex % warmupSourceLength;
|
||||
outputData[frameIndex] = sourceData[sourceIndex] * SCAN_SUCCESS_WARMUP_GAIN;
|
||||
}
|
||||
outputData.set(sourceData, warmupFrameCount);
|
||||
}
|
||||
|
||||
return outputBuffer;
|
||||
}
|
||||
|
||||
async function playSound(type: SoundType): Promise<void> {
|
||||
if (type === "scanSuccess") {
|
||||
await playScanSuccessBufferSound();
|
||||
return;
|
||||
}
|
||||
|
||||
loadSoundPlayers();
|
||||
if (!sound.value[type] || !soundPlayerMap.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
const player = soundPlayerMap.value[type];
|
||||
player.pause();
|
||||
player.muted = false;
|
||||
player.volume = sound.value.volume / 100;
|
||||
player.currentTime = 0;
|
||||
await player.play();
|
||||
}
|
||||
|
||||
async function playScanSuccessSound(): Promise<void> {
|
||||
await playSound("scanSuccess");
|
||||
}
|
||||
|
||||
async function playRegisterSuccessSound(): Promise<void> {
|
||||
await playSound("registerSuccess");
|
||||
}
|
||||
|
||||
return {
|
||||
sound,
|
||||
setSound,
|
||||
loadSoundPlayers,
|
||||
unlockSoundPlayers,
|
||||
playScanSuccessSound,
|
||||
playRegisterSuccessSound
|
||||
};
|
||||
});
|
||||
Reference in New Issue
Block a user