From 06dfed5cf95e54ed5a88bd095f70bb3b76617d8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lecomte=2C=20Timoth=C3=A9e?= Date: Sun, 9 Aug 2026 10:05:39 +0200 Subject: [PATCH] chore: add AGENTS.md --- AGENTS.md | 126 +++++++++++++++++++++++++++++++++++++++++ friture/test/runner.py | 31 ---------- 2 files changed, 126 insertions(+), 31 deletions(-) create mode 100644 AGENTS.md delete mode 100755 friture/test/runner.py diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..97a14901 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,126 @@ +# AGENTS.md + +## Overview + +**Friture** is a real-time audio visualization and analysis application. It captures +live audio via PortAudio, processes it with NumPy, and renders visualizations in a +PyQt6 hybrid QWidget + QML GUI. + +## Repository Organization + +| Path | Purpose | +|------------------------|-----------------------------------------------------------| +| `main.py` | Entry point; calls `friture.analyzer:main` | +| `friture/` | Main application package | +| `friture/analyzer.py` | `Friture` QMainWindow — sets up QML engine, audio backend, docks, timers; `main()` entry point | +| `friture/audiobackend.py` | Audio I/O via `sounddevice` + `rtmixer`; singleton accessed as `AudioBackend()` | +| `friture/audiobuffer.py` | `AudioBuffer` — ring buffer wrapper, forwards data to widgets | +| `friture/ringbuffer.py` | `RingBuffer` — circular buffer for audio samples | +| `friture/audioproc.py` | `audioproc` — FFT analysis for the spectrum widget | +| `friture/dockmanager.py`| `DockManager` — manages dock creation/closure/ordering | +| `friture/dock.py` | `Dock` — QML dock container hosting an audio widget | +| `friture/widgetdict.py` | Registry of available visualization widgets (8 types) | +| `friture/store.py` | `Store` singleton exposed to QML for dock state | +| `friture/plotting/` | Plotting infrastructure (CoordinateTransform, ScaleDivision, frequency scales, color maps) | +| `friture/signal/` | Signal processing modules (IIR/FIR filters, resampling, transforms) | +| `friture/generators/` | Built-in signal generator widgets (sine, white, pink, sweep, burst) | +| `friture/playback/` | Audio playback subsystem | +| `friture/test/` | Test suite | +| `friture/*.qml` | QML view files for visualizations and main window | +| `ui/` | Qt Designer `.ui` files (settings, main window) | +| `resources/` | Qt resource files (`.qrc`, icons, splash) | +| `installer/` | PyInstaller spec and hooks for packaging | + +## Architecture + +### Data Flow + +``` +Audio hardware → audiobackend (rtmixer ringbuffer) → AudioBuffer → DockManager → Widget.handle_new_data() + ↓ + display_timer (10 ms) slow_timer (1 s) + ↓ ↓ + DockManager.canvasUpdate() text/label refresh + Widget.canvasUpdate() → QML view update +``` + +1. **Audio capture**: `AudioBackend` (singleton in `audiobackend.py`) opens a + PortAudio stream via `rtmixer` and fills a `rtmixer.RingBuffer`. +2. **Buffering**: `AudioBackend.fetchAudioData()` (called on the display timer) + drains the ring buffer, emits `new_data_available`, which `AudioBuffer` + receives and pushes into its own `RingBuffer`. +3. **Processing**: `AudioBuffer.new_data_available` fans out to every dock's + audio widget. Each widget computes its own FFT/spectrogram/scattering in + `handle_new_data()`. +4. **Rendering**: The display timer (10 ms) calls `DockManager.canvasUpdate()`, + which calls each widget's `canvasUpdate()`, updating the QML views. + +### Widget Architecture + +Each visualization widget follows a consistent pattern: + +- **Python class** (e.g. `Spectrum_Widget` in `spectrum.py`) — audio data + processing, buffer management, settings dialog, state save/restore. + Registered in `widgetdict.py` and instantiated by `Dock`. +- **QML file** (e.g. `Spectrum.qml`) — declarative rendering and UI layout. +- **View model** (e.g. `Scope_Data`, `Spectrum_Data`) — QObject subclass with + `pyqtProperty` fields, bridged to QML. + +Available widgets (see `widgetdict.py`): + +| Widget ID | Class | QML file | Description | +|-----------|--------------------|-----------------|-------------------------| +| 1 | `Scope_Widget` | `Scope.qml` | Oscilloscope | +| 2 | `Spectrum_Widget` | `Spectrum.qml` | FFT spectrum analyzer | +| 3 | `Spectrogram_Widget` | `Spectrogram.qml` | 2D rolling spectrogram | +| 4 | `OctaveSpectrum_Widget` | `OctaveSpectrum.qml` | Octave-band spectrum | +| 5 | `Generator_Widget` | `Generator.qml` | Signal generator | +| 6 | `Delay_Estimator_Widget` | `DelayEstimator.qml` | Delay estimation | +| 7 | `LongLevelWidget` | — | Long-time level meters | +| 8 | `PitchTrackerWidget` | `PitchView.qml` | Pitch tracking | + +### Signal Processing + +`friture/signal/` contains reusable DSP modules: + +- `lfilter.py` — IIR/FIR filtering, IIR-to-minimum-phase-FIR conversion +- `decimate.py` — decimation +- `exp_smoothing.py` — exponential smoothing for display +- `frequency_resampler.py` — resampling for FFT display +- `transform_pipeline.py` — generic pipeline of processing blocks +- `lookup_table.py` — fast lookup tables +- `linear_interp.py` — linear interpolation +- `correlation.py` — cross-correlation (used by delay estimator) +- `color_tranform.py` — color transformations + +## Development Setup + +```bash +uv sync +uv run python main.py +``` + +Dependencies are managed by `uv` (see `uv.lock`). See `INSTALL.md` for +platform-specific instructions. + +## Testing & Tooling + +| Command | Tool | Purpose | +|----------------------|--------|----------------------------------| +| `uv run pytest` | pytest | Run the test suite (`friture/test/`) | +| `uv run mypy` | mypy | Type-check the codebase | +| `uv run python main.py` | — | Run the application | + +Tests live in `friture/test/` and cover signal processing, filters, pitch +tracking, and QML integration. + +## Regenerating Generated Files + +If UI or resource files change, regenerate: + +```bash +uv run pyuic6 ui/settings.ui -o friture/ui_settings.py +uv run pyuic6 ui/friture.ui -o friture/ui_friture.py +uv run pyrcc6 resources/friture.qrc -o friture/friture_rc.py +uv run python friture/filter_design.py # regenerate generated_filters.py / generated_fft.py +``` diff --git a/friture/test/runner.py b/friture/test/runner.py deleted file mode 100755 index 04f51436..00000000 --- a/friture/test/runner.py +++ /dev/null @@ -1,31 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -# Copyright (C) 2024 Celeste Sinéad - -# This file is part of Friture. -# -# Friture is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License version 3 as published by -# the Free Software Foundation. -# -# Friture is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Friture. If not, see . - - -import logging -import os.path -import numpy as np -import unittest - -if __name__ == '__main__': - logging.basicConfig(level=logging.WARNING) - np.set_printoptions(threshold=1024) - loader = unittest.TestLoader() - suite = loader.discover(os.path.dirname(__file__), '*.py') - unittest.TextTestRunner(verbosity=2).run(suite)