chore: remove lame encoder (#11529)
This commit is contained in:
31
Cargo.lock
generated
31
Cargo.lock
generated
@@ -81,7 +81,6 @@ dependencies = [
|
|||||||
"coreaudio-rs",
|
"coreaudio-rs",
|
||||||
"dispatch2",
|
"dispatch2",
|
||||||
"libc",
|
"libc",
|
||||||
"mp3lame-encoder",
|
|
||||||
"napi",
|
"napi",
|
||||||
"napi-build",
|
"napi-build",
|
||||||
"napi-derive",
|
"napi-derive",
|
||||||
@@ -358,15 +357,6 @@ version = "1.4.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
|
checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "autotools"
|
|
||||||
version = "0.2.7"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "ef941527c41b0fc0dd48511a8154cd5fc7e29200a0ff8b7203c5d777dbc795cf"
|
|
||||||
dependencies = [
|
|
||||||
"cc",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "backtrace"
|
name = "backtrace"
|
||||||
version = "0.3.74"
|
version = "0.3.74"
|
||||||
@@ -2212,27 +2202,6 @@ dependencies = [
|
|||||||
"windows-sys 0.52.0",
|
"windows-sys 0.52.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "mp3lame-encoder"
|
|
||||||
version = "0.2.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "fc8c8b5cdbe788ccd1098c3d3635298a011cffdebdd3460c9ca5060a7551557b"
|
|
||||||
dependencies = [
|
|
||||||
"libc",
|
|
||||||
"mp3lame-sys",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "mp3lame-sys"
|
|
||||||
version = "0.1.8"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "a21460ca4d833756cb700430888c67969e40b4560f50c226a0d258de551931ec"
|
|
||||||
dependencies = [
|
|
||||||
"autotools",
|
|
||||||
"cc",
|
|
||||||
"libc",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nanoid"
|
name = "nanoid"
|
||||||
version = "0.4.0"
|
version = "0.4.0"
|
||||||
|
|||||||
@@ -31,7 +31,6 @@ homedir = "0.3"
|
|||||||
infer = { version = "0.19.0" }
|
infer = { version = "0.19.0" }
|
||||||
libc = "0.2"
|
libc = "0.2"
|
||||||
mimalloc = "0.1"
|
mimalloc = "0.1"
|
||||||
mp3lame-encoder = "0.2"
|
|
||||||
napi = { version = "3.0.0-alpha.31", features = ["async", "chrono_date", "error_anyhow", "napi9", "serde"] }
|
napi = { version = "3.0.0-alpha.31", features = ["async", "chrono_date", "error_anyhow", "napi9", "serde"] }
|
||||||
napi-build = { version = "2" }
|
napi-build = { version = "2" }
|
||||||
napi-derive = { version = "3.0.0-alpha.28" }
|
napi-derive = { version = "3.0.0-alpha.28" }
|
||||||
|
|||||||
@@ -177,7 +177,6 @@ We would also like to give thanks to open-source projects that make AFFiNE possi
|
|||||||
- [Jotai](https://github.com/pmndrs/jotai) - Primitive and flexible state management for React.
|
- [Jotai](https://github.com/pmndrs/jotai) - Primitive and flexible state management for React.
|
||||||
- [async-call-rpc](https://github.com/Jack-Works/async-call-rpc) - A lightweight JSON RPC client & server.
|
- [async-call-rpc](https://github.com/Jack-Works/async-call-rpc) - A lightweight JSON RPC client & server.
|
||||||
- [Vite](https://github.com/vitejs/vite) - Next generation frontend tooling.
|
- [Vite](https://github.com/vitejs/vite) - Next generation frontend tooling.
|
||||||
- [lame](https://lame.sourceforge.io/) - High quality MPEG Audio Layer III (MP3) encoder.
|
|
||||||
- Other upstream [dependencies](https://github.com/toeverything/AFFiNE/network/dependencies).
|
- Other upstream [dependencies](https://github.com/toeverything/AFFiNE/network/dependencies).
|
||||||
|
|
||||||
Thanks a lot to the community for providing such powerful and simple libraries, so that we can focus more on the implementation of the product logic, and we hope that in the future our projects will also provide a more easy-to-use knowledge base for everyone.
|
Thanks a lot to the community for providing such powerful and simple libraries, so that we can focus more on the implementation of the product logic, and we hope that in the future our projects will also provide a more easy-to-use knowledge base for everyone.
|
||||||
|
|||||||
58
packages/frontend/media-capture-playground/server/encode.ts
Normal file
58
packages/frontend/media-capture-playground/server/encode.ts
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
export function createWavBuffer(
|
||||||
|
samples: Float32Array,
|
||||||
|
options: {
|
||||||
|
sampleRate: number;
|
||||||
|
numChannels: number;
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
const { sampleRate = 44100, numChannels = 1 } = options;
|
||||||
|
const bitsPerSample = 16;
|
||||||
|
const bytesPerSample = bitsPerSample / 8;
|
||||||
|
const dataSize = samples.length * bytesPerSample;
|
||||||
|
const buffer = new ArrayBuffer(44 + dataSize); // WAV header is 44 bytes
|
||||||
|
const view = new DataView(buffer);
|
||||||
|
|
||||||
|
// Write WAV header
|
||||||
|
// "RIFF" chunk descriptor
|
||||||
|
writeString(view, 0, 'RIFF');
|
||||||
|
view.setUint32(4, 36 + dataSize, true); // File size - 8
|
||||||
|
writeString(view, 8, 'WAVE');
|
||||||
|
|
||||||
|
// "fmt " sub-chunk
|
||||||
|
writeString(view, 12, 'fmt ');
|
||||||
|
view.setUint32(16, 16, true); // Sub-chunk size
|
||||||
|
view.setUint16(20, 1, true); // Audio format (1 = PCM)
|
||||||
|
view.setUint16(22, numChannels, true); // Channels
|
||||||
|
view.setUint32(24, sampleRate, true); // Sample rate
|
||||||
|
view.setUint32(28, sampleRate * numChannels * bytesPerSample, true); // Byte rate
|
||||||
|
view.setUint16(32, numChannels * bytesPerSample, true); // Block align
|
||||||
|
view.setUint16(34, bitsPerSample, true); // Bits per sample
|
||||||
|
|
||||||
|
// "data" sub-chunk
|
||||||
|
writeString(view, 36, 'data');
|
||||||
|
view.setUint32(40, dataSize, true); // Sub-chunk size
|
||||||
|
|
||||||
|
// Write audio data
|
||||||
|
const offset = 44;
|
||||||
|
for (let i = 0; i < samples.length; i++) {
|
||||||
|
// Convert float32 to int16
|
||||||
|
const s = Math.max(-1, Math.min(1, samples[i]));
|
||||||
|
view.setInt16(
|
||||||
|
offset + i * bytesPerSample,
|
||||||
|
s < 0 ? s * 0x8000 : s * 0x7fff,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeString(
|
||||||
|
view: DataView<ArrayBuffer>,
|
||||||
|
offset: number,
|
||||||
|
string: string
|
||||||
|
) {
|
||||||
|
for (let i = 0; i < string.length; i++) {
|
||||||
|
view.setUint8(offset + i, string.charCodeAt(i));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -98,8 +98,8 @@ export async function gemini(
|
|||||||
try {
|
try {
|
||||||
// Upload the audio file
|
// Upload the audio file
|
||||||
uploadResult = await fileManager.uploadFile(audioFilePath, {
|
uploadResult = await fileManager.uploadFile(audioFilePath, {
|
||||||
mimeType: 'audio/mp3',
|
mimeType: 'audio/wav',
|
||||||
displayName: 'audio_transcription.mp3',
|
displayName: 'audio_transcription.wav',
|
||||||
});
|
});
|
||||||
console.log('File uploaded:', uploadResult.file.uri);
|
console.log('File uploaded:', uploadResult.file.uri);
|
||||||
|
|
||||||
|
|||||||
@@ -5,8 +5,6 @@ import path from 'node:path';
|
|||||||
import {
|
import {
|
||||||
type Application,
|
type Application,
|
||||||
type AudioTapStream,
|
type AudioTapStream,
|
||||||
Bitrate,
|
|
||||||
Mp3Encoder,
|
|
||||||
ShareableContent,
|
ShareableContent,
|
||||||
type TappableApplication,
|
type TappableApplication,
|
||||||
} from '@affine/native';
|
} from '@affine/native';
|
||||||
@@ -19,6 +17,7 @@ import { debounce } from 'lodash-es';
|
|||||||
import multer from 'multer';
|
import multer from 'multer';
|
||||||
import { Server } from 'socket.io';
|
import { Server } from 'socket.io';
|
||||||
|
|
||||||
|
import { createWavBuffer } from './encode';
|
||||||
import { gemini, type TranscriptionResult } from './gemini';
|
import { gemini, type TranscriptionResult } from './gemini';
|
||||||
|
|
||||||
// Constants
|
// Constants
|
||||||
@@ -206,36 +205,34 @@ async function saveRecording(recording: Recording): Promise<string | null> {
|
|||||||
const recordingDir = path.join(RECORDING_DIR, sanitizedFilename);
|
const recordingDir = path.join(RECORDING_DIR, sanitizedFilename);
|
||||||
await fs.ensureDir(recordingDir);
|
await fs.ensureDir(recordingDir);
|
||||||
|
|
||||||
const mp3Filename = path.join(recordingDir, 'recording.mp3');
|
const wavFilename = path.join(recordingDir, 'recording.wav');
|
||||||
const transcriptionMp3Filename = path.join(
|
const transcriptionWavFilename = path.join(
|
||||||
recordingDir,
|
recordingDir,
|
||||||
'transcription.mp3'
|
'transcription.wav'
|
||||||
);
|
);
|
||||||
const metadataFilename = path.join(recordingDir, 'metadata.json');
|
const metadataFilename = path.join(recordingDir, 'metadata.json');
|
||||||
const iconFilename = path.join(recordingDir, 'icon.png');
|
const iconFilename = path.join(recordingDir, 'icon.png');
|
||||||
|
|
||||||
// Save MP3 file with the actual sample rate from the stream
|
console.log(`📝 Muxing Wav buffer ${wavFilename}`);
|
||||||
console.log(`📝 Writing MP3 file to ${mp3Filename}`);
|
const wavBuffer = new Uint8Array(
|
||||||
const mp3Encoder = new Mp3Encoder({
|
createWavBuffer(buffer, {
|
||||||
channels: channelCount,
|
|
||||||
sampleRate: actualSampleRate,
|
sampleRate: actualSampleRate,
|
||||||
});
|
numChannels: channelCount,
|
||||||
const mp3Data = mp3Encoder.encode(buffer);
|
})
|
||||||
await fs.writeFile(mp3Filename, mp3Data);
|
|
||||||
console.log('✅ MP3 file written successfully');
|
|
||||||
|
|
||||||
// Save low-quality MP3 file for transcription (8kHz)
|
|
||||||
console.log(
|
|
||||||
`📝 Writing transcription MP3 file to ${transcriptionMp3Filename}`
|
|
||||||
);
|
);
|
||||||
const transcriptionMp3Encoder = new Mp3Encoder({
|
|
||||||
channels: channelCount,
|
// Save Wav file with the actual sample rate from the stream
|
||||||
bitrate: Bitrate.Kbps8,
|
console.log(`📝 Writing Wav file to ${wavFilename}`);
|
||||||
sampleRate: actualSampleRate,
|
await fs.writeFile(wavFilename, wavBuffer);
|
||||||
});
|
console.log('✅ Wav file written successfully');
|
||||||
const transcriptionMp3Data = transcriptionMp3Encoder.encode(buffer);
|
|
||||||
await fs.writeFile(transcriptionMp3Filename, transcriptionMp3Data);
|
// Save low-quality Wav file for transcription (8kHz)
|
||||||
console.log('✅ Transcription MP3 file written successfully');
|
console.log(
|
||||||
|
`📝 Writing transcription wav file to ${transcriptionWavFilename}`
|
||||||
|
);
|
||||||
|
|
||||||
|
await fs.writeFile(transcriptionWavFilename, wavBuffer);
|
||||||
|
console.log('✅ Transcription Wav file written successfully');
|
||||||
|
|
||||||
// Save app icon if available
|
// Save app icon if available
|
||||||
if (app?.icon) {
|
if (app?.icon) {
|
||||||
@@ -367,7 +364,7 @@ async function stopRecording(processId: number) {
|
|||||||
// File management
|
// File management
|
||||||
async function getRecordings(): Promise<
|
async function getRecordings(): Promise<
|
||||||
{
|
{
|
||||||
mp3: string;
|
wav: string;
|
||||||
metadata?: RecordingMetadata;
|
metadata?: RecordingMetadata;
|
||||||
transcription?: TranscriptionMetadata;
|
transcription?: TranscriptionMetadata;
|
||||||
}[]
|
}[]
|
||||||
@@ -411,7 +408,7 @@ async function getRecordings(): Promise<
|
|||||||
if (transcriptionExists) {
|
if (transcriptionExists) {
|
||||||
transcription = await fs.readJson(transcriptionPath);
|
transcription = await fs.readJson(transcriptionPath);
|
||||||
} else {
|
} else {
|
||||||
// If transcription.mp3 exists but no transcription.json, it means transcription is available but not started
|
// If transcription.Wav exists but no transcription.json, it means transcription is available but not started
|
||||||
transcription = {
|
transcription = {
|
||||||
transcriptionStartTime: 0,
|
transcriptionStartTime: 0,
|
||||||
transcriptionEndTime: 0,
|
transcriptionEndTime: 0,
|
||||||
@@ -423,7 +420,7 @@ async function getRecordings(): Promise<
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
mp3: dir,
|
wav: dir,
|
||||||
metadata,
|
metadata,
|
||||||
transcription,
|
transcription,
|
||||||
};
|
};
|
||||||
@@ -473,21 +470,21 @@ async function setupRecordingsWatcher() {
|
|||||||
// Handle file events
|
// Handle file events
|
||||||
fsWatcher
|
fsWatcher
|
||||||
.on('add', async path => {
|
.on('add', async path => {
|
||||||
if (path.endsWith('.mp3') || path.endsWith('.json')) {
|
if (path.endsWith('.wav') || path.endsWith('.json')) {
|
||||||
console.log(`📝 File added: ${path}`);
|
console.log(`📝 File added: ${path}`);
|
||||||
const files = await getRecordings();
|
const files = await getRecordings();
|
||||||
io.emit('apps:saved', { recordings: files });
|
io.emit('apps:saved', { recordings: files });
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.on('change', async path => {
|
.on('change', async path => {
|
||||||
if (path.endsWith('.mp3') || path.endsWith('.json')) {
|
if (path.endsWith('.wav') || path.endsWith('.json')) {
|
||||||
console.log(`📝 File changed: ${path}`);
|
console.log(`📝 File changed: ${path}`);
|
||||||
const files = await getRecordings();
|
const files = await getRecordings();
|
||||||
io.emit('apps:saved', { recordings: files });
|
io.emit('apps:saved', { recordings: files });
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.on('unlink', async path => {
|
.on('unlink', async path => {
|
||||||
if (path.endsWith('.mp3') || path.endsWith('.json')) {
|
if (path.endsWith('.wav') || path.endsWith('.json')) {
|
||||||
console.log(`🗑️ File removed: ${path}`);
|
console.log(`🗑️ File removed: ${path}`);
|
||||||
const files = await getRecordings();
|
const files = await getRecordings();
|
||||||
io.emit('apps:saved', { recordings: files });
|
io.emit('apps:saved', { recordings: files });
|
||||||
@@ -797,11 +794,11 @@ app.post(
|
|||||||
// Check if directory exists
|
// Check if directory exists
|
||||||
await fs.access(recordingDir);
|
await fs.access(recordingDir);
|
||||||
|
|
||||||
const transcriptionMp3Path = `${recordingDir}/transcription.mp3`;
|
const transcriptionWavPath = `${recordingDir}/transcription.wav`;
|
||||||
const transcriptionMetadataPath = `${recordingDir}/transcription.json`;
|
const transcriptionMetadataPath = `${recordingDir}/transcription.json`;
|
||||||
|
|
||||||
// Check if transcription file exists
|
// Check if transcription file exists
|
||||||
await fs.access(transcriptionMp3Path);
|
await fs.access(transcriptionWavPath);
|
||||||
|
|
||||||
// Create initial transcription metadata
|
// Create initial transcription metadata
|
||||||
const initialMetadata: TranscriptionMetadata = {
|
const initialMetadata: TranscriptionMetadata = {
|
||||||
@@ -814,7 +811,7 @@ app.post(
|
|||||||
// Notify clients that transcription has started
|
// Notify clients that transcription has started
|
||||||
io.emit('apps:recording-transcription-start', { filename: foldername });
|
io.emit('apps:recording-transcription-start', { filename: foldername });
|
||||||
|
|
||||||
const transcription = await gemini(transcriptionMp3Path, {
|
const transcription = await gemini(transcriptionWavPath, {
|
||||||
mode: 'transcript',
|
mode: 'transcript',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -591,7 +591,7 @@ export function SavedRecordingItem({
|
|||||||
|
|
||||||
const metadata = recording.metadata;
|
const metadata = recording.metadata;
|
||||||
// Ensure we have a valid filename, fallback to an empty string if undefined
|
// Ensure we have a valid filename, fallback to an empty string if undefined
|
||||||
const fileName = recording.mp3 || '';
|
const fileName = recording.wav || '';
|
||||||
const recordingDate = metadata
|
const recordingDate = metadata
|
||||||
? new Date(metadata.recordingStartTime).toLocaleString()
|
? new Date(metadata.recordingStartTime).toLocaleString()
|
||||||
: 'Unknown date';
|
: 'Unknown date';
|
||||||
@@ -638,7 +638,7 @@ export function SavedRecordingItem({
|
|||||||
throw new Error('Invalid recording filename');
|
throw new Error('Invalid recording filename');
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await fetch(`/api/recordings/${fileName}/recording.mp3`);
|
const response = await fetch(`/api/recordings/${fileName}/recording.wav`);
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Failed to fetch audio file (${response.status}): ${response.statusText}`
|
`Failed to fetch audio file (${response.status}): ${response.statusText}`
|
||||||
@@ -754,11 +754,11 @@ export function SavedRecordingItem({
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// Check if filename is valid
|
// Check if filename is valid
|
||||||
if (!recording.mp3) {
|
if (!recording.wav) {
|
||||||
throw new Error('Invalid recording filename');
|
throw new Error('Invalid recording filename');
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await fetch(`/api/recordings/${recording.mp3}`, {
|
const response = await fetch(`/api/recordings/${recording.wav}`, {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -782,7 +782,7 @@ export function SavedRecordingItem({
|
|||||||
} finally {
|
} finally {
|
||||||
setIsDeleting(false);
|
setIsDeleting(false);
|
||||||
}
|
}
|
||||||
}, [recording.mp3]);
|
}, [recording.wav]);
|
||||||
|
|
||||||
const handleDeleteClick = React.useCallback(() => {
|
const handleDeleteClick = React.useCallback(() => {
|
||||||
void handleDelete().catch(err => {
|
void handleDelete().catch(err => {
|
||||||
@@ -796,7 +796,7 @@ export function SavedRecordingItem({
|
|||||||
socket.on(
|
socket.on(
|
||||||
'apps:recording-transcription-start',
|
'apps:recording-transcription-start',
|
||||||
(data: { filename: string }) => {
|
(data: { filename: string }) => {
|
||||||
if (recording.mp3 && data.filename === recording.mp3) {
|
if (recording.wav && data.filename === recording.wav) {
|
||||||
setTranscriptionError(null);
|
setTranscriptionError(null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -810,7 +810,7 @@ export function SavedRecordingItem({
|
|||||||
transcription?: string;
|
transcription?: string;
|
||||||
error?: string;
|
error?: string;
|
||||||
}) => {
|
}) => {
|
||||||
if (recording.mp3 && data.filename === recording.mp3 && !data.success) {
|
if (recording.wav && data.filename === recording.wav && !data.success) {
|
||||||
setTranscriptionError(data.error || 'Transcription failed');
|
setTranscriptionError(data.error || 'Transcription failed');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -820,17 +820,17 @@ export function SavedRecordingItem({
|
|||||||
socket.off('apps:recording-transcription-start');
|
socket.off('apps:recording-transcription-start');
|
||||||
socket.off('apps:recording-transcription-end');
|
socket.off('apps:recording-transcription-end');
|
||||||
};
|
};
|
||||||
}, [recording.mp3]);
|
}, [recording.wav]);
|
||||||
|
|
||||||
const handleTranscribe = React.useCallback(async () => {
|
const handleTranscribe = React.useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
// Check if filename is valid
|
// Check if filename is valid
|
||||||
if (!recording.mp3) {
|
if (!recording.wav) {
|
||||||
throw new Error('Invalid recording filename');
|
throw new Error('Invalid recording filename');
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`/api/recordings/${recording.mp3}/transcribe`,
|
`/api/recordings/${recording.wav}/transcribe`,
|
||||||
{
|
{
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
}
|
}
|
||||||
@@ -845,7 +845,7 @@ export function SavedRecordingItem({
|
|||||||
err instanceof Error ? err.message : 'Failed to start transcription'
|
err instanceof Error ? err.message : 'Failed to start transcription'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}, [recording.mp3]);
|
}, [recording.wav]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-white rounded-lg shadow-sm hover:shadow-md transition-all duration-300 overflow-hidden mb-3 border border-gray-100 hover:border-gray-200">
|
<div className="bg-white rounded-lg shadow-sm hover:shadow-md transition-all duration-300 overflow-hidden mb-3 border border-gray-100 hover:border-gray-200">
|
||||||
@@ -876,7 +876,7 @@ export function SavedRecordingItem({
|
|||||||
/>
|
/>
|
||||||
<audio
|
<audio
|
||||||
ref={audioRef}
|
ref={audioRef}
|
||||||
src={fileName ? `/api/recordings/${fileName}/recording.mp3` : ''}
|
src={fileName ? `/api/recordings/${fileName}/recording.wav` : ''}
|
||||||
preload="metadata"
|
preload="metadata"
|
||||||
className="hidden"
|
className="hidden"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ export function SavedRecordings(): React.ReactElement {
|
|||||||
return (
|
return (
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
{recordings.map(recording => (
|
{recordings.map(recording => (
|
||||||
<SavedRecordingItem key={recording.mp3} recording={recording} />
|
<SavedRecordingItem key={recording.wav} recording={recording} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ export interface TranscriptionMetadata {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface SavedRecording {
|
export interface SavedRecording {
|
||||||
mp3: string;
|
wav: string;
|
||||||
metadata?: RecordingMetadata;
|
metadata?: RecordingMetadata;
|
||||||
transcription?: TranscriptionMetadata;
|
transcription?: TranscriptionMetadata;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
import { readFile, writeFile } from 'node:fs/promises';
|
|
||||||
import { join } from 'node:path';
|
|
||||||
import { fileURLToPath } from 'node:url';
|
|
||||||
import { tmpdir } from 'node:os';
|
|
||||||
|
|
||||||
import test from 'ava';
|
|
||||||
|
|
||||||
import { decodeAudio, Mp3Encoder } from '../index.js';
|
|
||||||
|
|
||||||
const __dirname = join(fileURLToPath(import.meta.url), '..');
|
|
||||||
|
|
||||||
const wav = await readFile(join(__dirname, 'fixtures', 'recording.wav'));
|
|
||||||
|
|
||||||
test('convert wav to mp3', async t => {
|
|
||||||
const audio = await decodeAudio(wav);
|
|
||||||
const mp3 = new Mp3Encoder({
|
|
||||||
channels: 1,
|
|
||||||
});
|
|
||||||
await t.notThrowsAsync(async () => {
|
|
||||||
const mp3Data = mp3.encode(audio);
|
|
||||||
await writeFile(join(tmpdir(), 'recording.mp3'), mp3Data);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
85
packages/frontend/native/index.d.ts
vendored
85
packages/frontend/native/index.d.ts
vendored
@@ -63,11 +63,6 @@ export declare class DocStoragePool {
|
|||||||
getBlobUploadedAt(universalId: string, peer: string, blobId: string): Promise<Date | null>
|
getBlobUploadedAt(universalId: string, peer: string, blobId: string): Promise<Date | null>
|
||||||
}
|
}
|
||||||
|
|
||||||
export declare class Mp3Encoder {
|
|
||||||
constructor(options: EncodeOptions)
|
|
||||||
encode(input: Float32Array): Uint8Array
|
|
||||||
}
|
|
||||||
|
|
||||||
export declare class RecordingPermissions {
|
export declare class RecordingPermissions {
|
||||||
audio: boolean
|
audio: boolean
|
||||||
screen: boolean
|
screen: boolean
|
||||||
@@ -135,42 +130,6 @@ export declare class TappableApplication {
|
|||||||
tapAudio(audioStreamCallback: ((err: Error | null, arg: Float32Array) => void)): AudioTapStream
|
tapAudio(audioStreamCallback: ((err: Error | null, arg: Float32Array) => void)): AudioTapStream
|
||||||
}
|
}
|
||||||
|
|
||||||
/**Enumeration of valid values for `set_brate` */
|
|
||||||
export declare enum Bitrate {
|
|
||||||
/**8_000 */
|
|
||||||
Kbps8 = 8,
|
|
||||||
/**16_000 */
|
|
||||||
Kbps16 = 16,
|
|
||||||
/**24_000 */
|
|
||||||
Kbps24 = 24,
|
|
||||||
/**32_000 */
|
|
||||||
Kbps32 = 32,
|
|
||||||
/**40_000 */
|
|
||||||
Kbps40 = 40,
|
|
||||||
/**48_000 */
|
|
||||||
Kbps48 = 48,
|
|
||||||
/**64_000 */
|
|
||||||
Kbps64 = 64,
|
|
||||||
/**80_000 */
|
|
||||||
Kbps80 = 80,
|
|
||||||
/**96_000 */
|
|
||||||
Kbps96 = 96,
|
|
||||||
/**112_000 */
|
|
||||||
Kbps112 = 112,
|
|
||||||
/**128_000 */
|
|
||||||
Kbps128 = 128,
|
|
||||||
/**160_000 */
|
|
||||||
Kbps160 = 160,
|
|
||||||
/**192_000 */
|
|
||||||
Kbps192 = 192,
|
|
||||||
/**224_000 */
|
|
||||||
Kbps224 = 224,
|
|
||||||
/**256_000 */
|
|
||||||
Kbps256 = 256,
|
|
||||||
/**320_000 */
|
|
||||||
Kbps320 = 320
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Blob {
|
export interface Blob {
|
||||||
key: string
|
key: string
|
||||||
data: Uint8Array
|
data: Uint8Array
|
||||||
@@ -212,14 +171,6 @@ export interface DocUpdate {
|
|||||||
bin: Uint8Array
|
bin: Uint8Array
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EncodeOptions {
|
|
||||||
channels: number
|
|
||||||
quality?: Quality
|
|
||||||
bitrate?: Bitrate
|
|
||||||
sampleRate?: number
|
|
||||||
mode?: Mode
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface InsertRow {
|
export interface InsertRow {
|
||||||
docId?: string
|
docId?: string
|
||||||
data: Uint8Array
|
data: Uint8Array
|
||||||
@@ -234,42 +185,6 @@ export interface ListedBlob {
|
|||||||
|
|
||||||
export declare function mintChallengeResponse(resource: string, bits?: number | undefined | null): Promise<string>
|
export declare function mintChallengeResponse(resource: string, bits?: number | undefined | null): Promise<string>
|
||||||
|
|
||||||
/** MPEG mode */
|
|
||||||
export declare enum Mode {
|
|
||||||
Mono = 0,
|
|
||||||
Stereo = 1,
|
|
||||||
JointStereo = 2,
|
|
||||||
DualChannel = 3,
|
|
||||||
NotSet = 4
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*Possible quality parameter.
|
|
||||||
*From best(0) to worst(9)
|
|
||||||
*/
|
|
||||||
export declare enum Quality {
|
|
||||||
/**Best possible quality */
|
|
||||||
Best = 0,
|
|
||||||
/**Second best */
|
|
||||||
SecondBest = 1,
|
|
||||||
/**Close to best */
|
|
||||||
NearBest = 2,
|
|
||||||
/**Very nice */
|
|
||||||
VeryNice = 3,
|
|
||||||
/**Nice */
|
|
||||||
Nice = 4,
|
|
||||||
/**Good */
|
|
||||||
Good = 5,
|
|
||||||
/**Decent */
|
|
||||||
Decent = 6,
|
|
||||||
/**Okayish */
|
|
||||||
Ok = 7,
|
|
||||||
/**Almost worst */
|
|
||||||
SecondWorst = 8,
|
|
||||||
/**Worst */
|
|
||||||
Worst = 9
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SetBlob {
|
export interface SetBlob {
|
||||||
key: string
|
key: string
|
||||||
data: Uint8Array
|
data: Uint8Array
|
||||||
|
|||||||
@@ -35,7 +35,11 @@ const isMuslFromFilesystem = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const isMuslFromReport = () => {
|
const isMuslFromReport = () => {
|
||||||
const report = typeof process.report.getReport === 'function' ? process.report.getReport() : null
|
let report = null
|
||||||
|
if (typeof process.report?.getReport === 'function') {
|
||||||
|
process.report.excludeNetwork = true
|
||||||
|
report = process.report.getReport()
|
||||||
|
}
|
||||||
if (!report) {
|
if (!report) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@@ -376,16 +380,12 @@ module.exports.ApplicationStateChangedSubscriber = nativeBinding.ApplicationStat
|
|||||||
module.exports.AudioTapStream = nativeBinding.AudioTapStream
|
module.exports.AudioTapStream = nativeBinding.AudioTapStream
|
||||||
module.exports.DocStorage = nativeBinding.DocStorage
|
module.exports.DocStorage = nativeBinding.DocStorage
|
||||||
module.exports.DocStoragePool = nativeBinding.DocStoragePool
|
module.exports.DocStoragePool = nativeBinding.DocStoragePool
|
||||||
module.exports.Mp3Encoder = nativeBinding.Mp3Encoder
|
|
||||||
module.exports.RecordingPermissions = nativeBinding.RecordingPermissions
|
module.exports.RecordingPermissions = nativeBinding.RecordingPermissions
|
||||||
module.exports.ShareableContent = nativeBinding.ShareableContent
|
module.exports.ShareableContent = nativeBinding.ShareableContent
|
||||||
module.exports.SqliteConnection = nativeBinding.SqliteConnection
|
module.exports.SqliteConnection = nativeBinding.SqliteConnection
|
||||||
module.exports.TappableApplication = nativeBinding.TappableApplication
|
module.exports.TappableApplication = nativeBinding.TappableApplication
|
||||||
module.exports.Bitrate = nativeBinding.Bitrate
|
|
||||||
module.exports.decodeAudio = nativeBinding.decodeAudio
|
module.exports.decodeAudio = nativeBinding.decodeAudio
|
||||||
module.exports.decodeAudioSync = nativeBinding.decodeAudioSync
|
module.exports.decodeAudioSync = nativeBinding.decodeAudioSync
|
||||||
module.exports.mintChallengeResponse = nativeBinding.mintChallengeResponse
|
module.exports.mintChallengeResponse = nativeBinding.mintChallengeResponse
|
||||||
module.exports.Mode = nativeBinding.Mode
|
|
||||||
module.exports.Quality = nativeBinding.Quality
|
|
||||||
module.exports.ValidationResult = nativeBinding.ValidationResult
|
module.exports.ValidationResult = nativeBinding.ValidationResult
|
||||||
module.exports.verifyChallengeResponse = nativeBinding.verifyChallengeResponse
|
module.exports.verifyChallengeResponse = nativeBinding.verifyChallengeResponse
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ version = "0.0.0"
|
|||||||
crate-type = ["cdylib", "rlib"]
|
crate-type = ["cdylib", "rlib"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
mp3lame-encoder = { workspace = true, features = ["std"] }
|
|
||||||
napi = { workspace = true, features = ["napi4"] }
|
napi = { workspace = true, features = ["napi4"] }
|
||||||
napi-derive = { workspace = true, features = ["type-def"] }
|
napi-derive = { workspace = true, features = ["type-def"] }
|
||||||
rubato = { workspace = true }
|
rubato = { workspace = true }
|
||||||
|
|||||||
@@ -3,4 +3,3 @@ pub mod macos;
|
|||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
pub(crate) use macos::*;
|
pub(crate) use macos::*;
|
||||||
pub mod audio_decoder;
|
pub mod audio_decoder;
|
||||||
pub mod mp3;
|
|
||||||
|
|||||||
@@ -1,219 +0,0 @@
|
|||||||
use mp3lame_encoder::{Builder, Encoder, FlushNoGap, MonoPcm};
|
|
||||||
use napi::bindgen_prelude::{Result, Uint8Array};
|
|
||||||
use napi_derive::napi;
|
|
||||||
use thiserror::Error;
|
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
|
||||||
pub enum LameError {
|
|
||||||
#[error("Create builder failed")]
|
|
||||||
CreateBuilderFailed,
|
|
||||||
#[error("Failed to create encoder")]
|
|
||||||
BuildError(#[from] mp3lame_encoder::BuildError),
|
|
||||||
#[error("Failed to encode")]
|
|
||||||
EncodeError(#[from] mp3lame_encoder::EncodeError),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<LameError> for napi::Error {
|
|
||||||
fn from(value: LameError) -> Self {
|
|
||||||
napi::Error::new(napi::Status::GenericFailure, value.to_string())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[napi]
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
///Possible quality parameter.
|
|
||||||
///From best(0) to worst(9)
|
|
||||||
pub enum Quality {
|
|
||||||
///Best possible quality
|
|
||||||
Best = 0,
|
|
||||||
///Second best
|
|
||||||
SecondBest = 1,
|
|
||||||
///Close to best
|
|
||||||
NearBest = 2,
|
|
||||||
///Very nice
|
|
||||||
VeryNice = 3,
|
|
||||||
///Nice
|
|
||||||
Nice = 4,
|
|
||||||
///Good
|
|
||||||
Good = 5,
|
|
||||||
///Decent
|
|
||||||
Decent = 6,
|
|
||||||
///Okayish
|
|
||||||
Ok = 7,
|
|
||||||
///Almost worst
|
|
||||||
SecondWorst = 8,
|
|
||||||
///Worst
|
|
||||||
Worst = 9,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Quality> for mp3lame_encoder::Quality {
|
|
||||||
fn from(value: Quality) -> Self {
|
|
||||||
match value {
|
|
||||||
Quality::Best => mp3lame_encoder::Quality::Best,
|
|
||||||
Quality::SecondBest => mp3lame_encoder::Quality::SecondBest,
|
|
||||||
Quality::NearBest => mp3lame_encoder::Quality::NearBest,
|
|
||||||
Quality::VeryNice => mp3lame_encoder::Quality::VeryNice,
|
|
||||||
Quality::Nice => mp3lame_encoder::Quality::Nice,
|
|
||||||
Quality::Good => mp3lame_encoder::Quality::Good,
|
|
||||||
Quality::Decent => mp3lame_encoder::Quality::Decent,
|
|
||||||
Quality::Ok => mp3lame_encoder::Quality::Ok,
|
|
||||||
Quality::SecondWorst => mp3lame_encoder::Quality::SecondWorst,
|
|
||||||
Quality::Worst => mp3lame_encoder::Quality::Worst,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[napi]
|
|
||||||
#[repr(u16)]
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
///Enumeration of valid values for `set_brate`
|
|
||||||
pub enum Bitrate {
|
|
||||||
///8_000
|
|
||||||
Kbps8 = 8,
|
|
||||||
///16_000
|
|
||||||
Kbps16 = 16,
|
|
||||||
///24_000
|
|
||||||
Kbps24 = 24,
|
|
||||||
///32_000
|
|
||||||
Kbps32 = 32,
|
|
||||||
///40_000
|
|
||||||
Kbps40 = 40,
|
|
||||||
///48_000
|
|
||||||
Kbps48 = 48,
|
|
||||||
///64_000
|
|
||||||
Kbps64 = 64,
|
|
||||||
///80_000
|
|
||||||
Kbps80 = 80,
|
|
||||||
///96_000
|
|
||||||
Kbps96 = 96,
|
|
||||||
///112_000
|
|
||||||
Kbps112 = 112,
|
|
||||||
///128_000
|
|
||||||
Kbps128 = 128,
|
|
||||||
///160_000
|
|
||||||
Kbps160 = 160,
|
|
||||||
///192_000
|
|
||||||
Kbps192 = 192,
|
|
||||||
///224_000
|
|
||||||
Kbps224 = 224,
|
|
||||||
///256_000
|
|
||||||
Kbps256 = 256,
|
|
||||||
///320_000
|
|
||||||
Kbps320 = 320,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Bitrate> for mp3lame_encoder::Bitrate {
|
|
||||||
fn from(value: Bitrate) -> Self {
|
|
||||||
match value {
|
|
||||||
Bitrate::Kbps8 => mp3lame_encoder::Bitrate::Kbps8,
|
|
||||||
Bitrate::Kbps16 => mp3lame_encoder::Bitrate::Kbps16,
|
|
||||||
Bitrate::Kbps24 => mp3lame_encoder::Bitrate::Kbps24,
|
|
||||||
Bitrate::Kbps32 => mp3lame_encoder::Bitrate::Kbps32,
|
|
||||||
Bitrate::Kbps40 => mp3lame_encoder::Bitrate::Kbps40,
|
|
||||||
Bitrate::Kbps48 => mp3lame_encoder::Bitrate::Kbps48,
|
|
||||||
Bitrate::Kbps64 => mp3lame_encoder::Bitrate::Kbps64,
|
|
||||||
Bitrate::Kbps80 => mp3lame_encoder::Bitrate::Kbps80,
|
|
||||||
Bitrate::Kbps96 => mp3lame_encoder::Bitrate::Kbps96,
|
|
||||||
Bitrate::Kbps112 => mp3lame_encoder::Bitrate::Kbps112,
|
|
||||||
Bitrate::Kbps128 => mp3lame_encoder::Bitrate::Kbps128,
|
|
||||||
Bitrate::Kbps160 => mp3lame_encoder::Bitrate::Kbps160,
|
|
||||||
Bitrate::Kbps192 => mp3lame_encoder::Bitrate::Kbps192,
|
|
||||||
Bitrate::Kbps224 => mp3lame_encoder::Bitrate::Kbps224,
|
|
||||||
Bitrate::Kbps256 => mp3lame_encoder::Bitrate::Kbps256,
|
|
||||||
Bitrate::Kbps320 => mp3lame_encoder::Bitrate::Kbps320,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[napi]
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
/// MPEG mode
|
|
||||||
pub enum Mode {
|
|
||||||
Mono,
|
|
||||||
Stereo,
|
|
||||||
JointStereo,
|
|
||||||
DualChannel,
|
|
||||||
NotSet,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Mode> for mp3lame_encoder::Mode {
|
|
||||||
fn from(value: Mode) -> Self {
|
|
||||||
match value {
|
|
||||||
Mode::Mono => mp3lame_encoder::Mode::Mono,
|
|
||||||
Mode::Stereo => mp3lame_encoder::Mode::Stereo,
|
|
||||||
Mode::JointStereo => mp3lame_encoder::Mode::JointStereo,
|
|
||||||
Mode::DualChannel => mp3lame_encoder::Mode::DaulChannel,
|
|
||||||
Mode::NotSet => mp3lame_encoder::Mode::NotSet,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[napi(object, object_to_js = false)]
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct EncodeOptions {
|
|
||||||
pub channels: u32,
|
|
||||||
pub quality: Option<Quality>,
|
|
||||||
pub bitrate: Option<Bitrate>,
|
|
||||||
pub sample_rate: Option<u32>,
|
|
||||||
pub mode: Option<Mode>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[napi]
|
|
||||||
pub struct Mp3Encoder {
|
|
||||||
encoder: Encoder,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[napi]
|
|
||||||
impl Mp3Encoder {
|
|
||||||
#[napi(constructor)]
|
|
||||||
pub fn new(options: EncodeOptions) -> Result<Self> {
|
|
||||||
let mut builder = Builder::new().ok_or(LameError::CreateBuilderFailed)?;
|
|
||||||
builder
|
|
||||||
.set_num_channels(options.channels as u8)
|
|
||||||
.map_err(LameError::BuildError)?;
|
|
||||||
if let Some(quality) = options.quality {
|
|
||||||
builder
|
|
||||||
.set_quality(quality.into())
|
|
||||||
.map_err(LameError::BuildError)?;
|
|
||||||
}
|
|
||||||
if let Some(bitrate) = options.bitrate {
|
|
||||||
builder
|
|
||||||
.set_brate(bitrate.into())
|
|
||||||
.map_err(LameError::BuildError)?;
|
|
||||||
}
|
|
||||||
if let Some(sample_rate) = options.sample_rate {
|
|
||||||
builder
|
|
||||||
.set_sample_rate(sample_rate)
|
|
||||||
.map_err(LameError::BuildError)?;
|
|
||||||
}
|
|
||||||
if let Some(mode) = options.mode {
|
|
||||||
builder
|
|
||||||
.set_mode(mode.into())
|
|
||||||
.map_err(LameError::BuildError)?;
|
|
||||||
}
|
|
||||||
Ok(Self {
|
|
||||||
encoder: builder.build().map_err(LameError::BuildError)?,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
#[napi]
|
|
||||||
pub fn encode(&mut self, input: &[f32]) -> Result<Uint8Array> {
|
|
||||||
let mut output = Vec::with_capacity(input.len());
|
|
||||||
output.reserve(mp3lame_encoder::max_required_buffer_size(input.len()));
|
|
||||||
let encoded_size = self
|
|
||||||
.encoder
|
|
||||||
.encode(MonoPcm(input), output.spare_capacity_mut())
|
|
||||||
.map_err(LameError::EncodeError)?;
|
|
||||||
unsafe {
|
|
||||||
output.set_len(output.len().wrapping_add(encoded_size));
|
|
||||||
}
|
|
||||||
let encoded_size = self
|
|
||||||
.encoder
|
|
||||||
.flush::<FlushNoGap>(output.spare_capacity_mut())
|
|
||||||
.map_err(LameError::EncodeError)?;
|
|
||||||
unsafe {
|
|
||||||
output.set_len(output.len().wrapping_add(encoded_size));
|
|
||||||
}
|
|
||||||
Ok(output.into())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user