11import { convertBase64ToBlob } from 'hume' ;
2- import { useCallback , useRef , useState } from 'react' ;
2+ import { useCallback , useEffect , useRef , useState } from 'react' ;
33
44import { convertLinearFrequenciesToBark } from './convertFrequencyScale' ;
55import { generateEmptyFft } from './generateEmptyFft' ;
66import type { AudioOutputMessage } from '../models/messages' ;
77
8+ const FADE_DURATION = 0.1 ;
9+ const FADE_TARGET = 0.0001 ;
10+
811export const useSoundPlayer = ( props : {
912 onError : ( message : string ) => void ;
1013 onPlayAudio : ( id : string ) => void ;
1114 onStopAudio : ( id : string ) => void ;
1215} ) => {
1316 const [ isPlaying , setIsPlaying ] = useState ( false ) ;
1417 const [ isAudioMuted , setIsAudioMuted ] = useState ( false ) ;
18+ const isFadeCancelled = useRef ( false ) ;
1519 const [ volume , setVolumeState ] = useState < number > ( 1.0 ) ;
1620 const [ fft , setFft ] = useState < number [ ] > ( generateEmptyFft ( ) ) ;
1721
@@ -87,46 +91,65 @@ export const useSoundPlayer = (props: {
8791 }
8892 } , [ ] ) ;
8993
90- const addToQueue = useCallback ( async ( message : AudioOutputMessage ) => {
91- if ( ! isInitialized . current || ! audioContext . current ) {
92- onError . current ( 'Audio player has not been initialized' ) ;
93- return ;
94- }
95-
96- try {
97- const blob = convertBase64ToBlob ( message . data ) ;
98- const arrayBuffer = await blob . arrayBuffer ( ) ;
99- const audioBuffer =
100- await audioContext . current . decodeAudioData ( arrayBuffer ) ;
101-
102- const pcmData = audioBuffer . getChannelData ( 0 ) ;
103-
104- if ( gainNode . current ) {
105- const now = audioContext . current . currentTime ;
106- gainNode . current . gain . cancelScheduledValues ( now ) ;
107- const targetGain = isAudioMuted ? 0 : volume ;
108- gainNode . current . gain . setValueAtTime ( targetGain , now ) ;
94+ const addToQueue = useCallback (
95+ async ( message : AudioOutputMessage ) => {
96+ if ( ! isInitialized . current || ! audioContext . current ) {
97+ onError . current ( 'Audio player has not been initialized' ) ;
98+ return ;
10999 }
110100
111- workletNode . current ?. port . postMessage ( { type : 'audio' , data : pcmData } ) ;
101+ try {
102+ const blob = convertBase64ToBlob ( message . data ) ;
103+ const arrayBuffer = await blob . arrayBuffer ( ) ;
104+ const audioBuffer =
105+ await audioContext . current . decodeAudioData ( arrayBuffer ) ;
112106
113- setIsPlaying ( true ) ;
114- onPlayAudio . current ( message . id ) ;
115- } catch ( e ) {
116- const eMessage = e instanceof Error ? e . message : 'Unknown error' ;
117- onError . current ( `Failed to add clip to queue: ${ eMessage } ` ) ;
118- }
119- } , [ ] ) ;
107+ const pcmData = audioBuffer . getChannelData ( 0 ) ;
108+
109+ if ( gainNode . current ) {
110+ const now = audioContext . current . currentTime ;
111+ gainNode . current . gain . cancelScheduledValues ( now ) ;
112+ const targetGain = isAudioMuted ? 0 : volume ;
113+ gainNode . current . gain . setValueAtTime ( targetGain , now ) ;
114+ }
115+
116+ workletNode . current ?. port . postMessage ( { type : 'audio' , data : pcmData } ) ;
120117
121- const fadeOutAndPostStopMessage = async ( type : 'end' | 'clear' ) => {
122- const FADE_DURATION = 0.1 ;
118+ setIsPlaying ( true ) ;
119+ onPlayAudio . current ( message . id ) ;
120+ } catch ( e ) {
121+ const eMessage = e instanceof Error ? e . message : 'Unknown error' ;
122+ onError . current ( `Failed to add clip to queue: ${ eMessage } ` ) ;
123+ }
124+ } ,
125+ [ isAudioMuted , volume ] ,
126+ ) ;
127+
128+ const waitForAudioTime = (
129+ targetTime : number ,
130+ ctx : AudioContext ,
131+ isCancelled : ( ) => boolean ,
132+ ) : Promise < void > =>
133+ new Promise ( ( resolve ) => {
134+ const check = ( ) => {
135+ if ( isCancelled ( ) ) return ;
136+
137+ if ( ctx . currentTime >= targetTime ) {
138+ resolve ( ) ;
139+ } else {
140+ requestAnimationFrame ( check ) ;
141+ }
142+ } ;
143+ check ( ) ;
144+ } ) ;
145+
146+ const fadeOutAndPostMessage = useCallback ( async ( type : 'end' | 'clear' ) => {
123147 if ( ! gainNode . current || ! audioContext . current ) {
124148 workletNode . current ?. port . postMessage ( { type } ) ;
125149 return ;
126150 }
127151
128152 const now = audioContext . current . currentTime ;
129- const FADE_TARGET = 0.0001 ;
130153
131154 gainNode . current . gain . cancelScheduledValues ( now ) ;
132155 gainNode . current . gain . setValueAtTime ( gainNode . current . gain . value , now ) ;
@@ -135,12 +158,26 @@ export const useSoundPlayer = (props: {
135158 now + FADE_DURATION ,
136159 ) ;
137160
138- await new Promise ( ( resolve ) => setTimeout ( resolve , FADE_DURATION * 1000 ) ) ;
161+ isFadeCancelled . current = false ;
162+ await waitForAudioTime (
163+ now + FADE_DURATION ,
164+ audioContext . current ,
165+ ( ) => isFadeCancelled . current ,
166+ ) ;
139167
140168 workletNode . current ?. port . postMessage ( { type } ) ;
141169
142- gainNode . current . gain . setValueAtTime ( 1.0 , audioContext . current . currentTime ) ;
143- } ;
170+ gainNode . current ?. gain . setValueAtTime (
171+ 1.0 ,
172+ audioContext . current ?. currentTime || 0 ,
173+ ) ;
174+ } , [ ] ) ;
175+
176+ useEffect ( ( ) => {
177+ return ( ) => {
178+ isFadeCancelled . current = true ;
179+ } ;
180+ } , [ ] ) ;
144181
145182 const stopAll = useCallback ( async ( ) => {
146183 isInitialized . current = false ;
@@ -153,7 +190,7 @@ export const useSoundPlayer = (props: {
153190 window . clearInterval ( frequencyDataIntervalId . current ) ;
154191 }
155192
156- await fadeOutAndPostStopMessage ( 'end' ) ;
193+ await fadeOutAndPostMessage ( 'end' ) ;
157194
158195 if ( analyserNode . current ) {
159196 analyserNode . current . disconnect ( ) ;
@@ -181,14 +218,14 @@ export const useSoundPlayer = (props: {
181218 }
182219
183220 setFft ( generateEmptyFft ( ) ) ;
184- } , [ ] ) ;
221+ } , [ fadeOutAndPostMessage ] ) ;
185222
186223 const clearQueue = useCallback ( ( ) => {
187- void fadeOutAndPostStopMessage ( 'clear' ) ;
224+ void fadeOutAndPostMessage ( 'clear' ) ;
188225 isProcessing . current = false ;
189226 setIsPlaying ( false ) ;
190227 setFft ( generateEmptyFft ( ) ) ;
191- } , [ ] ) ;
228+ } , [ fadeOutAndPostMessage ] ) ;
192229
193230 const setVolume = useCallback (
194231 ( newLevel : number ) => {
0 commit comments