Closed loop test suite, vibe coded, needs some refinement.

This commit is contained in:
Pbopbo
2026-02-26 13:27:58 +01:00
commit 80f7522159
12 changed files with 927 additions and 0 deletions

14
.gitignore vendored Normal file
View File

@@ -0,0 +1,14 @@
test_results/
__pycache__/
*.pyc
*.pyo
*.pyd
.Python
*.so
*.egg-info/
dist/
build/
.venv/
venv/
*.wav
*.png

103
QUICKSTART.md Normal file
View File

@@ -0,0 +1,103 @@
# Quick Start Guide
## Installation
```bash
# Install dependencies
pip install -r requirements.txt
```
## Basic Usage
### 1. Run Your First Test
```bash
python run_test.py \
--pcb-version "v1.0" \
--pcb-revision "A" \
--software-version "initial" \
--notes "First test run"
```
**What happens:**
- Auto-detects your Scarlett audio interface
- Plays test tones at 7 frequencies (100 Hz to 8 kHz)
- Records input/output on both channels
- Calculates latency, THD, and SNR
- Saves results to `test_results/YYYYMMDD_HHMMSS_results.yaml`
### 2. View Results
```bash
# List all test results
python view_results.py --list
# View specific test (example)
python view_results.py test_results/20260226_123456_results.yaml
# View example result
python view_results.py example_test_result.yaml
```
### 3. Compare Different PCB Versions
Run multiple tests with different metadata:
```bash
# Test PCB v1.0
python run_test.py --pcb-version "v1.0" --pcb-revision "A" --software-version "abc123"
# Test PCB v2.0
python run_test.py --pcb-version "v2.0" --pcb-revision "A" --software-version "abc123"
# Compare by viewing both YAML files
python view_results.py test_results/20260226_120000_results.yaml
python view_results.py test_results/20260226_130000_results.yaml
```
## Understanding the Output
Each test produces metrics at 7 frequencies:
- **Latency (ms)**: Delay between channels (should be near 0 for loopback)
- **THD Input (%)**: Distortion in channel 1 (lower is better)
- **THD Output (%)**: Distortion in channel 2 (lower is better)
- **SNR Input (dB)**: Signal quality in channel 1 (higher is better)
- **SNR Output (dB)**: Signal quality in channel 2 (higher is better)
**Good values:**
- THD: < 0.1% (< 0.01% is excellent)
- SNR: > 80 dB (> 90 dB is excellent)
- Latency: < 5 ms for loopback
## Configuration
Edit `config.yaml` to customize test parameters:
```yaml
test_tones:
frequencies: [1000] # Test only 1 kHz
duration: 3.0 # Shorter test (3 seconds)
```
## Troubleshooting
**Audio device not found:**
```bash
python -c "import sounddevice as sd; print(sd.query_devices())"
```
Update `device_name` in `config.yaml` to match your device.
**Permission errors:**
Make scripts executable (Linux/Mac):
```bash
chmod +x run_test.py view_results.py
```
## Next Steps
1. Run baseline tests with known-good hardware
2. Test different PCB revisions
3. Track software version changes
4. Archive YAML files for long-term tracking
5. Build comparison scripts as needed

107
README.md Normal file
View File

@@ -0,0 +1,107 @@
# Closed Loop Audio Test Suite
Simple Python-based testing system for PCB audio hardware validation.
## Features
- **Automated Testing**: Latency, THD, and SNR measurements across multiple frequencies
- **Metadata Tracking**: PCB version, revision, software version, timestamps, notes
- **YAML Output**: Human-readable structured results
- **Simple Workflow**: Run tests, view results, compare versions
## Quick Start
### 1. Install Dependencies
```bash
pip install -r requirements.txt
```
### 2. Run a Test
```bash
python run_test.py \
--pcb-version "v2.1" \
--pcb-revision "A" \
--software-version "a3f2b1c" \
--notes "Replaced capacitor C5"
```
### 3. View Results
```bash
# View specific test
python view_results.py test_results/20260226_123456_results.yaml
# List all tests
python view_results.py --list
# View latest test
python view_results.py test_results/*.yaml | tail -1
```
## Test Metrics
- **Latency**: Round-trip delay between input and output channels (ms)
- **THD**: Total Harmonic Distortion for input and output (%)
- **SNR**: Signal-to-Noise Ratio for input and output (dB)
Tests run at multiple frequencies: 100 Hz, 250 Hz, 500 Hz, 1 kHz, 2 kHz, 4 kHz, 8 kHz
## Output Format
Results are saved as YAML files in `test_results/`:
```yaml
metadata:
test_id: 20260226_123456
timestamp: '2026-02-26T12:34:56.789012'
pcb_version: v2.1
pcb_revision: A
software_version: a3f2b1c
notes: Replaced capacitor C5
test_results:
- frequency_hz: 1000
latency_ms: 2.345
thd_input_percent: 0.012
thd_output_percent: 0.034
snr_input_db: 92.5
snr_output_db: 89.2
```
## Configuration
Edit `config.yaml` to customize:
- Audio device settings
- Test frequencies
- Test duration
- Output options
## Hardware Setup
```
Laptop <-> Audio Interface (Scarlett) <-> DUT <-> Audio Interface (Scarlett) <-> Laptop
Output Channels 1&2 Input Channels 1&2
```
The system auto-detects Focusrite Scarlett audio interfaces.
## File Structure
```
closed_loop_audio_test_suite/
├── config.yaml # Test configuration
├── run_test.py # Main test runner
├── view_results.py # Results viewer
├── src/
│ └── audio_tests.py # Core test functions
└── test_results/ # YAML output files
└── YYYYMMDD_HHMMSS_results.yaml
```
## Tips
- Each test run creates a timestamped YAML file
- Results are self-contained with full metadata
- Compare tests by opening multiple YAML files
- Archive old tests by moving YAML files to subdirectories

38
ai_stuff/prompts.md Normal file
View File

@@ -0,0 +1,38 @@
I want to perform tests of hardware. I need a good system. The main thing is a repo with python scripts. What should they output? I want to be able to trigger a test run for a PCB where i can give some infos about the PCB: Version/Revision, Adjustments (comment), Software version (git commit hash e.g.). And i want the results to be structuredtly saved, with all the information, + datetime. The test results are a latency, a THD input vs output, a SNR input vs output, in the future possible also images. But for now 4 numbers basically. What are my options to structure this nicely, run tests easily and see the results
# Closed Loop Audio Test Suite
## Overview
Laptop <-> Audio Interface -> Beacon -> nrf Ref Board or Scout -> Audio Interface (-> Laptop)
Audio Interface -> Audio Interface (Loop back)
Compare the two paths
Loop back vs Radio
## Capabilities
- Measure latency (round trip)
- Measure THD input vs output
- Measure SNR input vs output
- Compare fourier transform of input vs output
For now start with detecting the audio interface and play a test tone on both channels.
Record the audio on both input channels and measure the latency. (most likley 0ms right now, dont be confused)
Calculate the THD and SNR for both channels.
Use the connected scarlett focusrite interface, figure out how to use it.
Play a test tone (that is not repeating) to figure out the latency between input channel 1 and input channel 2.
Visualize all the results.
Play a sine in different frequencies, and for every frequency 5 sec long and do thd of channel 1 and of channel 2, to compare the quality loss.
Dont do fourier yet.
Do a simple project.

17
config.yaml Normal file
View File

@@ -0,0 +1,17 @@
# Hardware Test Configuration
audio:
sample_rate: 44100
channels: 2
device_name: "Scarlett" # Will auto-detect Focusrite Scarlett
test_tones:
frequencies: [100, 250, 500, 1000, 2000, 4000, 8000] # Hz
duration: 5.0 # seconds per frequency
amplitude: 0.5 # 0.0 to 1.0
latency_runs: 5 # Number of latency measurements to average
output:
results_dir: "test_results"
save_plots: true
save_raw_audio: false

50
example_test_result.yaml Normal file
View File

@@ -0,0 +1,50 @@
metadata:
test_id: 20260226_123456
timestamp: '2026-02-26T12:34:56.789012'
pcb_version: v2.1
pcb_revision: A
software_version: a3f2b1c8d9e
notes: Baseline test with new capacitor values
test_results:
- frequency_hz: 100
latency_ms: 2.341
thd_input_percent: 0.015
thd_output_percent: 0.042
snr_input_db: 88.5
snr_output_db: 85.2
- frequency_hz: 250
latency_ms: 2.338
thd_input_percent: 0.012
thd_output_percent: 0.038
snr_input_db: 90.3
snr_output_db: 87.1
- frequency_hz: 500
latency_ms: 2.345
thd_input_percent: 0.011
thd_output_percent: 0.035
snr_input_db: 91.7
snr_output_db: 88.9
- frequency_hz: 1000
latency_ms: 2.342
thd_input_percent: 0.009
thd_output_percent: 0.029
snr_input_db: 93.2
snr_output_db: 90.5
- frequency_hz: 2000
latency_ms: 2.339
thd_input_percent: 0.013
thd_output_percent: 0.041
snr_input_db: 92.1
snr_output_db: 89.3
- frequency_hz: 4000
latency_ms: 2.346
thd_input_percent: 0.018
thd_output_percent: 0.052
snr_input_db: 89.8
snr_output_db: 86.7
- frequency_hz: 8000
latency_ms: 2.344
thd_input_percent: 0.024
thd_output_percent: 0.067
snr_input_db: 87.4
snr_output_db: 84.1

5
requirements.txt Normal file
View File

@@ -0,0 +1,5 @@
numpy>=1.24.0
scipy>=1.10.0
sounddevice>=0.4.6
PyYAML>=6.0
matplotlib>=3.7.0

96
run_test.py Normal file
View File

@@ -0,0 +1,96 @@
#!/usr/bin/env python3
import argparse
import yaml
from datetime import datetime
from pathlib import Path
import sys
sys.path.insert(0, str(Path(__file__).parent))
from src.audio_tests import run_single_test, run_latency_test
def main():
parser = argparse.ArgumentParser(description='Run PCB hardware audio tests')
parser.add_argument('--pcb-version', required=True, help='PCB version (e.g., v2.1)')
parser.add_argument('--pcb-revision', required=True, help='PCB revision (e.g., A, B, C)')
parser.add_argument('--software-version', required=True, help='Software version (git commit hash)')
parser.add_argument('--notes', default='', help='Adjustments or comments about this test')
parser.add_argument('--config', default='config.yaml', help='Path to config file')
args = parser.parse_args()
with open(args.config, 'r') as f:
config = yaml.safe_load(f)
timestamp = datetime.now()
test_id = timestamp.strftime('%Y%m%d_%H%M%S')
results_dir = Path(config['output']['results_dir'])
results_dir.mkdir(exist_ok=True)
test_output_dir = results_dir / test_id
test_output_dir.mkdir(exist_ok=True)
save_plots = config['output'].get('save_plots', False)
print(f"Starting audio test run: {test_id}")
print(f"PCB: {args.pcb_version} Rev {args.pcb_revision}")
print(f"Software: {args.software_version}")
if save_plots:
print(f"Plots will be saved to: {test_output_dir}")
print("-" * 60)
print("\n[1/2] Running chirp-based latency test (5 measurements)...")
try:
latency_stats = run_latency_test(config, num_measurements=5,
save_plots=save_plots, output_dir=test_output_dir)
print(f"✓ Latency: avg={latency_stats['avg']:.3f}ms, "
f"min={latency_stats['min']:.3f}ms, max={latency_stats['max']:.3f}ms")
except Exception as e:
print(f"✗ Error: {e}")
latency_stats = {'avg': 0.0, 'min': 0.0, 'max': 0.0, 'std': 0.0, 'error': str(e)}
print("\n[2/2] Running frequency sweep tests...")
test_results = []
frequencies = config['test_tones']['frequencies']
for i, freq in enumerate(frequencies, 1):
print(f"Testing frequency {i}/{len(frequencies)}: {freq} Hz...", end=' ', flush=True)
try:
result = run_single_test(freq, config, save_plots=save_plots, output_dir=test_output_dir)
test_results.append(result)
print("")
except Exception as e:
print(f"✗ Error: {e}")
test_results.append({
'frequency_hz': freq,
'error': str(e)
})
output_data = {
'metadata': {
'test_id': test_id,
'timestamp': timestamp.isoformat(),
'pcb_version': args.pcb_version,
'pcb_revision': args.pcb_revision,
'software_version': args.software_version,
'notes': args.notes
},
'latency_test': latency_stats,
'test_results': test_results
}
output_file = results_dir / f"{test_id}_results.yaml"
with open(output_file, 'w') as f:
yaml.dump(output_data, f, default_flow_style=False, sort_keys=False)
print("-" * 60)
print(f"✓ Test complete! Results saved to: {output_file}")
if save_plots:
print(f"✓ Plots saved to: {test_output_dir}/")
print(f"\nTo view results: python view_results.py {output_file}")
print(f"To view plots: ls {test_output_dir}/*.png")
if __name__ == '__main__':
main()

0
src/__init__.py Normal file
View File

281
src/audio_tests.py Normal file
View File

@@ -0,0 +1,281 @@
import numpy as np
import sounddevice as sd
from scipy import signal
from typing import Tuple, Dict, List
import matplotlib.pyplot as plt
from pathlib import Path
def find_audio_device(device_name: str = "Scarlett") -> tuple:
devices = sd.query_devices()
for idx, device in enumerate(devices):
if device_name.lower() in device['name'].lower():
if device['max_input_channels'] >= 2 and device['max_output_channels'] >= 2:
return (idx, idx)
default_device = sd.default.device
if hasattr(default_device, '__getitem__'):
input_dev = int(default_device[0]) if default_device[0] is not None else 0
output_dev = int(default_device[1]) if default_device[1] is not None else 0
else:
input_dev = output_dev = int(default_device) if default_device is not None else 0
input_info = devices[input_dev]
output_info = devices[output_dev]
if input_info['max_input_channels'] >= 2 and output_info['max_output_channels'] >= 2:
print(f"Using default device - Input: {input_info['name']}, Output: {output_info['name']}")
return (input_dev, output_dev)
raise RuntimeError(f"No suitable audio device found with 2+ input/output channels")
def generate_test_tone(frequency: float, duration: float, sample_rate: int, amplitude: float = 0.5) -> np.ndarray:
t = np.linspace(0, duration, int(sample_rate * duration), endpoint=False)
tone = amplitude * np.sin(2 * np.pi * frequency * t)
return tone
def generate_chirp(duration: float, sample_rate: int, f0: float = 100, f1: float = 8000, amplitude: float = 0.5) -> np.ndarray:
t = np.linspace(0, duration, int(sample_rate * duration), endpoint=False)
chirp = amplitude * signal.chirp(t, f0, duration, f1, method='linear')
return chirp
def play_and_record(tone: np.ndarray, sample_rate: int, device_id: tuple, channels: int = 2) -> np.ndarray:
output_signal = np.column_stack([tone, tone])
input_dev, output_dev = device_id
recording = sd.playrec(output_signal, samplerate=sample_rate,
channels=channels, device=(input_dev, output_dev), blocking=True)
return recording
def calculate_latency(channel_1: np.ndarray, channel_2: np.ndarray, sample_rate: int) -> Tuple[float, np.ndarray, np.ndarray]:
correlation = signal.correlate(channel_2, channel_1, mode='full')
lag = np.argmax(np.abs(correlation)) - (len(channel_1) - 1)
latency_ms = (lag / sample_rate) * 1000
lags = np.arange(-len(channel_1) + 1, len(channel_1))
return latency_ms, correlation, lags
def calculate_latency_stats(channel_1: np.ndarray, channel_2: np.ndarray, sample_rate: int, num_runs: int = 5) -> Dict:
latencies = []
for _ in range(num_runs):
latency, _, _ = calculate_latency(channel_1, channel_2, sample_rate)
latencies.append(latency)
return {
'avg': float(np.mean(latencies)),
'min': float(np.min(latencies)),
'max': float(np.max(latencies)),
'std': float(np.std(latencies))
}
def calculate_thd(signal_data: np.ndarray, sample_rate: int, fundamental_freq: float) -> float:
# Use middle segment (seconds 2-4) for calculation
segment_start = int(sample_rate * 2)
segment_end = int(sample_rate * 4)
segment_end = min(segment_end, len(signal_data))
if segment_start >= len(signal_data):
segment_start = len(signal_data) // 4
segment_end = len(signal_data) * 3 // 4
signal_segment = signal_data[segment_start:segment_end]
fft = np.fft.rfft(signal_segment)
freqs = np.fft.rfftfreq(len(signal_segment), 1/sample_rate)
fundamental_idx = np.argmin(np.abs(freqs - fundamental_freq))
fundamental_power = np.abs(fft[fundamental_idx])**2
harmonics_power = 0
for n in range(2, 6):
harmonic_freq = n * fundamental_freq
if harmonic_freq > sample_rate / 2:
break
harmonic_idx = np.argmin(np.abs(freqs - harmonic_freq))
harmonics_power += np.abs(fft[harmonic_idx])**2
if fundamental_power == 0:
return 0.0
thd = np.sqrt(harmonics_power / fundamental_power) * 100
return thd
def calculate_snr(signal_data: np.ndarray, sample_rate: int, fundamental_freq: float) -> float:
# Use middle segment (seconds 2-4) for calculation
segment_start = int(sample_rate * 2)
segment_end = int(sample_rate * 4)
segment_end = min(segment_end, len(signal_data))
if segment_start >= len(signal_data):
segment_start = len(signal_data) // 4
segment_end = len(signal_data) * 3 // 4
signal_segment = signal_data[segment_start:segment_end]
fft = np.fft.rfft(signal_segment)
freqs = np.fft.rfftfreq(len(signal_segment), 1/sample_rate)
fundamental_idx = np.argmin(np.abs(freqs - fundamental_freq))
fundamental_power = np.abs(fft[fundamental_idx])**2
total_power = np.sum(np.abs(fft)**2)
noise_power = total_power - fundamental_power
if noise_power <= 0:
return 100.0
snr_db = 10 * np.log10(fundamental_power / noise_power)
return snr_db
def run_single_test(frequency: float, config: Dict, save_plots: bool = False, output_dir: Path = None) -> Dict:
sample_rate = config['audio']['sample_rate']
duration = config['test_tones']['duration']
amplitude = config['test_tones']['amplitude']
device_name = config['audio']['device_name']
channels = config['audio']['channels']
device_ids = find_audio_device(device_name)
tone = generate_test_tone(frequency, duration, sample_rate, amplitude)
recording = play_and_record(tone, sample_rate, device_ids, channels)
channel_1 = recording[:, 0]
channel_2 = recording[:, 1]
thd_ch1 = calculate_thd(channel_1, sample_rate, frequency)
thd_ch2 = calculate_thd(channel_2, sample_rate, frequency)
snr_ch1 = calculate_snr(channel_1, sample_rate, frequency)
snr_ch2 = calculate_snr(channel_2, sample_rate, frequency)
if save_plots and output_dir:
plot_test_results(channel_1, channel_2, frequency, sample_rate, output_dir)
return {
'frequency_hz': int(frequency),
'thd_ch1_loopback_percent': round(float(thd_ch1), 3),
'thd_ch2_dut_percent': round(float(thd_ch2), 3),
'snr_ch1_loopback_db': round(float(snr_ch1), 2),
'snr_ch2_dut_db': round(float(snr_ch2), 2)
}
def plot_test_results(channel_1: np.ndarray, channel_2: np.ndarray, frequency: float,
sample_rate: int, output_dir: Path):
fig, ax = plt.subplots(1, 1, figsize=(12, 6))
# Calculate samples needed for 5 full waves
waves_to_show = 5
period_samples = int(sample_rate / frequency)
samples_needed = waves_to_show * period_samples
# Center around 1 second, but ensure we have enough samples
center_sample = int(sample_rate * 1.0)
start_sample = max(0, center_sample - samples_needed // 2)
end_sample = min(len(channel_1), start_sample + samples_needed)
# Adjust if we're at the end of the recording
if end_sample - start_sample < samples_needed:
end_sample = min(len(channel_1), start_sample + samples_needed)
start_sample = max(0, end_sample - samples_needed)
time = np.arange(start_sample, end_sample) / sample_rate * 1000
ch1_segment = channel_1[start_sample:end_sample]
ch2_segment = channel_2[start_sample:end_sample]
ax.plot(time, ch1_segment, label='Channel 1 (Loopback)', alpha=0.7, linewidth=1.2)
ax.plot(time, ch2_segment, label='Channel 2 (DUT)', alpha=0.7, linewidth=1.2)
ax.set_xlabel('Time (ms)')
ax.set_ylabel('Amplitude')
ax.set_title(f'Time Domain Comparison - {frequency} Hz (5 waves @ 1 sec)')
ax.legend()
ax.grid(True, alpha=0.3)
plt.tight_layout()
plot_file = output_dir / f'{frequency}Hz_analysis.png'
plt.savefig(plot_file, dpi=150, bbox_inches='tight')
plt.close()
def run_latency_test(config: Dict, num_measurements: int = 5, save_plots: bool = False, output_dir: Path = None) -> Dict:
sample_rate = config['audio']['sample_rate']
duration = 1.0
amplitude = config['test_tones']['amplitude']
device_name = config['audio']['device_name']
channels = config['audio']['channels']
device_ids = find_audio_device(device_name)
chirp_signal = generate_chirp(duration, sample_rate, amplitude=amplitude)
latencies = []
last_recording = None
last_correlation = None
last_lags = None
for i in range(num_measurements):
recording = play_and_record(chirp_signal, sample_rate, device_ids, channels)
channel_1 = recording[:, 0]
channel_2 = recording[:, 1]
latency, correlation, lags = calculate_latency(channel_1, channel_2, sample_rate)
latencies.append(latency)
if i == num_measurements - 1:
last_recording = recording
last_correlation = correlation
last_lags = lags
latency_stats = {
'avg': float(np.mean(latencies)),
'min': float(np.min(latencies)),
'max': float(np.max(latencies)),
'std': float(np.std(latencies))
}
if save_plots and output_dir and last_recording is not None:
channel_1 = last_recording[:, 0]
channel_2 = last_recording[:, 1]
plot_latency_test(channel_1, channel_2, last_correlation, last_lags, latency_stats['avg'],
sample_rate, output_dir)
return latency_stats
def plot_latency_test(channel_1: np.ndarray, channel_2: np.ndarray, correlation: np.ndarray,
lags: np.ndarray, latency_ms: float, sample_rate: int, output_dir: Path):
fig, axes = plt.subplots(2, 1, figsize=(12, 8))
time = np.arange(len(channel_1)) / sample_rate * 1000
plot_samples = min(int(sample_rate * 0.9), len(channel_1))
axes[0].plot(time[:plot_samples], channel_1[:plot_samples], label='Channel 1 (Loopback)', alpha=0.7, linewidth=0.8)
axes[0].plot(time[:plot_samples], channel_2[:plot_samples], label='Channel 2 (DUT)', alpha=0.7, linewidth=0.8)
axes[0].set_xlabel('Time (ms)')
axes[0].set_ylabel('Amplitude')
axes[0].set_title(f'Chirp Signal Time Domain (Latency: {latency_ms:.3f} ms)')
axes[0].legend()
axes[0].grid(True, alpha=0.3)
lag_time = lags / sample_rate * 1000
axes[1].plot(lag_time, correlation)
axes[1].set_xlabel('Lag (ms)')
axes[1].set_ylabel('Correlation')
axes[1].set_title('Cross-Correlation: Channel 1 (Loopback) vs Channel 2 (DUT)')
axes[1].axvline(x=latency_ms, color='r', linestyle='--', label=f'Peak at {latency_ms:.3f} ms')
axes[1].legend()
axes[1].grid(True, alpha=0.3)
plt.tight_layout()
plot_file = output_dir / 'latency_chirp_analysis.png'
plt.savefig(plot_file, dpi=150, bbox_inches='tight')
plt.close()

70
test_audio_playback.py Normal file
View File

@@ -0,0 +1,70 @@
#!/usr/bin/env python3
import numpy as np
import sounddevice as sd
import yaml
from pathlib import Path
print("Audio Device Diagnostic Test")
print("=" * 60)
devices = sd.query_devices()
print("\nAvailable devices:")
for i, dev in enumerate(devices):
print(f"{i}: {dev['name']}")
print(f" Inputs: {dev['max_input_channels']}, Outputs: {dev['max_output_channels']}")
print(f" Sample Rate: {dev['default_samplerate']}")
print()
config_path = Path('config.yaml')
with open(config_path, 'r') as f:
config = yaml.safe_load(f)
sample_rate = config['audio']['sample_rate']
duration = 1.0
frequency = 1000
print(f"\nGenerating {frequency}Hz tone for {duration}s at {sample_rate}Hz...")
t = np.linspace(0, duration, int(sample_rate * duration), endpoint=False)
tone = 0.3 * np.sin(2 * np.pi * frequency * t)
print(f"Tone amplitude: min={tone.min():.3f}, max={tone.max():.3f}, mean={tone.mean():.3f}")
print(f"Tone shape: {tone.shape}")
default_device = 6
print(f"\nTest Configuration:")
print(f"Using device: {default_device} ({devices[default_device]['name']})")
print(f"This routes through PulseAudio to the Scarlett 2i2")
stereo_tone = np.column_stack([tone, tone])
print(f"Stereo signal shape: {stereo_tone.shape}")
print("\nPlaying and recording...")
print("(You should hear a 1kHz tone through the Scarlett outputs)")
print("(Connect Scarlett outputs to inputs for loopback test)")
print()
try:
recording = sd.playrec(stereo_tone, samplerate=sample_rate,
channels=2,
device=default_device,
blocking=True)
print("Recording completed!")
print(f"Recording shape: {recording.shape}")
print(f"Channel 1 - min={recording[:, 0].min():.6f}, max={recording[:, 0].max():.6f}, RMS={np.sqrt(np.mean(recording[:, 0]**2)):.6f}")
print(f"Channel 2 - min={recording[:, 1].min():.6f}, max={recording[:, 1].max():.6f}, RMS={np.sqrt(np.mean(recording[:, 1]**2)):.6f}")
ch1_rms = np.sqrt(np.mean(recording[:, 0]**2))
ch2_rms = np.sqrt(np.mean(recording[:, 1]**2))
if ch1_rms < 0.001 and ch2_rms < 0.001:
print("\n⚠️ WARNING: Very low signal detected - likely just noise!")
print("The audio output may not be reaching the input.")
elif ch1_rms > 0.01 or ch2_rms > 0.01:
print("\n✓ Good signal detected!")
else:
print("\n⚠️ Low signal - check connections")
except Exception as e:
print(f"\n✗ Error: {e}")

146
view_results.py Normal file
View File

@@ -0,0 +1,146 @@
#!/usr/bin/env python3
import argparse
import yaml
from pathlib import Path
from datetime import datetime
def print_separator(char='-', length=70):
print(char * length)
def format_value(value, suffix=''):
if isinstance(value, float):
return f"{value:.3f}{suffix}"
return f"{value}{suffix}"
def display_results(yaml_file: Path):
with open(yaml_file, 'r') as f:
data = yaml.safe_load(f)
metadata = data['metadata']
results = data['test_results']
print("\n" + "=" * 70)
print(f" AUDIO TEST RESULTS")
print("=" * 70)
print("\n📋 Test Metadata:")
print_separator()
print(f" Test ID: {metadata['test_id']}")
print(f" Timestamp: {metadata['timestamp']}")
print(f" PCB Version: {metadata['pcb_version']}")
print(f" PCB Revision: {metadata['pcb_revision']}")
print(f" Software Version: {metadata['software_version']}")
if metadata.get('notes'):
print(f" Notes: {metadata['notes']}")
if 'latency_test' in data:
latency = data['latency_test']
print("\n🎯 Latency Test (Chirp):")
print_separator()
if 'error' in latency:
print(f" Error: {latency['error']}")
else:
print(f" Average: {latency['avg']:.3f} ms")
print(f" Min: {latency['min']:.3f} ms")
print(f" Max: {latency['max']:.3f} ms")
print(f" Std Dev: {latency['std']:.4f} ms")
print("\n📊 Frequency Sweep Results:")
print_separator()
header = f"{'Freq (Hz)':<12} {'THD CH1 (%)':<13} {'THD CH2 (%)':<13} {'SNR CH1 (dB)':<14} {'SNR CH2 (dB)':<14}"
print(header)
print(f"{'':12} {'(Loopback)':<13} {'(DUT)':<13} {'(Loopback)':<14} {'(DUT)':<14}")
print_separator()
for result in results:
if 'error' in result:
print(f"{result['frequency_hz']:<12} ERROR: {result['error']}")
else:
freq = result['frequency_hz']
thd_ch1 = format_value(result.get('thd_ch1_loopback_percent', result.get('thd_input_percent', 0)))
thd_ch2 = format_value(result.get('thd_ch2_dut_percent', result.get('thd_output_percent', 0)))
snr_ch1 = format_value(result.get('snr_ch1_loopback_db', result.get('snr_input_db', 0)))
snr_ch2 = format_value(result.get('snr_ch2_dut_db', result.get('snr_output_db', 0)))
print(f"{freq:<12} {thd_ch1:<13} {thd_ch2:<13} {snr_ch1:<14} {snr_ch2:<14}")
print_separator()
valid_results = [r for r in results if 'error' not in r]
if valid_results:
avg_thd_ch1 = sum(r.get('thd_ch1_loopback_percent', r.get('thd_input_percent', 0)) for r in valid_results) / len(valid_results)
avg_thd_ch2 = sum(r.get('thd_ch2_dut_percent', r.get('thd_output_percent', 0)) for r in valid_results) / len(valid_results)
avg_snr_ch1 = sum(r.get('snr_ch1_loopback_db', r.get('snr_input_db', 0)) for r in valid_results) / len(valid_results)
avg_snr_ch2 = sum(r.get('snr_ch2_dut_db', r.get('snr_output_db', 0)) for r in valid_results) / len(valid_results)
print("\n📈 Average Values:")
print_separator()
print(f" THD CH1 (Loopback): {avg_thd_ch1:.3f} %")
print(f" THD CH2 (DUT): {avg_thd_ch2:.3f} %")
print(f" SNR CH1 (Loopback): {avg_snr_ch1:.2f} dB")
print(f" SNR CH2 (DUT): {avg_snr_ch2:.2f} dB")
print("\n" + "=" * 70 + "\n")
def list_all_results(results_dir: Path):
yaml_files = sorted(results_dir.glob("*_results.yaml"))
if not yaml_files:
print("No test results found.")
return
print("\n" + "=" * 70)
print(" AVAILABLE TEST RESULTS")
print("=" * 70 + "\n")
for yaml_file in yaml_files:
try:
with open(yaml_file, 'r') as f:
data = yaml.safe_load(f)
metadata = data['metadata']
timestamp = datetime.fromisoformat(metadata['timestamp'])
print(f"📄 {yaml_file.name}")
print(f" Date: {timestamp.strftime('%Y-%m-%d %H:%M:%S')}")
print(f" PCB: {metadata['pcb_version']} Rev {metadata['pcb_revision']}")
print(f" SW: {metadata['software_version']}")
if metadata.get('notes'):
print(f" Note: {metadata['notes']}")
print()
except Exception as e:
print(f"⚠️ {yaml_file.name} (corrupted - skipping)")
print()
def main():
parser = argparse.ArgumentParser(description='View PCB test results')
parser.add_argument('file', nargs='?', help='YAML results file to view')
parser.add_argument('--list', action='store_true', help='List all available test results')
parser.add_argument('--results-dir', default='test_results', help='Directory containing results')
args = parser.parse_args()
results_dir = Path(args.results_dir)
if args.list or not args.file:
list_all_results(results_dir)
else:
yaml_file = Path(args.file)
if not yaml_file.exists():
yaml_file = results_dir / args.file
if not yaml_file.exists():
print(f"Error: File not found: {yaml_file}")
return 1
display_results(yaml_file)
if __name__ == '__main__':
main()