| `github.com/BurntSushi/toml` | v1.6.0 | Parse `netsynth.toml` config files | Single-function `toml.Decode()` into a struct. The `MetaData.Undecoded()` method catches unknown keys in user configs — surfacing typos like `frequncy` rather than silently ignoring them. This is the right behavior for a config file tool. v1.6.0 released December 2025, Go 1.18+ required. Zero indirect dependencies. |
**Why not `pelletier/go-toml v2`:** go-toml v2.3.0 (March 2026) is faster but the performance difference is irrelevant — config is read once at startup. go-toml v2's `Strict` mode can detect unknown keys but requires more setup than BurntSushi's `MetaData.Undecoded()`. BurntSushi's API is simpler for this use case and has clearer error message patterns for user-facing config mistakes.
`os.UserConfigDir()` returns `$XDG_CONFIG_HOME` if set, else `$HOME/.config` on Linux/macOS — confirmed against Go stdlib docs. No third-party XDG library needed.
### Additional Waveform Types
No new dependency. Extend the existing `synth.Oscillator` in place.
Square, sawtooth, and triangle are pure math — each is ~3 lines. The existing oscillator uses a phase accumulator (0.0–1.0 range), which is the right representation for all four waveforms:
```go
// Waveform enum addition to synth package
typeWaveformint
const(
WaveformSineWaveform=iota
WaveformSquare
WaveformSawtooth
WaveformTriangle
)
// Per-sample generation (replaces math.Sin call in Advance())
funcsample(phasefloat64,wWaveform)float64{
switchw{
caseWaveformSquare:
ifphase<0.5{return1.0}
return-1.0
caseWaveformSawtooth:
return2*phase-1.0
caseWaveformTriangle:
ifphase<0.5{return4*phase-1.0}
return3.0-4*phase
default:// WaveformSine
returnmath.Sin(2*math.Pi*phase)
}
}
```
The `Oscillator` struct gains a `Waveform` field; `Advance()` dispatches to `sample()`. Harmonics still work the same way — each harmonic's phase is `phase * ratio`, which maps correctly for all waveform types.
---
## Installation Delta
```bash
# Add only this new dependency
go get github.com/BurntSushi/toml@v1.6.0
```
No changes to build flags. `CGO_ENABLED=1` still required for go-lame.
---
## Integration Points
### Where Config Feeds Existing Code
The TOML config needs to override two existing data structures:
1.**`synth.ClassFreqConfigs`** (map in `synth/config.go`) — user can override `BaseHz` and add a `Waveform` field per class
2.**`classify.DefaultRules`** (slice in `classify/rules.go`) — user can prepend custom rules before the defaults
The config loader should apply overrides at startup before any other initialization. The cleanest integration is:
```
cmd/netsynth/main.go
-> config.Load(path) // returns *AppConfig
-> classify.MergeRules(cfg) // prepend user rules to DefaultRules
Both `classify.DefaultRules` and `synth.ClassFreqConfigs` are currently package-level vars — they can be replaced or cloned at startup without changing the downstream pipeline.
### TOML Struct Shape
The config schema maps naturally to the existing types:
Use `toml.Decode()` and check `meta.Undecoded()` to warn on unknown keys.
---
## What NOT to Add
| Avoid | Why | What to Do Instead |
|-------|-----|-------------------|
| `adrg/xdg` or any XDG library | `os.UserConfigDir()` in stdlib already handles `$XDG_CONFIG_HOME` on Linux — confirmed | Use `os.UserConfigDir()` directly |
| `pelletier/go-toml v2` | No advantage over BurntSushi for a single-file startup read; `MetaData.Undecoded()` in BurntSushi is more ergonomic for typo detection | `github.com/BurntSushi/toml` |
| `spf13/viper` | Massive dependency (brings in 20+ transitive deps) for a use case that is one TOML file — Viper adds remote config, env var binding, hot reload, none of which are needed | `BurntSushi/toml` + manual flag override |
| Any waveform/audio library | Square/sawtooth/triangle are 3 lines of math each; no library adds value | Extend `synth.Oscillator` in place |
| `gopkg.in/yaml.v3` or JSON config | TOML is explicitly specified for this milestone and is the right format for user-editable config files (comments supported, less noisy than JSON) | TOML only |