diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..164b166 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,208 @@ +name: Release + +on: + push: + tags: + - "v*" + workflow_dispatch: + inputs: + tag: + description: "Tag to release (e.g. v0.1.0)" + required: true + type: string + +permissions: + contents: write + +jobs: + build: + name: ${{ matrix.label }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + include: + - label: macos-amd64 + os: macos-latest + target: x86_64-apple-darwin + - label: macos-aarch64 + os: macos-latest + target: aarch64-apple-darwin + - label: linux-amd64 + os: ubuntu-latest + target: x86_64-unknown-linux-gnu + # Native ARM Linux runner — no `cross`, no QEMU. Faster and + # matches production glibc more closely. + - label: linux-aarch64 + os: ubuntu-24.04-arm + target: aarch64-unknown-linux-gnu + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + with: + targets: ${{ matrix.target }} + + - name: Cache cargo + uses: Swatinem/rust-cache@v2 + with: + key: ${{ matrix.target }} + + # cpal links against ALSA on Linux (macOS uses CoreAudio, no dev pkg needed). + - name: Install ALSA dev headers + if: startsWith(matrix.os, 'ubuntu') + run: | + sudo apt-get update + sudo apt-get install -y libasound2-dev pkg-config + + - name: Build + run: cargo build --release --locked --target ${{ matrix.target }} --bin equalizer + + - name: Package tarball + shell: bash + run: | + set -euo pipefail + if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then + version="${{ inputs.tag }}" + else + version="${GITHUB_REF_NAME}" + fi + name="equalizer-${version}-${{ matrix.label }}" + src="target/${{ matrix.target }}/release/equalizer" + mkdir -p dist/staging + cp "$src" dist/staging/equalizer + cp README.md LICENSE dist/staging/ + tar -C dist/staging -czf "dist/${name}.tar.gz" . + (cd dist && shasum -a 256 "${name}.tar.gz" > "${name}.tar.gz.sha256") + ls -lh dist + + - name: Install dpkg-deb and rpmbuild + if: startsWith(matrix.os, 'ubuntu') + run: | + sudo apt-get update + sudo apt-get install -y dpkg-dev rpm + + - name: Build .deb + if: startsWith(matrix.os, 'ubuntu') + shell: bash + run: | + set -euo pipefail + if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then + version="${{ inputs.tag }}" + else + version="${GITHUB_REF_NAME}" + fi + version_no_v="${version#v}" + case "${{ matrix.label }}" in + linux-amd64) deb_arch=amd64 ;; + linux-aarch64) deb_arch=arm64 ;; + esac + pkg_root="dist/debian/${deb_arch}" + mkdir -p "$pkg_root/usr/local/bin" + install -m 0755 "target/${{ matrix.target }}/release/equalizer" "$pkg_root/usr/local/bin/equalizer" + sed -i "s/^Version: .*/Version: ${version_no_v}/" "$pkg_root/DEBIAN/control" + dpkg-deb --build --root-owner-group "$pkg_root" "dist/equalizer_${version_no_v}_${deb_arch}.deb" + + - name: Build .rpm + if: matrix.label == 'linux-amd64' + shell: bash + run: | + set -euo pipefail + if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then + version="${{ inputs.tag }}" + else + version="${GITHUB_REF_NAME}" + fi + version_no_v="${version#v}" + rpm_arch=amd64 + rpm_target=x86_64 + src_root="dist/rpm/${rpm_arch}" + mkdir -p "${src_root}/usr/local/bin" + install -m 0755 "target/${{ matrix.target }}/release/equalizer" "${src_root}/usr/local/bin/equalizer" + sed -i "s/^Version: .*/Version: ${version_no_v}/" "${src_root}/equalizer.spec" + + rpm_top="$(pwd)/dist/rpmbuild" + mkdir -p "${rpm_top}"/{BUILD,BUILDROOT,RPMS,SOURCES,SPECS,SRPMS} + cp -r "${src_root}" "${rpm_top}/SOURCES/${rpm_arch}" + rpmbuild --define "_topdir ${rpm_top}" \ + --define "_sourcedir ${rpm_top}/SOURCES" \ + --target "${rpm_target}" \ + -bb "${src_root}/equalizer.spec" + find "${rpm_top}/RPMS" -name '*.rpm' -exec mv {} dist/ \; + + - name: Checksum packages + if: startsWith(matrix.os, 'ubuntu') + shell: bash + run: | + set -euo pipefail + cd dist + for f in *.deb *.rpm; do + [ -e "$f" ] || continue + shasum -a 256 "$f" > "$f.sha256" + done + ls -lh + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: ${{ matrix.label }} + path: | + dist/*.tar.gz* + dist/*.deb* + dist/*.rpm* + if-no-files-found: error + + release: + name: Publish GitHub release + needs: build + runs-on: ubuntu-latest + steps: + - name: Download all artifacts + uses: actions/download-artifact@v4 + with: + path: dist + merge-multiple: true + + - name: Show artifacts + run: ls -lh dist + + - name: Create release + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ github.event.inputs.tag || github.ref_name }} + files: dist/* + generate_release_notes: true + draft: false + prerelease: false + + - name: Check Gemfury secrets + id: gemfury + env: + FURY_TOKEN: ${{ secrets.FURY_TOKEN }} + FURY_ACCOUNT: ${{ secrets.FURY_ACCOUNT }} + run: | + if [ -n "$FURY_TOKEN" ] && [ -n "$FURY_ACCOUNT" ]; then + echo "available=true" >> "$GITHUB_OUTPUT" + else + echo "available=false" >> "$GITHUB_OUTPUT" + echo "Gemfury secrets not set; skipping push" + fi + + - name: Push .deb and .rpm to Gemfury + if: steps.gemfury.outputs.available == 'true' + env: + FURY_TOKEN: ${{ secrets.FURY_TOKEN }} + FURY_ACCOUNT: ${{ secrets.FURY_ACCOUNT }} + shell: bash + run: | + set -euo pipefail + shopt -s nullglob + for pkg in dist/*.deb dist/*.rpm; do + echo "Pushing $pkg to Gemfury (${FURY_ACCOUNT})" + curl -fsS -F package=@"${pkg}" \ + "https://${FURY_TOKEN}@push.fury.io/${FURY_ACCOUNT}/" + echo + done diff --git a/.tangled/workflows/build.yml b/.tangled/workflows/build.yml new file mode 100644 index 0000000..a9b3ea1 --- /dev/null +++ b/.tangled/workflows/build.yml @@ -0,0 +1,27 @@ +when: + - event: ["push", "pull_request"] + branch: main + +engine: microvm +image: nixos + +dependencies: + - cargo + - rustc + - rustfmt + - gcc + - pkg-config + - alsa-lib + +steps: + - name: nix build + command: | + XDG_CACHE_HOME=/tmp/nix-cache nix --extra-experimental-features 'nix-command flakes' build .#default --print-build-logs + ls -l result/bin + - name: build cli + command: | + cargo build --release --locked + ls -l target/release + - name: test + command: | + cargo test --release --locked diff --git a/Cargo.toml b/Cargo.toml index 3e9d565..c5b5bc7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,8 +2,24 @@ name = "equalizer" version = "0.1.0" edition = "2024" +rust-version = "1.85" description = "Pipe raw PCM through the Rockbox 10-band equalizer to the sound card, with a ratatui front-end" license = "GPL-2.0-or-later" +authors = ["Tsiry Sandratraina "] +repository = "https://github.com/tsirysndr/equalizer" +homepage = "https://github.com/tsirysndr/equalizer" +readme = "README.md" +keywords = ["equalizer", "audio", "dsp", "rockbox", "tui"] +categories = ["multimedia::audio", "command-line-utilities"] +# Keep the published crate lean: CI, packaging, and the screenshot stay in git. +exclude = [ + "preview.png", + "dist/", + ".github/", + ".tangled/", + "flake.nix", + "flake.lock", +] [dependencies] anyhow = "1" diff --git a/README.md b/README.md index 7b1c92c..197c3ec 100644 --- a/README.md +++ b/README.md @@ -7,12 +7,18 @@ bass/treble shelves, resampling) and plays the result on your sound card via [cpal](https://crates.io/crates/cpal) — while a [ratatui](https://ratatui.rs) interface lets you tweak the bands live. -![equalizer TUI playing through a FIFO with the Synthwave '84 theme](preview.png) +![equalizer TUI playing through a FIFO with the Synthwave '84 theme](https://raw.githubusercontent.com/tsirysndr/equalizer/main/preview.png) ## Table of Contents - [Features](#features) - [Installation](#installation) + - [macOS / Linux — Homebrew](#macos--linux--homebrew) + - [Debian / Ubuntu — `.deb`](#debian--ubuntu--deb) + - [Fedora / RHEL / openSUSE — `.rpm`](#fedora--rhel--opensuse--rpm) + - [Prebuilt tarballs](#prebuilt-tarballs) + - [Nix](#nix) + - [From source](#from-source) - [Quick Start](#quick-start) - [Input Sources](#input-sources) - [stdin pipe](#stdin-pipe) @@ -45,6 +51,75 @@ via [cpal](https://crates.io/crates/cpal) — while a ## Installation +### macOS / Linux — Homebrew + +```sh +brew install tsirysndr/tap/equalizer +``` + +### Debian / Ubuntu — `.deb` + +Add the Gemfury apt repo once and install normally: + +```sh +echo "deb [trusted=yes] https://apt.fury.io/tsiry/ /" \ + | sudo tee /etc/apt/sources.list.d/tsiry.list +sudo apt update && sudo apt install equalizer +``` + +Or download the `.deb` for your architecture (amd64 / arm64) from the +[latest release](https://github.com/tsirysndr/equalizer/releases/latest): + +```sh +curl -LO https://github.com/tsirysndr/equalizer/releases/latest/download/equalizer_0.1.0_amd64.deb +sudo apt install ./equalizer_0.1.0_amd64.deb +``` + +`apt` pulls in `libasound2` (the ALSA runtime cpal needs) automatically. + +### Fedora / RHEL / openSUSE — `.rpm` + +Via the Gemfury yum repo: + +```sh +sudo tee /etc/yum.repos.d/tsiry.repo <<'EOF' +[tsiry] +name=tsiry +baseurl=https://yum.fury.io/tsiry/ +enabled=1 +gpgcheck=0 +EOF +sudo dnf install equalizer +``` + +Or straight from the release asset: + +```sh +sudo dnf install \ + https://github.com/tsirysndr/equalizer/releases/latest/download/equalizer-0.1.0-1.x86_64.rpm +``` + +### Prebuilt tarballs + +Tarballs for macOS (Intel / Apple Silicon) and Linux (amd64 / arm64) are on +the [releases page](https://github.com/tsirysndr/equalizer/releases), each +with a `.sha256` alongside: + +```sh +tar -xzf equalizer--.tar.gz +sudo mv equalizer /usr/local/bin/ +``` + +### Nix + +```sh +nix run github:tsirysndr/equalizer # run directly +nix profile install github:tsirysndr/equalizer +nix develop # dev shell (in a checkout) +``` + +### From source + ```sh git clone https://github.com/tsirysndr/equalizer cd equalizer @@ -52,7 +127,12 @@ cargo install --path . ``` Requires a C compiler (the `rockbox-dsp` crate compiles the Rockbox DSP -sources with `cc`). +sources with `cc`). On Linux you also need the ALSA development headers +and pkg-config for cpal's audio output — +`sudo apt install libasound2-dev pkg-config` (Debian/Ubuntu) or +`sudo dnf install alsa-lib-devel pkgconf` (Fedora); at runtime the +packages depend on `libasound2` / `alsa-lib`. On macOS, CoreAudio is +used and nothing extra is needed. ## Quick Start diff --git a/dist/debian/amd64/DEBIAN/control b/dist/debian/amd64/DEBIAN/control new file mode 100644 index 0000000..3fe05b5 --- /dev/null +++ b/dist/debian/amd64/DEBIAN/control @@ -0,0 +1,14 @@ +Package: equalizer +Version: 0.1.0 +Section: sound +Priority: optional +Architecture: amd64 +Maintainer: Tsiry Sandratraina +Homepage: https://github.com/tsirysndr/equalizer +Depends: libc6, libasound2 +Description: A real-time terminal equalizer for raw PCM pipes + equalizer reads raw PCM from stdin, a FIFO, or a unix socket, runs it + through the Rockbox DSP (10-band EQ, bass/treble shelves, resampling) + and plays the result on your sound card — while a Synthwave '84 ratatui + interface lets you tweak the bands live. Settings persist to a TOML + file in Rockbox's own eq_band_settings format. diff --git a/dist/debian/arm64/DEBIAN/control b/dist/debian/arm64/DEBIAN/control new file mode 100644 index 0000000..55b4eef --- /dev/null +++ b/dist/debian/arm64/DEBIAN/control @@ -0,0 +1,14 @@ +Package: equalizer +Version: 0.1.0 +Section: sound +Priority: optional +Architecture: arm64 +Maintainer: Tsiry Sandratraina +Homepage: https://github.com/tsirysndr/equalizer +Depends: libc6, libasound2 +Description: A real-time terminal equalizer for raw PCM pipes + equalizer reads raw PCM from stdin, a FIFO, or a unix socket, runs it + through the Rockbox DSP (10-band EQ, bass/treble shelves, resampling) + and plays the result on your sound card — while a Synthwave '84 ratatui + interface lets you tweak the bands live. Settings persist to a TOML + file in Rockbox's own eq_band_settings format. diff --git a/dist/rpm/amd64/equalizer.spec b/dist/rpm/amd64/equalizer.spec new file mode 100644 index 0000000..40bc0eb --- /dev/null +++ b/dist/rpm/amd64/equalizer.spec @@ -0,0 +1,36 @@ +Name: equalizer +Version: 0.1.0 +Release: 1%{?dist} +Summary: A real-time terminal equalizer for raw PCM pipes + +License: GPL-2.0-or-later +URL: https://github.com/tsirysndr/equalizer + +BuildArch: x86_64 + +Requires: glibc, alsa-lib + +%description +equalizer reads raw PCM from stdin, a FIFO, or a unix socket, runs it +through the Rockbox DSP (10-band EQ, bass/treble shelves, resampling) and +plays the result on your sound card — while a Synthwave '84 ratatui +interface lets you tweak the bands live. Settings persist to a TOML file +in Rockbox's own eq_band_settings format. + +%prep +# Nothing to prep — the binary is prebuilt. + +%build +# Nothing to build — the binary is prebuilt. + +%install +mkdir -p %{buildroot}/usr/local/bin +cp -r %{_sourcedir}/amd64/usr %{buildroot}/ + +%files +/usr/local/bin/equalizer + +%post +if [ "$1" -eq 1 ]; then + echo "equalizer: installed. Try: ffmpeg -i track.flac -f s16le -ac 2 -ar 44100 - | equalizer" +fi diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..ac10711 --- /dev/null +++ b/flake.lock @@ -0,0 +1,116 @@ +{ + "nodes": { + "advisory-db": { + "flake": false, + "locked": { + "lastModified": 1783328914, + "narHash": "sha256-aTHnDKwMe+arezu2nvQYawdlJIr6zquvYxu+knd0wk8=", + "owner": "rustsec", + "repo": "advisory-db", + "rev": "1c0f2cf9ae48eb4a221a6e3ec711c4f1f0fbc17e", + "type": "github" + }, + "original": { + "owner": "rustsec", + "repo": "advisory-db", + "type": "github" + } + }, + "crane": { + "locked": { + "lastModified": 1783203018, + "narHash": "sha256-G6R9IT/xwFuu+CYBWDUAok6AdC4ERC4ZfPPFtEpxnZE=", + "owner": "ipetkov", + "repo": "crane", + "rev": "80db5bdc391be8a1794f6d8a2d56e3a84ebcede2", + "type": "github" + }, + "original": { + "owner": "ipetkov", + "repo": "crane", + "type": "github" + } + }, + "fenix": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ], + "rust-analyzer-src": [] + }, + "locked": { + "lastModified": 1782900513, + "narHash": "sha256-GHrsl1+ysDFgmDQtQSqWS7TiYD2Q58VlwLvnf1+crJM=", + "owner": "nix-community", + "repo": "fenix", + "rev": "16810aa8f4ad89ca480b1513774e8b6f485fe368", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "fenix", + "type": "github" + } + }, + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1783224372, + "narHash": "sha256-8i/87eeoqiGE4yOTjwSA3Eh/ziJRQEmd/unYU+K27sk=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "d407951447dcd00442e97087bf374aad70c04cea", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "advisory-db": "advisory-db", + "crane": "crane", + "fenix": "fenix", + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..4da6811 --- /dev/null +++ b/flake.nix @@ -0,0 +1,162 @@ +{ + description = "equalizer - a real-time terminal equalizer for raw PCM pipes (Rockbox DSP + ratatui)"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + + # Current crane doesn't expose a `nixpkgs` input, so we don't follow it. + crane.url = "github:ipetkov/crane"; + + fenix = { + url = "github:nix-community/fenix"; + inputs.nixpkgs.follows = "nixpkgs"; + inputs.rust-analyzer-src.follows = ""; + }; + + flake-utils.url = "github:numtide/flake-utils"; + + advisory-db = { + url = "github:rustsec/advisory-db"; + flake = false; + }; + }; + + outputs = { self, nixpkgs, crane, fenix, flake-utils, advisory-db, ... }: + flake-utils.lib.eachDefaultSystem (system: + let + pkgs = import nixpkgs { + inherit system; + }; + + inherit (pkgs) lib; + + craneLib = crane.mkLib pkgs; + + src = craneLib.cleanCargoSource ./.; + + # The rockbox-dsp crate vendors and compiles the Rockbox DSP C + # sources via `cc` (stdenv provides the compiler). cpal links + # CoreAudio on Darwin automatically and ALSA on Linux via + # pkg-config. + commonArgs = { + inherit src; + + pname = "equalizer"; + version = "0.1.0"; + strictDeps = true; + + nativeBuildInputs = [ + pkgs.pkg-config + ]; + + buildInputs = lib.optionals pkgs.stdenv.isDarwin [ + pkgs.libiconv + ] ++ lib.optionals pkgs.stdenv.isLinux [ + # cpal links against ALSA on Linux for the audio output path. + pkgs.alsa-lib + ]; + + cargoExtraArgs = "--locked --bin equalizer"; + + meta = { + description = "A real-time terminal equalizer for raw PCM pipes — Rockbox 10-band EQ with a Synthwave '84 ratatui UI"; + homepage = "https://github.com/tsirysndr/equalizer"; + # The bundled Rockbox DSP firmware code is GPL-2.0-or-later. + license = lib.licenses.gpl2Plus; + mainProgram = "equalizer"; + platforms = lib.platforms.unix; + }; + }; + + craneLibLLvmTools = craneLib.overrideToolchain + (fenix.packages.${system}.complete.withComponents [ + "cargo" + "llvm-tools" + "rustc" + ]); + + # Cache the dependency graph separately from the crate source. + cargoArtifacts = craneLib.buildDepsOnly commonArgs; + + equalizer = craneLib.buildPackage (commonArgs // { + inherit cargoArtifacts; + doCheck = false; + }); + + in + { + checks = { + inherit equalizer; + + equalizer-clippy = craneLib.cargoClippy (commonArgs // { + inherit cargoArtifacts; + cargoClippyExtraArgs = "--all-targets -- --deny warnings"; + }); + + equalizer-doc = craneLib.cargoDoc (commonArgs // { + inherit cargoArtifacts; + }); + + equalizer-fmt = craneLib.cargoFmt { + inherit src; + }; + + equalizer-audit = craneLib.cargoAudit { + inherit src advisory-db; + }; + + equalizer-nextest = craneLib.cargoNextest (commonArgs // { + inherit cargoArtifacts; + partitions = 1; + partitionType = "count"; + }); + } // lib.optionalAttrs (system == "x86_64-linux") { + equalizer-coverage = craneLib.cargoTarpaulin (commonArgs // { + inherit cargoArtifacts; + }); + }; + + packages = { + default = equalizer; + equalizer = equalizer; + + equalizer-llvm-coverage = craneLibLLvmTools.cargoLlvmCov (commonArgs // { + inherit cargoArtifacts; + }); + }; + + apps.default = flake-utils.lib.mkApp { + drv = equalizer; + name = "equalizer"; + }; + + devShells.default = pkgs.mkShell { + inputsFrom = builtins.attrValues self.checks.${system}; + + # Build-time tools. pkg-config is required so cpal's build.rs can + # resolve libasound on Linux. + nativeBuildInputs = with pkgs; [ + cargo + rustc + rustfmt + clippy + rust-analyzer + pkg-config + ffmpeg # handy for piping test audio into the equalizer + ]; + + # Link-time libraries. Position matters: pkg-config only picks up + # `.pc` files from `buildInputs`, so alsa-lib MUST live here (not + # in nativeBuildInputs) for the cpal → ALSA link to resolve. + buildInputs = with pkgs; lib.optionals stdenv.isDarwin [ + libiconv + ] ++ lib.optionals stdenv.isLinux [ + alsa-lib + ]; + + shellHook = '' + echo "🎛️ equalizer dev shell — try: ffmpeg -i track.flac -f s16le -ac 2 -ar 44100 - | cargo run --release" + ''; + }; + }); +}