|
1 | | -import { BoxRenderable, MouseButton, MouseEvent, RGBA, TextAttributes } from "@opentui/core" |
| 1 | +import { |
| 2 | + Audio, |
| 3 | + BoxRenderable, |
| 4 | + MouseButton, |
| 5 | + MouseEvent, |
| 6 | + RGBA, |
| 7 | + TextAttributes, |
| 8 | + type AudioSound, |
| 9 | + type AudioVoice, |
| 10 | +} from "@opentui/core" |
2 | 11 | import { useRenderer } from "@opentui/solid" |
3 | 12 | import { For, createMemo, createSignal, onCleanup, onMount, type JSX } from "solid-js" |
4 | 13 | import { useTheme, tint } from "@tui/context/theme" |
5 | | -import * as Sound from "@tui/util/sound" |
6 | 14 | import { go, logo } from "@/cli/logo" |
| 15 | +import pulseA from "../asset/pulse-a.wav" with { type: "file" } |
| 16 | +import pulseB from "../asset/pulse-b.wav" with { type: "file" } |
| 17 | +import pulseC from "../asset/pulse-c.wav" with { type: "file" } |
| 18 | +import charge from "../asset/charge.wav" with { type: "file" } |
7 | 19 |
|
8 | 20 | export type LogoShape = { |
9 | 21 | left: string[] |
@@ -88,6 +100,94 @@ const TAIL = 1.8 |
88 | 100 | const TRACE_IN = 200 |
89 | 101 | const GLOW_OUT = 1600 |
90 | 102 | const PEAK = RGBA.fromInts(255, 255, 255) |
| 103 | +const PULSE_SOUNDS = [pulseA, pulseB, pulseC] |
| 104 | + |
| 105 | +let logoAudio: Audio | undefined |
| 106 | +let logoAudioSounds: Promise<{ charge: AudioSound | null; pulse: (AudioSound | null)[] }> | undefined |
| 107 | +let logoAudioVoice: AudioVoice | undefined |
| 108 | +let logoAudioTail: ReturnType<typeof setTimeout> | undefined |
| 109 | +let logoAudioSeq = 0 |
| 110 | +let logoAudioShot = 0 |
| 111 | + |
| 112 | +function createLogoAudio() { |
| 113 | + if (logoAudio) return logoAudio |
| 114 | + try { |
| 115 | + logoAudio = Audio.create({ autoStart: false }) |
| 116 | + logoAudio.on("error", () => undefined) |
| 117 | + return logoAudio |
| 118 | + } catch { |
| 119 | + return |
| 120 | + } |
| 121 | +} |
| 122 | + |
| 123 | +function loadLogoSounds(audio: Audio) { |
| 124 | + logoAudioSounds ??= Promise.all([ |
| 125 | + audio.loadSoundFile(charge).catch(() => null), |
| 126 | + Promise.all(PULSE_SOUNDS.map((file) => audio.loadSoundFile(file).catch(() => null))), |
| 127 | + ]).then(([charge, pulse]) => ({ charge, pulse })) |
| 128 | + return logoAudioSounds |
| 129 | +} |
| 130 | + |
| 131 | +function startLogoSound() { |
| 132 | + stopLogoSound() |
| 133 | + const id = ++logoAudioSeq |
| 134 | + const audio = createLogoAudio() |
| 135 | + if (!audio || (!audio.isStarted() && !audio.start())) return |
| 136 | + void loadLogoSounds(audio) |
| 137 | + .then((sounds) => { |
| 138 | + if (id !== logoAudioSeq || sounds.charge == null) return |
| 139 | + const voice = audio.play(sounds.charge, { volume: 0.24, loop: true }) |
| 140 | + if (voice == null) return |
| 141 | + logoAudioVoice = voice |
| 142 | + }) |
| 143 | + .catch(() => undefined) |
| 144 | +} |
| 145 | + |
| 146 | +function clearLogoSoundTail() { |
| 147 | + if (!logoAudioTail) return |
| 148 | + clearTimeout(logoAudioTail) |
| 149 | + logoAudioTail = undefined |
| 150 | +} |
| 151 | + |
| 152 | +function stopLogoSound(delay = 0) { |
| 153 | + logoAudioSeq++ |
| 154 | + clearLogoSoundTail() |
| 155 | + if (logoAudioVoice === undefined) return |
| 156 | + const voice = logoAudioVoice |
| 157 | + if (delay <= 0) { |
| 158 | + logoAudioVoice = undefined |
| 159 | + logoAudio?.stopVoice(voice) |
| 160 | + return |
| 161 | + } |
| 162 | + logoAudioTail = setTimeout(() => { |
| 163 | + logoAudioTail = undefined |
| 164 | + if (logoAudioVoice !== voice) return |
| 165 | + logoAudioVoice = undefined |
| 166 | + logoAudio?.stopVoice(voice) |
| 167 | + }, delay) |
| 168 | +} |
| 169 | + |
| 170 | +function pulseLogoSound(scale = 1) { |
| 171 | + stopLogoSound(140) |
| 172 | + const id = logoAudioSeq |
| 173 | + const audio = createLogoAudio() |
| 174 | + if (!audio || (!audio.isStarted() && !audio.start())) return |
| 175 | + void loadLogoSounds(audio) |
| 176 | + .then((sounds) => { |
| 177 | + if (id !== logoAudioSeq) return |
| 178 | + const sound = sounds.pulse[logoAudioShot++ % PULSE_SOUNDS.length] |
| 179 | + if (sound == null) return |
| 180 | + audio.play(sound, { volume: 0.26 + 0.14 * scale }) |
| 181 | + }) |
| 182 | + .catch(() => undefined) |
| 183 | +} |
| 184 | + |
| 185 | +function disposeLogoSound() { |
| 186 | + stopLogoSound() |
| 187 | + logoAudio?.dispose() |
| 188 | + logoAudio = undefined |
| 189 | + logoAudioSounds = undefined |
| 190 | +} |
91 | 191 |
|
92 | 192 | type Ring = { |
93 | 193 | x: number |
@@ -577,7 +677,7 @@ export function Logo(props: { shape?: LogoShape; ink?: RGBA; idle?: boolean } = |
577 | 677 | const item = hold() |
578 | 678 | if (item && !hum && t - item.at >= HOLD) { |
579 | 679 | hum = true |
580 | | - Sound.start() |
| 680 | + startLogoSound() |
581 | 681 | } |
582 | 682 | if (item && t - item.at >= CHARGE) { |
583 | 683 | burst(item.x, item.y) |
@@ -606,7 +706,7 @@ export function Logo(props: { shape?: LogoShape; ink?: RGBA; idle?: boolean } = |
606 | 706 | onCleanup(() => { |
607 | 707 | stop() |
608 | 708 | hum = false |
609 | | - Sound.dispose() |
| 709 | + disposeLogoSound() |
610 | 710 | }) |
611 | 711 |
|
612 | 712 | onMount(() => { |
@@ -655,7 +755,7 @@ export function Logo(props: { shape?: LogoShape; ink?: RGBA; idle?: boolean } = |
655 | 755 | ]) |
656 | 756 | setNow(t) |
657 | 757 | start() |
658 | | - Sound.pulse(lerp(0.8, 1, level)) |
| 758 | + pulseLogoSound(lerp(0.8, 1, level)) |
659 | 759 | } |
660 | 760 |
|
661 | 761 | const frame = createMemo(() => { |
|
0 commit comments