sound.ts 2.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778
  1. let audioCtx: AudioContext | null = null;
  2. let unlocked = false;
  3. function getAudioContext(): AudioContext | null {
  4. if (typeof window === "undefined") return null;
  5. if (audioCtx) return audioCtx;
  6. const Ctx = window.AudioContext || (window as unknown as { webkitAudioContext?: typeof AudioContext }).webkitAudioContext;
  7. if (!Ctx) return null;
  8. audioCtx = new Ctx();
  9. return audioCtx;
  10. }
  11. /** 尝试解锁音频播放(需用户手势触发更稳定)。 */
  12. export async function unlockSound() {
  13. const ctx = getAudioContext();
  14. if (!ctx) return;
  15. try {
  16. if (ctx.state === "suspended") {
  17. await ctx.resume();
  18. }
  19. // 轻触发一次极低音量,避免某些浏览器仍阻止后续播放
  20. const osc = ctx.createOscillator();
  21. const gain = ctx.createGain();
  22. gain.gain.value = 0.00001;
  23. osc.type = "sine";
  24. osc.frequency.value = 440;
  25. osc.connect(gain);
  26. gain.connect(ctx.destination);
  27. osc.start();
  28. osc.stop(ctx.currentTime + 0.01);
  29. unlocked = true;
  30. } catch {
  31. // ignore
  32. }
  33. }
  34. function playTone(opts: { frequency: number; durationMs: number; volume: number; type?: OscillatorType; whenMs?: number }) {
  35. const ctx = getAudioContext();
  36. if (!ctx) return;
  37. // 未解锁时也尝试播放;如果被阻止,保持静默(不抛错)
  38. const startAt = ctx.currentTime + (opts.whenMs ?? 0) / 1000;
  39. const endAt = startAt + opts.durationMs / 1000;
  40. const osc = ctx.createOscillator();
  41. const gain = ctx.createGain();
  42. osc.type = opts.type ?? "sine";
  43. osc.frequency.setValueAtTime(opts.frequency, startAt);
  44. // 快速起音 + 平滑衰减(避免“哔”得刺耳)
  45. gain.gain.setValueAtTime(0.00001, startAt);
  46. gain.gain.linearRampToValueAtTime(opts.volume, startAt + 0.01);
  47. gain.gain.exponentialRampToValueAtTime(0.00001, endAt);
  48. osc.connect(gain);
  49. gain.connect(ctx.destination);
  50. try {
  51. osc.start(startAt);
  52. osc.stop(endAt);
  53. } catch {
  54. // ignore
  55. }
  56. }
  57. /** 新消息提示音(蜂鸣:两段短音)。 */
  58. export function playNotificationSound() {
  59. // 默认音量尽量克制;用户可通过系统音量调节
  60. const volume = 0.08;
  61. playTone({ frequency: 880, durationMs: 70, volume, type: "sine" });
  62. playTone({ frequency: 660, durationMs: 90, volume: volume * 0.9, type: "sine", whenMs: 90 });
  63. }
  64. /** 轻提示音(更柔和,用于非关键提示)。 */
  65. export function playMessageSound() {
  66. const volume = 0.05;
  67. playTone({ frequency: 520, durationMs: 80, volume, type: "triangle" });
  68. }