chore: remove lame encoder (#11529)

This commit is contained in:
darkskygit
2025-04-08 05:02:30 +00:00
parent 49c6ad7c04
commit 558e84138c
15 changed files with 111 additions and 418 deletions

31
Cargo.lock generated
View File

@@ -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"

View File

@@ -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" }

View File

@@ -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.

View 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));
}
}

View File

@@ -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);

View File

@@ -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',
}); });

View File

@@ -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"
/> />

View File

@@ -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>
); );

View File

@@ -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;
} }

View File

@@ -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);
});
});

View File

@@ -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

View File

@@ -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

View File

@@ -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 }

View File

@@ -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;

View File

@@ -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())
}
}