This repository was archived by the owner on Nov 16, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 150
/
Copy pathFileAudioSource.ts
190 lines (157 loc) · 7.32 KB
/
FileAudioSource.ts
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
184
185
186
187
188
189
190
import {
AudioSourceErrorEvent,
AudioSourceEvent,
AudioSourceInitializingEvent,
AudioSourceOffEvent,
AudioSourceReadyEvent,
AudioStreamNodeAttachedEvent,
AudioStreamNodeAttachingEvent,
AudioStreamNodeDetachedEvent,
AudioStreamNodeErrorEvent,
CreateNoDashGuid,
Events,
EventSource,
IAudioSource,
IAudioStreamNode,
IStringDictionary,
PlatformEvent,
Promise,
PromiseHelper,
Stream,
StreamReader,
} from "../common/Exports";
import { Timer } from "../common.browser/Exports";
export class FileAudioSource implements IAudioSource {
// Recommended sample rate (bytes/second).
private static readonly SAMPLE_RATE: number = 16000 * 2; // 16 kHz * 16 bits
// We should stream audio at no faster than 2x real-time (i.e., send five chunks
// per second, with the chunk size == sample rate in bytes per second * 2 / 5).
private static readonly CHUNK_SIZE: number = FileAudioSource.SAMPLE_RATE * 2 / 5;
private static readonly UPLOAD_INTERVAL: number = 200; // milliseconds
// 10 seconds of audio in bytes =
// sample rate (bytes/second) * 600 (seconds) + 44 (size of the wave header).
private static readonly MAX_SIZE: number = FileAudioSource.SAMPLE_RATE * 600 + 44;
private streams: IStringDictionary<Stream<ArrayBuffer>> = {};
private id: string;
private events: EventSource<AudioSourceEvent>;
private file: File;
public constructor(file: File, audioSourceId?: string) {
this.id = audioSourceId ? audioSourceId : CreateNoDashGuid();
this.events = new EventSource<AudioSourceEvent>();
this.file = file;
}
public TurnOn = (): Promise<boolean> => {
if (typeof FileReader === "undefined") {
const errorMsg = "Browser does not support FileReader.";
this.OnEvent(new AudioSourceErrorEvent(errorMsg, "")); // initialization error - no streamid at this point
return PromiseHelper.FromError<boolean>(errorMsg);
} else if (this.file.name.lastIndexOf(".wav") !== this.file.name.length - 4) {
const errorMsg = this.file.name + " is not supported. Only WAVE files are allowed at the moment.";
this.OnEvent(new AudioSourceErrorEvent(errorMsg, ""));
return PromiseHelper.FromError<boolean>(errorMsg);
} else if (this.file.size > FileAudioSource.MAX_SIZE) {
const errorMsg = this.file.name + " exceeds the maximum allowed file size (" + FileAudioSource.MAX_SIZE + ").";
this.OnEvent(new AudioSourceErrorEvent(errorMsg, ""));
return PromiseHelper.FromError<boolean>(errorMsg);
}
this.OnEvent(new AudioSourceInitializingEvent(this.id)); // no stream id
this.OnEvent(new AudioSourceReadyEvent(this.id));
return PromiseHelper.FromResult(true);
}
public Id = (): string => {
return this.id;
}
public Attach = (audioNodeId: string): Promise<IAudioStreamNode> => {
this.OnEvent(new AudioStreamNodeAttachingEvent(this.id, audioNodeId));
return this.Upload(audioNodeId).OnSuccessContinueWith<IAudioStreamNode>(
(streamReader: StreamReader<ArrayBuffer>) => {
this.OnEvent(new AudioStreamNodeAttachedEvent(this.id, audioNodeId));
return {
Detach: () => {
streamReader.Close();
delete this.streams[audioNodeId];
this.OnEvent(new AudioStreamNodeDetachedEvent(this.id, audioNodeId));
this.TurnOff();
},
Id: () => {
return audioNodeId;
},
Read: () => {
return streamReader.Read();
},
};
});
}
public Detach = (audioNodeId: string): void => {
if (audioNodeId && this.streams[audioNodeId]) {
this.streams[audioNodeId].Close();
delete this.streams[audioNodeId];
this.OnEvent(new AudioStreamNodeDetachedEvent(this.id, audioNodeId));
}
}
public TurnOff = (): Promise<boolean> => {
for (const streamId in this.streams) {
if (streamId) {
const stream = this.streams[streamId];
if (stream && !stream.IsClosed) {
stream.Close();
}
}
}
this.OnEvent(new AudioSourceOffEvent(this.id)); // no stream now
return PromiseHelper.FromResult(true);
}
public get Events(): EventSource<AudioSourceEvent> {
return this.events;
}
private Upload = (audioNodeId: string): Promise<StreamReader<ArrayBuffer>> => {
return this.TurnOn()
.OnSuccessContinueWith<StreamReader<ArrayBuffer>>((_: boolean) => {
const stream = new Stream<ArrayBuffer>(audioNodeId);
this.streams[audioNodeId] = stream;
const reader: FileReader = new FileReader();
let startOffset = 0;
let endOffset = FileAudioSource.CHUNK_SIZE;
let lastWriteTimestamp = 0;
const processNextChunk = (event: Event): void => {
if (stream.IsClosed) {
return; // output stream was closed (somebody called TurnOff). We're done here.
}
if (lastWriteTimestamp !== 0) {
const delay = Date.now() - lastWriteTimestamp;
if (delay < FileAudioSource.UPLOAD_INTERVAL) {
// It's been less than the "upload interval" since we've uploaded the
// last chunk. Schedule the next upload to make sure that we're sending
// upstream roughly one chunk per upload interval.
new Timer(FileAudioSource.UPLOAD_INTERVAL - delay, processNextChunk).start();
return;
}
}
stream.Write(reader.result);
lastWriteTimestamp = Date.now();
if (endOffset < this.file.size) {
startOffset = endOffset;
endOffset = Math.min(endOffset + FileAudioSource.CHUNK_SIZE, this.file.size);
const chunk = this.file.slice(startOffset, endOffset);
reader.readAsArrayBuffer(chunk);
} else {
// we've written the entire file to the output stream, can close it now.
stream.Close();
}
};
reader.onload = processNextChunk;
reader.onerror = (event: ErrorEvent) => {
const errorMsg = `Error occurred while processing '${this.file.name}'. ${event.error}`;
this.OnEvent(new AudioStreamNodeErrorEvent(this.id, audioNodeId, event.error));
throw new Error(errorMsg);
};
const chunk = this.file.slice(startOffset, endOffset);
reader.readAsArrayBuffer(chunk);
return stream.GetReader();
});
}
private OnEvent = (event: AudioSourceEvent): void => {
this.events.OnEvent(event);
Events.Instance.OnEvent(event);
}
}