Compare commits
@@ -16,28 +16,64 @@ jobs:
|
|||||||
- arch: x86_64
|
- arch: x86_64
|
||||||
target: x86_64-unknown-linux-gnu
|
target: x86_64-unknown-linux-gnu
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
sccache_dir: /sccache
|
||||||
- arch: aarch64
|
- arch: aarch64
|
||||||
target: aarch64-unknown-linux-gnu
|
target: aarch64-unknown-linux-gnu
|
||||||
runs-on: aarch64
|
runs-on: aarch64
|
||||||
|
sccache_dir: /var/lib/gitea-runner/sccache
|
||||||
|
|
||||||
runs-on: ${{ matrix.runs-on }}
|
runs-on: ${{ matrix.runs-on }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: builtin:checkout
|
||||||
|
|
||||||
- name: Install Rust stable
|
- name: Install Rust stable
|
||||||
uses: dtolnay/rust-toolchain@stable
|
uses: dtolnay/rust-toolchain@stable
|
||||||
with:
|
with:
|
||||||
targets: ${{ matrix.target }}
|
targets: ${{ matrix.target }}
|
||||||
|
|
||||||
- name: Cache cargo registry
|
# Compiled-crate cache via sccache. x86_64 uses /sccache (a persistent
|
||||||
uses: actions/cache@v3
|
# host dir bind-mounted into the job container by the runner config);
|
||||||
with:
|
# aarch64 runs on the klokka host and uses a runner-owned dir. Both
|
||||||
path: ~/.cargo/registry
|
# persist across releases, unlike the (unreachable) Gitea cache service.
|
||||||
key: ${{ runner.os }}-${{ matrix.arch }}-cargo-${{ hashFiles('Cargo.lock') }}
|
- name: Set up sccache
|
||||||
|
run: |
|
||||||
|
echo "RUSTC_WRAPPER=sccache" >> "$GITHUB_ENV"
|
||||||
|
echo "SCCACHE_DIR=${{ matrix.sccache_dir }}" >> "$GITHUB_ENV"
|
||||||
|
mkdir -p "${{ matrix.sccache_dir }}"
|
||||||
|
if command -v sccache >/dev/null 2>&1; then sccache --version; exit 0; fi
|
||||||
|
# Bare runners (aarch64 = klokka host) are pre-provisioned; CI must not
|
||||||
|
# install onto them (and this tarball is x86_64-only). Only the
|
||||||
|
# ephemeral x86_64 container installs; a bare host missing it fails loud.
|
||||||
|
if [ "${{ matrix.arch }}" != x86_64 ]; then
|
||||||
|
echo "::error::sccache missing on the bare ${{ matrix.arch }} runner — provision klokka; CI must not install on bare hosts"; exit 1
|
||||||
|
fi
|
||||||
|
V=0.8.2
|
||||||
|
curl -sSL "https://github.com/mozilla/sccache/releases/download/v${V}/sccache-v${V}-x86_64-unknown-linux-musl.tar.gz" | tar -xz
|
||||||
|
sudo install -m0755 "sccache-v${V}-x86_64-unknown-linux-musl/sccache" /usr/local/bin/sccache
|
||||||
|
sccache --version
|
||||||
|
|
||||||
|
- name: Ensure audio build deps (libpipewire + clang for native capture)
|
||||||
|
run: |
|
||||||
|
if command -v apt-get >/dev/null; then
|
||||||
|
# Ephemeral container runner: safe to install into it.
|
||||||
|
SUDO=""; command -v sudo >/dev/null && SUDO=sudo
|
||||||
|
$SUDO apt-get update
|
||||||
|
$SUDO apt-get install -y libpipewire-0.3-dev libspa-0.2-dev clang libclang-dev pkg-config
|
||||||
|
else
|
||||||
|
# Bare/host runner: never mutate it - require deps preinstalled.
|
||||||
|
missing=""
|
||||||
|
pkg-config --exists libpipewire-0.3 2>/dev/null || missing="$missing libpipewire(+dev headers)"
|
||||||
|
command -v clang >/dev/null || missing="$missing clang"
|
||||||
|
if [ -n "$missing" ]; then
|
||||||
|
echo "::error::bare runner missing build deps:$missing — install once on the host; CI does not modify bare runners"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "bare runner already has libpipewire + clang"
|
||||||
|
fi
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: cargo build --release --target ${{ matrix.target }}
|
run: cargo build --release --locked --target ${{ matrix.target }}
|
||||||
|
|
||||||
- name: Package
|
- name: Package
|
||||||
run: |
|
run: |
|
||||||
@@ -53,7 +89,7 @@ jobs:
|
|||||||
- name: Create release
|
- name: Create release
|
||||||
run: |
|
run: |
|
||||||
curl -sX POST \
|
curl -sX POST \
|
||||||
-H "Authorization: token ${{ secrets.GITEA_TOKEN }}" \
|
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
"${{ gitea.server_url }}/api/v1/repos/${{ gitea.repository }}/releases" \
|
"${{ gitea.server_url }}/api/v1/repos/${{ gitea.repository }}/releases" \
|
||||||
-d "{\"tag_name\":\"${{ gitea.ref_name }}\",\"name\":\"${{ gitea.ref_name }}\"}" \
|
-d "{\"tag_name\":\"${{ gitea.ref_name }}\",\"name\":\"${{ gitea.ref_name }}\"}" \
|
||||||
@@ -62,23 +98,23 @@ jobs:
|
|||||||
- name: Upload assets
|
- name: Upload assets
|
||||||
run: |
|
run: |
|
||||||
RELEASE_ID=$(curl -s \
|
RELEASE_ID=$(curl -s \
|
||||||
-H "Authorization: token ${{ secrets.GITEA_TOKEN }}" \
|
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
|
||||||
"${{ gitea.server_url }}/api/v1/repos/${{ gitea.repository }}/releases/tags/${{ gitea.ref_name }}" \
|
"${{ gitea.server_url }}/api/v1/repos/${{ gitea.repository }}/releases/tags/${{ gitea.ref_name }}" \
|
||||||
| jq -r '.id')
|
| jq -r '.id')
|
||||||
|
|
||||||
for FILE in "${{ env.TARBALL }}" "${{ env.TARBALL }}.sha256"; do
|
for FILE in "${{ env.TARBALL }}" "${{ env.TARBALL }}.sha256"; do
|
||||||
EXISTING=$(curl -s \
|
EXISTING=$(curl -s \
|
||||||
-H "Authorization: token ${{ secrets.GITEA_TOKEN }}" \
|
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
|
||||||
"${{ gitea.server_url }}/api/v1/repos/${{ gitea.repository }}/releases/${RELEASE_ID}/assets" \
|
"${{ gitea.server_url }}/api/v1/repos/${{ gitea.repository }}/releases/${RELEASE_ID}/assets" \
|
||||||
| jq -r ".[] | select(.name == \"${FILE}\") | .id")
|
| jq -r ".[] | select(.name == \"${FILE}\") | .id")
|
||||||
for AID in $EXISTING; do
|
for AID in $EXISTING; do
|
||||||
curl -sX DELETE \
|
curl -sX DELETE \
|
||||||
-H "Authorization: token ${{ secrets.GITEA_TOKEN }}" \
|
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
|
||||||
"${{ gitea.server_url }}/api/v1/repos/${{ gitea.repository }}/releases/${RELEASE_ID}/assets/${AID}"
|
"${{ gitea.server_url }}/api/v1/repos/${{ gitea.repository }}/releases/${RELEASE_ID}/assets/${AID}"
|
||||||
done
|
done
|
||||||
|
|
||||||
curl -sX POST \
|
curl -sX POST \
|
||||||
-H "Authorization: token ${{ secrets.GITEA_TOKEN }}" \
|
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
|
||||||
-H "Content-Type: application/octet-stream" \
|
-H "Content-Type: application/octet-stream" \
|
||||||
"${{ gitea.server_url }}/api/v1/repos/${{ gitea.repository }}/releases/${RELEASE_ID}/assets?name=${FILE}" \
|
"${{ gitea.server_url }}/api/v1/repos/${{ gitea.repository }}/releases/${RELEASE_ID}/assets?name=${FILE}" \
|
||||||
--data-binary "@${FILE}" --fail-with-body
|
--data-binary "@${FILE}" --fail-with-body
|
||||||
@@ -89,7 +125,7 @@ jobs:
|
|||||||
runs-on: aarch64
|
runs-on: aarch64
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: builtin:checkout
|
||||||
|
|
||||||
- name: Compute checksums and update PKGBUILD
|
- name: Compute checksums and update PKGBUILD
|
||||||
run: |
|
run: |
|
||||||
@@ -102,6 +138,35 @@ jobs:
|
|||||||
sed -i "s/sha256sums_x86_64=('.*')/sha256sums_x86_64=('${SUM_X86}')/" aur/PKGBUILD
|
sed -i "s/sha256sums_x86_64=('.*')/sha256sums_x86_64=('${SUM_X86}')/" aur/PKGBUILD
|
||||||
sed -i "s/sha256sums_aarch64=('.*')/sha256sums_aarch64=('${SUM_AARCH}')/" aur/PKGBUILD
|
sed -i "s/sha256sums_aarch64=('.*')/sha256sums_aarch64=('${SUM_AARCH}')/" aur/PKGBUILD
|
||||||
|
|
||||||
|
# Also publish the built packages to this instance's Arch registry
|
||||||
|
# (docs.gitea.com/usage/packages/arch). The PKGBUILD only repacks the
|
||||||
|
# release tarballs, so CARCH can produce both architectures from this
|
||||||
|
# one host. Consumers: see the infrastructure README.
|
||||||
|
# Best-effort mirror to the instance Arch registry. The ephemeral
|
||||||
|
# GITHUB_TOKEN is not accepted as a package-write credential, so this
|
||||||
|
# uses a dedicated REGISTRY_TOKEN secret (a write:package token for bl);
|
||||||
|
# if it is unset the step is skipped, and continue-on-error keeps a
|
||||||
|
# registry hiccup from failing the release or the AUR push.
|
||||||
|
- name: Publish to the Arch package registry
|
||||||
|
continue-on-error: true
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
if [ -z "${{ secrets.REGISTRY_TOKEN }}" ]; then
|
||||||
|
echo "::warning::REGISTRY_TOKEN not set — skipping Arch registry publish"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
cd aur
|
||||||
|
# --packagelist gives the exact filename (honours the runner's
|
||||||
|
# PKGEXT); !strip (in the PKGBUILD) lets the foreign-arch binary be
|
||||||
|
# packaged on this host without stripping.
|
||||||
|
for carch in aarch64 x86_64; do
|
||||||
|
pkgfile=$(CARCH="$carch" makepkg --packagelist | tail -1)
|
||||||
|
CARCH="$carch" makepkg -f --nodeps --noconfirm --skipinteg
|
||||||
|
curl --fail-with-body --user "bl:${{ secrets.REGISTRY_TOKEN }}" \
|
||||||
|
--upload-file "$pkgfile" \
|
||||||
|
"${{ gitea.server_url }}/api/packages/${{ gitea.repository_owner }}/arch/uhhm"
|
||||||
|
done
|
||||||
|
|
||||||
- name: Push to AUR
|
- name: Push to AUR
|
||||||
env:
|
env:
|
||||||
AUR_SSH_KEY: ${{ secrets.AUR_SSH_KEY }}
|
AUR_SSH_KEY: ${{ secrets.AUR_SSH_KEY }}
|
||||||
|
|||||||
Generated
+403
-7
@@ -2,6 +2,25 @@
|
|||||||
# It is not intended for manual editing.
|
# It is not intended for manual editing.
|
||||||
version = 4
|
version = 4
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "aho-corasick"
|
||||||
|
version = "1.1.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "annotate-snippets"
|
||||||
|
version = "0.11.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "710e8eae58854cdc1790fcb56cca04d712a17be849eeb81da2a724bf4bae2bc4"
|
||||||
|
dependencies = [
|
||||||
|
"anstyle",
|
||||||
|
"unicode-width",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "anstream"
|
name = "anstream"
|
||||||
version = "0.6.18"
|
version = "0.6.18"
|
||||||
@@ -57,6 +76,25 @@ 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 = "bindgen"
|
||||||
|
version = "0.72.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895"
|
||||||
|
dependencies = [
|
||||||
|
"annotate-snippets",
|
||||||
|
"bitflags",
|
||||||
|
"cexpr",
|
||||||
|
"clang-sys",
|
||||||
|
"itertools",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"regex",
|
||||||
|
"rustc-hash",
|
||||||
|
"shlex 1.3.0",
|
||||||
|
"syn 2.0.87",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bitflags"
|
name = "bitflags"
|
||||||
version = "2.13.0"
|
version = "2.13.0"
|
||||||
@@ -69,12 +107,52 @@ version = "1.5.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cc"
|
||||||
|
version = "1.4.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "005ec2760ca554fae18df7a11195552ec576cd665632a881bc011d5bb2fd4d80"
|
||||||
|
dependencies = [
|
||||||
|
"find-msvc-tools",
|
||||||
|
"shlex 2.0.1",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cexpr"
|
||||||
|
version = "0.6.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766"
|
||||||
|
dependencies = [
|
||||||
|
"nom 7.1.3",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cfg-expr"
|
||||||
|
version = "0.20.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fe4ece8474b5f766c63426647e7b4b316b67431ade1036a8313cee24a03ae917"
|
||||||
|
dependencies = [
|
||||||
|
"smallvec",
|
||||||
|
"target-lexicon",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cfg-if"
|
name = "cfg-if"
|
||||||
version = "1.0.4"
|
version = "1.0.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "clang-sys"
|
||||||
|
version = "1.9.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "157a8ba7b480713b56f4c09fd13fc3e0a22a5dfab8097ba61cbc5feef950788a"
|
||||||
|
dependencies = [
|
||||||
|
"glob",
|
||||||
|
"libc",
|
||||||
|
"libloading",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clap"
|
name = "clap"
|
||||||
version = "4.5.21"
|
version = "4.5.21"
|
||||||
@@ -106,7 +184,7 @@ dependencies = [
|
|||||||
"heck",
|
"heck",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn",
|
"syn 2.0.87",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -130,6 +208,12 @@ dependencies = [
|
|||||||
"unicode-segmentation",
|
"unicode-segmentation",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cookie-factory"
|
||||||
|
version = "0.3.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9885fa71e26b8ab7855e2ec7cae6e9b380edff76cd052e07c683a0319d51b3a2"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crossterm"
|
name = "crossterm"
|
||||||
version = "0.29.0"
|
version = "0.29.0"
|
||||||
@@ -176,7 +260,7 @@ dependencies = [
|
|||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"rustc_version",
|
"rustc_version",
|
||||||
"syn",
|
"syn 2.0.87",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -188,6 +272,18 @@ dependencies = [
|
|||||||
"litrs",
|
"litrs",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "either"
|
||||||
|
version = "1.18.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "equivalent"
|
||||||
|
version = "1.0.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "errno"
|
name = "errno"
|
||||||
version = "0.3.14"
|
version = "0.3.14"
|
||||||
@@ -195,27 +291,101 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"windows-sys 0.59.0",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "find-msvc-tools"
|
||||||
|
version = "0.1.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3e0f1c7c3a72c66fd80abe965175f7523475c0489a87d3ff9d6e8c87d87a9d2d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "glob"
|
||||||
|
version = "0.3.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hashbrown"
|
||||||
|
version = "0.17.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "heck"
|
name = "heck"
|
||||||
version = "0.5.0"
|
version = "0.5.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "indexmap"
|
||||||
|
version = "2.14.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855"
|
||||||
|
dependencies = [
|
||||||
|
"equivalent",
|
||||||
|
"hashbrown",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "is_terminal_polyfill"
|
name = "is_terminal_polyfill"
|
||||||
version = "1.70.1"
|
version = "1.70.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
|
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "itertools"
|
||||||
|
version = "0.12.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569"
|
||||||
|
dependencies = [
|
||||||
|
"either",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libc"
|
name = "libc"
|
||||||
version = "0.2.186"
|
version = "0.2.186"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
|
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libloading"
|
||||||
|
version = "0.8.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"windows-link",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libspa"
|
||||||
|
version = "0.10.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "882f7427e7989dcc9d388b7f05c4630390a1d7696f9ffa469cd4a7a48f0b4c40"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"cc",
|
||||||
|
"cookie-factory",
|
||||||
|
"libc",
|
||||||
|
"libspa-sys",
|
||||||
|
"nom 8.0.0",
|
||||||
|
"rustix",
|
||||||
|
"system-deps",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libspa-sys"
|
||||||
|
version = "0.10.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3b6e17bdaf63ed0d5e4144022624032b41fd9733112e8c74ac26fc9bf1291924"
|
||||||
|
dependencies = [
|
||||||
|
"bindgen",
|
||||||
|
"cc",
|
||||||
|
"system-deps",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "linux-raw-sys"
|
name = "linux-raw-sys"
|
||||||
version = "0.12.1"
|
version = "0.12.1"
|
||||||
@@ -243,6 +413,18 @@ version = "0.4.33"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad"
|
checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "memchr"
|
||||||
|
version = "2.8.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "minimal-lexical"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mio"
|
name = "mio"
|
||||||
version = "1.2.1"
|
version = "1.2.1"
|
||||||
@@ -255,6 +437,25 @@ dependencies = [
|
|||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "nom"
|
||||||
|
version = "7.1.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
"minimal-lexical",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "nom"
|
||||||
|
version = "8.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num-complex"
|
name = "num-complex"
|
||||||
version = "0.4.6"
|
version = "0.4.6"
|
||||||
@@ -305,6 +506,37 @@ dependencies = [
|
|||||||
"windows-link",
|
"windows-link",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pipewire"
|
||||||
|
version = "0.10.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bde71084c4e25959d68f1ea54daa75e5ecdb338e5caf0b5510143b79baa32d5c"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"libc",
|
||||||
|
"libspa",
|
||||||
|
"libspa-sys",
|
||||||
|
"pipewire-sys",
|
||||||
|
"rustix",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pipewire-sys"
|
||||||
|
version = "0.10.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9ce653f53e63e5b93853218092ee9a8906a5d082c92f3f1db26316955dd63ce0"
|
||||||
|
dependencies = [
|
||||||
|
"bindgen",
|
||||||
|
"libspa-sys",
|
||||||
|
"system-deps",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pkg-config"
|
||||||
|
version = "0.3.34"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "primal-check"
|
name = "primal-check"
|
||||||
version = "0.3.4"
|
version = "0.3.4"
|
||||||
@@ -316,9 +548,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proc-macro2"
|
name = "proc-macro2"
|
||||||
version = "1.0.89"
|
version = "1.0.107"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e"
|
checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
@@ -341,6 +573,41 @@ dependencies = [
|
|||||||
"bitflags",
|
"bitflags",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "regex"
|
||||||
|
version = "1.13.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d"
|
||||||
|
dependencies = [
|
||||||
|
"aho-corasick",
|
||||||
|
"memchr",
|
||||||
|
"regex-automata",
|
||||||
|
"regex-syntax",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "regex-automata"
|
||||||
|
version = "0.4.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2"
|
||||||
|
dependencies = [
|
||||||
|
"aho-corasick",
|
||||||
|
"memchr",
|
||||||
|
"regex-syntax",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "regex-syntax"
|
||||||
|
version = "0.8.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustc-hash"
|
||||||
|
version = "2.1.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustc_version"
|
name = "rustc_version"
|
||||||
version = "0.4.1"
|
version = "0.4.1"
|
||||||
@@ -375,7 +642,7 @@ dependencies = [
|
|||||||
"errno",
|
"errno",
|
||||||
"libc",
|
"libc",
|
||||||
"linux-raw-sys",
|
"linux-raw-sys",
|
||||||
"windows-sys 0.59.0",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -390,6 +657,47 @@ version = "1.0.28"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"
|
checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_core"
|
||||||
|
version = "1.0.229"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
|
||||||
|
dependencies = [
|
||||||
|
"serde_derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_derive"
|
||||||
|
version = "1.0.229"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 3.0.5",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_spanned"
|
||||||
|
version = "1.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26"
|
||||||
|
dependencies = [
|
||||||
|
"serde_core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "shlex"
|
||||||
|
version = "1.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "shlex"
|
||||||
|
version = "2.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "signal-hook"
|
name = "signal-hook"
|
||||||
version = "0.3.18"
|
version = "0.3.18"
|
||||||
@@ -429,11 +737,12 @@ checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "spectrust"
|
name = "spectrust"
|
||||||
version = "0.2.3"
|
version = "0.2.9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"byteorder",
|
"byteorder",
|
||||||
"clap",
|
"clap",
|
||||||
"crossterm",
|
"crossterm",
|
||||||
|
"pipewire",
|
||||||
"rustfft",
|
"rustfft",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -460,6 +769,75 @@ dependencies = [
|
|||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "syn"
|
||||||
|
version = "3.0.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "12df2e0110f65b775f769bb17ef989067a1d931b2eb822bd4346631eeada89f9"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "system-deps"
|
||||||
|
version = "7.0.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "396a35feb67335377e0251fcbc1092fc85c484bd4e3a7a54319399da127796e7"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-expr",
|
||||||
|
"heck",
|
||||||
|
"pkg-config",
|
||||||
|
"toml",
|
||||||
|
"version-compare",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "target-lexicon"
|
||||||
|
version = "0.13.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml"
|
||||||
|
version = "1.1.6+spec-1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "920602543f0911ab71da12c50d59701da54c196d1a2bf5cb4b75667f137a406a"
|
||||||
|
dependencies = [
|
||||||
|
"indexmap",
|
||||||
|
"serde_core",
|
||||||
|
"serde_spanned",
|
||||||
|
"toml_datetime",
|
||||||
|
"toml_parser",
|
||||||
|
"toml_writer",
|
||||||
|
"winnow",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml_datetime"
|
||||||
|
version = "1.1.1+spec-1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7"
|
||||||
|
dependencies = [
|
||||||
|
"serde_core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml_parser"
|
||||||
|
version = "1.1.3+spec-1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56"
|
||||||
|
dependencies = [
|
||||||
|
"winnow",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml_writer"
|
||||||
|
version = "1.1.2+spec-1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "transpose"
|
name = "transpose"
|
||||||
version = "0.2.3"
|
version = "0.2.3"
|
||||||
@@ -482,12 +860,24 @@ version = "1.13.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8"
|
checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-width"
|
||||||
|
version = "0.2.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "utf8parse"
|
name = "utf8parse"
|
||||||
version = "0.2.2"
|
version = "0.2.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "version-compare"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "version_check"
|
name = "version_check"
|
||||||
version = "0.9.5"
|
version = "0.9.5"
|
||||||
@@ -609,3 +999,9 @@ name = "windows_x86_64_msvc"
|
|||||||
version = "0.52.6"
|
version = "0.52.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winnow"
|
||||||
|
version = "1.0.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81"
|
||||||
|
|||||||
+2
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "spectrust"
|
name = "spectrust"
|
||||||
version = "0.2.3"
|
version = "0.2.9"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
@@ -12,3 +12,4 @@ byteorder = "1.5.0"
|
|||||||
clap = { version = "4.5.21", features = ["derive"] }
|
clap = { version = "4.5.21", features = ["derive"] }
|
||||||
crossterm = "0.29.0"
|
crossterm = "0.29.0"
|
||||||
rustfft = "6.2.0"
|
rustfft = "6.2.0"
|
||||||
|
pipewire = "0.10"
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2024 Bendik Aagaard Lynghaug
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
# spectrust
|
||||||
|
|
||||||
|
A terminal audio spectrum analyzer written in Rust. Visualizes PCM audio in real time using block characters — reads from PipeWire, a file, or stdin.
|
||||||
|
|
||||||
|
```
|
||||||
|
▁▂▃▄▅▆▇█▇▆▅▄▃▂▁▁▂▃▄▅▆▇█▇▆▅▄▃▂▁▁▂▃▄▅▆▇█
|
||||||
|
```
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
**Arch Linux (AUR)**
|
||||||
|
```
|
||||||
|
paru -S spectrust
|
||||||
|
```
|
||||||
|
|
||||||
|
**From source**
|
||||||
|
```
|
||||||
|
cargo install --path .
|
||||||
|
```
|
||||||
|
|
||||||
|
Requires Rust 1.70+. PipeWire (`pw-record`) is only needed for the `-p` capture mode.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```
|
||||||
|
spectrust [OPTIONS]
|
||||||
|
```
|
||||||
|
|
||||||
|
| Option | Default | Description |
|
||||||
|
|--------|---------|-------------|
|
||||||
|
| `-f, --fps <N>` | `60` | Target frames per second |
|
||||||
|
| `-p, --pipewire` | — | Capture audio directly from PipeWire |
|
||||||
|
| `-i, --input <FILE>` | — | Read PCM from a file (stdin if omitted) |
|
||||||
|
| `-l, --log-power <N>` | `1.4` | Frequency scaling power (0.5–3.0); higher values give more space to bass |
|
||||||
|
| `-d, --drop-off <N>` | `0.75` | Bar drop-off factor per frame (0.0 = instant, 1.0 = no drop) |
|
||||||
|
| `--rate <HZ>` | `48000` | Sample rate — must match the audio source |
|
||||||
|
| `--channels <N>` | `2` | Channel count — must match the audio source |
|
||||||
|
|
||||||
|
### Input modes
|
||||||
|
|
||||||
|
**PipeWire** — capture system audio directly:
|
||||||
|
```
|
||||||
|
spectrust -p
|
||||||
|
```
|
||||||
|
|
||||||
|
**stdin** — pipe from any recorder:
|
||||||
|
```
|
||||||
|
pw-record --format=s16 --rate=48000 --channels=2 --raw - | spectrust
|
||||||
|
arecord -f S16_LE -r 48000 -c 2 | spectrust
|
||||||
|
```
|
||||||
|
|
||||||
|
**File** — replay a raw PCM file:
|
||||||
|
```
|
||||||
|
spectrust -i recording.pcm
|
||||||
|
```
|
||||||
|
|
||||||
|
### Runtime controls
|
||||||
|
|
||||||
|
| Key | Action |
|
||||||
|
|-----|--------|
|
||||||
|
| `q` / `Ctrl+C` | Quit |
|
||||||
|
| `[` / `]` | Decrease / increase drop-off speed |
|
||||||
|
| `,` / `.` | Decrease / increase log frequency scaling |
|
||||||
|
| `-` / `+` | Decrease / increase sensitivity |
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- Input is expected as signed 16-bit little-endian PCM (s16le). This matches `pw-record --format=s16 --raw` and `arecord -f S16_LE`.
|
||||||
|
- If `--rate` or `--channels` don't match the actual source the visualization will be distorted.
|
||||||
|
- The sensitivity control scales bar heights at render time; it has the most noticeable effect on quiet signals.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT
|
||||||
+6
-5
@@ -1,16 +1,17 @@
|
|||||||
# Maintainer: Bendik Aagaard Lynghaug <bendik.lynghaug@gmail.com>
|
# Maintainer: Bendik Aagaard Lynghaug <bendik.lynghaug@gmail.com>
|
||||||
pkgname=spectrust
|
pkgname=spectrust
|
||||||
pkgver=0.2.3
|
pkgver=0.2.9
|
||||||
pkgrel=1
|
pkgrel=1
|
||||||
pkgdesc="Terminal audio spectrum analyzer"
|
pkgdesc="Terminal audio spectrum analyzer"
|
||||||
arch=('x86_64' 'aarch64')
|
arch=('x86_64' 'aarch64')
|
||||||
url="https://prosjekt.klingenbergbygg.no/bl/spectrust"
|
options=('!strip')
|
||||||
|
url="https://project.uhhm.no/bl/spectrust"
|
||||||
license=('MIT')
|
license=('MIT')
|
||||||
depends=()
|
depends=('libpipewire')
|
||||||
provides=('spectrust')
|
provides=('spectrust')
|
||||||
conflicts=('spectrust-git' 'spectrust-bin')
|
conflicts=('spectrust-git' 'spectrust-bin')
|
||||||
source_x86_64=("spectrust-v${pkgver}-x86_64.tar.gz::https://prosjekt.klingenbergbygg.no/bl/spectrust/releases/download/v${pkgver}/spectrust-v${pkgver}-x86_64.tar.gz")
|
source_x86_64=("spectrust-v${pkgver}-x86_64.tar.gz::https://project.uhhm.no/bl/spectrust/releases/download/v${pkgver}/spectrust-v${pkgver}-x86_64.tar.gz")
|
||||||
source_aarch64=("spectrust-v${pkgver}-aarch64.tar.gz::https://prosjekt.klingenbergbygg.no/bl/spectrust/releases/download/v${pkgver}/spectrust-v${pkgver}-aarch64.tar.gz")
|
source_aarch64=("spectrust-v${pkgver}-aarch64.tar.gz::https://project.uhhm.no/bl/spectrust/releases/download/v${pkgver}/spectrust-v${pkgver}-aarch64.tar.gz")
|
||||||
sha256sums_x86_64=('SKIP')
|
sha256sums_x86_64=('SKIP')
|
||||||
sha256sums_aarch64=('SKIP')
|
sha256sums_aarch64=('SKIP')
|
||||||
|
|
||||||
|
|||||||
+132
-111
@@ -13,9 +13,12 @@ use rustfft::{algorithm::Radix4, num_complex::Complex, Fft, FftDirection};
|
|||||||
use std::collections::VecDeque;
|
use std::collections::VecDeque;
|
||||||
use std::io::{stdout, Read, Write};
|
use std::io::{stdout, Read, Write};
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
|
use std::sync::mpsc;
|
||||||
|
use std::thread;
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
use byteorder::{LittleEndian, ReadBytesExt};
|
use byteorder::{LittleEndian, ReadBytesExt};
|
||||||
use std::process::{Command, Stdio};
|
|
||||||
|
mod pipewire_capture;
|
||||||
|
|
||||||
const EMPTY_CELL: Cell = Cell { character: ' ', filled: false };
|
const EMPTY_CELL: Cell = Cell { character: ' ', filled: false };
|
||||||
const BLOCK_CELL: Cell = Cell { character: '█', filled: true };
|
const BLOCK_CELL: Cell = Cell { character: '█', filled: true };
|
||||||
@@ -63,6 +66,15 @@ fn render_bar(buf: &mut Vec<Vec<Cell>>, col: usize, sr: usize, bar_height_f: f32
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn build_freq_mapping(sc: usize, log_power: f32, fft_size: usize) -> Vec<usize> {
|
||||||
|
(0..sc)
|
||||||
|
.map(|i| {
|
||||||
|
let log_pos = (i as f32 / sc as f32).powf(log_power * 0.8);
|
||||||
|
((log_pos * (fft_size / 2) as f32) as usize).min(fft_size / 2 - 1)
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
fn handle_terminal_resize(
|
fn handle_terminal_resize(
|
||||||
rows: &mut u16,
|
rows: &mut u16,
|
||||||
cols: &mut u16,
|
cols: &mut u16,
|
||||||
@@ -96,12 +108,7 @@ fn handle_terminal_resize(
|
|||||||
freq_bands.clear();
|
freq_bands.clear();
|
||||||
freq_bands.resize(sc, 0.0);
|
freq_bands.resize(sc, 0.0);
|
||||||
|
|
||||||
freq_mapping.clear();
|
*freq_mapping = build_freq_mapping(sc, log_power, fft_size);
|
||||||
for i in 0..sc {
|
|
||||||
let log_pos = (i as f32 / sc as f32).powf(log_power * 0.8);
|
|
||||||
let index = (log_pos * (fft_size / 2) as f32) as usize;
|
|
||||||
freq_mapping.push(index.min(fft_size / 2 - 1));
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut out = stdout();
|
let mut out = stdout();
|
||||||
let _ = queue!(out, Clear(ClearType::All));
|
let _ = queue!(out, Clear(ClearType::All));
|
||||||
@@ -110,29 +117,43 @@ fn handle_terminal_resize(
|
|||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
#[command(name = "Spectrust")]
|
#[command(name = "Spectrust")]
|
||||||
#[command(version = "0.1.0")]
|
#[command(version)]
|
||||||
#[command(about = "A PCM spectrum analyzer for audio visualization",
|
#[command(about = "A PCM spectrum analyzer for audio visualization",
|
||||||
long_about = "Spectrust is a PCM spectrum analyzer that visualizes audio in your terminal.\n\
|
long_about = "Spectrust is a PCM spectrum analyzer that visualizes audio in your terminal.\n\
|
||||||
It can read from stdin, a file, or capture directly from PipeWire.\n\n\
|
It can read from stdin, a file, or capture directly from PipeWire.\n\n\
|
||||||
Examples:\n\
|
Examples:\n\
|
||||||
- Direct PipeWire capture: spectrust -p\n\
|
- Direct PipeWire capture (native, low-latency): spectrust -p\n\
|
||||||
- From audio file: spectrust -i audio.pcm\n\
|
- From audio file: spectrust -i audio.pcm\n\
|
||||||
- From stdin: pw-record --raw - | spectrust")]
|
- From stdin: pw-record --raw - | spectrust\n\n\
|
||||||
|
Runtime controls:\n\
|
||||||
|
- q / Ctrl+C quit\n\
|
||||||
|
- [ / ] decrease / increase drop-off\n\
|
||||||
|
- , / . decrease / increase log scaling\n\
|
||||||
|
- - / + decrease / increase sensitivity")]
|
||||||
struct Cli {
|
struct Cli {
|
||||||
#[arg(value_name = "FPS", default_value_t = 60)]
|
#[arg(short, long, help = "Target frames per second", default_value_t = 60)]
|
||||||
fps: u16,
|
fps: u16,
|
||||||
|
|
||||||
#[arg(short, long, help = "Use PipeWire to capture audio directly")]
|
#[arg(short, long, help = "Capture audio directly via native low-latency PipeWire (default sink monitor)")]
|
||||||
pipewire: bool,
|
pipewire: bool,
|
||||||
|
|
||||||
|
#[arg(long, help = "PipeWire capture quantum in frames — lower is lower latency (more CPU/xrun risk)", default_value_t = 256)]
|
||||||
|
latency: u32,
|
||||||
|
|
||||||
#[arg(short, long, help = "Input file (reads from stdin if not provided)")]
|
#[arg(short, long, help = "Input file (reads from stdin if not provided)")]
|
||||||
input: Option<String>,
|
input: Option<String>,
|
||||||
|
|
||||||
#[arg(short, long, help = "Logarithmic scaling power (higher values emphasize lower frequencies)", default_value_t = 1.4)]
|
#[arg(short, long, help = "Logarithmic frequency scaling power (0.5–3.0; higher values give more space to bass)", default_value_t = 1.4)]
|
||||||
log_power: f32,
|
log_power: f32,
|
||||||
|
|
||||||
#[arg(short, long, help = "Drop-off factor for bar animation (0.0-1.0)", default_value_t = 0.75)]
|
#[arg(short, long, help = "Bar drop-off factor per frame (0.0 = instant drop, 1.0 = no drop)", default_value_t = 0.75)]
|
||||||
drop_off: f32,
|
drop_off: f32,
|
||||||
|
|
||||||
|
#[arg(long, help = "Sample rate in Hz — must match the audio source", default_value_t = 48000)]
|
||||||
|
rate: u32,
|
||||||
|
|
||||||
|
#[arg(long, help = "Number of audio channels — must match the audio source", default_value_t = 2)]
|
||||||
|
channels: u8,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
@@ -151,44 +172,76 @@ fn main() {
|
|||||||
|
|
||||||
let (mut cols, mut rows) = terminal::size().expect("failed to get terminal size");
|
let (mut cols, mut rows) = terminal::size().expect("failed to get terminal size");
|
||||||
|
|
||||||
let sample_rate: usize = 48000;
|
let sample_rate: usize = cli.rate as usize;
|
||||||
let bit_depth: usize = 16;
|
let bit_depth: usize = 16;
|
||||||
let num_channels: usize = 2;
|
let num_channels: usize = cli.channels as usize;
|
||||||
let samples_per_frame: usize = sample_rate / usize::from(cli.fps);
|
let samples_per_frame: usize = sample_rate / usize::from(cli.fps);
|
||||||
let fft_size: usize = 1024;
|
let fft_size: usize = 1024;
|
||||||
|
|
||||||
let fft = Radix4::new(fft_size, FftDirection::Forward);
|
let fft = Radix4::new(fft_size, FftDirection::Forward);
|
||||||
|
|
||||||
let mut input_buffer = vec![0u8; samples_per_frame * num_channels * (bit_depth / 8)];
|
|
||||||
let mut complex_buffer = vec![Complex { re: 0.0f32, im: 0.0f32 }; fft_size];
|
let mut complex_buffer = vec![Complex { re: 0.0f32, im: 0.0f32 }; fft_size];
|
||||||
|
|
||||||
let hann_window: Vec<f32> = (0..fft_size)
|
let hann_window: Vec<f32> = (0..fft_size)
|
||||||
.map(|i| 0.5 * (1.0 - (2.0 * std::f32::consts::PI * i as f32 / fft_size as f32).cos()))
|
.map(|i| 0.5 * (1.0 - (2.0 * std::f32::consts::PI * i as f32 / fft_size as f32).cos()))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
enum AudioSource {
|
|
||||||
StdIn(std::io::StdinLock<'static>),
|
|
||||||
File(File),
|
|
||||||
PipeWire(std::process::ChildStdout),
|
|
||||||
}
|
|
||||||
|
|
||||||
let _ = out.flush();
|
let _ = out.flush();
|
||||||
|
|
||||||
let mut audio_source = if cli.pipewire {
|
let (tx, rx) = mpsc::channel::<Vec<f32>>();
|
||||||
let process = Command::new("pw-record")
|
|
||||||
.args(["--format=s16", "--rate=48000", "--channels=2", "--raw", "-"])
|
if cli.pipewire {
|
||||||
.stdout(Stdio::piped())
|
// Native low-latency capture: pushes mono f32 straight into `tx`.
|
||||||
.spawn()
|
if let Err(e) = pipewire_capture::spawn(tx, cli.rate, cli.channels, cli.latency) {
|
||||||
.expect("failed to start pw-record");
|
let _ = execute!(out, LeaveAlternateScreen);
|
||||||
let pipe = process.stdout.expect("failed to capture pw-record stdout");
|
let _ = disable_raw_mode();
|
||||||
let _ = out.flush();
|
eprintln!("error: {e}");
|
||||||
AudioSource::PipeWire(pipe)
|
std::process::exit(1);
|
||||||
} else if let Some(filename) = &cli.input {
|
}
|
||||||
let file = File::open(filename).expect("failed to open input file");
|
|
||||||
AudioSource::File(file)
|
|
||||||
} else {
|
} else {
|
||||||
AudioSource::StdIn(std::io::stdin().lock())
|
let reader: Box<dyn Read + Send + 'static> = if let Some(filename) = &cli.input {
|
||||||
};
|
Box::new(File::open(filename).expect("failed to open input file"))
|
||||||
|
} else {
|
||||||
|
Box::new(std::io::stdin())
|
||||||
|
};
|
||||||
|
|
||||||
|
let _audio_thread = thread::spawn(move || {
|
||||||
|
let mut reader = reader;
|
||||||
|
let mut input_buffer = vec![0u8; samples_per_frame * num_channels * (bit_depth / 8)];
|
||||||
|
loop {
|
||||||
|
match reader.read(&mut input_buffer) {
|
||||||
|
Ok(0) => break,
|
||||||
|
Ok(n) => {
|
||||||
|
let mut samples = Vec::with_capacity(n / (num_channels * (bit_depth / 8)));
|
||||||
|
let mut cursor = std::io::Cursor::new(&input_buffer[..n]);
|
||||||
|
while cursor.position() < (n as u64 - (num_channels * (bit_depth / 8)) as u64 + 1) {
|
||||||
|
let mut frame_sum = 0.0f32;
|
||||||
|
let mut valid_channels = 0u32;
|
||||||
|
for _ in 0..num_channels {
|
||||||
|
match cursor.read_i16::<LittleEndian>() {
|
||||||
|
Ok(sample) => {
|
||||||
|
frame_sum += sample as f32;
|
||||||
|
valid_channels += 1;
|
||||||
|
}
|
||||||
|
Err(_) => break,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if valid_channels > 0 {
|
||||||
|
samples.push((frame_sum / valid_channels as f32) / 32768.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !samples.is_empty() && tx.send(samples).is_err() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => {
|
||||||
|
thread::sleep(Duration::from_millis(1));
|
||||||
|
}
|
||||||
|
Err(_) => break,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
let max_width: u16 = 1000;
|
let max_width: u16 = 1000;
|
||||||
let max_height: u16 = 1000;
|
let max_height: u16 = 1000;
|
||||||
@@ -204,19 +257,16 @@ fn main() {
|
|||||||
|
|
||||||
let mut last_bars: Vec<f32> = vec![0.0; sc];
|
let mut last_bars: Vec<f32> = vec![0.0; sc];
|
||||||
let mut freq_bands: Vec<f32> = vec![0.0; sc];
|
let mut freq_bands: Vec<f32> = vec![0.0; sc];
|
||||||
let drop_factor: f32 = cli.drop_off.clamp(0.0, 1.0);
|
let mut drop_factor: f32 = cli.drop_off.clamp(0.0, 1.0);
|
||||||
|
let mut log_power: f32 = cli.log_power;
|
||||||
|
let mut sensitivity: f32 = 1.0;
|
||||||
|
|
||||||
let interval = Duration::from_secs_f32(1.0 / f32::from(cli.fps));
|
let interval = Duration::from_secs_f32(1.0 / f32::from(cli.fps));
|
||||||
let mut last_frame_time = Instant::now();
|
let mut last_frame_time = Instant::now();
|
||||||
|
|
||||||
let mut max_magnitudes: VecDeque<f32> = VecDeque::with_capacity(usize::from(cli.fps / 2));
|
let mut max_magnitudes: VecDeque<f32> = VecDeque::with_capacity(usize::from(cli.fps / 2));
|
||||||
|
|
||||||
let mut freq_mapping: Vec<usize> = (0..sc)
|
let mut freq_mapping: Vec<usize> = build_freq_mapping(sc, log_power, fft_size);
|
||||||
.map(|i| {
|
|
||||||
let log_pos = (i as f32 / sc as f32).powf(cli.log_power * 0.8);
|
|
||||||
((log_pos * (fft_size / 2) as f32) as usize).min(fft_size / 2 - 1)
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let mut last_terminal_resize = Instant::now();
|
let mut last_terminal_resize = Instant::now();
|
||||||
let resize_check_interval = Duration::from_millis(500);
|
let resize_check_interval = Duration::from_millis(500);
|
||||||
@@ -226,7 +276,8 @@ fn main() {
|
|||||||
let mut max_magnitude: f32 = 1.0;
|
let mut max_magnitude: f32 = 1.0;
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
if event::poll(Duration::ZERO).unwrap_or(false) {
|
let time_to_next_frame = interval.saturating_sub(last_frame_time.elapsed());
|
||||||
|
if event::poll(time_to_next_frame).unwrap_or(false) {
|
||||||
match event::read() {
|
match event::read() {
|
||||||
Ok(Event::Key(KeyEvent { code: KeyCode::Char('q'), .. })) => break,
|
Ok(Event::Key(KeyEvent { code: KeyCode::Char('q'), .. })) => break,
|
||||||
Ok(Event::Key(KeyEvent {
|
Ok(Event::Key(KeyEvent {
|
||||||
@@ -234,13 +285,33 @@ fn main() {
|
|||||||
modifiers: KeyModifiers::CONTROL,
|
modifiers: KeyModifiers::CONTROL,
|
||||||
..
|
..
|
||||||
})) => break,
|
})) => break,
|
||||||
|
Ok(Event::Key(KeyEvent { code: KeyCode::Char('['), .. })) => {
|
||||||
|
drop_factor = (drop_factor - 0.05).max(0.0);
|
||||||
|
}
|
||||||
|
Ok(Event::Key(KeyEvent { code: KeyCode::Char(']'), .. })) => {
|
||||||
|
drop_factor = (drop_factor + 0.05).min(1.0);
|
||||||
|
}
|
||||||
|
Ok(Event::Key(KeyEvent { code: KeyCode::Char(','), .. })) => {
|
||||||
|
log_power = (log_power - 0.1).max(0.5);
|
||||||
|
freq_mapping = build_freq_mapping(sc, log_power, fft_size);
|
||||||
|
}
|
||||||
|
Ok(Event::Key(KeyEvent { code: KeyCode::Char('.'), .. })) => {
|
||||||
|
log_power = (log_power + 0.1).min(3.0);
|
||||||
|
freq_mapping = build_freq_mapping(sc, log_power, fft_size);
|
||||||
|
}
|
||||||
|
Ok(Event::Key(KeyEvent { code: KeyCode::Char('-'), .. })) => {
|
||||||
|
sensitivity = (sensitivity / 1.2).max(0.1);
|
||||||
|
}
|
||||||
|
Ok(Event::Key(KeyEvent { code: KeyCode::Char('+' | '='), .. })) => {
|
||||||
|
sensitivity = (sensitivity * 1.2).min(10.0);
|
||||||
|
}
|
||||||
Ok(Event::Resize(new_cols, new_rows)) => {
|
Ok(Event::Resize(new_cols, new_rows)) => {
|
||||||
handle_terminal_resize(
|
handle_terminal_resize(
|
||||||
&mut rows, &mut cols, new_rows, new_cols,
|
&mut rows, &mut cols, new_rows, new_cols,
|
||||||
max_width, max_height, &mut safe_rows, &mut safe_cols,
|
max_width, max_height, &mut safe_rows, &mut safe_cols,
|
||||||
&mut current_buffer, &mut next_buffer,
|
&mut current_buffer, &mut next_buffer,
|
||||||
&mut last_bars, &mut freq_bands, &mut freq_mapping,
|
&mut last_bars, &mut freq_bands, &mut freq_mapping,
|
||||||
cli.log_power, fft_size,
|
log_power, fft_size,
|
||||||
);
|
);
|
||||||
sc = safe_cols as usize;
|
sc = safe_cols as usize;
|
||||||
sr = safe_rows as usize;
|
sr = safe_rows as usize;
|
||||||
@@ -257,7 +328,7 @@ fn main() {
|
|||||||
max_width, max_height, &mut safe_rows, &mut safe_cols,
|
max_width, max_height, &mut safe_rows, &mut safe_cols,
|
||||||
&mut current_buffer, &mut next_buffer,
|
&mut current_buffer, &mut next_buffer,
|
||||||
&mut last_bars, &mut freq_bands, &mut freq_mapping,
|
&mut last_bars, &mut freq_bands, &mut freq_mapping,
|
||||||
cli.log_power, fft_size,
|
log_power, fft_size,
|
||||||
);
|
);
|
||||||
sc = safe_cols as usize;
|
sc = safe_cols as usize;
|
||||||
sr = safe_rows as usize;
|
sr = safe_rows as usize;
|
||||||
@@ -272,69 +343,17 @@ fn main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
all_samples.clear();
|
all_samples.clear();
|
||||||
let mut buffer_is_empty = false;
|
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
let read_result = match &mut audio_source {
|
match rx.try_recv() {
|
||||||
AudioSource::StdIn(input) => input.read(&mut input_buffer),
|
Ok(batch) => all_samples.extend_from_slice(&batch),
|
||||||
AudioSource::File(file) => file.read(&mut input_buffer),
|
Err(_) => break,
|
||||||
AudioSource::PipeWire(pipe) => pipe.read(&mut input_buffer),
|
|
||||||
};
|
|
||||||
|
|
||||||
match read_result {
|
|
||||||
Ok(0) => {
|
|
||||||
buffer_is_empty = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
Ok(n) => {
|
|
||||||
let mut cursor = std::io::Cursor::new(&input_buffer[..n]);
|
|
||||||
while cursor.position() < (n as u64 - (num_channels * (bit_depth / 8)) as u64 + 1) {
|
|
||||||
let mut frame_sum = 0.0f32;
|
|
||||||
let mut valid_channels = 0u32;
|
|
||||||
for _ in 0..num_channels {
|
|
||||||
match cursor.read_i16::<LittleEndian>() {
|
|
||||||
Ok(sample) => {
|
|
||||||
frame_sum += sample as f32;
|
|
||||||
valid_channels += 1;
|
|
||||||
}
|
|
||||||
Err(_) => break,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if valid_channels > 0 {
|
|
||||||
all_samples.push((frame_sum / valid_channels as f32) / 32768.0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if all_samples.len() >= fft_size * 2 {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => break,
|
|
||||||
Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => {
|
|
||||||
buffer_is_empty = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
let error_msg = format!("Error: {}", e);
|
|
||||||
for (i, c) in error_msg.chars().enumerate() {
|
|
||||||
if i < sc {
|
|
||||||
next_buffer[0][i] = Cell { character: c, filled: true };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
view_diff.clear();
|
|
||||||
for y in 0..sr {
|
|
||||||
for x in 0..sc {
|
|
||||||
if current_buffer[y][x] != next_buffer[y][x] {
|
|
||||||
view_diff.add_change(y, x, next_buffer[y][x].character);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
view_diff.apply();
|
|
||||||
std::mem::swap(&mut current_buffer, &mut next_buffer);
|
|
||||||
let _ = out.flush();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Latency guard: if the audio thread got ahead, drop old samples
|
||||||
|
if all_samples.len() > fft_size * 2 {
|
||||||
|
let excess = all_samples.len() - fft_size * 2;
|
||||||
|
all_samples.drain(..excess);
|
||||||
|
}
|
||||||
|
|
||||||
if !all_samples.is_empty() {
|
if !all_samples.is_empty() {
|
||||||
if all_samples.len() > fft_size {
|
if all_samples.len() > fft_size {
|
||||||
@@ -364,7 +383,7 @@ fn main() {
|
|||||||
if mapping_idx < fft_size / 2 {
|
if mapping_idx < fft_size / 2 {
|
||||||
let magnitude = complex_buffer[mapping_idx].norm();
|
let magnitude = complex_buffer[mapping_idx].norm();
|
||||||
let freq_boost = 1.0 + (mapping_idx as f32 / (fft_size / 2) as f32) * 4.0;
|
let freq_boost = 1.0 + (mapping_idx as f32 / (fft_size / 2) as f32) * 4.0;
|
||||||
let scaled = (magnitude * 80.0 * freq_boost).powf(0.5);
|
let scaled = (magnitude * 80.0 * freq_boost).sqrt();
|
||||||
freq_bands[i] = if scaled > last_bars[i] { scaled } else { last_bars[i] * drop_factor };
|
freq_bands[i] = if scaled > last_bars[i] { scaled } else { last_bars[i] * drop_factor };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -382,7 +401,8 @@ fn main() {
|
|||||||
max_magnitude = max_magnitudes.iter().copied().fold(0.0f32, f32::max).max(0.1);
|
max_magnitude = max_magnitudes.iter().copied().fold(0.0f32, f32::max).max(0.1);
|
||||||
|
|
||||||
for (i, &magnitude) in last_bars[..sc].iter().enumerate() {
|
for (i, &magnitude) in last_bars[..sc].iter().enumerate() {
|
||||||
render_bar(&mut next_buffer, i, sr, (magnitude / max_magnitude) * sr as f32);
|
let height = ((magnitude / max_magnitude) * sr as f32 * sensitivity).min(sr as f32);
|
||||||
|
render_bar(&mut next_buffer, i, sr, height);
|
||||||
}
|
}
|
||||||
|
|
||||||
view_diff.clear();
|
view_diff.clear();
|
||||||
@@ -397,10 +417,11 @@ fn main() {
|
|||||||
std::mem::swap(&mut current_buffer, &mut next_buffer);
|
std::mem::swap(&mut current_buffer, &mut next_buffer);
|
||||||
last_frame_time = Instant::now();
|
last_frame_time = Instant::now();
|
||||||
let _ = out.flush();
|
let _ = out.flush();
|
||||||
} else if buffer_is_empty {
|
} else {
|
||||||
for i in 0..sc {
|
for i in 0..sc {
|
||||||
last_bars[i] *= drop_factor;
|
last_bars[i] *= drop_factor;
|
||||||
render_bar(&mut next_buffer, i, sr, (last_bars[i] / max_magnitude) * sr as f32);
|
let height = ((last_bars[i] / max_magnitude) * sr as f32 * sensitivity).min(sr as f32);
|
||||||
|
render_bar(&mut next_buffer, i, sr, height);
|
||||||
}
|
}
|
||||||
|
|
||||||
view_diff.clear();
|
view_diff.clear();
|
||||||
|
|||||||
@@ -0,0 +1,153 @@
|
|||||||
|
//! Native, low-latency PipeWire capture.
|
||||||
|
//!
|
||||||
|
//! Instead of shelling out to `pw-record` (an extra process, its own
|
||||||
|
//! buffering, and an s16 round-trip), this connects a real capture stream to
|
||||||
|
//! the graph, negotiates F32 at a small quantum, and pushes mono f32 samples
|
||||||
|
//! straight into the render channel from the stream's realtime `process`
|
||||||
|
//! callback. By default it captures the monitor of the default sink (what you
|
||||||
|
//! hear); `latency` is a frames/rate hint the server honours by shrinking the
|
||||||
|
//! quantum. Rate and channel count follow the native graph format.
|
||||||
|
|
||||||
|
use std::sync::mpsc::Sender;
|
||||||
|
use std::thread;
|
||||||
|
|
||||||
|
use pipewire as pw;
|
||||||
|
use pw::spa;
|
||||||
|
use pw::{properties::properties, spa::pod::Pod};
|
||||||
|
use spa::param::audio::AudioInfoRaw;
|
||||||
|
use spa::param::format::{MediaSubtype, MediaType};
|
||||||
|
use spa::param::format_utils;
|
||||||
|
|
||||||
|
/// Start capturing on a dedicated thread. Returns once the stream is connected
|
||||||
|
/// (or with an error describing why it could not start), so the caller can
|
||||||
|
/// report cleanly before taking over the terminal.
|
||||||
|
pub fn spawn(tx: Sender<Vec<f32>>, rate: u32, channels: u8, latency: u32) -> Result<(), String> {
|
||||||
|
let (ready_tx, ready_rx) = std::sync::mpsc::channel::<Result<(), String>>();
|
||||||
|
thread::Builder::new()
|
||||||
|
.name("pipewire-capture".into())
|
||||||
|
.spawn(move || {
|
||||||
|
if let Err(e) = run(tx, rate, channels, latency, ready_tx.clone()) {
|
||||||
|
let _ = ready_tx.send(Err(e));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.map_err(|e| format!("failed to spawn capture thread: {e}"))?;
|
||||||
|
ready_rx
|
||||||
|
.recv()
|
||||||
|
.map_err(|_| "pipewire capture thread exited before connecting".to_string())?
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(
|
||||||
|
tx: Sender<Vec<f32>>,
|
||||||
|
rate: u32,
|
||||||
|
_channels: u8,
|
||||||
|
latency: u32,
|
||||||
|
ready: Sender<Result<(), String>>,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
pw::init();
|
||||||
|
|
||||||
|
let mainloop = pw::main_loop::MainLoopRc::new(None).map_err(|e| format!("mainloop: {e}"))?;
|
||||||
|
let context =
|
||||||
|
pw::context::ContextRc::new(&mainloop, None).map_err(|e| format!("context: {e}"))?;
|
||||||
|
let core = context
|
||||||
|
.connect_rc(None)
|
||||||
|
.map_err(|e| format!("connect to PipeWire (is it running?): {e}"))?;
|
||||||
|
|
||||||
|
let props = properties! {
|
||||||
|
*pw::keys::MEDIA_TYPE => "Audio",
|
||||||
|
*pw::keys::MEDIA_CATEGORY => "Capture",
|
||||||
|
*pw::keys::MEDIA_ROLE => "Music",
|
||||||
|
// Capture the monitor of the default sink - visualize what's playing.
|
||||||
|
*pw::keys::STREAM_CAPTURE_SINK => "true",
|
||||||
|
// Ask the server for a small quantum: lower = lower latency.
|
||||||
|
*pw::keys::NODE_LATENCY => format!("{latency}/{rate}"),
|
||||||
|
*pw::keys::NODE_NAME => "spectrust",
|
||||||
|
};
|
||||||
|
|
||||||
|
let stream = pw::stream::StreamBox::new(&core, "spectrust", props)
|
||||||
|
.map_err(|e| format!("stream: {e}"))?;
|
||||||
|
|
||||||
|
// Channel count is learned from the negotiated format (param_changed) and
|
||||||
|
// read back in the realtime process callback; 0 until negotiated.
|
||||||
|
let _listener = stream
|
||||||
|
.add_local_listener_with_user_data(AudioInfoRaw::default())
|
||||||
|
.param_changed(|_, info, id, param| {
|
||||||
|
let Some(param) = param else { return };
|
||||||
|
if id != pw::spa::param::ParamType::Format.as_raw() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let Ok((mt, ms)) = format_utils::parse_format(param) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if mt != MediaType::Audio || ms != MediaSubtype::Raw {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let _ = info.parse(param);
|
||||||
|
})
|
||||||
|
.process(move |stream, info| {
|
||||||
|
let Some(mut buffer) = stream.dequeue_buffer() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let ch = info.channels() as usize;
|
||||||
|
if ch == 0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let datas = buffer.datas_mut();
|
||||||
|
let Some(data) = datas.first_mut() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let n_bytes = data.chunk().size() as usize;
|
||||||
|
let Some(bytes) = data.data() else { return };
|
||||||
|
let bytes = &bytes[..n_bytes.min(bytes.len())];
|
||||||
|
let frame_bytes = ch * 4;
|
||||||
|
let frames = bytes.len() / frame_bytes;
|
||||||
|
if frames == 0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let mut mono = Vec::with_capacity(frames);
|
||||||
|
for f in 0..frames {
|
||||||
|
let base = f * frame_bytes;
|
||||||
|
let mut sum = 0.0f32;
|
||||||
|
for c in 0..ch {
|
||||||
|
let o = base + c * 4;
|
||||||
|
sum += f32::from_le_bytes([bytes[o], bytes[o + 1], bytes[o + 2], bytes[o + 3]]);
|
||||||
|
}
|
||||||
|
mono.push(sum / ch as f32);
|
||||||
|
}
|
||||||
|
let _ = tx.send(mono);
|
||||||
|
})
|
||||||
|
.register()
|
||||||
|
.map_err(|e| format!("listener: {e}"))?;
|
||||||
|
|
||||||
|
// EnumFormat pod: force F32; leave rate/channels empty to accept the
|
||||||
|
// native graph format (the server converts as needed).
|
||||||
|
let mut audio_info = AudioInfoRaw::new();
|
||||||
|
audio_info.set_format(spa::param::audio::AudioFormat::F32LE);
|
||||||
|
let obj = spa::pod::Object {
|
||||||
|
type_: spa::utils::SpaTypes::ObjectParamFormat.as_raw(),
|
||||||
|
id: spa::param::ParamType::EnumFormat.as_raw(),
|
||||||
|
properties: audio_info.into(),
|
||||||
|
};
|
||||||
|
let values: Vec<u8> = spa::pod::serialize::PodSerializer::serialize(
|
||||||
|
std::io::Cursor::new(Vec::new()),
|
||||||
|
&spa::pod::Value::Object(obj),
|
||||||
|
)
|
||||||
|
.map_err(|e| format!("pod serialize: {e}"))?
|
||||||
|
.0
|
||||||
|
.into_inner();
|
||||||
|
let mut params = [Pod::from_bytes(&values).ok_or("invalid format pod")?];
|
||||||
|
|
||||||
|
stream
|
||||||
|
.connect(
|
||||||
|
spa::utils::Direction::Input,
|
||||||
|
None,
|
||||||
|
pw::stream::StreamFlags::AUTOCONNECT
|
||||||
|
| pw::stream::StreamFlags::MAP_BUFFERS
|
||||||
|
| pw::stream::StreamFlags::RT_PROCESS,
|
||||||
|
&mut params,
|
||||||
|
)
|
||||||
|
.map_err(|e| format!("connect stream: {e}"))?;
|
||||||
|
|
||||||
|
let _ = ready.send(Ok(()));
|
||||||
|
mainloop.run();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user