| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153 |
- 'use client';
- import { useEffect, useRef, useCallback, useState } from 'react';
- /**
- * bitFlyer Lightning 스타일 효과음
- * sound-parlor.mp3 오디오 스프라이트 사용
- *
- * 스프라이트 구간 [startMs, durationMs]:
- * electricalMagicPoof [0, 761] → 호가 제거
- * dropHeavyPlastic [761, 574] → 호가 추가
- * MagicDing [1330, 1221] → 소형 매도 체결
- * DescendingMagic [2550, 1183] → 대형 매도 체결
- * coinGameWinPack1 [3740, 1140] → 소형 매수 체결
- * coinGameWinPack2 [4882, 1017] → 대형 매수 체결
- */
- // [startMs, durationMs]
- const SPRITES = {
- orderbookRemove: [0, 761],
- orderbookAdd: [761, 574],
- sellSmall: [1330, 1221],
- sellBig: [2550, 1183],
- buySmall: [3740, 1140],
- buyBig: [4882, 1017],
- } as const;
- // 대형 체결 기준 (이 수량 이상이면 Big 사운드)
- const BIG_TRADE_THRESHOLD = 1;
- // 호가 사운드 쓰로틀 간격 (ms) — 짧게 설정하여 bitFlyer처럼 연속 재생
- const OB_THROTTLE_ADD = 60;
- const OB_THROTTLE_REMOVE = 150;
- export default function useTradeSound() {
- const audioCtxRef = useRef<AudioContext | null>(null);
- const audioBufferRef = useRef<AudioBuffer | null>(null);
- const [muted, setMuted] = useState(true);
- const loadedRef = useRef(false);
- const loadingRef = useRef(false);
- const obThrottleRef = useRef<{ add: number; remove: number }>({ add: 0, remove: 0 });
- // AudioContext 초기화
- const ensureContext = useCallback(() => {
- if (!audioCtxRef.current) {
- audioCtxRef.current = new AudioContext();
- }
- if (audioCtxRef.current.state === 'suspended') {
- audioCtxRef.current.resume();
- }
- return audioCtxRef.current;
- }, []);
- // MP3 스프라이트 파일 로드
- const loadSound = useCallback(async () => {
- if (loadedRef.current || loadingRef.current) return;
- loadingRef.current = true;
- try {
- const ctx = ensureContext();
- const response = await fetch('/sounds/sound-parlor.mp3');
- const arrayBuffer = await response.arrayBuffer();
- const decoded = await ctx.decodeAudioData(arrayBuffer);
- audioBufferRef.current = decoded;
- loadedRef.current = true;
- } catch (err) {
- console.error('Failed to load trade sound:', err);
- } finally {
- loadingRef.current = false;
- }
- }, [ensureContext]);
- // 스프라이트 구간 재생
- const playSprite = useCallback((startMs: number, durationMs: number, volume: number) => {
- if (!audioBufferRef.current || !audioCtxRef.current) return;
- const ctx = audioCtxRef.current;
- if (ctx.state === 'suspended') {
- ctx.resume();
- }
- const source = ctx.createBufferSource();
- const gainNode = ctx.createGain();
- source.buffer = audioBufferRef.current;
- gainNode.gain.value = Math.min(Math.max(volume, 0.1), 1.0);
- source.connect(gainNode);
- gainNode.connect(ctx.destination);
- const startSec = startMs / 1000;
- const durationSec = durationMs / 1000;
- source.start(0, startSec, durationSec);
- }, []);
- // 체결 사운드 재생
- const playTradeSound = useCallback((side: 'BID' | 'ASK', tradeVolume: number = 0.5) => {
- if (muted || !loadedRef.current) return;
- const isBig = tradeVolume >= BIG_TRADE_THRESHOLD;
- let sprite: readonly [number, number];
- if (side === 'BID') {
- sprite = isBig ? SPRITES.buyBig : SPRITES.buySmall;
- } else {
- sprite = isBig ? SPRITES.sellBig : SPRITES.sellSmall;
- }
- // 거래량 기반 볼륨 (0.3 ~ 0.8)
- const vol = Math.min(0.3 + tradeVolume * 0.3, 0.8);
- playSprite(sprite[0], sprite[1], vol);
- }, [muted, playSprite]);
- // 호가 사운드 재생 (짧은 쓰로틀 → bitFlyer 스타일 연속 딸깍)
- const playOrderbookSound = useCallback((type: 'add' | 'remove') => {
- if (muted || !loadedRef.current) return;
- const now = Date.now();
- const throttle = type === 'add' ? OB_THROTTLE_ADD : OB_THROTTLE_REMOVE;
- if (now - obThrottleRef.current[type] < throttle) return;
- obThrottleRef.current[type] = now;
- // electricalMagicPoof (remove) 는 일단 비활성화
- if (type === 'remove') return;
- const sprite = SPRITES.orderbookAdd;
- playSprite(sprite[0], sprite[1], 0.15);
- }, [muted, playSprite]);
- // 토글 함수
- const toggleMute = useCallback(() => {
- setMuted((prev) => {
- if (prev) {
- // 음소거 해제 시 사운드 로드
- ensureContext();
- loadSound();
- }
- return !prev;
- });
- }, [ensureContext, loadSound]);
- // cleanup
- useEffect(() => {
- return () => {
- if (audioCtxRef.current) {
- audioCtxRef.current.close();
- audioCtxRef.current = null;
- }
- };
- }, []);
- return { playTradeSound, playOrderbookSound, muted, toggleMute };
- }
|