bug: NFC broken HW/SW?

implement webui (in OTA mode)
implemented startup and shutdown sound
moved audio playback (and led) to seperate task for better audio latency/stabililty
This commit is contained in:
Willem Oldemans
2026-06-01 16:16:46 +02:00
parent 1d7b8fb492
commit 2ca4679079
23 changed files with 2084 additions and 268 deletions
@@ -0,0 +1,152 @@
#!/usr/bin/env python3
"""Generate short mechanical startup/shutdown ping MP3 files for LittleFS.
Outputs by default:
- data/ping_mech_up.mp3
- data/ping_mech_down.mp3
Requires:
- pip install lameenc
"""
from __future__ import annotations
import argparse
import math
import random
import struct
from pathlib import Path
import lameenc
SAMPLE_RATE = 44100
def damped_ping(t: float, f: float, click_amt: float, rng: random.Random) -> float:
"""A bright, metallic-ish pluck with fast decay and a tiny striker click."""
env1 = math.exp(-t * 11.0)
env2 = math.exp(-t * 17.0)
env3 = math.exp(-t * 24.0)
body = (
0.72 * math.sin(2 * math.pi * f * t) * env1
+ 0.23 * math.sin(2 * math.pi * f * 2.05 * t) * env2
+ 0.12 * math.sin(2 * math.pi * f * 3.87 * t) * env3
)
click = rng.uniform(-1.0, 1.0) * click_amt * math.exp(-t * 120.0)
return body + click
def synth_sequence(
hit_times: list[float],
freqs: list[float],
length_s: float,
fade_start_s: float,
fade_len_s: float,
gain: float,
click_amt: float,
seed: int,
) -> bytes:
rng = random.Random(seed)
n = int(SAMPLE_RATE * length_s)
samples: list[int] = []
for i in range(n):
t = i / SAMPLE_RATE
x = 0.0
for ht, f in zip(hit_times, freqs):
dt = t - ht
if 0.0 <= dt <= 0.22:
x += damped_ping(dt, f, click_amt, rng)
fade = 1.0 if t < fade_start_s else max(0.0, (length_s - t) / fade_len_s)
x *= gain * fade
x = math.tanh(1.85 * x)
s = int(max(-1.0, min(1.0, x)) * 32767)
samples.append(s)
return struct.pack("<" + "h" * len(samples), *samples)
def encode_mp3(pcm16: bytes, bitrate_kbps: int) -> bytes:
enc = lameenc.Encoder()
enc.set_in_sample_rate(SAMPLE_RATE)
enc.set_channels(1)
enc.set_bit_rate(bitrate_kbps)
enc.set_quality(2)
out = enc.encode(pcm16)
out += enc.flush()
return out
def write_mp3(path: Path, pcm16: bytes, bitrate_kbps: int) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
data = encode_mp3(pcm16, bitrate_kbps)
path.write_bytes(data)
print(f"Wrote {path} ({path.stat().st_size} bytes)")
def generate_up(out_dir: Path, bitrate_kbps: int, seed: int) -> None:
hit_times = [0.00, 0.18, 0.36, 0.56, 0.78, 1.00]
freqs = [740, 830, 930, 1040, 1160, 1290]
pcm = synth_sequence(
hit_times=hit_times,
freqs=freqs,
length_s=1.25,
fade_start_s=1.10,
fade_len_s=0.15,
gain=0.28,
click_amt=0.18,
seed=seed,
)
write_mp3(out_dir / "ping_mech_up.mp3", pcm, bitrate_kbps)
def generate_down(out_dir: Path, bitrate_kbps: int, seed: int) -> None:
hit_times = [0.00, 0.16, 0.33, 0.51, 0.71, 0.92]
freqs = [1280, 1140, 1010, 900, 800, 710]
pcm = synth_sequence(
hit_times=hit_times,
freqs=freqs,
length_s=1.20,
fade_start_s=1.04,
fade_len_s=0.16,
gain=0.28,
click_amt=0.16,
seed=seed + 1,
)
write_mp3(out_dir / "ping_mech_down.mp3", pcm, bitrate_kbps)
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Generate mechanical ping startup/shutdown MP3 files")
parser.add_argument(
"--out-dir",
default="data",
help="Output directory for generated MP3 files (default: data)",
)
parser.add_argument(
"--bitrate",
type=int,
default=128,
help="MP3 bitrate in kbps (default: 128)",
)
parser.add_argument(
"--seed",
type=int,
default=42,
help="Random seed for click texture reproducibility (default: 42)",
)
return parser.parse_args()
def main() -> None:
args = parse_args()
out_dir = Path(args.out_dir)
generate_up(out_dir, args.bitrate, args.seed)
generate_down(out_dir, args.bitrate, args.seed)
if __name__ == "__main__":
main()