Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
d77a643
Add test suite with CI and Linux UX improvements.
Skeyelab Jun 12, 2026
36af9a1
Install PortAudio runtime for CI unit tests.
Skeyelab Jun 12, 2026
f7eaac0
Use editable install in CI so coverage tracks local source.
Skeyelab Jun 12, 2026
7cc3620
Disable packaging build jobs in CI to save runner minutes.
Skeyelab Jun 12, 2026
68a5e8a
Merge pull request #1 from ericdahl-dev/test-coverage-foundation
Skeyelab Jun 12, 2026
3f6d0b1
Expand unit tests for core audio display logic.
Skeyelab Jun 12, 2026
d69183b
Add integration tests for dock widgets and app bootstrap.
Skeyelab Jun 12, 2026
0e8750d
Merge pull request #9 from ericdahl-dev/improve-test-suite
Skeyelab Jun 12, 2026
8d91547
Introduce injectable audio ingest seam for capture.
Skeyelab Jun 12, 2026
ddbbcbd
Merge pull request #10 from ericdahl-dev/audio-ingest-seam
Skeyelab Jun 12, 2026
355349b
Formalize dock analysis widget protocol and frame reader.
Skeyelab Jun 12, 2026
47f8f08
Merge pull request #11 from ericdahl-dev/dock-analysis-protocol
Skeyelab Jun 12, 2026
ff54b30
Extract SpectrumFrameAnalyzer for testable FFT peak logic.
Skeyelab Jun 12, 2026
1397015
Document SpectrumFrameAnalyzer in agent domain docs
Skeyelab Jun 12, 2026
bb7a60b
Merge pull request #12 from ericdahl-dev/spectrum-frame-analyzer
Skeyelab Jun 12, 2026
1a66107
Split settings from input device catalog and restore last-used mic.
Skeyelab Jun 12, 2026
d902d7b
Merge branch 'master' into settings-device-catalog
Skeyelab Jun 12, 2026
9c6d0ab
Bump version to 0.55 for fork release.
Skeyelab Jun 13, 2026
2223b1e
Enable packaging CI jobs on version tags only.
Skeyelab Jun 13, 2026
57a8c81
Merge pull request #13 from ericdahl-dev/settings-device-catalog
Skeyelab Jun 13, 2026
5585f90
Merge pull request #15 from ericdahl-dev/release/0.55
Skeyelab Jun 13, 2026
f85dc16
Add calibrated dB levels dock with A/B/C weighting for release 0.56.
Skeyelab Jun 13, 2026
a95ad5a
Merge pull request #33 from ericdahl-dev/feature/db-levels-dock
Skeyelab Jun 13, 2026
ac6bd14
Add global input calibration with mic cal files and per-dock overrides.
Skeyelab Jun 13, 2026
a59c2a1
Fix dB levels dock layout for stereo inputs.
Skeyelab Jun 13, 2026
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
27 changes: 27 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,31 @@ on:
pull_request:

jobs:
test-linux:
runs-on: ubuntu-22.04

steps:
- name: Checkout
uses: actions/checkout@v5

- name: Set up Python
uses: actions/setup-python@v6

- name: Install Qt and audio build dependencies
run: |
sudo apt-get update
sudo apt-get install -y \
libxkbcommon-x11-0 \
libxcb-xinerama0 \
libasound2-dev \
libjack-dev \
libportaudio2

- name: Run unit tests with coverage
run: bash ./.github/workflows/test-linux.sh

build-windows:
if: startsWith(github.ref, 'refs/tags/v')
runs-on: windows-2022

steps:
Expand Down Expand Up @@ -69,6 +93,7 @@ jobs:
if-no-files-found: error

build-linux:
if: startsWith(github.ref, 'refs/tags/v')
runs-on: ubuntu-22.04

steps:
Expand All @@ -92,6 +117,7 @@ jobs:
if-no-files-found: error

build-macos:
if: startsWith(github.ref, 'refs/tags/v')
runs-on: macos-13

steps:
Expand All @@ -115,6 +141,7 @@ jobs:
if-no-files-found: error

release:
if: startsWith(github.ref, 'refs/tags/v')
name: Create release and upload artifacts
runs-on: ubuntu-latest
needs: [build-windows, build-linux, build-macos]
Expand Down
14 changes: 14 additions & 0 deletions .github/workflows/test-linux.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
#!/bin/bash

set -euxo pipefail

export QT_QPA_PLATFORM=offscreen
export QT_QUICK_CONTROLS_STYLE=Fusion

python3 -m pip install --upgrade pip
python3 -m pip install -e ".[dev]"

python3 setup.py build_ext --inplace

python3 -m coverage run --source=friture friture/test/runner.py
python3 -m coverage report --fail-under=22
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ Notepad++
.qt_for_python

.mypy_cache
.coverage

# Macos
.DS_Store
17 changes: 17 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Friture

Real-time audio visualization and analysis (PyQt5, Python).

## Agent skills

### Issue tracker

GitHub Issues on `ericdahl-dev/friture` via the `gh` CLI (`--repo ericdahl-dev/friture`). See `docs/agents/issue-tracker.md`.

### Triage labels

Five canonical triage roles mapped to GitHub labels (defaults). See `docs/agents/triage-labels.md`.

### Domain docs

Single-context layout: `CONTEXT.md` at repo root, ADRs in `docs/adr/`. See `docs/agents/domain.md`.
50 changes: 50 additions & 0 deletions docs/agents/domain.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Domain Docs

How the engineering skills should consume this repo's domain documentation when exploring the codebase.

## Before exploring, read these

- **`CONTEXT.md`** at the repo root, or
- **`CONTEXT-MAP.md`** at the repo root if it exists — it points at one `CONTEXT.md` per context. Read each one relevant to the topic.
- **`docs/adr/`** — read ADRs that touch the area you're about to work in. In multi-context repos, also check `src/<context>/docs/adr/` for context-scoped decisions.

If any of these files don't exist, **proceed silently**. Don't flag their absence; don't suggest creating them upfront. The producer skill (`/grill-with-docs`) creates them lazily when terms or decisions actually get resolved.

## File structure

Single-context repo:

```
/
├── CONTEXT.md ← not created yet; add when domain terms stabilize
├── docs/adr/ ← not created yet
├── docs/agents/ ← agent skills configuration (this folder)
└── friture/ ← application source
```

## Dock analysis widget

Dock-hosted analyzers implement **`DockAnalysisWidget`** (`friture/dock_analysis_widget.py`):

- `set_buffer` / `handle_new_data` — audio path from shared `AudioBuffer`
- `canvasUpdate` — display timer refresh (~25 ms)
- `pause` / `restart` — optional; `Dock` skips if missing
- `saveState` / `restoreState` / `settings_called` / `qml_file_name` / `view_model`

FFT-style docks should read history via **`RingBufferFrameReader`** (`friture/ring_buffer_frame_reader.py`). Spectrum FFT/smoothing/peak logic lives in **`SpectrumFrameAnalyzer`** (`friture/spectrum_frame_analyzer.py`) — test without Qt. Chunk-only docks (levels, octave spectrum) may consume the latest `floatdata` chunk directly.

Integration tests should use ``AudioHarness`` + ``wire_dock_analysis_widget``
in `friture/test/helpers.py`. Settings tests inject ``InputDeviceCatalog`` directly
(see `friture/input_device_catalog.py`) — no need to patch ``get_audio_ingest``.

## Use the glossary's vocabulary

When your output names a domain concept (in an issue title, a refactor proposal, a hypothesis, a test name), use the term as defined in `CONTEXT.md`. Don't drift to synonyms the glossary explicitly avoids.

If the concept you need isn't in the glossary yet, that's a signal — either you're inventing language the project doesn't use (reconsider) or there's a real gap (note it for `/grill-with-docs`).

## Flag ADR conflicts

If your output contradicts an existing ADR, surface it explicitly rather than silently overriding:

> _Contradicts ADR-0007 (event-sourced orders) — but worth reopening because…_
22 changes: 22 additions & 0 deletions docs/agents/issue-tracker.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Issue tracker: GitHub

Issues and PRDs for this repo live as GitHub issues on **`ericdahl-dev/friture`**. Use the `gh` CLI for all operations.

This clone also has `upstream` (`tlecomte/friture`). When working on upstream bugs or contributing back, read upstream issues with `--repo tlecomte/friture`; create and triage work for this fork with `--repo ericdahl-dev/friture` unless the user says otherwise.

## Conventions

- **Create an issue**: `gh issue create --repo ericdahl-dev/friture --title "..." --body "..."`. Use a heredoc for multi-line bodies.
- **Read an issue**: `gh issue view <number> --repo ericdahl-dev/friture --comments`, filtering comments by `jq` and also fetching labels.
- **List issues**: `gh issue list --repo ericdahl-dev/friture --state open --json number,title,body,labels,comments --jq '[.[] | {number, title, body, labels: [.labels[].name], comments: [.comments[].body]}]'` with appropriate `--label` and `--state` filters.
- **Comment on an issue**: `gh issue comment <number> --repo ericdahl-dev/friture --body "..."`
- **Apply / remove labels**: `gh issue edit <number> --repo ericdahl-dev/friture --add-label "..."` / `--remove-label "..."`
- **Close**: `gh issue close <number> --repo ericdahl-dev/friture --comment "..."`

## When a skill says "publish to the issue tracker"

Create a GitHub issue on `ericdahl-dev/friture`.

## When a skill says "fetch the relevant ticket"

Run `gh issue view <number> --repo ericdahl-dev/friture --comments`.
15 changes: 15 additions & 0 deletions docs/agents/triage-labels.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Triage Labels

The skills speak in terms of five canonical triage roles. This file maps those roles to the actual label strings used in this repo's issue tracker (`ericdahl-dev/friture`).

| Label in mattpocock/skills | Label in our tracker | Meaning |
| -------------------------- | -------------------- | ---------------------------------------- |
| `needs-triage` | `needs-triage` | Maintainer needs to evaluate this issue |
| `needs-info` | `needs-info` | Waiting on reporter for more information |
| `ready-for-agent` | `ready-for-agent` | Fully specified, ready for an AFK agent |
| `ready-for-human` | `ready-for-human` | Requires human implementation |
| `wontfix` | `wontfix` | Will not be actioned |

When a skill mentions a role (e.g. "apply the AFK-ready triage label"), use the corresponding label string from this table.

The fork also has GitHub default labels (`bug`, `enhancement`, `documentation`, etc.). Use those for **category**; use the triage labels above for **workflow state**.
3 changes: 2 additions & 1 deletion friture/ControlBar.qml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ RowLayout {
"Generator",
"Delay Estimator",
"Long-time levels",
"Pitch Tracker"
"Pitch Tracker",
"dB levels"
]
currentIndex: viewModel.currentIndex
onCurrentIndexChanged: viewModel.currentIndex = currentIndex
Expand Down
199 changes: 199 additions & 0 deletions friture/DbLevelsDock.qml
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
import QtQuick 2.15
import QtQuick.Layouts 1.15
import Friture 1.0

Rectangle {
id: root
anchors.fill: parent
SystemPalette { id: systemPalette; colorGroup: SystemPalette.Active }
color: systemPalette.window

required property var viewModel
required property string fixedFont

readonly property int captionSize: Math.max(11, Math.round(Math.min(width, height) / 22))
readonly property int monoValuePixelSize: Math.max(
32, Math.min(Math.floor(Math.min(width, height) / 3.5), 200))
readonly property int stereoValuePixelSize: fitValuePixelSize(stereoSampleText())

readonly property real layoutMargin: Math.max(10, Math.min(width, height) * 0.04)
readonly property real layoutSpacing: Math.max(6, height * 0.02)

FontMetrics {
id: valueFontMetrics
font.family: fixedFont
font.bold: true
}

function stereoSampleText() {
return level_to_text(viewModel.level_data_slow.level_max)
+ level_to_text(viewModel.level_data_slow_2.level_max);
}

function fitValuePixelSize(sampleText) {
var margin = layoutMargin;
var columnWidth = Math.max(40, (width - 2 * margin) / 3);
var size = Math.max(24, Math.min(Math.floor(Math.min(width, height) / 5.5), 120));
valueFontMetrics.font.pixelSize = size;
while (size > 20 && valueFontMetrics.advanceWidth(sampleText) > columnWidth * 0.9) {
size -= 2;
valueFontMetrics.font.pixelSize = size;
}
return size;
}

function level_to_text(dB) {
if (dB < -150.) {
return "-Inf";
}
return dB.toFixed(1);
}

function peakCaption() {
return "PEAK · " + viewModel.unit_label + viewModel.weighting_suffix;
}

function rmsCaption() {
return "RMS · " + viewModel.unit_label + viewModel.weighting_suffix;
}

// Single input: large centered readout
ColumnLayout {
visible: !viewModel.two_channels
anchors.fill: parent
anchors.margins: layoutMargin
spacing: layoutSpacing

Text {
text: peakCaption()
font.family: fixedFont
font.pixelSize: captionSize
font.capitalization: Font.AllUppercase
color: systemPalette.windowText
opacity: 0.85
Layout.fillWidth: true
horizontalAlignment: Text.AlignHCenter
}

Text {
Layout.fillWidth: true
Layout.fillHeight: true
Layout.minimumHeight: 48
font.pixelSize: monoValuePixelSize
font.bold: true
font.family: fixedFont
color: systemPalette.windowText
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
text: level_to_text(viewModel.level_data_slow.level_max)
}

Text {
text: rmsCaption()
font.family: fixedFont
font.pixelSize: captionSize
font.capitalization: Font.AllUppercase
color: systemPalette.windowText
opacity: 0.85
Layout.fillWidth: true
horizontalAlignment: Text.AlignHCenter
}

Text {
Layout.fillWidth: true
Layout.fillHeight: true
Layout.minimumHeight: 48
font.pixelSize: monoValuePixelSize
font.bold: true
font.family: fixedFont
color: systemPalette.windowText
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
text: level_to_text(viewModel.level_data_slow.level_rms)
}
}

// Two inputs: one column per channel, captions in the middle
GridLayout {
visible: viewModel.two_channels
anchors.fill: parent
anchors.margins: layoutMargin
columns: 3
rowSpacing: layoutSpacing
columnSpacing: 4

Text {
Layout.fillWidth: true
Layout.fillHeight: true
Layout.minimumHeight: 48
font.pixelSize: stereoValuePixelSize
font.bold: true
font.family: fixedFont
color: systemPalette.windowText
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
text: level_to_text(viewModel.level_data_slow.level_max)
}

Text {
text: peakCaption()
font.family: fixedFont
font.pixelSize: captionSize
font.capitalization: Font.AllUppercase
color: systemPalette.windowText
opacity: 0.85
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}

Text {
Layout.fillWidth: true
Layout.fillHeight: true
Layout.minimumHeight: 48
font.pixelSize: stereoValuePixelSize
font.bold: true
font.family: fixedFont
color: systemPalette.windowText
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
text: level_to_text(viewModel.level_data_slow_2.level_max)
}

Text {
Layout.fillWidth: true
Layout.fillHeight: true
Layout.minimumHeight: 48
font.pixelSize: stereoValuePixelSize
font.bold: true
font.family: fixedFont
color: systemPalette.windowText
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
text: level_to_text(viewModel.level_data_slow.level_rms)
}

Text {
text: rmsCaption()
font.family: fixedFont
font.pixelSize: captionSize
font.capitalization: Font.AllUppercase
color: systemPalette.windowText
opacity: 0.85
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}

Text {
Layout.fillWidth: true
Layout.fillHeight: true
Layout.minimumHeight: 48
font.pixelSize: stereoValuePixelSize
font.bold: true
font.family: fixedFont
color: systemPalette.windowText
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
text: level_to_text(viewModel.level_data_slow_2.level_rms)
}
}
}
Loading