@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.
- package/helper-apps/cortex-autogen/agents.py +31 -2
- package/helper-apps/cortex-realtime-voice-server/.env.sample +6 -0
- package/helper-apps/cortex-realtime-voice-server/README.md +22 -0
- package/helper-apps/cortex-realtime-voice-server/bun.lockb +0 -0
- package/helper-apps/cortex-realtime-voice-server/client/bun.lockb +0 -0
- package/helper-apps/cortex-realtime-voice-server/client/index.html +12 -0
- package/helper-apps/cortex-realtime-voice-server/client/package.json +65 -0
- package/helper-apps/cortex-realtime-voice-server/client/postcss.config.js +6 -0
- package/helper-apps/cortex-realtime-voice-server/client/public/favicon.ico +0 -0
- package/helper-apps/cortex-realtime-voice-server/client/public/index.html +43 -0
- package/helper-apps/cortex-realtime-voice-server/client/public/logo192.png +0 -0
- package/helper-apps/cortex-realtime-voice-server/client/public/logo512.png +0 -0
- package/helper-apps/cortex-realtime-voice-server/client/public/manifest.json +25 -0
- package/helper-apps/cortex-realtime-voice-server/client/public/robots.txt +3 -0
- package/helper-apps/cortex-realtime-voice-server/client/public/sounds/connect.mp3 +0 -0
- package/helper-apps/cortex-realtime-voice-server/client/public/sounds/disconnect.mp3 +0 -0
- package/helper-apps/cortex-realtime-voice-server/client/src/App.test.tsx +9 -0
- package/helper-apps/cortex-realtime-voice-server/client/src/App.tsx +126 -0
- package/helper-apps/cortex-realtime-voice-server/client/src/SettingsModal.tsx +207 -0
- package/helper-apps/cortex-realtime-voice-server/client/src/chat/Chat.tsx +553 -0
- package/helper-apps/cortex-realtime-voice-server/client/src/chat/ChatBubble.tsx +22 -0
- package/helper-apps/cortex-realtime-voice-server/client/src/chat/ChatBubbleLeft.tsx +22 -0
- package/helper-apps/cortex-realtime-voice-server/client/src/chat/ChatBubbleRight.tsx +21 -0
- package/helper-apps/cortex-realtime-voice-server/client/src/chat/ChatMessage.tsx +27 -0
- package/helper-apps/cortex-realtime-voice-server/client/src/chat/ChatMessageInput.tsx +74 -0
- package/helper-apps/cortex-realtime-voice-server/client/src/chat/ChatTile.tsx +211 -0
- package/helper-apps/cortex-realtime-voice-server/client/src/chat/audio/SoundEffects.ts +56 -0
- package/helper-apps/cortex-realtime-voice-server/client/src/chat/audio/WavPacker.ts +112 -0
- package/helper-apps/cortex-realtime-voice-server/client/src/chat/audio/WavRecorder.ts +571 -0
- package/helper-apps/cortex-realtime-voice-server/client/src/chat/audio/WavStreamPlayer.ts +290 -0
- package/helper-apps/cortex-realtime-voice-server/client/src/chat/audio/analysis/AudioAnalysis.ts +186 -0
- package/helper-apps/cortex-realtime-voice-server/client/src/chat/audio/analysis/constants.ts +59 -0
- package/helper-apps/cortex-realtime-voice-server/client/src/chat/audio/worklets/AudioProcessor.ts +214 -0
- package/helper-apps/cortex-realtime-voice-server/client/src/chat/audio/worklets/StreamProcessor.ts +183 -0
- package/helper-apps/cortex-realtime-voice-server/client/src/chat/components/AudioVisualizer.tsx +151 -0
- package/helper-apps/cortex-realtime-voice-server/client/src/chat/components/CopyButton.tsx +32 -0
- package/helper-apps/cortex-realtime-voice-server/client/src/chat/components/ImageOverlay.tsx +166 -0
- package/helper-apps/cortex-realtime-voice-server/client/src/chat/components/MicrophoneVisualizer.tsx +95 -0
- package/helper-apps/cortex-realtime-voice-server/client/src/chat/components/ScreenshotCapture.tsx +116 -0
- package/helper-apps/cortex-realtime-voice-server/client/src/chat/hooks/useWindowResize.ts +27 -0
- package/helper-apps/cortex-realtime-voice-server/client/src/chat/utils/audio.ts +33 -0
- package/helper-apps/cortex-realtime-voice-server/client/src/index.css +20 -0
- package/helper-apps/cortex-realtime-voice-server/client/src/index.tsx +19 -0
- package/helper-apps/cortex-realtime-voice-server/client/src/logo.svg +1 -0
- package/helper-apps/cortex-realtime-voice-server/client/src/react-app-env.d.ts +1 -0
- package/helper-apps/cortex-realtime-voice-server/client/src/reportWebVitals.ts +15 -0
- package/helper-apps/cortex-realtime-voice-server/client/src/setupTests.ts +5 -0
- package/helper-apps/cortex-realtime-voice-server/client/src/utils/logger.ts +45 -0
- package/helper-apps/cortex-realtime-voice-server/client/tailwind.config.js +14 -0
- package/helper-apps/cortex-realtime-voice-server/client/tsconfig.json +30 -0
- package/helper-apps/cortex-realtime-voice-server/client/vite.config.ts +22 -0
- package/helper-apps/cortex-realtime-voice-server/index.ts +19 -0
- package/helper-apps/cortex-realtime-voice-server/package.json +28 -0
- package/helper-apps/cortex-realtime-voice-server/src/ApiServer.ts +35 -0
- package/helper-apps/cortex-realtime-voice-server/src/SocketServer.ts +737 -0
- package/helper-apps/cortex-realtime-voice-server/src/Tools.ts +520 -0
- package/helper-apps/cortex-realtime-voice-server/src/cortex/expert.ts +29 -0
- package/helper-apps/cortex-realtime-voice-server/src/cortex/image.ts +29 -0
- package/helper-apps/cortex-realtime-voice-server/src/cortex/memory.ts +91 -0
- package/helper-apps/cortex-realtime-voice-server/src/cortex/reason.ts +29 -0
- package/helper-apps/cortex-realtime-voice-server/src/cortex/search.ts +30 -0
- package/helper-apps/cortex-realtime-voice-server/src/cortex/style.ts +31 -0
- package/helper-apps/cortex-realtime-voice-server/src/cortex/utils.ts +95 -0
- package/helper-apps/cortex-realtime-voice-server/src/cortex/vision.ts +34 -0
- package/helper-apps/cortex-realtime-voice-server/src/realtime/client.ts +499 -0
- package/helper-apps/cortex-realtime-voice-server/src/realtime/realtimeTypes.ts +279 -0
- package/helper-apps/cortex-realtime-voice-server/src/realtime/socket.ts +27 -0
- package/helper-apps/cortex-realtime-voice-server/src/realtime/transcription.ts +75 -0
- package/helper-apps/cortex-realtime-voice-server/src/realtime/utils.ts +33 -0
- package/helper-apps/cortex-realtime-voice-server/src/utils/logger.ts +45 -0
- package/helper-apps/cortex-realtime-voice-server/src/utils/prompt.ts +81 -0
- package/helper-apps/cortex-realtime-voice-server/tsconfig.json +28 -0
- package/package.json +1 -1
- package/pathways/basePathway.js +3 -1
- package/pathways/system/entity/memory/sys_memory_manager.js +3 -0
- package/pathways/system/entity/memory/sys_memory_update.js +44 -45
- package/pathways/system/entity/memory/sys_read_memory.js +86 -6
- package/pathways/system/entity/memory/sys_search_memory.js +66 -0
- package/pathways/system/entity/shared/sys_entity_constants.js +2 -2
- package/pathways/system/entity/sys_entity_continue.js +2 -1
- package/pathways/system/entity/sys_entity_start.js +10 -0
- package/pathways/system/entity/sys_generator_expert.js +0 -2
- package/pathways/system/entity/sys_generator_memory.js +31 -0
- package/pathways/system/entity/sys_generator_voice_sample.js +36 -0
- package/pathways/system/entity/sys_router_tool.js +13 -10
- package/pathways/system/sys_parse_numbered_object_list.js +1 -1
- package/server/pathwayResolver.js +41 -31
- package/server/plugins/azureVideoTranslatePlugin.js +28 -16
- package/server/plugins/claude3VertexPlugin.js +0 -9
- package/server/plugins/gemini15ChatPlugin.js +18 -5
- package/server/plugins/modelPlugin.js +27 -6
- package/server/plugins/openAiChatPlugin.js +10 -8
- package/server/plugins/openAiVisionPlugin.js +56 -0
- package/tests/memoryfunction.test.js +73 -1
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import React, { useCallback, useRef, useState } from "react";
|
|
2
|
+
|
|
3
|
+
type ChatMessageInputProps = {
|
|
4
|
+
placeholder: string;
|
|
5
|
+
onSend?: (message: string) => void;
|
|
6
|
+
onStartStop?: () => void;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export const ChatMessageInput = ({
|
|
10
|
+
placeholder,
|
|
11
|
+
onSend,
|
|
12
|
+
onStartStop
|
|
13
|
+
}: ChatMessageInputProps) => {
|
|
14
|
+
const [message, setMessage] = useState("");
|
|
15
|
+
const [isRecording, setIsRecording] = useState(false);
|
|
16
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
17
|
+
|
|
18
|
+
const handleSend = useCallback(() => {
|
|
19
|
+
if (!onSend) {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
if (message === "") {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
onSend(message);
|
|
27
|
+
setMessage("");
|
|
28
|
+
}, [onSend, message]);
|
|
29
|
+
|
|
30
|
+
const handleStartStop = useCallback(() => {
|
|
31
|
+
if (!onStartStop) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
setIsRecording(!isRecording);
|
|
35
|
+
onStartStop();
|
|
36
|
+
}, [isRecording, onStartStop]);
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<div className="flex flex-col gap-2 px-2 py-2 border-t border-t-gray-800">
|
|
40
|
+
<button
|
|
41
|
+
className="bg-transparent p-1.5 rounded-lg border-2 border-gray-600"
|
|
42
|
+
onClick={handleStartStop}>
|
|
43
|
+
<p className="text-s uppercase text-gray-200">{isRecording ? 'Stop' : 'Start'}</p>
|
|
44
|
+
</button>
|
|
45
|
+
<div className="flex flex-row pt-3 items-center">
|
|
46
|
+
<input
|
|
47
|
+
ref={inputRef}
|
|
48
|
+
className={`grow text-gray-200 bg-transparent border-2 border-gray-600 p-1.5 rounded-lg`}
|
|
49
|
+
style={{
|
|
50
|
+
paddingLeft: message.length > 0 ? 12 : 24,
|
|
51
|
+
caretShape: "block",
|
|
52
|
+
}}
|
|
53
|
+
placeholder={placeholder}
|
|
54
|
+
value={message}
|
|
55
|
+
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
|
56
|
+
e.target?.value && setMessage(e.target.value);
|
|
57
|
+
}}
|
|
58
|
+
onKeyDown={(e: React.KeyboardEvent<HTMLInputElement>) => {
|
|
59
|
+
if (e.key === "Enter") {
|
|
60
|
+
handleSend();
|
|
61
|
+
}
|
|
62
|
+
}}
|
|
63
|
+
/>
|
|
64
|
+
<button
|
|
65
|
+
disabled={message.length === 0 || !onSend || !isRecording}
|
|
66
|
+
onClick={handleSend}
|
|
67
|
+
className={'bg-transparent p-1.5 ms-2 rounded-lg border-2 border-gray-600'}
|
|
68
|
+
>
|
|
69
|
+
<p className="text-s uppercase text-gray-200">Send</p>
|
|
70
|
+
</button>
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
);
|
|
74
|
+
};
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import React, { useEffect, useRef, useState } from 'react';
|
|
2
|
+
import ReactMarkdown, { Components } from 'react-markdown';
|
|
3
|
+
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
|
|
4
|
+
import { nightOwl } from 'react-syntax-highlighter/dist/esm/styles/prism';
|
|
5
|
+
import remarkGfm from 'remark-gfm';
|
|
6
|
+
import remarkMath from 'remark-math';
|
|
7
|
+
import rehypeKatex from 'rehype-katex';
|
|
8
|
+
import rehypeRaw from 'rehype-raw';
|
|
9
|
+
import SendIcon from '@mui/icons-material/Send';
|
|
10
|
+
import 'katex/dist/katex.min.css';
|
|
11
|
+
import { CopyButton } from './components/CopyButton';
|
|
12
|
+
|
|
13
|
+
// Define the code component props interface
|
|
14
|
+
interface CodeComponentProps {
|
|
15
|
+
node?: any;
|
|
16
|
+
inline?: boolean;
|
|
17
|
+
className?: string;
|
|
18
|
+
children?: React.ReactNode;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Add types for math components
|
|
22
|
+
interface MathComponentProps {
|
|
23
|
+
value: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Extend Components type to include math components
|
|
27
|
+
interface MarkdownComponents extends Components {
|
|
28
|
+
math?: (props: MathComponentProps) => JSX.Element;
|
|
29
|
+
inlineMath?: (props: MathComponentProps) => JSX.Element;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export type ChatMessage = {
|
|
33
|
+
id: string;
|
|
34
|
+
isSelf: boolean;
|
|
35
|
+
name: string;
|
|
36
|
+
message: string;
|
|
37
|
+
timestamp: number;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export type ChatTileProps = {
|
|
41
|
+
messages: ChatMessage[];
|
|
42
|
+
onSend: (message: string) => void;
|
|
43
|
+
isConnected?: boolean;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const MessageContent = ({ message }: { message: string }) => {
|
|
47
|
+
if (message.match(/^https?:\/\/.*\.(jpg|jpeg|png|gif|webp)$/i)) {
|
|
48
|
+
return (
|
|
49
|
+
<img
|
|
50
|
+
src={message}
|
|
51
|
+
alt="Shared"
|
|
52
|
+
className="max-w-full rounded-lg max-h-64 object-contain"
|
|
53
|
+
/>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const components: MarkdownComponents = {
|
|
58
|
+
code({ inline, className, children }: CodeComponentProps) {
|
|
59
|
+
const match = /language-(\w+)|^(\w+)/.exec(className || '');
|
|
60
|
+
const language = match ? match[1] || match[2] : '';
|
|
61
|
+
|
|
62
|
+
if (!inline && language) {
|
|
63
|
+
return (
|
|
64
|
+
<div className="rounded-lg overflow-hidden my-1.5 relative group">
|
|
65
|
+
<div className="flex justify-between items-center px-2 py-1 bg-gray-800/50">
|
|
66
|
+
<div className="text-xs text-gray-400 select-none">
|
|
67
|
+
{language}
|
|
68
|
+
</div>
|
|
69
|
+
<div className="opacity-0 group-hover:opacity-100 transition-opacity duration-200">
|
|
70
|
+
<CopyButton text={String(children)} />
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
<SyntaxHighlighter
|
|
74
|
+
style={nightOwl}
|
|
75
|
+
language={language}
|
|
76
|
+
customStyle={{
|
|
77
|
+
margin: 0,
|
|
78
|
+
background: 'transparent',
|
|
79
|
+
fontSize: '0.75rem',
|
|
80
|
+
}}
|
|
81
|
+
>
|
|
82
|
+
{String(children).replace(/\n$/, '')}
|
|
83
|
+
</SyntaxHighlighter>
|
|
84
|
+
</div>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return (
|
|
89
|
+
<code className="bg-gray-800/50 px-1 py-0.5 rounded text-cyan-300 text-xs inline-block">
|
|
90
|
+
{String(children)}
|
|
91
|
+
</code>
|
|
92
|
+
);
|
|
93
|
+
},
|
|
94
|
+
// Style links
|
|
95
|
+
a: ({ node, children, ...props }) => (
|
|
96
|
+
<a
|
|
97
|
+
{...props}
|
|
98
|
+
className="text-cyan-400 hover:text-cyan-300 underline text-sm"
|
|
99
|
+
target="_blank"
|
|
100
|
+
rel="noopener noreferrer"
|
|
101
|
+
>
|
|
102
|
+
{children}
|
|
103
|
+
</a>
|
|
104
|
+
),
|
|
105
|
+
// Style tables
|
|
106
|
+
table: ({ node, ...props }) => (
|
|
107
|
+
<div className="overflow-x-auto my-2">
|
|
108
|
+
<table {...props} className="border-collapse table-auto w-full text-sm" />
|
|
109
|
+
</div>
|
|
110
|
+
),
|
|
111
|
+
th: ({ node, ...props }) => (
|
|
112
|
+
<th {...props} className="border border-gray-600 px-3 py-1.5 bg-gray-800 text-sm" />
|
|
113
|
+
),
|
|
114
|
+
td: ({ node, ...props }) => (
|
|
115
|
+
<td {...props} className="border border-gray-600 px-3 py-1.5 text-sm" />
|
|
116
|
+
),
|
|
117
|
+
// Add special styling for math blocks
|
|
118
|
+
math: ({ value }) => (
|
|
119
|
+
<div className="py-1.5 overflow-x-auto text-sm">
|
|
120
|
+
<span>{value}</span>
|
|
121
|
+
</div>
|
|
122
|
+
),
|
|
123
|
+
inlineMath: ({ value }) => (
|
|
124
|
+
<span className="px-1 text-sm">{value}</span>
|
|
125
|
+
),
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
return (
|
|
129
|
+
<ReactMarkdown
|
|
130
|
+
className="prose prose-invert prose-sm max-w-none text-sm [&>p]:text-sm [&>ul]:text-sm [&>ol]:text-sm"
|
|
131
|
+
remarkPlugins={[remarkGfm, remarkMath]}
|
|
132
|
+
rehypePlugins={[rehypeRaw, rehypeKatex]}
|
|
133
|
+
components={components}
|
|
134
|
+
>
|
|
135
|
+
{message}
|
|
136
|
+
</ReactMarkdown>
|
|
137
|
+
);
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
export function ChatTile({ messages, onSend, isConnected = false }: ChatTileProps) {
|
|
141
|
+
const [message, setMessage] = useState('');
|
|
142
|
+
const messagesEndRef = useRef<HTMLDivElement>(null);
|
|
143
|
+
|
|
144
|
+
// Sort messages by timestamp before rendering
|
|
145
|
+
const sortedMessages = [...messages].sort((a, b) => a.timestamp - b.timestamp);
|
|
146
|
+
|
|
147
|
+
const handleSubmit = (e: React.FormEvent) => {
|
|
148
|
+
e.preventDefault();
|
|
149
|
+
if (message.trim()) {
|
|
150
|
+
onSend(message.trim());
|
|
151
|
+
setMessage('');
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
useEffect(() => {
|
|
156
|
+
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
|
157
|
+
}, [messages]);
|
|
158
|
+
|
|
159
|
+
return (
|
|
160
|
+
<div className="h-full flex flex-col">
|
|
161
|
+
{/* Messages container - scrollable */}
|
|
162
|
+
<div className="flex-1 h-0 overflow-y-auto">
|
|
163
|
+
<div className="p-4 space-y-4">
|
|
164
|
+
{sortedMessages.map((msg) => (
|
|
165
|
+
<div key={msg.id} className="flex flex-col">
|
|
166
|
+
<div className={`w-full rounded-lg p-2.5 relative group ${
|
|
167
|
+
msg.isSelf
|
|
168
|
+
? 'bg-blue-500/30 border border-blue-500/20'
|
|
169
|
+
: 'bg-gray-700/30 border border-gray-600/20'
|
|
170
|
+
}`}>
|
|
171
|
+
<div className="flex justify-between items-start">
|
|
172
|
+
<div className="text-2xs text-gray-400 mb-1">{msg.name}</div>
|
|
173
|
+
<div className="opacity-0 group-hover:opacity-100 transition-opacity duration-200">
|
|
174
|
+
<CopyButton text={msg.message} />
|
|
175
|
+
</div>
|
|
176
|
+
</div>
|
|
177
|
+
<MessageContent message={msg.message} />
|
|
178
|
+
</div>
|
|
179
|
+
</div>
|
|
180
|
+
))}
|
|
181
|
+
<div ref={messagesEndRef} aria-hidden="true" />
|
|
182
|
+
</div>
|
|
183
|
+
</div>
|
|
184
|
+
|
|
185
|
+
{/* Input area - fixed height */}
|
|
186
|
+
<div className="flex-none h-[68px] border-t border-gray-700/50 p-4 bg-gray-900/30 backdrop-blur-sm">
|
|
187
|
+
<form onSubmit={handleSubmit} className="flex space-x-2">
|
|
188
|
+
<input
|
|
189
|
+
type="text"
|
|
190
|
+
value={message}
|
|
191
|
+
onChange={(e) => setMessage(e.target.value)}
|
|
192
|
+
className={`flex-grow bg-gray-800/50 border border-gray-700/50 rounded-lg px-3 py-1.5
|
|
193
|
+
text-sm text-gray-100 focus:outline-none focus:ring-2 focus:ring-cyan-500/50
|
|
194
|
+
${!isConnected && 'opacity-50 cursor-not-allowed'}`}
|
|
195
|
+
placeholder={isConnected ? "Type a message..." : "Connect to send messages..."}
|
|
196
|
+
disabled={!isConnected}
|
|
197
|
+
/>
|
|
198
|
+
<button
|
|
199
|
+
type="submit"
|
|
200
|
+
disabled={!isConnected}
|
|
201
|
+
className={`p-1.5 rounded-lg bg-gradient-to-r from-blue-500 to-cyan-500
|
|
202
|
+
${isConnected ? 'hover:from-blue-600 hover:to-cyan-600' : 'opacity-50 cursor-not-allowed'}
|
|
203
|
+
shadow-lg shadow-cyan-500/20`}
|
|
204
|
+
>
|
|
205
|
+
<SendIcon sx={{ fontSize: 16 }} />
|
|
206
|
+
</button>
|
|
207
|
+
</form>
|
|
208
|
+
</div>
|
|
209
|
+
</div>
|
|
210
|
+
);
|
|
211
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
export class SoundEffects {
|
|
2
|
+
private static audioContext: AudioContext | null = null;
|
|
3
|
+
private static connectBuffer: AudioBuffer | null = null;
|
|
4
|
+
private static disconnectBuffer: AudioBuffer | null = null;
|
|
5
|
+
|
|
6
|
+
private static async getAudioContext() {
|
|
7
|
+
if (!this.audioContext) {
|
|
8
|
+
this.audioContext = new AudioContext();
|
|
9
|
+
}
|
|
10
|
+
return this.audioContext;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
private static async loadSound(url: string): Promise<AudioBuffer> {
|
|
14
|
+
const context = await this.getAudioContext();
|
|
15
|
+
const response = await fetch(url);
|
|
16
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
17
|
+
return await context.decodeAudioData(arrayBuffer);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
static async init() {
|
|
21
|
+
try {
|
|
22
|
+
this.connectBuffer = await this.loadSound('/sounds/connect.mp3');
|
|
23
|
+
this.disconnectBuffer = await this.loadSound('/sounds/disconnect.mp3');
|
|
24
|
+
} catch (error) {
|
|
25
|
+
console.error('Failed to load sound effects:', error);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
static async playConnect() {
|
|
30
|
+
if (!this.connectBuffer) return;
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
const context = await this.getAudioContext();
|
|
34
|
+
const source = context.createBufferSource();
|
|
35
|
+
source.buffer = this.connectBuffer;
|
|
36
|
+
source.connect(context.destination);
|
|
37
|
+
source.start(0);
|
|
38
|
+
} catch (error) {
|
|
39
|
+
console.error('Failed to play connect sound:', error);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
static async playDisconnect() {
|
|
44
|
+
if (!this.disconnectBuffer) return;
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
const context = await this.getAudioContext();
|
|
48
|
+
const source = context.createBufferSource();
|
|
49
|
+
source.buffer = this.disconnectBuffer;
|
|
50
|
+
source.connect(context.destination);
|
|
51
|
+
source.start(0);
|
|
52
|
+
} catch (error) {
|
|
53
|
+
console.error('Failed to play disconnect sound:', error);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Raw wav audio file contents
|
|
3
|
+
*/
|
|
4
|
+
export interface WavPackerAudioType {
|
|
5
|
+
blob: Blob;
|
|
6
|
+
url: string;
|
|
7
|
+
channelCount: number;
|
|
8
|
+
sampleRate: number;
|
|
9
|
+
duration: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Utility class for assembling PCM16 "audio/wav" data
|
|
14
|
+
* @class
|
|
15
|
+
*/
|
|
16
|
+
export class WavPacker {
|
|
17
|
+
/**
|
|
18
|
+
* Converts Float32Array of amplitude data to ArrayBuffer in Int16Array format
|
|
19
|
+
* @param {Float32Array} float32Array
|
|
20
|
+
* @returns {ArrayBuffer}
|
|
21
|
+
*/
|
|
22
|
+
static floatTo16BitPCM(float32Array: Float32Array): ArrayBuffer {
|
|
23
|
+
const buffer = new ArrayBuffer(float32Array.length * 2);
|
|
24
|
+
const view = new DataView(buffer);
|
|
25
|
+
let offset = 0;
|
|
26
|
+
for (let i = 0; i < float32Array.length; i++, offset += 2) {
|
|
27
|
+
let s = Math.max(-1, Math.min(1, float32Array[i] || 0));
|
|
28
|
+
view.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7fff, true);
|
|
29
|
+
}
|
|
30
|
+
return buffer;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Concatenates two ArrayBuffers
|
|
35
|
+
* @param {ArrayBuffer} leftBuffer
|
|
36
|
+
* @param {ArrayBuffer} rightBuffer
|
|
37
|
+
* @returns {ArrayBuffer}
|
|
38
|
+
*/
|
|
39
|
+
static mergeBuffers(leftBuffer: ArrayBuffer, rightBuffer: ArrayBuffer): ArrayBuffer {
|
|
40
|
+
const tmpArray = new Uint8Array(
|
|
41
|
+
leftBuffer.byteLength + rightBuffer.byteLength
|
|
42
|
+
);
|
|
43
|
+
tmpArray.set(new Uint8Array(leftBuffer), 0);
|
|
44
|
+
tmpArray.set(new Uint8Array(rightBuffer), leftBuffer.byteLength);
|
|
45
|
+
return tmpArray.buffer as ArrayBuffer;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Packs data into an Int16 format
|
|
50
|
+
* @private
|
|
51
|
+
* @param {number} size 0 = 1x Int16, 1 = 2x Int16
|
|
52
|
+
* @param {number} arg value to pack
|
|
53
|
+
* @returns
|
|
54
|
+
*/
|
|
55
|
+
_packData(size: number, arg: number): Uint8Array {
|
|
56
|
+
return [
|
|
57
|
+
new Uint8Array([arg, arg >> 8]),
|
|
58
|
+
new Uint8Array([arg, arg >> 8, arg >> 16, arg >> 24]),
|
|
59
|
+
][size] || new Uint8Array();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Packs audio into "audio/wav" Blob
|
|
64
|
+
* @param {number} sampleRate
|
|
65
|
+
* @param {{bitsPerSample: number, channels: Array<Float32Array>, data: Int16Array}} audio
|
|
66
|
+
* @returns {WavPackerAudioType}
|
|
67
|
+
*/
|
|
68
|
+
pack(sampleRate: number, audio: { bitsPerSample: number; channels: Array<Float32Array>; data: Int16Array; }): WavPackerAudioType {
|
|
69
|
+
if (!audio?.bitsPerSample) {
|
|
70
|
+
throw new Error(`Missing "bitsPerSample"`);
|
|
71
|
+
} else if (!audio?.channels) {
|
|
72
|
+
throw new Error(`Missing "channels"`);
|
|
73
|
+
} else if (!audio?.data) {
|
|
74
|
+
throw new Error(`Missing "data"`);
|
|
75
|
+
}
|
|
76
|
+
const { bitsPerSample, channels, data } = audio;
|
|
77
|
+
const output: (string | Uint8Array | Int16Array)[] = [
|
|
78
|
+
// Header
|
|
79
|
+
'RIFF',
|
|
80
|
+
this._packData(
|
|
81
|
+
1,
|
|
82
|
+
4 + (8 + 24) /* chunk 1 length */ + (8 + 8) /* chunk 2 length */
|
|
83
|
+
), // Length
|
|
84
|
+
'WAVE',
|
|
85
|
+
// chunk 1
|
|
86
|
+
'fmt ', // Sub-chunk identifier
|
|
87
|
+
this._packData(1, 16), // Chunk length
|
|
88
|
+
this._packData(0, 1), // Audio format (1 is linear quantization)
|
|
89
|
+
this._packData(0, channels.length),
|
|
90
|
+
this._packData(1, sampleRate),
|
|
91
|
+
this._packData(1, (sampleRate * channels.length * bitsPerSample) / 8), // Byte rate
|
|
92
|
+
this._packData(0, (channels.length * bitsPerSample) / 8),
|
|
93
|
+
this._packData(0, bitsPerSample),
|
|
94
|
+
// chunk 2
|
|
95
|
+
'data', // Sub-chunk identifier
|
|
96
|
+
this._packData(
|
|
97
|
+
1,
|
|
98
|
+
((channels[0] ? channels[0].length : 0) * channels.length * bitsPerSample) / 8
|
|
99
|
+
), // Chunk length
|
|
100
|
+
data,
|
|
101
|
+
];
|
|
102
|
+
const blob = new Blob(output, { type: 'audio/mpeg' });
|
|
103
|
+
const url = URL.createObjectURL(blob);
|
|
104
|
+
return {
|
|
105
|
+
blob,
|
|
106
|
+
url,
|
|
107
|
+
channelCount: channels.length,
|
|
108
|
+
sampleRate,
|
|
109
|
+
duration: data.byteLength / (channels.length * sampleRate * 2),
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
}
|