File size: 5,425 Bytes
5f663a9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
import { useCallback, useEffect, useRef, useState } from 'react';

// Helper Types
type UseAudioResponse = {
  playback: (base64Data?: string, isLastTrackOfPlaylist?: boolean) => Promise<boolean>;
  progress: number;
  isLoaded: boolean;
  isPlaying: boolean;
  isSwitchingTracks: boolean; // when audio is temporary cut (but it's not a real pause)
  togglePause: () => void;
};

export function useAudio(): UseAudioResponse {
  const audioContextRef = useRef<AudioContext | null>(null);
  const sourceNodeRef = useRef<AudioBufferSourceNode | null>(null);
  const [progress, setProgress] = useState(0.0);
  const [isPlaying, setIsPlaying] = useState(false);
  const [isLoaded, setIsLoaded] = useState(false);
  const [isSwitchingTracks, setSwitchingTracks] = useState(false);
  const startTimeRef = useRef(0);
  const pauseTimeRef = useRef(0);
  
  const stopAudio = useCallback(() => {
    try {
      audioContextRef.current?.close();
    } catch (err) {
      // already closed probably
    }
    setSwitchingTracks(false);

    sourceNodeRef.current = null;
    sourceNodeRef.current = null;

    // setProgress(0); // Reset progress
  }, []);

  // Helper function to handle conversion from Base64 to an ArrayBuffer
  async function base64ToArrayBuffer(base64: string): Promise<ArrayBuffer> {
    const response = await fetch(base64);
    return response.arrayBuffer();
  }

  const playback = useCallback(
    async (base64Data?: string, isLastTrackOfPlaylist?: boolean): Promise<boolean> => {
      stopAudio(); // Stop any playing audio first

      // If no base64 data provided, we don't attempt to play any audio
      if (!base64Data) {
        return false;
      }

      // Initialize AudioContext
      const audioContext = new AudioContext();
      audioContextRef.current = audioContext;

      // Format Base64 string if necessary and get ArrayBuffer
      const formattedBase64 =
        base64Data.startsWith('data:audio/wav') || base64Data.startsWith('data:audio/wav;base64,')
          ? base64Data
          : `data:audio/wav;base64,${base64Data}`;

      console.log(`formattedBase64: ${formattedBase64.slice(0, 50)} (len: ${formattedBase64.length})`);

      const arrayBuffer = await base64ToArrayBuffer(formattedBase64);

      return new Promise((resolve, reject) => {
        // Decode the audio data and play
        audioContext.decodeAudioData(arrayBuffer, (audioBuffer) => {
          // Create a source node and gain node
          const source = audioContext.createBufferSource();
          const gainNode = audioContext.createGain();
          
          // Set buffer and gain
          source.buffer = audioBuffer;
          gainNode.gain.value = 1.0; 

          // Connect nodes
          source.connect(gainNode);
          gainNode.connect(audioContext.destination);

          // Assign source node to ref for progress tracking
          sourceNodeRef.current = source;
          source.start(0, pauseTimeRef.current % audioBuffer.duration); // Start at the correct offset if paused previously
          startTimeRef.current = audioContextRef.current!.currentTime - pauseTimeRef.current;
          
          setSwitchingTracks(false);
          setProgress(0);
          setIsLoaded(true);
          setIsPlaying(true);

          // Set up progress interval
          const totalDuration = audioBuffer.duration;
          const updateProgressInterval = setInterval(() => {
            if (sourceNodeRef.current && audioContextRef.current) {
              const currentTime = audioContextRef.current.currentTime;
              const currentProgress = currentTime / totalDuration;
              setProgress(currentProgress);
              if (currentProgress >= 1.0) {
                clearInterval(updateProgressInterval);
              }
            }
          }, 50); // Update every 50ms
          
          if (source) {
            source.onended = () => {
              // used to indicate a temporary stop, while we switch tracks
              if (!isLastTrackOfPlaylist) {
                setSwitchingTracks(true);
              }
              setIsPlaying(false);
              clearInterval(updateProgressInterval);
              stopAudio();
              resolve(true);
            };
          }
        }, (error) => {
          console.error('Error decoding audio data:', error);
          reject(error);
        });
      })
    },
    [stopAudio]
  );

  const togglePause = useCallback(() => {
    if (!audioContextRef.current || !sourceNodeRef.current) {
      return; // Do nothing if audio is not initialized
    }

    if (isPlaying) {
      // Pause the audio
      pauseTimeRef.current += audioContextRef.current.currentTime - startTimeRef.current;
      sourceNodeRef.current.stop(); // This effectively "pauses" the audio, but it also means the sourceNode will be unusable
      sourceNodeRef.current = null; // As the node is now unusable, we nullify it
      setIsPlaying(false);
    } else {
      // Resume playing
      audioContextRef.current.resume().then(() => {
        playback(); // This will pick up where we left off due to pauseTimeRef
      });
    }
  }, [audioContextRef, sourceNodeRef, isPlaying, playback]);

  // Effect to handle cleanup on component unmount
  useEffect(() => {
    return () => {
      stopAudio();
    };
  }, [stopAudio]);

  return { playback, isPlaying, isSwitchingTracks, isLoaded, progress, togglePause };
}