mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-04 00:28:33 +00:00
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",
|
||||
"dispatch2",
|
||||
"libc",
|
||||
"mp3lame-encoder",
|
||||
"napi",
|
||||
"napi-build",
|
||||
"napi-derive",
|
||||
@@ -358,15 +357,6 @@ version = "1.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
|
||||
|
||||
[[package]]
|
||||
name = "autotools"
|
||||
version = "0.2.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ef941527c41b0fc0dd48511a8154cd5fc7e29200a0ff8b7203c5d777dbc795cf"
|
||||
dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "backtrace"
|
||||
version = "0.3.74"
|
||||
@@ -2212,27 +2202,6 @@ dependencies = [
|
||||
"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]]
|
||||
name = "nanoid"
|
||||
version = "0.4.0"
|
||||
|
||||
@@ -31,7 +31,6 @@ homedir = "0.3"
|
||||
infer = { version = "0.19.0" }
|
||||
libc = "0.2"
|
||||
mimalloc = "0.1"
|
||||
mp3lame-encoder = "0.2"
|
||||
napi = { version = "3.0.0-alpha.31", features = ["async", "chrono_date", "error_anyhow", "napi9", "serde"] }
|
||||
napi-build = { version = "2" }
|
||||
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.
|
||||
- [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.
|
||||
- [lame](https://lame.sourceforge.io/) - High quality MPEG Audio Layer III (MP3) encoder.
|
||||
- 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.
|
||||
|
||||
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 {
|
||||
// Upload the audio file
|
||||
uploadResult = await fileManager.uploadFile(audioFilePath, {
|
||||
mimeType: 'audio/mp3',
|
||||
displayName: 'audio_transcription.mp3',
|
||||
mimeType: 'audio/wav',
|
||||
displayName: 'audio_transcription.wav',
|
||||
});
|
||||
console.log('File uploaded:', uploadResult.file.uri);
|
||||
|
||||
|
||||
@@ -5,8 +5,6 @@ import path from 'node:path';
|
||||
import {
|
||||
type Application,
|
||||
type AudioTapStream,
|
||||
Bitrate,
|
||||
Mp3Encoder,
|
||||
ShareableContent,
|
||||
type TappableApplication,
|
||||
} from '@affine/native';
|
||||
@@ -19,6 +17,7 @@ import { debounce } from 'lodash-es';
|
||||
import multer from 'multer';
|
||||
import { Server } from 'socket.io';
|
||||
|
||||
import { createWavBuffer } from './encode';
|
||||
import { gemini, type TranscriptionResult } from './gemini';
|
||||
|
||||
// Constants
|
||||
@@ -206,36 +205,34 @@ async function saveRecording(recording: Recording): Promise<string | null> {
|
||||
const recordingDir = path.join(RECORDING_DIR, sanitizedFilename);
|
||||
await fs.ensureDir(recordingDir);
|
||||
|
||||
const mp3Filename = path.join(recordingDir, 'recording.mp3');
|
||||
const transcriptionMp3Filename = path.join(
|
||||
const wavFilename = path.join(recordingDir, 'recording.wav');
|
||||
const transcriptionWavFilename = path.join(
|
||||
recordingDir,
|
||||
'transcription.mp3'
|
||||
'transcription.wav'
|
||||
);
|
||||
const metadataFilename = path.join(recordingDir, 'metadata.json');
|
||||
const iconFilename = path.join(recordingDir, 'icon.png');
|
||||
|
||||
// Save MP3 file with the actual sample rate from the stream
|
||||
console.log(`📝 Writing MP3 file to ${mp3Filename}`);
|
||||
const mp3Encoder = new Mp3Encoder({
|
||||
channels: channelCount,
|
||||
sampleRate: actualSampleRate,
|
||||
});
|
||||
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}`
|
||||
console.log(`📝 Muxing Wav buffer ${wavFilename}`);
|
||||
const wavBuffer = new Uint8Array(
|
||||
createWavBuffer(buffer, {
|
||||
sampleRate: actualSampleRate,
|
||||
numChannels: channelCount,
|
||||
})
|
||||
);
|
||||
const transcriptionMp3Encoder = new Mp3Encoder({
|
||||
channels: channelCount,
|
||||
bitrate: Bitrate.Kbps8,
|
||||
sampleRate: actualSampleRate,
|
||||
});
|
||||
const transcriptionMp3Data = transcriptionMp3Encoder.encode(buffer);
|
||||
await fs.writeFile(transcriptionMp3Filename, transcriptionMp3Data);
|
||||
console.log('✅ Transcription MP3 file written successfully');
|
||||
|
||||
// Save Wav file with the actual sample rate from the stream
|
||||
console.log(`📝 Writing Wav file to ${wavFilename}`);
|
||||
await fs.writeFile(wavFilename, wavBuffer);
|
||||
console.log('✅ Wav file written successfully');
|
||||
|
||||
// Save low-quality Wav file for transcription (8kHz)
|
||||
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
|
||||
if (app?.icon) {
|
||||
@@ -367,7 +364,7 @@ async function stopRecording(processId: number) {
|
||||
// File management
|
||||
async function getRecordings(): Promise<
|
||||
{
|
||||
mp3: string;
|
||||
wav: string;
|
||||
metadata?: RecordingMetadata;
|
||||
transcription?: TranscriptionMetadata;
|
||||
}[]
|
||||
@@ -411,7 +408,7 @@ async function getRecordings(): Promise<
|
||||
if (transcriptionExists) {
|
||||
transcription = await fs.readJson(transcriptionPath);
|
||||
} 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 = {
|
||||
transcriptionStartTime: 0,
|
||||
transcriptionEndTime: 0,
|
||||
@@ -423,7 +420,7 @@ async function getRecordings(): Promise<
|
||||
}
|
||||
|
||||
return {
|
||||
mp3: dir,
|
||||
wav: dir,
|
||||
metadata,
|
||||
transcription,
|
||||
};
|
||||
@@ -473,21 +470,21 @@ async function setupRecordingsWatcher() {
|
||||
// Handle file events
|
||||
fsWatcher
|
||||
.on('add', async path => {
|
||||
if (path.endsWith('.mp3') || path.endsWith('.json')) {
|
||||
if (path.endsWith('.wav') || path.endsWith('.json')) {
|
||||
console.log(`📝 File added: ${path}`);
|
||||
const files = await getRecordings();
|
||||
io.emit('apps:saved', { recordings: files });
|
||||
}
|
||||
})
|
||||
.on('change', async path => {
|
||||
if (path.endsWith('.mp3') || path.endsWith('.json')) {
|
||||
if (path.endsWith('.wav') || path.endsWith('.json')) {
|
||||
console.log(`📝 File changed: ${path}`);
|
||||
const files = await getRecordings();
|
||||
io.emit('apps:saved', { recordings: files });
|
||||
}
|
||||
})
|
||||
.on('unlink', async path => {
|
||||
if (path.endsWith('.mp3') || path.endsWith('.json')) {
|
||||
if (path.endsWith('.wav') || path.endsWith('.json')) {
|
||||
console.log(`🗑️ File removed: ${path}`);
|
||||
const files = await getRecordings();
|
||||
io.emit('apps:saved', { recordings: files });
|
||||
@@ -797,11 +794,11 @@ app.post(
|
||||
// Check if directory exists
|
||||
await fs.access(recordingDir);
|
||||
|
||||
const transcriptionMp3Path = `${recordingDir}/transcription.mp3`;
|
||||
const transcriptionWavPath = `${recordingDir}/transcription.wav`;
|
||||
const transcriptionMetadataPath = `${recordingDir}/transcription.json`;
|
||||
|
||||
// Check if transcription file exists
|
||||
await fs.access(transcriptionMp3Path);
|
||||
await fs.access(transcriptionWavPath);
|
||||
|
||||
// Create initial transcription metadata
|
||||
const initialMetadata: TranscriptionMetadata = {
|
||||
@@ -814,7 +811,7 @@ app.post(
|
||||
// Notify clients that transcription has started
|
||||
io.emit('apps:recording-transcription-start', { filename: foldername });
|
||||
|
||||
const transcription = await gemini(transcriptionMp3Path, {
|
||||
const transcription = await gemini(transcriptionWavPath, {
|
||||
mode: 'transcript',
|
||||
});
|
||||
|
||||
|
||||
@@ -591,7 +591,7 @@ export function SavedRecordingItem({
|
||||
|
||||
const metadata = recording.metadata;
|
||||
// Ensure we have a valid filename, fallback to an empty string if undefined
|
||||
const fileName = recording.mp3 || '';
|
||||
const fileName = recording.wav || '';
|
||||
const recordingDate = metadata
|
||||
? new Date(metadata.recordingStartTime).toLocaleString()
|
||||
: 'Unknown date';
|
||||
@@ -638,7 +638,7 @@ export function SavedRecordingItem({
|
||||
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) {
|
||||
throw new Error(
|
||||
`Failed to fetch audio file (${response.status}): ${response.statusText}`
|
||||
@@ -754,11 +754,11 @@ export function SavedRecordingItem({
|
||||
|
||||
try {
|
||||
// Check if filename is valid
|
||||
if (!recording.mp3) {
|
||||
if (!recording.wav) {
|
||||
throw new Error('Invalid recording filename');
|
||||
}
|
||||
|
||||
const response = await fetch(`/api/recordings/${recording.mp3}`, {
|
||||
const response = await fetch(`/api/recordings/${recording.wav}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
|
||||
@@ -782,7 +782,7 @@ export function SavedRecordingItem({
|
||||
} finally {
|
||||
setIsDeleting(false);
|
||||
}
|
||||
}, [recording.mp3]);
|
||||
}, [recording.wav]);
|
||||
|
||||
const handleDeleteClick = React.useCallback(() => {
|
||||
void handleDelete().catch(err => {
|
||||
@@ -796,7 +796,7 @@ export function SavedRecordingItem({
|
||||
socket.on(
|
||||
'apps:recording-transcription-start',
|
||||
(data: { filename: string }) => {
|
||||
if (recording.mp3 && data.filename === recording.mp3) {
|
||||
if (recording.wav && data.filename === recording.wav) {
|
||||
setTranscriptionError(null);
|
||||
}
|
||||
}
|
||||
@@ -810,7 +810,7 @@ export function SavedRecordingItem({
|
||||
transcription?: 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');
|
||||
}
|
||||
}
|
||||
@@ -820,17 +820,17 @@ export function SavedRecordingItem({
|
||||
socket.off('apps:recording-transcription-start');
|
||||
socket.off('apps:recording-transcription-end');
|
||||
};
|
||||
}, [recording.mp3]);
|
||||
}, [recording.wav]);
|
||||
|
||||
const handleTranscribe = React.useCallback(async () => {
|
||||
try {
|
||||
// Check if filename is valid
|
||||
if (!recording.mp3) {
|
||||
if (!recording.wav) {
|
||||
throw new Error('Invalid recording filename');
|
||||
}
|
||||
|
||||
const response = await fetch(
|
||||
`/api/recordings/${recording.mp3}/transcribe`,
|
||||
`/api/recordings/${recording.wav}/transcribe`,
|
||||
{
|
||||
method: 'POST',
|
||||
}
|
||||
@@ -845,7 +845,7 @@ export function SavedRecordingItem({
|
||||
err instanceof Error ? err.message : 'Failed to start transcription'
|
||||
);
|
||||
}
|
||||
}, [recording.mp3]);
|
||||
}, [recording.wav]);
|
||||
|
||||
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">
|
||||
@@ -876,7 +876,7 @@ export function SavedRecordingItem({
|
||||
/>
|
||||
<audio
|
||||
ref={audioRef}
|
||||
src={fileName ? `/api/recordings/${fileName}/recording.mp3` : ''}
|
||||
src={fileName ? `/api/recordings/${fileName}/recording.wav` : ''}
|
||||
preload="metadata"
|
||||
className="hidden"
|
||||
/>
|
||||
|
||||
@@ -34,7 +34,7 @@ export function SavedRecordings(): React.ReactElement {
|
||||
return (
|
||||
<div className="space-y-1">
|
||||
{recordings.map(recording => (
|
||||
<SavedRecordingItem key={recording.mp3} recording={recording} />
|
||||
<SavedRecordingItem key={recording.wav} recording={recording} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -54,7 +54,7 @@ export interface TranscriptionMetadata {
|
||||
}
|
||||
|
||||
export interface SavedRecording {
|
||||
mp3: string;
|
||||
wav: string;
|
||||
metadata?: RecordingMetadata;
|
||||
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>
|
||||
}
|
||||
|
||||
export declare class Mp3Encoder {
|
||||
constructor(options: EncodeOptions)
|
||||
encode(input: Float32Array): Uint8Array
|
||||
}
|
||||
|
||||
export declare class RecordingPermissions {
|
||||
audio: boolean
|
||||
screen: boolean
|
||||
@@ -135,42 +130,6 @@ export declare class TappableApplication {
|
||||
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 {
|
||||
key: string
|
||||
data: Uint8Array
|
||||
@@ -212,14 +171,6 @@ export interface DocUpdate {
|
||||
bin: Uint8Array
|
||||
}
|
||||
|
||||
export interface EncodeOptions {
|
||||
channels: number
|
||||
quality?: Quality
|
||||
bitrate?: Bitrate
|
||||
sampleRate?: number
|
||||
mode?: Mode
|
||||
}
|
||||
|
||||
export interface InsertRow {
|
||||
docId?: string
|
||||
data: Uint8Array
|
||||
@@ -234,42 +185,6 @@ export interface ListedBlob {
|
||||
|
||||
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 {
|
||||
key: string
|
||||
data: Uint8Array
|
||||
|
||||
@@ -35,7 +35,11 @@ const isMuslFromFilesystem = () => {
|
||||
}
|
||||
|
||||
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) {
|
||||
return null
|
||||
}
|
||||
@@ -376,16 +380,12 @@ module.exports.ApplicationStateChangedSubscriber = nativeBinding.ApplicationStat
|
||||
module.exports.AudioTapStream = nativeBinding.AudioTapStream
|
||||
module.exports.DocStorage = nativeBinding.DocStorage
|
||||
module.exports.DocStoragePool = nativeBinding.DocStoragePool
|
||||
module.exports.Mp3Encoder = nativeBinding.Mp3Encoder
|
||||
module.exports.RecordingPermissions = nativeBinding.RecordingPermissions
|
||||
module.exports.ShareableContent = nativeBinding.ShareableContent
|
||||
module.exports.SqliteConnection = nativeBinding.SqliteConnection
|
||||
module.exports.TappableApplication = nativeBinding.TappableApplication
|
||||
module.exports.Bitrate = nativeBinding.Bitrate
|
||||
module.exports.decodeAudio = nativeBinding.decodeAudio
|
||||
module.exports.decodeAudioSync = nativeBinding.decodeAudioSync
|
||||
module.exports.mintChallengeResponse = nativeBinding.mintChallengeResponse
|
||||
module.exports.Mode = nativeBinding.Mode
|
||||
module.exports.Quality = nativeBinding.Quality
|
||||
module.exports.ValidationResult = nativeBinding.ValidationResult
|
||||
module.exports.verifyChallengeResponse = nativeBinding.verifyChallengeResponse
|
||||
|
||||
@@ -7,7 +7,6 @@ version = "0.0.0"
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
mp3lame-encoder = { workspace = true, features = ["std"] }
|
||||
napi = { workspace = true, features = ["napi4"] }
|
||||
napi-derive = { workspace = true, features = ["type-def"] }
|
||||
rubato = { workspace = true }
|
||||
|
||||
@@ -3,4 +3,3 @@ pub mod macos;
|
||||
#[cfg(target_os = "macos")]
|
||||
pub(crate) use macos::*;
|
||||
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