Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
File renamed without changes
22 changes: 11 additions & 11 deletions .github/workflows/builder.yml
Original file line number Diff line number Diff line change
Expand Up @@ -212,15 +212,15 @@ jobs:
run: |
VERSION=$(git describe --tags --always --dirty 2>/dev/null || echo "dev")
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "Building jivefire $VERSION for ${{ matrix.os }}/${{ matrix.arch }}"
echo "Building jive-visualiser $VERSION for ${{ matrix.os }}/${{ matrix.arch }}"
- name: Build binary
run: |
go build -ldflags="-X main.version=${{ steps.version.outputs.version }}" -o jivefire-${{ matrix.os }}-${{ matrix.arch }} ./cmd/jivefire
go build -ldflags="-X main.version=${{ steps.version.outputs.version }}" -o jive-visualiser-${{ matrix.os }}-${{ matrix.arch }} ./cmd/jive-visualiser
- name: Upload artifact
uses: actions/upload-artifact@v7
with:
name: jivefire-${{ matrix.os }}-${{ matrix.arch }}
path: jivefire-${{ matrix.os }}-${{ matrix.arch }}
name: jive-visualiser-${{ matrix.os }}-${{ matrix.arch }}
path: jive-visualiser-${{ matrix.os }}-${{ matrix.arch }}

release:
name: Release 📦
Expand Down Expand Up @@ -257,12 +257,12 @@ jobs:

```bash
# Linux (amd64)
chmod +x jivefire-linux-amd64
sudo mv jivefire-linux-amd64 /usr/local/bin/jivefire
chmod +x jive-visualiser-linux-amd64
sudo mv jive-visualiser-linux-amd64 /usr/local/bin/jive-visualiser

# macOS (Apple Silicon)
chmod +x jivefire-darwin-arm64
sudo mv jivefire-darwin-arm64 /usr/local/bin/jivefire
chmod +x jive-visualiser-darwin-arm64
sudo mv jive-visualiser-darwin-arm64 /usr/local/bin/jive-visualiser
```

## Checksums
Expand All @@ -275,16 +275,16 @@ jobs:
uses: actions/download-artifact@v8
with:
path: artifacts
pattern: jivefire-*
pattern: jive-visualiser-*
merge-multiple: false
- name: Create release
uses: softprops/action-gh-release@v3
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ steps.version.outputs.version }}
name: Jivefire ${{ steps.version.outputs.version }}
name: Jive Visualiser ${{ steps.version.outputs.version }}
body_path: CHANGELOG.md
draft: false
prerelease: false
files: artifacts/jivefire-*/jivefire-*
files: artifacts/jive-visualiser-*/jive-visualiser-*
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
.direnv
/jivefire
/jive-visualiser
testdata/
3 changes: 3 additions & 0 deletions .harper-dictionary.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Jivefire
Visualiser
visualiser
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
- Memory-efficient: ~50MB footprint for 30-minute audio vs 600MB for single-pass

### Key Modules
- `cmd/jivefire/main.go` — CLI entry, 2-pass coordinator
- `cmd/jive-visualiser/main.go` — CLI entry, 2-pass coordinator
- `internal/audio/` — `StreamingReader` (reader.go) chunk-based decode, FFT analysis
- `internal/encoder/` — ffmpeg-statigo wrapper, RGB→YUV conversion, FIFO buffer
- `internal/yuv/` — YCbCr coefficients, `RGBToY`/`RGBToCb`/`RGBToCr`, `ParallelRows`
Expand Down
20 changes: 11 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
# Jivefire 🔥
# Jive Visualiser 🔥

> Spin your podcast .wav into a groovy MP4 visualiser with spring-driven real-time audio frequencies.

_Formerly known as Jivefire._

## The Groove

Your podcast audio deserves more than a static image on YouTube. Jivefire transforms WAV/MP3/FLAC into delightful 720p visuals—bars that breathe with your dialogue, rise with your laughter, and groove through every frequency.
Your podcast audio deserves more than a static image on YouTube. Jive Visualiser transforms WAV/MP3/FLAC into delightful 720p visuals—bars that breathe with your dialogue, rise with your laughter, and groove through every frequency.

<div align="center"><img alt="Jivefire Demo" src=".github/jivefire.gif" width="860" /></div>
<div align="center"><img alt="Jive Visualiser Demo" src=".github/jive-visualiser.gif" width="860" /></div>

### What's Cooking

Expand All @@ -26,17 +28,17 @@ Your podcast audio deserves more than a static image on YouTube. Jivefire transf

### Generate Video
```bash
./jivefire input.wav output.mp4
./jive-visualiser input.wav output.mp4
```

### With Episode Number and Title
```bash
./jivefire --episode=42 --title="Linux Matters" input.wav output.mp4
./jive-visualiser --episode=42 --title="Linux Matters" input.wav output.mp4
```

### Without Episode Number (unnumbered audio)
```bash
./jivefire --title="Linux Matters" input.wav output.mp4
./jive-visualiser --title="Linux Matters" input.wav output.mp4
```

`--episode` is optional. Omitting it suppresses the episode number overlay entirely — useful for archive or bonus audio that has no episode number. Passing `--episode=0` still renders `00` on-screen (single-digit values are zero-padded, so `5` renders as `05`); absence is what controls the overlay, not the value.
Expand All @@ -51,7 +53,7 @@ Your podcast audio deserves more than a static image on YouTube. Jivefire transf

## Build

Jivefire uses [ffmpeg-statigo](https://github.com/linuxmatters/ffmpeg-statigo) for FFmpeg static bindings.
Jive Visualiser uses [ffmpeg-statigo](https://github.com/linuxmatters/ffmpeg-statigo) for FFmpeg static bindings.

```bash
# Setup or update ffmpeg-statigo submodule and library
Expand All @@ -63,10 +65,10 @@ just test # Run tests
just test-encoder # Test encoder
```

## Why Jivefire?
## Why Jive Visualiser?

FFmpeg's audio visualisation filters (`showfreqs`, `showspectrum`) render continuous frequency spectra, not discrete bars. No amount of FFmpeg filter chain kung-fu can achieve the discrete 64-bar aesthetic required for Linux Matters branding. Solution: Do the FFT analysis and bar rendering in Go, pipe frames to FFmpeg for encoding.

**Why Go over Python?** The original `djfun/audio-visualizer-python` tool is a moribund Qt5 GUI with significant tech debt. For our podcast production needs we wanted multi-archtitecture tools that's that can integrate into automation pipelines.

The Jivefire architecture, such as it is, is available in the [ARCHITECTURE.md](docs/ARCHITECTURE.md) document.
The Jive Visualiser architecture, such as it is, is available in the [ARCHITECTURE.md](docs/ARCHITECTURE.md) document.
2 changes: 1 addition & 1 deletion cmd/jivefire/main.go → cmd/jive-visualiser/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ var CLI struct {
func main() {
ctx := kong.Parse(
&CLI,
kong.Name("jivefire"),
kong.Name("jive-visualiser"),
kong.Description("Spin your podcast .wav into a groovy MP4 visualiser."),
kong.Vars{"version": version},
kong.UsageOnError(),
Expand Down
4 changes: 2 additions & 2 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Jivefire Architecture
# Jive Visualiser Architecture

**TL;DR:** 2-pass streaming audio visualiser that generates broadcast-ready MP4s from podcast audio. FFmpeg-based audio decoding + ffmpeg-statigo static linking = single deployable binary with broad format support.

Expand Down Expand Up @@ -140,7 +140,7 @@ Preview renders via Unicode blocks (`▁▂▃▄▅▆▇█`) using actual bar
## File Structure

```
cmd/jivefire/main.go → CLI entry, 2-pass coordinator
cmd/jive-visualiser/main.go → CLI entry, 2-pass coordinator
internal/audio/ → StreamingReader (chunk-based FFmpeg decode), FFT analysis
internal/encoder/ → ffmpeg-statigo wrapper, RGB→YUV conversion, FIFO buffer
├─ encoder.go → Video/audio encoding, frame submission
Expand Down
2 changes: 1 addition & 1 deletion internal/cli/help.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ func StyledHelpPrinter(options kong.HelpOptions) kong.HelpPrinter {
var sb strings.Builder

// Title and description
sb.WriteString(helpTitleStyle.Render("Jivefire 🔥"))
sb.WriteString(helpTitleStyle.Render("Jive Visualiser 🔥"))
sb.WriteString("\n")
sb.WriteString(helpDescStyle.Render("Spin your podcast .wav into a groovy MP4 visualiser with spring-driven real-time audio frequencies."))
sb.WriteString("\n")
Expand Down
8 changes: 4 additions & 4 deletions internal/cli/styles.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ var (
// Title style - bold red with fire emoji
TitleStyle = lipgloss.NewStyle().
Bold(true).
Foreground(theme.JivefireRed).
Foreground(theme.JiveRed).
MarginBottom(1)

// Section header style
Expand All @@ -26,7 +26,7 @@ var (
// Error message style
ErrorStyle = lipgloss.NewStyle().
Bold(true).
Foreground(theme.JivefireRed)
Foreground(theme.JiveRed)

// Warning message style
WarningStyle = lipgloss.NewStyle().
Expand All @@ -49,7 +49,7 @@ var (

// PrintVersion prints version information
func PrintVersion(version string) {
fmt.Println(TitleStyle.Render("Jivefire 🔥"))
fmt.Println(TitleStyle.Render("Jive Visualiser 🔥"))
fmt.Printf("%s %s\n", KeyStyle.Render("Version:"), ValueStyle.Render(version))
}

Expand All @@ -62,7 +62,7 @@ type EncoderInfo struct {

// PrintHardwareProbe prints a styled hardware encoder probe result
func PrintHardwareProbe(encoders []EncoderInfo) {
fmt.Println(TitleStyle.Render("Jivefire 🔥"))
fmt.Println(TitleStyle.Render("Jive Visualiser 🔥"))
fmt.Println(HeaderStyle.Render("Hardware Encoder Probe"))

for _, enc := range encoders {
Expand Down
2 changes: 1 addition & 1 deletion internal/encoder/sws_benchmark_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ func createTestFrames() ([]byte, *ffmpeg.AVFrame) {
// =============================================================================

// BenchmarkGoRGBToYUV measures the parallelised Go implementation.
// This is the production code path used by Jivefire.
// This is the production code path used by Jive Visualiser.
func BenchmarkGoRGBToYUV(b *testing.B) {
rgbData, yuvFrame := createTestFrames()
defer ffmpeg.AVFrameFree(&yuvFrame)
Expand Down
2 changes: 1 addition & 1 deletion internal/theme/theme.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ var (
WarmGray = lipgloss.Color("#B8860B") // Dark goldenrod for subtle text

// CLI output colours
JivefireRed = lipgloss.Color("#A40000") // Deep Jivefire red for titles and errors
JiveRed = lipgloss.Color("#A40000") // Deep red for titles and errors
GoldOrange = lipgloss.Color("#FFA500") // Orange-gold for section headers
NeonYellow = lipgloss.Color("#FFFF00") // Bright yellow for highlighted values
NeutralGray = lipgloss.Color("#888888") // Neutral grey for keys and labels
Expand Down
4 changes: 2 additions & 2 deletions internal/ui/progress.go
Original file line number Diff line number Diff line change
Expand Up @@ -443,7 +443,7 @@ func (m *Model) renderFinalProgress() string {
title := lipgloss.NewStyle().
Bold(true).
Foreground(theme.FireYellow).
Render("Jivefire 🔥")
Render("Jive Visualiser 🔥")

s.WriteString(title)
s.WriteString("\n")
Expand Down Expand Up @@ -496,7 +496,7 @@ func (m *Model) renderProgress() string {
title := lipgloss.NewStyle().
Bold(true).
Foreground(theme.FireYellow).
Render("Jivefire 🔥")
Render("Jive Visualiser 🔥")

s.WriteString(title)
s.WriteString("\n")
Expand Down
6 changes: 3 additions & 3 deletions jivefire.tape → jive-visualiser.tape
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,9 @@
# Hide Hide the subsequent commands from the output
# Show Show the subsequent commands in the output

Output .github/jivefire.gif
Output .github/jive-visualiser.gif

Require ./jivefire
Require ./jive-visualiser

Set Shell "fish"
Set FontSize 13
Expand All @@ -69,6 +69,6 @@ Set WindowBarSize 32
Set TypingSpeed 27ms
Set Theme catppuccin-mocha

Type "./jivefire --episode=69 --title='Linux Matters' testdata/LMP69s.flac testdata/LMP69.mp4" Sleep 250ms Enter
Type "./jive-visualiser --episode=69 --title='Linux Matters' testdata/LMP69s.flac testdata/LMP69.mp4" Sleep 250ms Enter

Sleep 13s
50 changes: 25 additions & 25 deletions justfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Jivefire - Just Commands
# Jive Visualiser - Just Commands

# List commands
default:
Expand Down Expand Up @@ -86,26 +86,26 @@ setup:
echo "Don't forget to commit: git commit -m 'chore: update ffmpeg-statigo to $TAG'"
fi

# Build jivefire
# Build jive-visualiser
build: _check-submodule
#!/usr/bin/env bash
VERSION=$(git describe --tags --always --dirty 2>/dev/null || echo "dev")
echo "Building jivefire version: $VERSION"
CGO_ENABLED=1 go build -ldflags="-X main.version=$VERSION" -o jivefire ./cmd/jivefire
echo "Building jive-visualiser version: $VERSION"
CGO_ENABLED=1 go build -ldflags="-X main.version=$VERSION" -o jive-visualiser ./cmd/jive-visualiser

# Clean build artifacts
clean:
rm -fv jivefire 2>/dev/null || true
rm -fv jive-visualiser 2>/dev/null || true
@rm testdata/*.mp4 2>/dev/null || true
@rm testdata/*.flac 2>/dev/null || true
@rm testdata/*.wav 2>/dev/null || true
@rm testdata/*-stereo.mp3 2>/dev/null || true

# Install jivefire to ~/.local/bin
# Install jive-visualiser to ~/.local/bin
install: build
@mkdir -p ~/.local/bin 2>/dev/null || true
@mv ./jivefire ~/.local/bin/jivefire
@echo "Installed jivefire to ~/.local/bin/jivefire"
@mv ./jive-visualiser ~/.local/bin/jive-visualiser
@echo "Installed jive-visualiser to ~/.local/bin/jive-visualiser"
@echo "Make sure ~/.local/bin is in your PATH"

# Benchmark RGB→YUV conversion (quick summary)
Expand Down Expand Up @@ -164,37 +164,37 @@ bench-encoders: build
# Clean up any previous benchmark outputs
rm -f testdata/bench-*.mp4

# Use jivefire's built-in hardware probe to detect available encoders
# Use jive-visualiser's built-in hardware probe to detect available encoders
echo "Probing hardware encoders..."
./jivefire --probe
./jive-visualiser --probe

# Build encoder list from probe results
ENCODERS=()

# Software is always available
ENCODERS+=("--command-name" "Software (libx264)" "./jivefire --no-preview --encoder=software '$INPUT' testdata/bench-software.mp4")
ENCODERS+=("--command-name" "Software (libx264)" "./jive-visualiser --no-preview --encoder=software '$INPUT' testdata/bench-software.mp4")

# Parse jivefire --probe output to detect available hardware encoders
PROBE_OUTPUT=$(./jivefire --probe 2>&1)
# Parse jive-visualiser --probe output to detect available hardware encoders
PROBE_OUTPUT=$(./jive-visualiser --probe 2>&1)

if echo "$PROBE_OUTPUT" | grep -q "h264_nvenc.*✓ available"; then
ENCODERS+=("--command-name" "NVENC (h264_nvenc)" "./jivefire --no-preview --encoder=nvenc '$INPUT' testdata/bench-nvenc.mp4")
ENCODERS+=("--command-name" "NVENC (h264_nvenc)" "./jive-visualiser --no-preview --encoder=nvenc '$INPUT' testdata/bench-nvenc.mp4")
fi

if echo "$PROBE_OUTPUT" | grep -q "h264_vaapi.*✓ available"; then
ENCODERS+=("--command-name" "VA-API (h264_vaapi)" "./jivefire --no-preview --encoder=vaapi '$INPUT' testdata/bench-vaapi.mp4")
ENCODERS+=("--command-name" "VA-API (h264_vaapi)" "./jive-visualiser --no-preview --encoder=vaapi '$INPUT' testdata/bench-vaapi.mp4")
fi

if echo "$PROBE_OUTPUT" | grep -q "h264_vulkan.*✓ available"; then
ENCODERS+=("--command-name" "Vulkan (h264_vulkan)" "./jivefire --no-preview --encoder=vulkan '$INPUT' testdata/bench-vulkan.mp4")
ENCODERS+=("--command-name" "Vulkan (h264_vulkan)" "./jive-visualiser --no-preview --encoder=vulkan '$INPUT' testdata/bench-vulkan.mp4")
fi

if echo "$PROBE_OUTPUT" | grep -q "h264_qsv.*✓ available"; then
ENCODERS+=("--command-name" "QSV (h264_qsv)" "./jivefire --no-preview --encoder=qsv '$INPUT' testdata/bench-qsv.mp4")
ENCODERS+=("--command-name" "QSV (h264_qsv)" "./jive-visualiser --no-preview --encoder=qsv '$INPUT' testdata/bench-qsv.mp4")
fi

if echo "$PROBE_OUTPUT" | grep -q "h264_videotoolbox.*✓ available"; then
ENCODERS+=("--command-name" "VideoToolbox (h264_videotoolbox)" "./jivefire --no-preview --encoder=videotoolbox '$INPUT' testdata/bench-videotoolbox.mp4")
ENCODERS+=("--command-name" "VideoToolbox (h264_videotoolbox)" "./jive-visualiser --no-preview --encoder=videotoolbox '$INPUT' testdata/bench-videotoolbox.mp4")
fi

echo ""
Expand All @@ -214,7 +214,7 @@ bench-encoders: build
# Record gif
vhs: build
# unset LD_LIBRARY_PATH to avoid ttyd/libwebsockets conflicts with GPU drivers
@env -u LD_LIBRARY_PATH vhs ./jivefire.tape
@env -u LD_LIBRARY_PATH vhs ./jive-visualiser.tape

# Show current version (from git tags or "dev" if no tags)
version:
Expand Down Expand Up @@ -309,12 +309,12 @@ test-encoder: build
fi
echo "OK: $2 audio ${out_dur}s matches source ${src_dur}s (Δ${delta}s)"
}
./jivefire --episode="01" --title "Linux Matters mp3 (mono)" testdata/LMP0.mp3 testdata/LMP0-mp3.mp4
./jivefire --no-preview --channels 2 --episode="02" --title "Linux Matters mp3 (stereo)" testdata/LMP0-stereo.mp3 testdata/LMP0-mp3-stereo.mp4
./jivefire --episode="01" --title "Linux Matters flac (mono)" testdata/LMP0.flac testdata/LMP0-flac.mp4
./jivefire --no-preview --channels 2 --episode="02" --title "Linux Matters flac (stereo)" testdata/LMP0-stereo.flac testdata/LMP0-flac-stereo.mp4
./jivefire --episode="01" --title "Linux Matters: wav (mono)" testdata/LMP0.wav testdata/LMP0-wav.mp4
./jivefire --no-preview --channels 2 --episode="02" --title "Linux Matters: wav (stereo)" testdata/LMP0-stereo.wav testdata/LMP0-wav-stereo.mp4
./jive-visualiser --episode="01" --title "Linux Matters mp3 (mono)" testdata/LMP0.mp3 testdata/LMP0-mp3.mp4
./jive-visualiser --no-preview --channels 2 --episode="02" --title "Linux Matters mp3 (stereo)" testdata/LMP0-stereo.mp3 testdata/LMP0-mp3-stereo.mp4
./jive-visualiser --episode="01" --title "Linux Matters flac (mono)" testdata/LMP0.flac testdata/LMP0-flac.mp4
./jive-visualiser --no-preview --channels 2 --episode="02" --title "Linux Matters flac (stereo)" testdata/LMP0-stereo.flac testdata/LMP0-flac-stereo.mp4
./jive-visualiser --episode="01" --title "Linux Matters: wav (mono)" testdata/LMP0.wav testdata/LMP0-wav.mp4
./jive-visualiser --no-preview --channels 2 --episode="02" --title "Linux Matters: wav (stereo)" testdata/LMP0-stereo.wav testdata/LMP0-wav-stereo.mp4
assert_audio_match testdata/LMP0.mp3 testdata/LMP0-mp3.mp4
assert_audio_match testdata/LMP0-stereo.mp3 testdata/LMP0-mp3-stereo.mp4
assert_audio_match testdata/LMP0.flac testdata/LMP0-flac.mp4
Expand Down
Loading