mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-04 08:38:34 +00:00
feat(native): decode audio and mp3 encoder (#10490)
This commit is contained in:
2
.github/actions/build-rust/action.yml
vendored
2
.github/actions/build-rust/action.yml
vendored
@@ -44,7 +44,7 @@ runs:
|
|||||||
RUSTUP_HOME: ${{ env.DEV_DRIVE }}/.rustup
|
RUSTUP_HOME: ${{ env.DEV_DRIVE }}/.rustup
|
||||||
|
|
||||||
- name: Set CC
|
- name: Set CC
|
||||||
if: ${{ contains(inputs.target, 'linux') && inputs.package != '@affine/native' && inputs.no-build != 'true' }}
|
if: ${{ contains(inputs.target, 'linux') && inputs.no-build != 'true' }}
|
||||||
working-directory: ${{ env.DEV_DRIVE_WORKSPACE || github.workspace }}
|
working-directory: ${{ env.DEV_DRIVE_WORKSPACE || github.workspace }}
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
23
.github/workflows/build-test.yml
vendored
23
.github/workflows/build-test.yml
vendored
@@ -387,6 +387,28 @@ jobs:
|
|||||||
path: dist.tar.gz
|
path: dist.tar.gz
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
|
native-unit-test:
|
||||||
|
name: Native Unit Test
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs:
|
||||||
|
- optimize_ci
|
||||||
|
- build-native
|
||||||
|
if: needs.optimize_ci.outputs.skip == 'false'
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: ./.github/actions/setup-node
|
||||||
|
with:
|
||||||
|
extra-flags: workspaces focus @affine-tools/cli @affine/monorepo @affine/native
|
||||||
|
electron-install: false
|
||||||
|
- name: Download affine.linux-x64-gnu.node
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: affine.linux-x64-gnu.node
|
||||||
|
path: ./packages/frontend/native
|
||||||
|
- name: Unit Test
|
||||||
|
run: yarn affine @affine/native test
|
||||||
|
|
||||||
server-test:
|
server-test:
|
||||||
name: Server Test
|
name: Server Test
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -897,6 +919,7 @@ jobs:
|
|||||||
- build-native
|
- build-native
|
||||||
- build-server-native
|
- build-server-native
|
||||||
- build-electron-renderer
|
- build-electron-renderer
|
||||||
|
- native-unit-test
|
||||||
- server-test
|
- server-test
|
||||||
- rust-test
|
- rust-test
|
||||||
- copilot-api-test
|
- copilot-api-test
|
||||||
|
|||||||
275
Cargo.lock
generated
275
Cargo.lock
generated
@@ -70,6 +70,7 @@ dependencies = [
|
|||||||
"coreaudio-rs",
|
"coreaudio-rs",
|
||||||
"dispatch2",
|
"dispatch2",
|
||||||
"libc",
|
"libc",
|
||||||
|
"mp3lame-encoder",
|
||||||
"napi",
|
"napi",
|
||||||
"napi-build",
|
"napi-build",
|
||||||
"napi-derive",
|
"napi-derive",
|
||||||
@@ -77,6 +78,7 @@ dependencies = [
|
|||||||
"objc2-foundation",
|
"objc2-foundation",
|
||||||
"rubato",
|
"rubato",
|
||||||
"screencapturekit",
|
"screencapturekit",
|
||||||
|
"symphonia",
|
||||||
"thiserror 2.0.11",
|
"thiserror 2.0.11",
|
||||||
"uuid",
|
"uuid",
|
||||||
]
|
]
|
||||||
@@ -287,6 +289,12 @@ dependencies = [
|
|||||||
"derive_arbitrary",
|
"derive_arbitrary",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "arrayvec"
|
||||||
|
version = "0.7.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "async-compat"
|
name = "async-compat"
|
||||||
version = "0.2.4"
|
version = "0.2.4"
|
||||||
@@ -339,6 +347,15 @@ 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"
|
||||||
@@ -501,6 +518,12 @@ version = "3.17.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf"
|
checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bytemuck"
|
||||||
|
version = "1.21.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ef657dfab802224e671f5818e9a4935f9b1957ed18e58292690cc39e7a4092a3"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "byteorder"
|
name = "byteorder"
|
||||||
version = "1.5.0"
|
version = "1.5.0"
|
||||||
@@ -953,9 +976,18 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ctor"
|
name = "ctor"
|
||||||
version = "0.3.0"
|
version = "0.3.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f06b1425736ba96096116f063c9d10be2352a7cde0cbea829a717008e114aec9"
|
checksum = "21d960ecacd0a1bf55e73144b72de745e7bf275c7952c50e36e8af0a0cb7ab1f"
|
||||||
|
dependencies = [
|
||||||
|
"ctor-proc-macro",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ctor-proc-macro"
|
||||||
|
version = "0.0.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c426d2ba3e525b39c1f0a9ba41b9fe61878dee11fa4e4a76b6ab440f46c5db5d"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dashmap"
|
name = "dashmap"
|
||||||
@@ -1174,6 +1206,12 @@ dependencies = [
|
|||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "extended"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "af9673d8203fcb076b19dfd17e38b3d4ae9f44959416ea532ce72415a6020365"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fancy-regex"
|
name = "fancy-regex"
|
||||||
version = "0.13.0"
|
version = "0.13.0"
|
||||||
@@ -2094,6 +2132,27 @@ 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.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "acaec8842b2ebd61692a6c8c2b9f3edbf5c36e5e5c4677b5911430eaf859377c"
|
||||||
|
dependencies = [
|
||||||
|
"autotools",
|
||||||
|
"cc",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nanoid"
|
name = "nanoid"
|
||||||
version = "0.4.0"
|
version = "0.4.0"
|
||||||
@@ -2105,9 +2164,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "napi"
|
name = "napi"
|
||||||
version = "3.0.0-alpha.28"
|
version = "3.0.0-alpha.31"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0dd957e2cc4bd62b730b10ff1f35775f8a81dac84a3bfac273b0ec4336f53ab8"
|
checksum = "b1911b4f0d33fbcb5f46ff68319ec053ab8a655f3a17440eae1246a23ba2ad78"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bitflags 2.8.0",
|
"bitflags 2.8.0",
|
||||||
@@ -2121,15 +2180,15 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "napi-build"
|
name = "napi-build"
|
||||||
version = "2.1.4"
|
version = "2.1.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "db836caddef23662b94e16bf1f26c40eceb09d6aee5d5b06a7ac199320b69b19"
|
checksum = "40685973218af4aa4b42486652692c294c44b5a67e4b2202df721c9063f2e51c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "napi-derive"
|
name = "napi-derive"
|
||||||
version = "3.0.0-alpha.26"
|
version = "3.0.0-alpha.28"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a0f0b6f3f77925d8fd2030855af659ce428a7bb6e10e94852e226f509186ba7c"
|
checksum = "c8097918a9af1976700eac6944b120b65ad17bf6d38906703d2b68e17ee89256"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"convert_case 0.7.1",
|
"convert_case 0.7.1",
|
||||||
"napi-derive-backend",
|
"napi-derive-backend",
|
||||||
@@ -2140,9 +2199,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "napi-derive-backend"
|
name = "napi-derive-backend"
|
||||||
version = "2.0.0-alpha.26"
|
version = "2.0.0-alpha.27"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c694bb49a2fa84dd9542d51eece39a57519f9cf1fc2deefa9d119ab8181e374d"
|
checksum = "8e5adc92fcdec3aa09f591bd2b139d7c669399f34b8211fe653641b52d40d3b3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"convert_case 0.7.1",
|
"convert_case 0.7.1",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
@@ -3558,6 +3617,202 @@ version = "2.6.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
|
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "symphonia"
|
||||||
|
version = "0.5.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "815c942ae7ee74737bb00f965fa5b5a2ac2ce7b6c01c0cc169bbeaf7abd5f5a9"
|
||||||
|
dependencies = [
|
||||||
|
"lazy_static",
|
||||||
|
"symphonia-bundle-flac",
|
||||||
|
"symphonia-bundle-mp3",
|
||||||
|
"symphonia-codec-aac",
|
||||||
|
"symphonia-codec-adpcm",
|
||||||
|
"symphonia-codec-alac",
|
||||||
|
"symphonia-codec-pcm",
|
||||||
|
"symphonia-codec-vorbis",
|
||||||
|
"symphonia-core",
|
||||||
|
"symphonia-format-caf",
|
||||||
|
"symphonia-format-isomp4",
|
||||||
|
"symphonia-format-mkv",
|
||||||
|
"symphonia-format-ogg",
|
||||||
|
"symphonia-format-riff",
|
||||||
|
"symphonia-metadata",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "symphonia-bundle-flac"
|
||||||
|
version = "0.5.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "72e34f34298a7308d4397a6c7fbf5b84c5d491231ce3dd379707ba673ab3bd97"
|
||||||
|
dependencies = [
|
||||||
|
"log",
|
||||||
|
"symphonia-core",
|
||||||
|
"symphonia-metadata",
|
||||||
|
"symphonia-utils-xiph",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "symphonia-bundle-mp3"
|
||||||
|
version = "0.5.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c01c2aae70f0f1fb096b6f0ff112a930b1fb3626178fba3ae68b09dce71706d4"
|
||||||
|
dependencies = [
|
||||||
|
"lazy_static",
|
||||||
|
"log",
|
||||||
|
"symphonia-core",
|
||||||
|
"symphonia-metadata",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "symphonia-codec-aac"
|
||||||
|
version = "0.5.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cdbf25b545ad0d3ee3e891ea643ad115aff4ca92f6aec472086b957a58522f70"
|
||||||
|
dependencies = [
|
||||||
|
"lazy_static",
|
||||||
|
"log",
|
||||||
|
"symphonia-core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "symphonia-codec-adpcm"
|
||||||
|
version = "0.5.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c94e1feac3327cd616e973d5be69ad36b3945f16b06f19c6773fc3ac0b426a0f"
|
||||||
|
dependencies = [
|
||||||
|
"log",
|
||||||
|
"symphonia-core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "symphonia-codec-alac"
|
||||||
|
version = "0.5.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2d8a6666649a08412906476a8b0efd9b9733e241180189e9f92b09c08d0e38f3"
|
||||||
|
dependencies = [
|
||||||
|
"log",
|
||||||
|
"symphonia-core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "symphonia-codec-pcm"
|
||||||
|
version = "0.5.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f395a67057c2ebc5e84d7bb1be71cce1a7ba99f64e0f0f0e303a03f79116f89b"
|
||||||
|
dependencies = [
|
||||||
|
"log",
|
||||||
|
"symphonia-core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "symphonia-codec-vorbis"
|
||||||
|
version = "0.5.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5a98765fb46a0a6732b007f7e2870c2129b6f78d87db7987e6533c8f164a9f30"
|
||||||
|
dependencies = [
|
||||||
|
"log",
|
||||||
|
"symphonia-core",
|
||||||
|
"symphonia-utils-xiph",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "symphonia-core"
|
||||||
|
version = "0.5.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "798306779e3dc7d5231bd5691f5a813496dc79d3f56bf82e25789f2094e022c3"
|
||||||
|
dependencies = [
|
||||||
|
"arrayvec",
|
||||||
|
"bitflags 1.3.2",
|
||||||
|
"bytemuck",
|
||||||
|
"lazy_static",
|
||||||
|
"log",
|
||||||
|
"rustfft",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "symphonia-format-caf"
|
||||||
|
version = "0.5.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e43c99c696a388295a29fe71b133079f5d8b18041cf734c5459c35ad9097af50"
|
||||||
|
dependencies = [
|
||||||
|
"log",
|
||||||
|
"symphonia-core",
|
||||||
|
"symphonia-metadata",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "symphonia-format-isomp4"
|
||||||
|
version = "0.5.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "abfdf178d697e50ce1e5d9b982ba1b94c47218e03ec35022d9f0e071a16dc844"
|
||||||
|
dependencies = [
|
||||||
|
"encoding_rs",
|
||||||
|
"log",
|
||||||
|
"symphonia-core",
|
||||||
|
"symphonia-metadata",
|
||||||
|
"symphonia-utils-xiph",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "symphonia-format-mkv"
|
||||||
|
version = "0.5.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1bb43471a100f7882dc9937395bd5ebee8329298e766250b15b3875652fe3d6f"
|
||||||
|
dependencies = [
|
||||||
|
"lazy_static",
|
||||||
|
"log",
|
||||||
|
"symphonia-core",
|
||||||
|
"symphonia-metadata",
|
||||||
|
"symphonia-utils-xiph",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "symphonia-format-ogg"
|
||||||
|
version = "0.5.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ada3505789516bcf00fc1157c67729eded428b455c27ca370e41f4d785bfa931"
|
||||||
|
dependencies = [
|
||||||
|
"log",
|
||||||
|
"symphonia-core",
|
||||||
|
"symphonia-metadata",
|
||||||
|
"symphonia-utils-xiph",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "symphonia-format-riff"
|
||||||
|
version = "0.5.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "05f7be232f962f937f4b7115cbe62c330929345434c834359425e043bfd15f50"
|
||||||
|
dependencies = [
|
||||||
|
"extended",
|
||||||
|
"log",
|
||||||
|
"symphonia-core",
|
||||||
|
"symphonia-metadata",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "symphonia-metadata"
|
||||||
|
version = "0.5.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bc622b9841a10089c5b18e99eb904f4341615d5aa55bbf4eedde1be721a4023c"
|
||||||
|
dependencies = [
|
||||||
|
"encoding_rs",
|
||||||
|
"lazy_static",
|
||||||
|
"log",
|
||||||
|
"symphonia-core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "symphonia-utils-xiph"
|
||||||
|
version = "0.5.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "484472580fa49991afda5f6550ece662237b00c6f562c7d9638d1b086ed010fe"
|
||||||
|
dependencies = [
|
||||||
|
"symphonia-core",
|
||||||
|
"symphonia-metadata",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "syn"
|
name = "syn"
|
||||||
version = "1.0.109"
|
version = "1.0.109"
|
||||||
|
|||||||
@@ -26,9 +26,10 @@ file-format = { version = "0.26", features = ["reader"] }
|
|||||||
homedir = "0.3"
|
homedir = "0.3"
|
||||||
libc = "0.2"
|
libc = "0.2"
|
||||||
mimalloc = "0.1"
|
mimalloc = "0.1"
|
||||||
napi = { version = "3.0.0-alpha.12", features = ["async", "chrono_date", "error_anyhow", "napi9", "serde"] }
|
mp3lame-encoder = "0.2"
|
||||||
|
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.12" }
|
napi-derive = { version = "3.0.0-alpha.28" }
|
||||||
notify = { version = "8", features = ["serde"] }
|
notify = { version = "8", features = ["serde"] }
|
||||||
objc2 = "0.6"
|
objc2 = "0.6"
|
||||||
objc2-foundation = "0.3"
|
objc2-foundation = "0.3"
|
||||||
@@ -42,6 +43,7 @@ serde = "1"
|
|||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
sha3 = "0.10"
|
sha3 = "0.10"
|
||||||
sqlx = { version = "0.8", default-features = false, features = ["chrono", "macros", "migrate", "runtime-tokio", "sqlite", "tls-rustls"] }
|
sqlx = { version = "0.8", default-features = false, features = ["chrono", "macros", "migrate", "runtime-tokio", "sqlite", "tls-rustls"] }
|
||||||
|
symphonia = { version = "0.5", features = ["all", "opt-simd"] }
|
||||||
thiserror = "2"
|
thiserror = "2"
|
||||||
tiktoken-rs = "0.6"
|
tiktoken-rs = "0.6"
|
||||||
tokio = "1.37"
|
tokio = "1.37"
|
||||||
|
|||||||
@@ -160,6 +160,7 @@ 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.
|
||||||
|
|||||||
@@ -33,7 +33,7 @@
|
|||||||
"build:debug": "napi build"
|
"build:debug": "napi build"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@napi-rs/cli": "3.0.0-alpha.70",
|
"@napi-rs/cli": "3.0.0-alpha.72",
|
||||||
"lib0": "^0.2.99",
|
"lib0": "^0.2.99",
|
||||||
"tiktoken": "^1.0.17",
|
"tiktoken": "^1.0.17",
|
||||||
"tinybench": "^3.0.7",
|
"tinybench": "^3.0.7",
|
||||||
|
|||||||
23
packages/frontend/native/__tests__/audio.spec.mts
Normal file
23
packages/frontend/native/__tests__/audio.spec.mts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
BIN
packages/frontend/native/__tests__/fixtures/recording.wav
Normal file
BIN
packages/frontend/native/__tests__/fixtures/recording.wav
Normal file
Binary file not shown.
90
packages/frontend/native/index.d.ts
vendored
90
packages/frontend/native/index.d.ts
vendored
@@ -61,6 +61,11 @@ export declare class DocStoragePool {
|
|||||||
clearClocks(universalId: string): Promise<void>
|
clearClocks(universalId: string): Promise<void>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
||||||
@@ -113,6 +118,42 @@ export declare class SqliteConnection {
|
|||||||
checkpoint(): Promise<void>
|
checkpoint(): Promise<void>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**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
|
||||||
@@ -127,6 +168,11 @@ export interface BlobRow {
|
|||||||
timestamp: Date
|
timestamp: Date
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export declare function decodeAudio(buf: Uint8Array, destSampleRate?: number | undefined | null, filename?: string | undefined | null, signal?: AbortSignal | undefined | null): Promise<Float32Array>
|
||||||
|
|
||||||
|
/** Decode audio file into a Float32Array */
|
||||||
|
export declare function decodeAudioSync(buf: Uint8Array, destSampleRate?: number | undefined | null, filename?: string | undefined | null): Float32Array
|
||||||
|
|
||||||
export interface DocClock {
|
export interface DocClock {
|
||||||
docId: string
|
docId: string
|
||||||
timestamp: Date
|
timestamp: Date
|
||||||
@@ -149,6 +195,14 @@ 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
|
||||||
@@ -163,6 +217,42 @@ 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
|
||||||
|
|||||||
@@ -60,7 +60,13 @@ const isMuslFromChildProcess = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function requireNative() {
|
function requireNative() {
|
||||||
if (process.platform === 'android') {
|
if (process.env.NAPI_RS_NATIVE_LIBRARY_PATH) {
|
||||||
|
try {
|
||||||
|
nativeBinding = require(process.env.NAPI_RS_NATIVE_LIBRARY_PATH);
|
||||||
|
} catch (err) {
|
||||||
|
loadErrors.push(err);
|
||||||
|
}
|
||||||
|
} else if (process.platform === 'android') {
|
||||||
if (process.arch === 'arm64') {
|
if (process.arch === 'arm64') {
|
||||||
try {
|
try {
|
||||||
return require('./affine.android-arm64.node')
|
return require('./affine.android-arm64.node')
|
||||||
@@ -370,9 +376,15 @@ 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.Bitrate = nativeBinding.Bitrate
|
||||||
|
module.exports.decodeAudio = nativeBinding.decodeAudio
|
||||||
|
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,9 +7,12 @@ 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 }
|
||||||
|
symphonia = { workspace = true, features = ["all", "opt-simd"] }
|
||||||
|
thiserror = { workspace = true }
|
||||||
|
|
||||||
[target.'cfg(target_os = "macos")'.dependencies]
|
[target.'cfg(target_os = "macos")'.dependencies]
|
||||||
block2 = { workspace = true }
|
block2 = { workspace = true }
|
||||||
@@ -20,7 +23,6 @@ libc = { workspace = true }
|
|||||||
objc2 = { workspace = true }
|
objc2 = { workspace = true }
|
||||||
objc2-foundation = { workspace = true }
|
objc2-foundation = { workspace = true }
|
||||||
screencapturekit = { workspace = true }
|
screencapturekit = { workspace = true }
|
||||||
thiserror = { workspace = true }
|
|
||||||
uuid = { workspace = true, features = ["v4"] }
|
uuid = { workspace = true, features = ["v4"] }
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
|
|||||||
179
packages/frontend/native/media_capture/src/audio_decoder.rs
Normal file
179
packages/frontend/native/media_capture/src/audio_decoder.rs
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
use std::{io::Cursor, path::Path};
|
||||||
|
|
||||||
|
use napi::{
|
||||||
|
bindgen_prelude::{AbortSignal, AsyncTask, Float32Array, Result, Status, Uint8Array},
|
||||||
|
Task,
|
||||||
|
};
|
||||||
|
use napi_derive::napi;
|
||||||
|
use rubato::{Resampler, SincFixedIn, SincInterpolationParameters, SincInterpolationType};
|
||||||
|
use symphonia::core::{
|
||||||
|
audio::{AudioBuffer, Signal},
|
||||||
|
codecs::DecoderOptions,
|
||||||
|
errors::Error,
|
||||||
|
formats::FormatOptions,
|
||||||
|
io::MediaSourceStream,
|
||||||
|
meta::MetadataOptions,
|
||||||
|
probe::Hint,
|
||||||
|
};
|
||||||
|
|
||||||
|
fn decode<B: AsRef<[u8]> + Send + Sync + 'static>(
|
||||||
|
buf: B,
|
||||||
|
dest_sample_rate: Option<u32>,
|
||||||
|
filename: Option<&str>,
|
||||||
|
) -> std::result::Result<Vec<f32>, Error> {
|
||||||
|
// Create the media source
|
||||||
|
let mss = MediaSourceStream::new(Box::new(Cursor::new(buf)), Default::default());
|
||||||
|
|
||||||
|
// Create a probe hint using the file extension
|
||||||
|
let mut hint = Hint::new();
|
||||||
|
if let Some(ext) =
|
||||||
|
filename.and_then(|filename| Path::new(filename).extension().and_then(|ext| ext.to_str()))
|
||||||
|
{
|
||||||
|
hint.with_extension(ext);
|
||||||
|
}
|
||||||
|
|
||||||
|
let format_opts = FormatOptions {
|
||||||
|
enable_gapless: true,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let metadata_opts = MetadataOptions::default();
|
||||||
|
let decoder_opts = DecoderOptions::default();
|
||||||
|
let probed = symphonia::default::get_probe().format(&hint, mss, &format_opts, &metadata_opts)?;
|
||||||
|
|
||||||
|
let mut format = probed.format;
|
||||||
|
|
||||||
|
let track = format
|
||||||
|
.default_track()
|
||||||
|
.ok_or(Error::Unsupported("No default track found"))?;
|
||||||
|
|
||||||
|
let totol_samples = track
|
||||||
|
.codec_params
|
||||||
|
.n_frames
|
||||||
|
.ok_or(Error::Unsupported("No duration found"))?;
|
||||||
|
let sample_rate = track
|
||||||
|
.codec_params
|
||||||
|
.sample_rate
|
||||||
|
.ok_or(Error::Unsupported("No samplerate found"))?;
|
||||||
|
|
||||||
|
let mut decoder = symphonia::default::get_codecs().make(&track.codec_params, &decoder_opts)?;
|
||||||
|
|
||||||
|
let mut output: Vec<f32> = Vec::with_capacity(totol_samples as usize);
|
||||||
|
// Decode loop
|
||||||
|
while let Ok(packet) = format.next_packet() {
|
||||||
|
let decoded = decoder.decode(&packet)?;
|
||||||
|
let spec = decoded.spec();
|
||||||
|
let mut audio_buf: AudioBuffer<f32> = AudioBuffer::new(decoded.capacity() as u64, *spec);
|
||||||
|
decoded.convert(&mut audio_buf);
|
||||||
|
|
||||||
|
if spec.channels.count() > 1 {
|
||||||
|
// Mix all channels into mono
|
||||||
|
for i in 0..audio_buf.chan(0).len() {
|
||||||
|
let mut sample_sum = 0.0;
|
||||||
|
for ch in 0..spec.channels.count() {
|
||||||
|
sample_sum += audio_buf.chan(ch)[i];
|
||||||
|
}
|
||||||
|
output.push(sample_sum / spec.channels.count() as f32);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
output.extend_from_slice(audio_buf.chan(0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(dest_sample_rate) = dest_sample_rate else {
|
||||||
|
return Ok(output);
|
||||||
|
};
|
||||||
|
|
||||||
|
if sample_rate != dest_sample_rate {
|
||||||
|
// Calculate parameters for resampling
|
||||||
|
let params = SincInterpolationParameters {
|
||||||
|
sinc_len: 256,
|
||||||
|
f_cutoff: 0.95,
|
||||||
|
interpolation: SincInterpolationType::Linear,
|
||||||
|
oversampling_factor: 256,
|
||||||
|
window: rubato::WindowFunction::BlackmanHarris2,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut resampler = SincFixedIn::<f32>::new(
|
||||||
|
dest_sample_rate as f64 / sample_rate as f64,
|
||||||
|
2.0,
|
||||||
|
params,
|
||||||
|
output.len(),
|
||||||
|
1,
|
||||||
|
)
|
||||||
|
.map_err(|_| Error::Unsupported("Failed to create resampler"))?;
|
||||||
|
|
||||||
|
let waves_in = vec![output];
|
||||||
|
let mut waves_out = resampler
|
||||||
|
.process(&waves_in, None)
|
||||||
|
.map_err(|_| Error::Unsupported("Failed to run resampler"))?;
|
||||||
|
output = waves_out
|
||||||
|
.pop()
|
||||||
|
.ok_or(Error::Unsupported("No resampled output found"))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(output)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi]
|
||||||
|
/// Decode audio file into a Float32Array
|
||||||
|
pub fn decode_audio_sync(
|
||||||
|
buf: Uint8Array,
|
||||||
|
dest_sample_rate: Option<u32>,
|
||||||
|
filename: Option<String>,
|
||||||
|
) -> Result<Float32Array> {
|
||||||
|
decode(buf, dest_sample_rate, filename.as_deref())
|
||||||
|
.map(Float32Array::new)
|
||||||
|
.map_err(|e| {
|
||||||
|
napi::Error::new(
|
||||||
|
Status::InvalidArg,
|
||||||
|
format!("Decode audio into Float32Array failed: {e}"),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct DecodeAudioTask {
|
||||||
|
buf: Uint8Array,
|
||||||
|
dest_sample_rate: Option<u32>,
|
||||||
|
filename: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi]
|
||||||
|
impl Task for DecodeAudioTask {
|
||||||
|
type Output = Vec<f32>;
|
||||||
|
type JsValue = Float32Array;
|
||||||
|
|
||||||
|
fn compute(&mut self) -> Result<Self::Output> {
|
||||||
|
decode(
|
||||||
|
std::mem::replace(&mut self.buf, Uint8Array::new(vec![])),
|
||||||
|
self.dest_sample_rate,
|
||||||
|
self.filename.as_deref(),
|
||||||
|
)
|
||||||
|
.map_err(|e| {
|
||||||
|
napi::Error::new(
|
||||||
|
Status::InvalidArg,
|
||||||
|
format!("Decode audio into Float32Array failed: {e}"),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolve(&mut self, _: napi::Env, output: Self::Output) -> Result<Self::JsValue> {
|
||||||
|
Ok(Float32Array::new(output))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi]
|
||||||
|
pub fn decode_audio(
|
||||||
|
buf: Uint8Array,
|
||||||
|
dest_sample_rate: Option<u32>,
|
||||||
|
filename: Option<String>,
|
||||||
|
signal: Option<AbortSignal>,
|
||||||
|
) -> AsyncTask<DecodeAudioTask> {
|
||||||
|
AsyncTask::with_optional_signal(
|
||||||
|
DecodeAudioTask {
|
||||||
|
buf,
|
||||||
|
dest_sample_rate,
|
||||||
|
filename,
|
||||||
|
},
|
||||||
|
signal,
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -2,3 +2,5 @@
|
|||||||
pub mod macos;
|
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 mp3;
|
||||||
|
|||||||
215
packages/frontend/native/media_capture/src/mp3.rs
Normal file
215
packages/frontend/native/media_capture/src/mp3.rs
Normal file
@@ -0,0 +1,215 @@
|
|||||||
|
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]
|
||||||
|
///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)]
|
||||||
|
///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]
|
||||||
|
/// 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)]
|
||||||
|
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())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -25,7 +25,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@napi-rs/cli": "3.0.0-alpha.70",
|
"@napi-rs/cli": "3.0.0-alpha.72",
|
||||||
"@napi-rs/whisper": "^0.0.4",
|
"@napi-rs/whisper": "^0.0.4",
|
||||||
"@types/node": "^22.0.0",
|
"@types/node": "^22.0.0",
|
||||||
"ava": "^6.2.0",
|
"ava": "^6.2.0",
|
||||||
@@ -38,8 +38,8 @@
|
|||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"artifacts": "napi artifacts",
|
"artifacts": "napi artifacts",
|
||||||
"build": "napi build -p affine_native --platform --release --no-const-enum",
|
"build": "napi build -p affine_native --platform --release --no-dts-cache",
|
||||||
"build:debug": "napi build -p affine_native --platform",
|
"build:debug": "napi build -p affine_native --platform --no-dts-cache",
|
||||||
"universal": "napi universal",
|
"universal": "napi universal",
|
||||||
"test": "ava",
|
"test": "ava",
|
||||||
"version": "napi version"
|
"version": "napi version"
|
||||||
|
|||||||
12
yarn.lock
12
yarn.lock
@@ -748,7 +748,7 @@ __metadata:
|
|||||||
version: 0.0.0-use.local
|
version: 0.0.0-use.local
|
||||||
resolution: "@affine/native@workspace:packages/frontend/native"
|
resolution: "@affine/native@workspace:packages/frontend/native"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@napi-rs/cli": "npm:3.0.0-alpha.70"
|
"@napi-rs/cli": "npm:3.0.0-alpha.72"
|
||||||
"@napi-rs/whisper": "npm:^0.0.4"
|
"@napi-rs/whisper": "npm:^0.0.4"
|
||||||
"@types/node": "npm:^22.0.0"
|
"@types/node": "npm:^22.0.0"
|
||||||
ava: "npm:^6.2.0"
|
ava: "npm:^6.2.0"
|
||||||
@@ -799,7 +799,7 @@ __metadata:
|
|||||||
version: 0.0.0-use.local
|
version: 0.0.0-use.local
|
||||||
resolution: "@affine/server-native@workspace:packages/backend/native"
|
resolution: "@affine/server-native@workspace:packages/backend/native"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@napi-rs/cli": "npm:3.0.0-alpha.70"
|
"@napi-rs/cli": "npm:3.0.0-alpha.72"
|
||||||
lib0: "npm:^0.2.99"
|
lib0: "npm:^0.2.99"
|
||||||
tiktoken: "npm:^1.0.17"
|
tiktoken: "npm:^1.0.17"
|
||||||
tinybench: "npm:^3.0.7"
|
tinybench: "npm:^3.0.7"
|
||||||
@@ -8163,9 +8163,9 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@napi-rs/cli@npm:3.0.0-alpha.70":
|
"@napi-rs/cli@npm:3.0.0-alpha.72":
|
||||||
version: 3.0.0-alpha.70
|
version: 3.0.0-alpha.72
|
||||||
resolution: "@napi-rs/cli@npm:3.0.0-alpha.70"
|
resolution: "@napi-rs/cli@npm:3.0.0-alpha.72"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@inquirer/prompts": "npm:^7.0.0"
|
"@inquirer/prompts": "npm:^7.0.0"
|
||||||
"@napi-rs/cross-toolchain": "npm:^0.0.19"
|
"@napi-rs/cross-toolchain": "npm:^0.0.19"
|
||||||
@@ -8192,7 +8192,7 @@ __metadata:
|
|||||||
bin:
|
bin:
|
||||||
napi: ./dist/cli.js
|
napi: ./dist/cli.js
|
||||||
napi-raw: ./cli.mjs
|
napi-raw: ./cli.mjs
|
||||||
checksum: 10/d3a48c1d20c351ef11ee26f04ef1aacad6abb750ba8e30cbbac21d380be8a70580af7b7a7dac05924db25d032a582ac3d0a78d91b88e9ce0ab70eb35184b661b
|
checksum: 10/8575e8eaab880bd3dd41dd3f7fc2e79ca7fcb3197038c4aedc41fcfe46ab10fa34f70b0370cb52325b07ca8ec6115c87f48b8bd7850ca10964933563718a6ede
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user