forked from rhulha/StreamingKokoroJS
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathAudioDiskSaver.js
More file actions
183 lines (159 loc) · 5.25 KB
/
Copy pathAudioDiskSaver.js
File metadata and controls
183 lines (159 loc) · 5.25 KB
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
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
const SAMPLE_RATE = 24000;
export class AudioDiskSaver {
constructor() {
this.audioContext = new AudioContext();
this.fileStream = null;
this.totalAudioChunks = 0;
this.processedAudioChunks = 0;
this.headerWritten = false;
this.bytesWritten = 0;
// For WAV header updates
this.fileSize = 0;
this.dataSize = 0;
}
async initSave() {
try {
const fileHandle = await window.showSaveFilePicker({
suggestedName: "audio_stream.wav",
types: [
{
description: "Audio Files",
accept: { "audio/wav": [".wav"] },
},
],
});
this.fileStream = await fileHandle.createWritable();
// Write placeholder WAV header
await this.writeWavHeader(); // Writes a standard header
this.headerWritten = true;
} catch (error) {
console.error("Error initializing file save:", error);
throw error;
}
}
setTotalChunks(totalChunks) {
this.totalAudioChunks = totalChunks;
this.processedAudioChunks = 0;
}
async addAudioChunk(audioData) {
try {
if (!this.fileStream) {
throw new Error("File stream not initialized");
}
await this.fileStream.write(audioData);
this.dataSize += audioData.byteLength;
this.processedAudioChunks++;
return Math.min((this.processedAudioChunks / this.totalAudioChunks) * 100, 99);
} catch (error) {
console.error("Error processing audio chunk:", error);
throw error;
}
}
async finalizeSave() {
if (!this.fileStream) {
throw new Error("No file stream available");
}
try {
// Update the WAV header with final sizes
await this.updateWavHeader();
await this.fileStream.close();
this.reset();
return true;
} catch (error) {
console.error("Error finalizing audio save:", error);
if (this.fileStream) {
await this.fileStream.close();
}
this.reset();
throw error;
}
}
async stopSave() {
if (!this.fileStream) {
console.log("No active file stream to stop");
return;
}
try {
// Give the worker a moment to process the stop message
await new Promise(resolve => setTimeout(resolve, 100));
// Update the WAV header before closing to ensure a valid WAV file
await this.updateWavHeader();
await this.fileStream.close();
console.log("Disk save operation stopped");
this.reset();
return true;
} catch (error) {
console.error("Error stopping disk save:", error);
if (this.fileStream) {
try {
await this.fileStream.close();
} catch (closeError) {
console.error("Error closing file stream:", closeError);
}
}
this.reset();
return false;
}
}
reset() {
this.fileStream = null;
this.processedAudioChunks = 0;
this.headerWritten = false;
this.bytesWritten = 0;
this.fileSize = 0;
this.dataSize = 0;
}
getProgress() {
return Math.min((this.processedAudioChunks / this.totalAudioChunks) * 100, 99);
}
// Write WAV header at the start
async writeWavHeader() {
const headerBuffer = new ArrayBuffer(44);
const view = new DataView(headerBuffer);
const bitsPerSample = 32; // This file saves as 32-bit float
const numChannels = 1;
const blockAlign = (numChannels * bitsPerSample) / 8;
const byteRate = SAMPLE_RATE * blockAlign;
function writeString(view, offset, string) {
for (let i = 0; i < string.length; i++) {
view.setUint8(offset + i, string.charCodeAt(i));
}
}
// "RIFF" chunk descriptor
writeString(view, 0, 'RIFF');
// Placeholder for file size (updated later)
view.setUint32(4, 0, true);
// "WAVE" format
writeString(view, 8, 'WAVE');
// "fmt " subchunk
writeString(view, 12, 'fmt ');
view.setUint32(16, 16, true); // Subchunk1Size (16 for PCM)
view.setUint16(20, 3, true); // AudioFormat (3 for float)
view.setUint16(22, numChannels, true); // NumChannels (1 for mono)
view.setUint32(24, SAMPLE_RATE, true); // SampleRate (STANDARD RATE)
view.setUint32(28, byteRate, true); // ByteRate (STANDARD RATE)
view.setUint16(32, blockAlign, true); // BlockAlign
view.setUint16(34, bitsPerSample, true); // BitsPerSample (32 for float)
// "data" subchunk
writeString(view, 36, 'data');
// Placeholder for data size (updated later)
view.setUint32(40, 0, true);
await this.fileStream.write(headerBuffer);
this.bytesWritten = 44; // Header size
}
// Update the WAV header with final sizes
async updateWavHeader() {
// File size = header (44) + data size - 8 bytes for RIFF/size
this.fileSize = this.dataSize + 36;
// Seek to file size position (offset 4) and update
await this.fileStream.seek(4);
const fileSizeBuffer = new ArrayBuffer(4);
new DataView(fileSizeBuffer).setUint32(0, this.fileSize, true);
await this.fileStream.write(fileSizeBuffer);
// Seek to data size position (offset 40) and update
await this.fileStream.seek(40);
const dataSizeBuffer = new ArrayBuffer(4);
new DataView(dataSizeBuffer).setUint32(0, this.dataSize, true);
await this.fileStream.write(dataSizeBuffer);
}
}