@aj-archipelago/cortex 1.3.5 → 1.3.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/helper-apps/cortex-autogen/agents.py +31 -2
  2. package/helper-apps/cortex-realtime-voice-server/.env.sample +6 -0
  3. package/helper-apps/cortex-realtime-voice-server/README.md +22 -0
  4. package/helper-apps/cortex-realtime-voice-server/bun.lockb +0 -0
  5. package/helper-apps/cortex-realtime-voice-server/client/bun.lockb +0 -0
  6. package/helper-apps/cortex-realtime-voice-server/client/index.html +12 -0
  7. package/helper-apps/cortex-realtime-voice-server/client/package.json +65 -0
  8. package/helper-apps/cortex-realtime-voice-server/client/postcss.config.js +6 -0
  9. package/helper-apps/cortex-realtime-voice-server/client/public/favicon.ico +0 -0
  10. package/helper-apps/cortex-realtime-voice-server/client/public/index.html +43 -0
  11. package/helper-apps/cortex-realtime-voice-server/client/public/logo192.png +0 -0
  12. package/helper-apps/cortex-realtime-voice-server/client/public/logo512.png +0 -0
  13. package/helper-apps/cortex-realtime-voice-server/client/public/manifest.json +25 -0
  14. package/helper-apps/cortex-realtime-voice-server/client/public/robots.txt +3 -0
  15. package/helper-apps/cortex-realtime-voice-server/client/public/sounds/connect.mp3 +0 -0
  16. package/helper-apps/cortex-realtime-voice-server/client/public/sounds/disconnect.mp3 +0 -0
  17. package/helper-apps/cortex-realtime-voice-server/client/src/App.test.tsx +9 -0
  18. package/helper-apps/cortex-realtime-voice-server/client/src/App.tsx +126 -0
  19. package/helper-apps/cortex-realtime-voice-server/client/src/SettingsModal.tsx +207 -0
  20. package/helper-apps/cortex-realtime-voice-server/client/src/chat/Chat.tsx +553 -0
  21. package/helper-apps/cortex-realtime-voice-server/client/src/chat/ChatBubble.tsx +22 -0
  22. package/helper-apps/cortex-realtime-voice-server/client/src/chat/ChatBubbleLeft.tsx +22 -0
  23. package/helper-apps/cortex-realtime-voice-server/client/src/chat/ChatBubbleRight.tsx +21 -0
  24. package/helper-apps/cortex-realtime-voice-server/client/src/chat/ChatMessage.tsx +27 -0
  25. package/helper-apps/cortex-realtime-voice-server/client/src/chat/ChatMessageInput.tsx +74 -0
  26. package/helper-apps/cortex-realtime-voice-server/client/src/chat/ChatTile.tsx +211 -0
  27. package/helper-apps/cortex-realtime-voice-server/client/src/chat/audio/SoundEffects.ts +56 -0
  28. package/helper-apps/cortex-realtime-voice-server/client/src/chat/audio/WavPacker.ts +112 -0
  29. package/helper-apps/cortex-realtime-voice-server/client/src/chat/audio/WavRecorder.ts +571 -0
  30. package/helper-apps/cortex-realtime-voice-server/client/src/chat/audio/WavStreamPlayer.ts +290 -0
  31. package/helper-apps/cortex-realtime-voice-server/client/src/chat/audio/analysis/AudioAnalysis.ts +186 -0
  32. package/helper-apps/cortex-realtime-voice-server/client/src/chat/audio/analysis/constants.ts +59 -0
  33. package/helper-apps/cortex-realtime-voice-server/client/src/chat/audio/worklets/AudioProcessor.ts +214 -0
  34. package/helper-apps/cortex-realtime-voice-server/client/src/chat/audio/worklets/StreamProcessor.ts +183 -0
  35. package/helper-apps/cortex-realtime-voice-server/client/src/chat/components/AudioVisualizer.tsx +151 -0
  36. package/helper-apps/cortex-realtime-voice-server/client/src/chat/components/CopyButton.tsx +32 -0
  37. package/helper-apps/cortex-realtime-voice-server/client/src/chat/components/ImageOverlay.tsx +166 -0
  38. package/helper-apps/cortex-realtime-voice-server/client/src/chat/components/MicrophoneVisualizer.tsx +95 -0
  39. package/helper-apps/cortex-realtime-voice-server/client/src/chat/components/ScreenshotCapture.tsx +116 -0
  40. package/helper-apps/cortex-realtime-voice-server/client/src/chat/hooks/useWindowResize.ts +27 -0
  41. package/helper-apps/cortex-realtime-voice-server/client/src/chat/utils/audio.ts +33 -0
  42. package/helper-apps/cortex-realtime-voice-server/client/src/index.css +20 -0
  43. package/helper-apps/cortex-realtime-voice-server/client/src/index.tsx +19 -0
  44. package/helper-apps/cortex-realtime-voice-server/client/src/logo.svg +1 -0
  45. package/helper-apps/cortex-realtime-voice-server/client/src/react-app-env.d.ts +1 -0
  46. package/helper-apps/cortex-realtime-voice-server/client/src/reportWebVitals.ts +15 -0
  47. package/helper-apps/cortex-realtime-voice-server/client/src/setupTests.ts +5 -0
  48. package/helper-apps/cortex-realtime-voice-server/client/src/utils/logger.ts +45 -0
  49. package/helper-apps/cortex-realtime-voice-server/client/tailwind.config.js +14 -0
  50. package/helper-apps/cortex-realtime-voice-server/client/tsconfig.json +30 -0
  51. package/helper-apps/cortex-realtime-voice-server/client/vite.config.ts +22 -0
  52. package/helper-apps/cortex-realtime-voice-server/index.ts +19 -0
  53. package/helper-apps/cortex-realtime-voice-server/package.json +28 -0
  54. package/helper-apps/cortex-realtime-voice-server/src/ApiServer.ts +35 -0
  55. package/helper-apps/cortex-realtime-voice-server/src/SocketServer.ts +737 -0
  56. package/helper-apps/cortex-realtime-voice-server/src/Tools.ts +520 -0
  57. package/helper-apps/cortex-realtime-voice-server/src/cortex/expert.ts +29 -0
  58. package/helper-apps/cortex-realtime-voice-server/src/cortex/image.ts +29 -0
  59. package/helper-apps/cortex-realtime-voice-server/src/cortex/memory.ts +91 -0
  60. package/helper-apps/cortex-realtime-voice-server/src/cortex/reason.ts +29 -0
  61. package/helper-apps/cortex-realtime-voice-server/src/cortex/search.ts +30 -0
  62. package/helper-apps/cortex-realtime-voice-server/src/cortex/style.ts +31 -0
  63. package/helper-apps/cortex-realtime-voice-server/src/cortex/utils.ts +95 -0
  64. package/helper-apps/cortex-realtime-voice-server/src/cortex/vision.ts +34 -0
  65. package/helper-apps/cortex-realtime-voice-server/src/realtime/client.ts +499 -0
  66. package/helper-apps/cortex-realtime-voice-server/src/realtime/realtimeTypes.ts +279 -0
  67. package/helper-apps/cortex-realtime-voice-server/src/realtime/socket.ts +27 -0
  68. package/helper-apps/cortex-realtime-voice-server/src/realtime/transcription.ts +75 -0
  69. package/helper-apps/cortex-realtime-voice-server/src/realtime/utils.ts +33 -0
  70. package/helper-apps/cortex-realtime-voice-server/src/utils/logger.ts +45 -0
  71. package/helper-apps/cortex-realtime-voice-server/src/utils/prompt.ts +81 -0
  72. package/helper-apps/cortex-realtime-voice-server/tsconfig.json +28 -0
  73. package/package.json +1 -1
  74. package/pathways/basePathway.js +3 -1
  75. package/pathways/system/entity/memory/sys_memory_manager.js +3 -0
  76. package/pathways/system/entity/memory/sys_memory_update.js +44 -45
  77. package/pathways/system/entity/memory/sys_read_memory.js +86 -6
  78. package/pathways/system/entity/memory/sys_search_memory.js +66 -0
  79. package/pathways/system/entity/shared/sys_entity_constants.js +2 -2
  80. package/pathways/system/entity/sys_entity_continue.js +2 -1
  81. package/pathways/system/entity/sys_entity_start.js +10 -0
  82. package/pathways/system/entity/sys_generator_expert.js +0 -2
  83. package/pathways/system/entity/sys_generator_memory.js +31 -0
  84. package/pathways/system/entity/sys_generator_voice_sample.js +36 -0
  85. package/pathways/system/entity/sys_router_tool.js +13 -10
  86. package/pathways/system/sys_parse_numbered_object_list.js +1 -1
  87. package/server/pathwayResolver.js +41 -31
  88. package/server/plugins/azureVideoTranslatePlugin.js +28 -16
  89. package/server/plugins/claude3VertexPlugin.js +0 -9
  90. package/server/plugins/gemini15ChatPlugin.js +18 -5
  91. package/server/plugins/modelPlugin.js +27 -6
  92. package/server/plugins/openAiChatPlugin.js +10 -8
  93. package/server/plugins/openAiVisionPlugin.js +56 -0
  94. package/tests/memoryfunction.test.js +73 -1
@@ -0,0 +1,290 @@
1
+ import { StreamProcessorSrc } from './worklets/StreamProcessor';
2
+ import { AudioAnalysis, AudioAnalysisOutputType } from './analysis/AudioAnalysis';
3
+
4
+ interface WavStreamPlayerOptions {
5
+ sampleRate?: number;
6
+ minBufferSize?: number;
7
+ }
8
+
9
+ interface TrackSampleOffset {
10
+ trackId: string | null;
11
+ offset: number;
12
+ currentTime: number;
13
+ }
14
+
15
+ /**
16
+ * Plays audio streams received in raw PCM16 chunks from the browser
17
+ */
18
+ export class WavStreamPlayer {
19
+ private readonly scriptSrc: string;
20
+ private readonly sampleRate: number;
21
+ private readonly minBufferSize: number;
22
+ private context: AudioContext | null;
23
+ private stream: AudioWorkletNode | null;
24
+ private analyser: AnalyserNode | null;
25
+ private trackSampleOffsets: Record<string, TrackSampleOffset>;
26
+ private interruptedTrackIds: Record<string, boolean>;
27
+ private isRestarting: boolean;
28
+ public onTrackComplete?: (trackId: string) => void;
29
+ public currentTrackId: string | null;
30
+
31
+ /**
32
+ * Creates a new WavStreamPlayer instance
33
+ * @param options
34
+ */
35
+ constructor({ sampleRate = 44100, minBufferSize = 10 }: WavStreamPlayerOptions = {}) {
36
+ this.scriptSrc = StreamProcessorSrc;
37
+ this.sampleRate = sampleRate;
38
+ this.minBufferSize = minBufferSize;
39
+ this.context = null;
40
+ this.stream = null;
41
+ this.analyser = null;
42
+ this.trackSampleOffsets = {};
43
+ this.interruptedTrackIds = {};
44
+ this.isRestarting = false;
45
+ this.currentTrackId = null;
46
+ }
47
+
48
+ /**
49
+ * Connects the audio context and enables output to speakers
50
+ */
51
+ async connect(): Promise<boolean> {
52
+ this.context = new AudioContext({ sampleRate: this.sampleRate });
53
+ if (this.context.state === 'suspended') {
54
+ await this.context.resume();
55
+ }
56
+ try {
57
+ await this.context.audioWorklet.addModule(this.scriptSrc);
58
+ } catch (e) {
59
+ console.error(e);
60
+ throw new Error(`Could not add audioWorklet module: ${this.scriptSrc}`);
61
+ }
62
+ const analyser = this.context.createAnalyser();
63
+ analyser.fftSize = 1024;
64
+ analyser.smoothingTimeConstant = 0.8;
65
+ this.analyser = analyser;
66
+ return true;
67
+ }
68
+
69
+ /**
70
+ * Gets the current frequency domain data from the playing track
71
+ * @param analysisType
72
+ * @param minDecibels default -100
73
+ * @param maxDecibels default -30
74
+ */
75
+ getFrequencies(
76
+ analysisType: 'frequency' | 'music' | 'voice' = 'frequency',
77
+ minDecibels = -100,
78
+ maxDecibels = -30,
79
+ ): AudioAnalysisOutputType {
80
+ if (!this.analyser) {
81
+ throw new Error('Not connected, please call .connect() first');
82
+ }
83
+ return AudioAnalysis.getFrequencies(
84
+ this.analyser,
85
+ this.sampleRate,
86
+ null,
87
+ analysisType,
88
+ minDecibels,
89
+ maxDecibels,
90
+ );
91
+ }
92
+
93
+ /**
94
+ * Starts audio streaming
95
+ * @private
96
+ */
97
+ private _start(): boolean {
98
+ if (!this.context) {
99
+ throw new Error('AudioContext not initialized');
100
+ }
101
+ if (this.isRestarting) {
102
+ return false;
103
+ }
104
+ try {
105
+ const streamNode = new AudioWorkletNode(this.context, 'stream_processor');
106
+ streamNode.connect(this.context.destination);
107
+ streamNode.port.onmessage = (e: MessageEvent) => {
108
+ const { event } = e.data;
109
+ if (event === 'stop') {
110
+ streamNode.disconnect();
111
+ this.stream = null;
112
+ this.isRestarting = false;
113
+ if (e.data.reason === 'max_underruns_reached') {
114
+ console.warn(`Audio stream stopped due to ${e.data.finalCount} consecutive underruns`);
115
+ }
116
+ } else if (event === 'offset') {
117
+ const { requestId, trackId, offset } = e.data;
118
+ const currentTime = offset / this.sampleRate;
119
+ this.trackSampleOffsets[requestId] = { trackId, offset, currentTime };
120
+ } else if (event === 'track_complete') {
121
+ const { trackId } = e.data;
122
+ this.onTrackComplete?.(trackId);
123
+ } else if (event === 'error') {
124
+ console.error('Stream processor error:', e.data.error);
125
+ this._handleStreamError();
126
+ } else if (event === 'underrun') {
127
+ console.warn(
128
+ `Audio buffer underrun: ${e.data.count} frames without data. ` +
129
+ `Buffer size: ${e.data.bufferSize}/${e.data.maxBuffers}`
130
+ );
131
+ }
132
+ };
133
+ if (this.analyser) {
134
+ this.analyser.disconnect();
135
+ streamNode.connect(this.analyser);
136
+ }
137
+ this.stream = streamNode;
138
+ // Send minBufferSize to the worklet
139
+ streamNode.port.postMessage({ event: 'config', minBufferSize: this.minBufferSize });
140
+ return true;
141
+ } catch (error) {
142
+ console.error('Error starting stream:', error);
143
+ this.isRestarting = false;
144
+ return false;
145
+ }
146
+ }
147
+
148
+ /**
149
+ * Handles stream errors by attempting to restart
150
+ * @private
151
+ */
152
+ private async _handleStreamError() {
153
+ if (this.isRestarting) return;
154
+
155
+ this.isRestarting = true;
156
+ try {
157
+ if (this.stream) {
158
+ this.stream.disconnect();
159
+ this.stream = null;
160
+ }
161
+ await new Promise(resolve => setTimeout(resolve, 100));
162
+ this._start();
163
+ } finally {
164
+ this.isRestarting = false;
165
+ }
166
+ }
167
+
168
+ /**
169
+ * Adds 16BitPCM data to the currently playing audio stream
170
+ * You can add chunks beyond the current play point and they will be queued for play
171
+ * @param arrayBuffer
172
+ * @param trackId
173
+ */
174
+ public add16BitPCM(pcmData: ArrayBuffer, trackId: string) {
175
+ if (!this.context || !this.analyser) {
176
+ return new Int16Array();
177
+ }
178
+
179
+ this.currentTrackId = trackId;
180
+ try {
181
+ if (this.interruptedTrackIds[trackId]) {
182
+ return new Int16Array();
183
+ }
184
+
185
+ if (!this.stream && !this._start()) {
186
+ throw new Error('Failed to start audio stream');
187
+ }
188
+
189
+ let buffer: Int16Array;
190
+ try {
191
+ if (pcmData instanceof Int16Array) {
192
+ buffer = pcmData;
193
+ } else {
194
+ buffer = new Int16Array(pcmData);
195
+ }
196
+ } catch (error) {
197
+ console.error('Error creating Int16Array:', error);
198
+ return new Int16Array();
199
+ }
200
+
201
+ if (!buffer.length) {
202
+ console.warn('Received empty buffer for track:', trackId);
203
+ return buffer;
204
+ }
205
+
206
+ this.stream?.port.postMessage({ event: 'write', buffer, trackId });
207
+ return buffer;
208
+ } catch (error) {
209
+ console.error('Error processing audio chunk:', error);
210
+ this._handleStreamError();
211
+ return new Int16Array();
212
+ }
213
+ }
214
+
215
+ /**
216
+ * Clears the interrupted state for a track
217
+ * @param trackId
218
+ */
219
+ clearInterruptedState(trackId: string): void {
220
+ delete this.interruptedTrackIds[trackId];
221
+ }
222
+
223
+ /**
224
+ * Clears all interrupted states
225
+ */
226
+ clearAllInterruptedStates(): void {
227
+ this.interruptedTrackIds = {};
228
+ }
229
+
230
+ /**
231
+ * Gets the offset (sample count) of the currently playing stream
232
+ * @param interrupt
233
+ */
234
+ async getTrackSampleOffset(interrupt = false): Promise<TrackSampleOffset | null> {
235
+ if (!this.stream) {
236
+ return null;
237
+ }
238
+ const requestId = crypto.randomUUID();
239
+ this.stream.port.postMessage({
240
+ event: interrupt ? 'interrupt' : 'offset',
241
+ requestId,
242
+ });
243
+ let trackSampleOffset: TrackSampleOffset | undefined;
244
+ while (!trackSampleOffset) {
245
+ trackSampleOffset = this.trackSampleOffsets[requestId];
246
+ await new Promise((r) => setTimeout(() => r(null), 1));
247
+ }
248
+ const { trackId } = trackSampleOffset;
249
+ if (interrupt && trackId) {
250
+ this.interruptedTrackIds[trackId] = true;
251
+ }
252
+ return trackSampleOffset;
253
+ }
254
+
255
+ /**
256
+ * Strips the current stream and returns the sample offset of the audio
257
+ */
258
+ async interrupt(): Promise<TrackSampleOffset | null> {
259
+ return this.getTrackSampleOffset(true);
260
+ }
261
+
262
+ /**
263
+ * Gets the analyser node
264
+ */
265
+ getAnalyser(): AnalyserNode | null {
266
+ return this.analyser;
267
+ }
268
+
269
+ /**
270
+ * Sets a callback to be called when a track completes playback
271
+ * @param callback The callback function that receives the trackId
272
+ */
273
+ setTrackCompleteCallback(callback: (trackId: string) => void) {
274
+ this.onTrackComplete = callback;
275
+ }
276
+
277
+ async fadeOut(durationMs: number) {
278
+ if (!this.context) return;
279
+ const gainNode = this.context.createGain();
280
+ gainNode.gain.setValueAtTime(1, this.context.currentTime);
281
+ gainNode.gain.linearRampToValueAtTime(0, this.context.currentTime + durationMs / 1000);
282
+
283
+ // Insert gain node before destination
284
+ this.stream?.disconnect();
285
+ this.stream?.connect(gainNode);
286
+ gainNode.connect(this.context.destination);
287
+
288
+ return new Promise(resolve => setTimeout(resolve, durationMs));
289
+ }
290
+ }
@@ -0,0 +1,186 @@
1
+ import {
2
+ noteFrequencies,
3
+ noteFrequencyLabels,
4
+ voiceFrequencies,
5
+ voiceFrequencyLabels,
6
+ } from './constants';
7
+
8
+ /**
9
+ * Output of AudioAnalysis for the frequency domain of the audio
10
+ */
11
+ export interface AudioAnalysisOutputType {
12
+ values: Float32Array;
13
+ frequencies: number[];
14
+ labels: string[];
15
+ }
16
+
17
+ type AnalysisType = 'frequency' | 'music' | 'voice';
18
+
19
+ /**
20
+ * Analyzes audio for visual output
21
+ */
22
+ export class AudioAnalysis {
23
+ private audio: HTMLAudioElement;
24
+ private context: AudioContext | OfflineAudioContext;
25
+ private analyser: AnalyserNode;
26
+ private sampleRate: number;
27
+ private audioBuffer: AudioBuffer | null;
28
+ private fftResults: Float32Array[] = [];
29
+
30
+ /**
31
+ * Retrieves frequency domain data from an AnalyserNode adjusted to a decibel range
32
+ * returns human-readable formatting and labels
33
+ */
34
+ static getFrequencies(
35
+ analyser: AnalyserNode,
36
+ sampleRate: number,
37
+ fftResult: Float32Array | null,
38
+ analysisType: AnalysisType = 'frequency',
39
+ minDecibels: number = -100,
40
+ maxDecibels: number = -30
41
+ ): AudioAnalysisOutputType {
42
+ if (!fftResult) {
43
+ fftResult = new Float32Array(analyser.frequencyBinCount);
44
+ analyser.getFloatFrequencyData(fftResult);
45
+ }
46
+ const nyquistFrequency = sampleRate / 2;
47
+ const frequencyStep = (1 / fftResult.length) * nyquistFrequency;
48
+ let outputValues: number[];
49
+ let frequencies: number[];
50
+ let labels: string[];
51
+
52
+ if (analysisType === 'music' || analysisType === 'voice') {
53
+ const useFrequencies = analysisType === 'voice' ? voiceFrequencies : noteFrequencies;
54
+ const aggregateOutput = Array(useFrequencies.length).fill(minDecibels);
55
+ for (let i = 0; i < fftResult.length; i++) {
56
+ const frequency = i * frequencyStep;
57
+ const amplitude = fftResult[i] || 0;
58
+ for (let n = useFrequencies.length - 1; n >= 0; n--) {
59
+ const useFrequency = useFrequencies[n] || 0;
60
+ if (frequency > useFrequency) {
61
+ aggregateOutput[n] = Math.max(aggregateOutput[n], amplitude);
62
+ break;
63
+ }
64
+ }
65
+ }
66
+ outputValues = aggregateOutput;
67
+ frequencies = analysisType === 'voice' ? voiceFrequencies : noteFrequencies;
68
+ labels = analysisType === 'voice' ? voiceFrequencyLabels : noteFrequencyLabels;
69
+ } else {
70
+ outputValues = Array.from(fftResult);
71
+ frequencies = outputValues.map((_, i) => frequencyStep * i);
72
+ labels = frequencies.map((f) => `${f.toFixed(2)} Hz`);
73
+ }
74
+
75
+ // We normalize to {0, 1}
76
+ const normalizedOutput = outputValues.map((v) =>
77
+ Math.max(0, Math.min((v - minDecibels) / (maxDecibels - minDecibels), 1))
78
+ );
79
+ const values = new Float32Array(normalizedOutput);
80
+
81
+ return {
82
+ values,
83
+ frequencies,
84
+ labels,
85
+ };
86
+ }
87
+
88
+ /**
89
+ * Creates a new AudioAnalysis instance for an HTMLAudioElement
90
+ */
91
+ constructor(audioElement: HTMLAudioElement, audioBuffer: AudioBuffer | null = null) {
92
+ this.audio = audioElement;
93
+ this.audioBuffer = audioBuffer;
94
+
95
+ if (audioBuffer) {
96
+ const { length, sampleRate } = audioBuffer;
97
+ const offlineAudioContext = new OfflineAudioContext({
98
+ length,
99
+ sampleRate,
100
+ });
101
+ const source = offlineAudioContext.createBufferSource();
102
+ source.buffer = audioBuffer;
103
+ const analyser = offlineAudioContext.createAnalyser();
104
+ analyser.fftSize = 8192;
105
+ analyser.smoothingTimeConstant = 0.1;
106
+ source.connect(analyser);
107
+
108
+ const renderQuantumInSeconds = 1 / 60;
109
+ const durationInSeconds = length / sampleRate;
110
+
111
+ const analyze = (index: number) => {
112
+ const suspendTime = renderQuantumInSeconds * index;
113
+ if (suspendTime < durationInSeconds) {
114
+ offlineAudioContext.suspend(suspendTime).then(() => {
115
+ const fftResult = new Float32Array(analyser.frequencyBinCount);
116
+ analyser.getFloatFrequencyData(fftResult);
117
+ this.fftResults.push(fftResult);
118
+ analyze(index + 1);
119
+ });
120
+ }
121
+ if (index === 1) {
122
+ offlineAudioContext.startRendering();
123
+ } else {
124
+ offlineAudioContext.resume();
125
+ }
126
+ };
127
+
128
+ source.start(0);
129
+ analyze(1);
130
+
131
+ this.context = offlineAudioContext;
132
+ this.analyser = analyser;
133
+ this.sampleRate = sampleRate;
134
+ } else {
135
+ const audioContext = new AudioContext();
136
+ const track = audioContext.createMediaElementSource(audioElement);
137
+ const analyser = audioContext.createAnalyser();
138
+ analyser.fftSize = 8192;
139
+ analyser.smoothingTimeConstant = 0.1;
140
+ track.connect(analyser);
141
+ analyser.connect(audioContext.destination);
142
+
143
+ this.context = audioContext;
144
+ this.analyser = analyser;
145
+ this.sampleRate = this.context.sampleRate;
146
+ }
147
+ }
148
+
149
+ /**
150
+ * Gets the current frequency domain data from the playing audio track
151
+ */
152
+ getFrequencies(
153
+ analysisType: AnalysisType = 'frequency',
154
+ minDecibels: number = -100,
155
+ maxDecibels: number = -30
156
+ ): AudioAnalysisOutputType {
157
+ let fftResult: Float32Array | null = null;
158
+ if (this.audioBuffer && this.fftResults.length) {
159
+ const pct = this.audio.currentTime / this.audio.duration;
160
+ const index = Math.min(
161
+ Math.floor(pct * this.fftResults.length),
162
+ this.fftResults.length - 1
163
+ );
164
+ fftResult = this.fftResults[index] ?? null;
165
+ }
166
+ return AudioAnalysis.getFrequencies(
167
+ this.analyser,
168
+ this.sampleRate,
169
+ fftResult ?? null,
170
+ analysisType,
171
+ minDecibels,
172
+ maxDecibels
173
+ );
174
+ }
175
+
176
+ /**
177
+ * Resume the internal AudioContext if it was suspended due to the lack of
178
+ * user interaction when the AudioAnalysis was instantiated.
179
+ */
180
+ async resumeIfSuspended(): Promise<true> {
181
+ if (this.context.state === 'suspended') {
182
+ await this.context.resume();
183
+ }
184
+ return true;
185
+ }
186
+ }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Constants for help with visualization
3
+ * Helps map frequency ranges from Fast Fourier Transform
4
+ * to human-interpretable ranges, notably music ranges and
5
+ * human vocal ranges.
6
+ */
7
+
8
+ // Eighth octave frequencies
9
+ const octave8Frequencies: number[] = [
10
+ 4186.01, 4434.92, 4698.63, 4978.03, 5274.04, 5587.65, 5919.91, 6271.93,
11
+ 6644.88, 7040.0, 7458.62, 7902.13,
12
+ ];
13
+
14
+ // Labels for each of the above frequencies
15
+ const octave8FrequencyLabels: string[] = [
16
+ 'C',
17
+ 'C#',
18
+ 'D',
19
+ 'D#',
20
+ 'E',
21
+ 'F',
22
+ 'F#',
23
+ 'G',
24
+ 'G#',
25
+ 'A',
26
+ 'A#',
27
+ 'B',
28
+ ];
29
+
30
+ /**
31
+ * All note frequencies from 1st to 8th octave
32
+ * in format "A#8" (A#, 8th octave)
33
+ */
34
+ export const noteFrequencies: number[] = [];
35
+ export const noteFrequencyLabels: string[] = [];
36
+ for (let i = 1; i <= 8; i++) {
37
+ for (let f = 0; f < octave8Frequencies.length; f++) {
38
+ const freq = octave8Frequencies[f] || 0;
39
+ const baseNote = octave8FrequencyLabels[f] || 'C';
40
+ noteFrequencies.push(freq / Math.pow(2, 8 - i));
41
+ noteFrequencyLabels.push( baseNote + i);
42
+ }
43
+ }
44
+
45
+ /**
46
+ * Subset of the note frequencies between 32 and 2000 Hz
47
+ * 6 octave range: C1 to B6
48
+ */
49
+ const voiceFrequencyRange: [number, number] = [32.0, 2000.0];
50
+ export const voiceFrequencies: number[] = noteFrequencies.filter((freq) => {
51
+ return freq > voiceFrequencyRange[0] && freq < voiceFrequencyRange[1];
52
+ });
53
+ export const voiceFrequencyLabels: string[] = noteFrequencyLabels.filter((_, i) => {
54
+ return (
55
+ noteFrequencies[i] &&
56
+ noteFrequencies[i] > voiceFrequencyRange[0] &&
57
+ noteFrequencies[i] < voiceFrequencyRange[1]
58
+ );
59
+ })