Files
yoloyolo/.planning/research/ARCHITECTURE.md
T
gurixandClaude Opus 4.6 6b2db48339 feat(synth): add LFO modulation, ADSR envelopes, pentatonic tuning, and soft limiter
Replace static EMA-smoothed drones with an evolving ambient soundscape:
- ADSR envelope system with sustained (2s attack, 4s release) and bursty
  (30ms attack, no sustain) modes per protocol group
- LFO pitch wobble and amplitude tremolo with incommensurable rates per
  group (Eno technique) so modulation patterns never repeat
- C major pentatonic frequency tuning (just intonation) — any combination
  of active protocols sounds consonant
- tanh soft limiter on master output prevents clipping
- Sync all documentation: README, PROJECT.md, ARCHITECTURE.md, v1.2
  requirements traceability

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 19:38:58 +01:00

28 KiB
Raw Blame History

Architecture Patterns

Domain: Network traffic sonification CLI (Go) — v1.2 Extended Protocol Coverage with Grouped Families Researched: 2026-03-26 (v1.0/v1.1), updated 2026-03-27 (v1.2 grouped protocol families) Confidence: HIGH — based on direct code inspection of the shipped v1.1 codebase


v1.2 Integration Overview

This document supersedes the pre-implementation v1.0/v1.1 architecture research. It is grounded in the actual shipped v1.1 codebase (~4,675 lines, 7 packages) and answers specifically: how does the group concept thread through classify, synth, config, and CLI? What changes, what stays, and in what order?


Current Package Map (v1.1 Baseline — the starting point)

cmd/netsynth/main.go      CLI, pipeline wiring, Cobra flags
capture/                  go-pcap live capture + pcap file reader + BPF
classify/
  types.go                TrafficClass (string type), ClassifiedPacket, WindowSnapshot, AllClasses()
  classifier.go           NewClassifier(rules []Rule) — first-match-wins
  rules.go                DefaultRules []Rule (12 built-in rules)
aggregate/
  window.go               500ms time-windowed snapshot accumulation
synth/
  config.go               FreqConfig{BaseHz, Harmonics, Pan, WaveformType}, ClassFreqConfigs map
  oscillator.go           Phase-accumulator oscillator — sine+additive harmonics
  layer.go                EMA amplitude smoothing per Layer
  bank.go                 NewBank(tau, cfgs map) — one Layer per config entry
  mixer.go                PanGains, StereoFramesToInt16Bytes
config/
  config.go               TOML parse, merge, validate, auto-freq assignment, PrintConfig
encode/
  mp3.go                  RunSynthesis(snapshots, path, freqCfgs) — NewBank + EncodeMP3

What v1.2 Adds

Two related but separable capabilities:

  1. More built-in protocol rules — extended DefaultRules covering Mail, Remote Access, Database, Discovery, File Transfer, VoIP, etc.
  2. Group concept — related protocols share a recognizable sound family (shared base frequency, detuned members, optional shared waveform character)

These must be designed together because the group concept directly affects frequency allocation, and frequency allocation directly affects DefaultRules ordering decisions.


The Group Concept: What It Means Architecturally

A "group" means: protocols in the same family share a musical neighborhood. Listeners learn "low rumble = infrastructure traffic" and "mid-range shimmer = web traffic" without needing to identify individual tones.

The group concept requires:

  • A group identifier stored somewhere accessible by both classify (for --print-config ordering) and synth (for within-group detuning computation)
  • A group base frequency from which individual protocol frequencies are derived by small detuning offsets
  • Within-group waveform consistency — all members of a group use the same waveform type so the family sounds recognizable even as individual pitches differ

The key architectural question is: where does the group concept live?


Decision: Group Lives in synth/config.go, Not in classify/

Option A: Group field on classify.Rule or new TrafficClassGroup type in classify

Adding Group string to classify.Rule would put group information at the classification boundary, visible from the capture pipeline forward. But:

  • The classifier does not care about groups — it only cares about protocol matching
  • AllClasses() in classify/types.go already returns ordered class names without needing sound semantics
  • Pollutes the classify package with audio-domain concerns

FreqConfig already owns all sound parameters (frequency, harmonics, pan, waveform). Adding a Group string field keeps group logic inside the audio domain. The classify package stays clean — it only knows about TrafficClass names.

// synth/config.go
type FreqConfig struct {
    BaseHz       float64
    Harmonics    []HarmonicDef
    Pan          float64
    WaveformType WaveformType
    Group        string  // NEW: e.g., "mail", "web", "remote-access", "" for ungrouped
}

classify.TrafficClass does NOT get a Group field. The group is a sound-design concept, not a classification concept. Classification says "this is SMTP"; synthesis says "SMTP belongs to the Mail group at 440 Hz + 0 cents detuning."

Why this is the right boundary

Package Knows About Does Not Know About
classify Protocol, port, traffic class name Group, frequency, waveform
synth Frequency, harmonics, pan, waveform, group Protocol, port
config Merges both sides; reads TOML; owns PrintConfig Packet capture

Frequency Allocation Strategy for Groups

Current allocation (v1.1)

The 14 existing classes span 65 Hz to 1047 Hz in a roughly logarithmic spread:

ICMP:       65 Hz   (sub-bass)
DNS:        110 Hz
HTTPS:      175 Hz
HTTP:       220 Hz
SSH:        330 Hz
SMTP:       440 Hz
NTP:        520 Hz
DHCP:       600 Hz
other-TCP:  700 Hz
other-UDP:  780 Hz
unknown-1:  862 Hz
unknown-2:  920 Hz
unknown-3:  981 Hz
unknown-4:  1047 Hz
auto-range: 1200-2350 Hz (FNV-32a hash for user-defined classes)

v1.2 group-based allocation

New protocols need frequency homes. With ~25-35 total built-in classes after expansion, maintaining individual hand-tuned frequencies becomes fragile and collision-prone. The group model solves this by assigning each group a frequency band, then placing members within that band at small detuning offsets.

Recommended group bands (within the 65-1100 Hz range already owned by built-ins):

Group Band Center Role Protocols
Infrastructure 65-130 Hz Core network services (barely heard, always present) ICMP, NTP, DHCP, mDNS/SSDP
Web 175-280 Hz HTTP-family traffic HTTP, HTTPS, HTTP/3, WebSocket
Mail 380-480 Hz Email protocols SMTP, IMAP, POP3, SMTP-submission
Remote Access 300-360 Hz Interactive sessions SSH, RDP, Telnet, VNC
Database 500-580 Hz Backend data stores MySQL, PostgreSQL, Redis, MongoDB
File Transfer 620-700 Hz File movement protocols FTP, SFTP, FTPS, SMB, NFS
VoIP / Streaming 720-820 Hz Real-time media SIP, RTP, RTSP
DNS 110 Hz Singleton — already well-placed DNS (UDP+TCP)
Unknown 850-1100 Hz Auto-bucketed unrecognized traffic unknown-1 through unknown-4

Within-group detuning model: Each group has a GroupBaseHz. Members are offset by small cent-based detuning or fixed Hz offsets:

Mail group base: 420 Hz
  SMTP:           420 Hz  (group base, no detune)
  IMAP:           430 Hz  (+10 Hz)
  POP3:           440 Hz  (+20 Hz)
  SMTP-submission: 450 Hz  (+30 Hz)

Offsets of 10-30 Hz are audible as distinct pitches but harmonically related enough to sound like a family. Do NOT use harmonic ratios (2x, 3x) for detuning — that would place group members an octave or fifth apart, destroying the "family sound" effect.

This is a design constraint, not a library feature. The detuning is expressed as explicit BaseHz values in ClassFreqConfigs. There is no runtime detuning computation needed — it is baked into the default config map.

Frequency rebalancing required

Adding ~15-20 new protocols means the existing 65-1100 Hz range must be replanned. The existing classes should be reassigned to group-coherent frequencies even if this moves them from v1.1 values. This is a conscious breaking change to the default sound, acceptable in a minor version bump where the goal is improved audio design.

The unknown-1..4 range at 850-1047 Hz is preserved as-is — those are intentionally dissonant and serve a distinct purpose.


Component-by-Component Changes

classify/rules.go — Extend DefaultRules

What changes: Add ~15-20 new Rule entries for the expanded protocol set.

What does NOT change: Rule struct, NewClassifier, the first-match-wins algorithm.

Required care: The catch-all rules (DstPort: 0 for TCP and UDP) must remain last. New protocol-specific rules must be inserted before these catch-alls. DefaultRules is an ordered slice — insertion order matters.

// New rules inserted before catch-alls, e.g.:
{Protocol: "tcp", DstPort: 143, Class: ClassIMAP},
{Protocol: "tcp", DstPort: 110, Class: ClassPOP3},
{Protocol: "tcp", DstPort: 587, Class: ClassSMTPSubmit},
{Protocol: "tcp", DstPort: 3306, Class: ClassMySQL},
{Protocol: "tcp", DstPort: 5432, Class: ClassPostgres},
{Protocol: "tcp", DstPort: 6379, Class: ClassRedis},
{Protocol: "tcp", DstPort: 3389, Class: ClassRDP},
{Protocol: "tcp", DstPort: 5900, Class: ClassVNC},
{Protocol: "udp", DstPort: 5060, Class: ClassSIP},
{Protocol: "tcp", DstPort: 5060, Class: ClassSIP},
// ... etc

classify/types.go — New Constants and Updated AllClasses()

What changes: Add new TrafficClass constants for each new protocol. Update AllClasses() to include them in display-sensible order (grouped by family, matching PrintConfig output order).

What does NOT change: ClassifiedPacket, WindowSnapshot, the TrafficClass string type itself.

// New constants (representative subset):
const (
    ClassIMAP         TrafficClass = "IMAP"
    ClassPOP3         TrafficClass = "POP3"
    ClassSMTPSubmit   TrafficClass = "SMTP-submit"
    ClassMySQL        TrafficClass = "MySQL"
    ClassPostgres     TrafficClass = "PostgreSQL"
    ClassRedis        TrafficClass = "Redis"
    ClassMongoDB      TrafficClass = "MongoDB"
    ClassRDP          TrafficClass = "RDP"
    ClassVNC          TrafficClass = "VNC"
    ClassTelnet       TrafficClass = "Telnet"
    ClassFTP          TrafficClass = "FTP"
    ClassFTPS         TrafficClass = "FTPS"
    ClassSMB          TrafficClass = "SMB"
    ClassNFS          TrafficClass = "NFS"
    ClassSIP          TrafficClass = "SIP"
    ClassHTTP3        TrafficClass = "HTTP3"   // UDP 443 (QUIC)
    ClassmDNS         TrafficClass = "mDNS"    // UDP 5353
    ClassSSDPDisc     TrafficClass = "SSDP"    // UDP 1900
    // ... additional as determined by feature research
)

AllClasses() ordering: Grouped by family in the same order as the group bands above. This order is consumed by config.PrintConfig for output ordering — so the output will naturally group protocols by family with no additional logic needed in config.

synth/config.go — Add Group Field and Rebalanced ClassFreqConfigs

What changes:

  1. Add Group string to FreqConfig:
type FreqConfig struct {
    BaseHz       float64
    Harmonics    []HarmonicDef
    Pan          float64
    WaveformType WaveformType
    Group        string  // NEW: sound family identifier; "" = ungrouped
}
  1. Add ClassFreqConfigs entries for all new protocols with group-coherent frequencies and within-group detuning.

  2. Update existing class frequencies to align with the group bands (frequency rebalancing). This is the highest-risk change in v1.2 because it alters the default sound of existing classes.

  3. Update NumLayers constant from 14 to the new total (e.g., 30-35 depending on final protocol list):

const (
    NumLayers    = 32  // updated count; GainPerLayer recomputed automatically
    GainPerLayer = 1.0 / float64(NumLayers)
)

What does NOT change: FreqConfig field types (only addition), WaveformType, HarmonicDef, WaveformPresetHarmonics, SampleRate, WindowMs, SamplesPerWindow, WhisperFloor. The Group field is metadata — it has no effect on synthesis math.

Pan assignment: With 30+ layers, the current hand-tuned pan positions become crowded. Recommendation: assign pan by group position (infrastructure = center, web = slight left, mail = slight right, etc.) rather than per-protocol. This creates a spatial "map" of the network that reinforces the group concept aurally.

config/config.go — PrintConfig Group Ordering

What changes: PrintConfig currently emits classes in AllClasses() order. With AllClasses() updated to group-coherent order, PrintConfig output will naturally be grouped. No structural change to PrintConfig is required.

Optional enhancement: Emit a group comment header between group sections:

# --- Infrastructure ---
# ICMP -- 65.0 Hz (default)
[sounds.ICMP]
frequency = 65.0
waveform = "sine"

# NTP -- 110.0 Hz (default)
[sounds.NTP]
...

# --- Web ---
# HTTP -- 175.0 Hz (default)
[sounds.HTTP]
...

This requires PrintConfig to detect group transitions in the class ordering and emit a comment. The Group field on FreqConfig provides this information. Implementation: iterate AllClasses(), track previous group, emit a comment when group changes.

What does NOT change: TOML schema shape (no group key in [sounds.*] blocks — groups are not user-configurable in v1.2), SoundOverride struct, RawRule, LoadResult, Load, merge, addAutoFreqEntries.

Group field in TOML config: Users should NOT be able to override the group of a class. Group is a built-in design decision, not a user parameter. The SoundOverride struct stays with only Frequency and Waveform pointer fields.

synth/bank.go — No Changes Required

NewBank already ranges over the passed-in cfgs map keys. Adding 20 new entries to ClassFreqConfigs and passing them through the existing chain requires no changes to bank.go. The gainPerLayer computation (1.0 / float64(len(cfgs))) automatically scales down with more layers.

encode/mp3.go — No Changes Required

RunSynthesis already accepts the full freqCfgs map and passes it to NewBank. No signature change needed.

cmd/netsynth/main.go — No Changes Required

The pipeline wiring (config.LoadNewClassifierencode.RunSynthesis) is already correct. Adding more built-in classes and a group field to FreqConfig is transparent to main.go.


Data Flow: Unchanged for v1.2

The v1.1 data flow is correct and does not need to change:

main.go
  └─ config.Load(configPath)
       └─ LoadResult{FreqCfgs, UserRules, ConfigPath, AutoClasses}
  └─ classify.NewClassifier(append(userRules, DefaultRules...))
  └─ encode.RunSynthesis(snapshots, path, result.FreqCfgs)
       └─ synth.NewBank(tau, freqCfgs)
            └─ one Layer per freqCfgs entry

The only change to the data flowing through this pipeline is: freqCfgs now contains ~30-35 entries (up from 14), each FreqConfig has a new Group field (ignored by synthesis, used only by PrintConfig).


Integration Points Summary

Integration Point Change Type Risk
classify/types.go — new constants + AllClasses() Additive LOW
classify/rules.go — extended DefaultRules Additive (new entries before catch-alls) MEDIUM (order matters)
synth/config.goGroup field on FreqConfig Additive (new field) LOW (no synthesis impact)
synth/config.goClassFreqConfigs entries for new protocols Additive LOW
synth/config.goClassFreqConfigs frequency rebalancing for existing protocols Modifying defaults HIGH (changes default audio output)
synth/config.goNumLayers constant update Single constant MEDIUM (affects GainPerLayer)
config/config.go — group-header comment in PrintConfig Optional enhancement LOW (isolated to string output)
synth/bank.go No change
encode/mp3.go No change
cmd/netsynth/main.go No change

New vs Modified Components

New

None — no new packages or files are required.

Modified

Component What Changes What Stays Same
classify/types.go New TrafficClass constants; AllClasses() extended + reordered by group ClassifiedPacket, WindowSnapshot, string type
classify/rules.go New Rule entries for all new protocols Rule struct, match algorithm, catch-all placement
synth/config.go Group field on FreqConfig; new entries in ClassFreqConfigs; existing frequency rebalancing; NumLayers updated All synthesis constants, waveform types, HarmonicDef
config/config.go Optional: group-header comments in PrintConfig All loading, parsing, merge, validation logic

Build Order

The following order ensures each step is independently testable.

Step 1: Define new TrafficClass constants and extend AllClasses()

Files: classify/types.go only.

Add all new protocol constants. Update AllClasses() to include them in group-coherent order. Tests: verify AllClasses() returns the expected slice with all new members.

No impact on synthesis, config, or main yet — new classes simply have no rules or FreqConfig entries yet, which is harmless.

Step 2: Extend DefaultRules for new protocols

Files: classify/rules.go only.

Add new Rule entries before the catch-alls. Tests: NewClassifier(DefaultRules).Classify(pkt) for each new protocol's port returns the expected new class. Confirm catch-alls still fire for unrecognized traffic.

Dependency: Step 1 must complete first (new constants must exist).

Step 3: Add Group field to FreqConfig

Files: synth/config.go only.

Add Group string to FreqConfig. Set Group on all existing 14 ClassFreqConfigs entries. Existing tests continue to pass (new field has zero value by default; synthesis ignores it). No callers need updating.

This step is independent of Steps 1 and 2.

Step 4: Add ClassFreqConfigs entries for new protocols (without frequency rebalancing)

Files: synth/config.go only.

Add FreqConfig entries for each new protocol using placeholder frequencies that avoid the 65-1100 Hz range (use 2400-4000 Hz temporarily). This makes the end-to-end pipeline work for new classes without disrupting existing audio.

Tests: verify ClassFreqConfigs contains entries for all constants from classify.AllClasses().

Dependency: Step 3 must complete first (Group field must exist). Step 1 must complete first (constants must exist).

Step 5: Frequency rebalancing — reassign all ClassFreqConfigs to group-coherent values

Files: synth/config.go only.

This is the riskiest step. Assign final group-coherent frequencies to all protocols (existing and new) following the band design above. Update NumLayers to the final count.

Test strategy: write a test that verifies all entries in ClassFreqConfigs have BaseHz in a specified range per group; verify no two entries have identical BaseHz; verify NumLayers matches len(ClassFreqConfigs).

Manual validation: run netsynth --read testfile.pcap with varied traffic and listen. This step requires subjective audio evaluation that tests cannot replace.

Dependency: Steps 3 and 4 must complete first.

Step 6: PrintConfig group-header comments (optional polish)

Files: config/config.go only.

Add group transition detection in PrintConfig. When the Group field changes between consecutive AllClasses() entries, emit a # --- GroupName --- comment.

Tests: verify PrintConfig output contains group header comments; verify ungrouped classes have no group header; verify user-defined classes (no group field) are not preceded by a group header.

Dependency: Steps 3 and 5 must complete first (Group field must be populated in ClassFreqConfigs).


Critical Integration Constraints

NumLayers Must Match len(ClassFreqConfigs) for Correct Gain Scaling

synth/config.go defines NumLayers = 14 as a constant. GainPerLayer = 1.0 / float64(NumLayers). However, bank.go:NewBank computes gainPerLayer dynamically from len(cfgs):

gainPerLayer: 1.0 / float64(len(cfgs)),

This means NumLayers and GainPerLayer in config.go are currently informational constants, not what the bank actually uses at runtime. The bank's dynamic computation is correct. Update NumLayers for documentation accuracy, but the audio math is not at risk even if this is temporarily inconsistent.

DefaultRules Catch-Alls Must Remain Last

classify/rules.go ends with {Protocol: "tcp", DstPort: 0, ...} and {Protocol: "udp", DstPort: 0, ...}. All new protocol rules must be inserted before these. A rule added after the catch-alls will never fire. This constraint must be enforced by convention (comment in the file) since Go has no static ordering guarantees for slice initialization beyond the literal order.

AllClasses() Ordering Drives PrintConfig Ordering

config.PrintConfig iterates classify.AllClasses() to emit the [sounds.*] section. If AllClasses() is updated to group-coherent order (Step 1), PrintConfig output naturally reflects groups. If Step 6's group-header comments are added, they depend on AllClasses() presenting entries in group-sorted order. The two changes must be coordinated.

Frequency Rebalancing Is a Default-Breaking Change

Users who rely on the default audio profile (no config file) will hear different tones for previously-familiar classes. This is an intentional design improvement but should be documented in the release notes. Users who have overridden frequencies via TOML config are unaffected (their overrides take precedence).

Group Field Is Not a TOML Configurable

The SoundOverride struct in config/config.go must not gain a group field. Groups are built-in design decisions. Allowing users to reassign protocols to different groups via TOML would create inconsistencies in PrintConfig output ordering and the AllClasses() group semantics. If this is requested in a future milestone, it warrants a separate design decision.


Anti-Patterns to Avoid

Anti-Pattern: Computing Detuning at Runtime from Group Metadata

It is tempting to define group base frequencies and detune offsets as data structures, then derive individual BaseHz values at synthesis startup. This adds complexity with no benefit — the final BaseHz values are static per-class constants that belong in ClassFreqConfigs directly. Bake the detuning arithmetic into the initial constant assignment, document it with a comment (// Mail group: +10 Hz from group base 420 Hz), and move on.

Anti-Pattern: Group as a classify.TrafficClass Field

Groups are audio concepts, not classification concepts. classify.Rule should not know about groups. TrafficClass is already a string type used as a map key throughout the codebase — adding a structured type would be a larger refactor than the feature warrants. Keep groups in synth.FreqConfig.

Anti-Pattern: Adding Group-Based Routing to bank.go

Do not make OscillatorBank aware of groups for mixing purposes (e.g., summing all Mail group layers before applying gain). The current architecture mixes all layers uniformly. Group-coherent mixing is a more complex feature than v1.2 requires and changes the synthesis model. Defer if requested.

Anti-Pattern: Overcrowding 65-1100 Hz with Too Many Protocols

At 30+ built-in classes, some protocols will be heard less often than others. Resist the urge to map rarely-seen protocols (Telnet, FTP) to prominent mid-range frequencies. Relegate low-signal protocols to the upper end of each group band so they contribute texture without dominating when not present.

Anti-Pattern: Hand-Tuning Frequencies Without Listening

Frequency assignments must be validated by ear, not just by looking at Hz values on a spreadsheet. Schedule a listening session with a realistic pcap file after Step 5 before Step 6. The architecture makes this easy — any frequency change is a one-line edit in ClassFreqConfigs.


Scalability Considerations

Concern With 14 Classes (v1.1) With 32 Classes (v1.2)
Gain per layer ~7.1% of max ~3.1% of max
Frequency collisions None Possible if not planned
AllClasses() iteration 14 items, trivial 32 items, still trivial
PrintConfig output lines ~60 lines ~130 lines
ADSR envelope convergence No change No change (per-layer, independent)
NewBank construction time Negligible Negligible

The main practical change is perceptual loudness: with 32 layers each at ~2.6% gain (accounting for tremolo headroom), the ambient mix becomes quieter when many protocols are active simultaneously. This is acceptable and matches the ambient aesthetic. The ADSR envelope system ensures inactive layers fade to silence, while the soft limiter prevents distortion when many protocols spike simultaneously.


Synthesis Layer: Envelope + LFO + Soft Limiter (post-v1.2)

The original synthesis pipeline used pure EMA (exponential moving average) smoothing and static oscillators, producing continuous drones. This was replaced with a richer signal chain to create an evolving ambient soundscape:

Signal Chain per Layer

Oscillator → ADSR Envelope → LFO Modulation → Per-layer Gain → Stereo Pan → Soft Limiter → Mix

ADSR Envelope System (synth/envelope.go)

Replaces the single-coefficient EMA with a four-stage envelope state machine:

Parameter Sustained Protocols Bursty Protocols
Attack 2.0s (slow fade-in) 0.03s (fast onset)
Decay 1.0s (slight dip) 0.3s (rapid drop)
Sustain 85% of peak 0% (no sustain)
Release 4.0s (long tail) 1.5s (medium tail)

Protocol categorization via FreqConfig.Bursty flag:

  • Sustained (Bursty=false): HTTPS, HTTP, QUIC, SSH, SMTP, IMAP, databases — produce ambient pads
  • Bursty (Bursty=true): ICMP, DNS, NTP, DHCP, mDNS, SSDP, SNMP, LDAP, Kerberos, Syslog — produce percussive accents

The envelope wraps traffic-rate EMA: during Sustain phase, the EMA-smoothed traffic rate modulates amplitude within the sustain level, preserving the original volume-follows-traffic behavior.

LFO Modulation (synth/lfo.go)

Each Layer has two LFOs:

  1. Pitch LFO — Modulates oscillator frequency by +/- 0.05-0.15 semitones. Creates subtle detuning that makes drones "breathe".
  2. Tremolo LFO — Modulates amplitude by +/- 8-20%. Creates gentle pulsing.

Group-specific LFO rates use incommensurable periods (Eno technique) so combined modulation never repeats:

Group Pitch Rate (Hz) Tremolo Rate (Hz)
Infrastructure 0.031 0.053
Web 0.043 0.071
Mail 0.037 0.059
Remote Access 0.029 0.047
File Transfer 0.041 0.067
Database 0.023 0.083
VoIP 0.019 0.091
Unknown 0.053 0.037

Pentatonic Frequency Tuning

The original major-second ladder was replaced with C major pentatonic (just intonation): C D E G A across octaves 2-8 (ratios 1/1, 9/8, 5/4, 3/2, 5/3). This guarantees that any subset of simultaneously active protocols produces consonant intervals — no dissonant beating.

Soft Limiter (synth/bank.go)

A tanh-based soft limiter on the master stereo output replaces hard clipping. Combined with tremolo-headroom-aware gain (1 / (N × 1.2)), this preserves dynamics while preventing distortion during traffic spikes.

Integration Points

  • FreqConfig.Bursty (config.go) → selects SustainedEnvParams vs BurstyEnvParams in NewLayer()
  • FreqConfig.Group (config.go) selects LFOConfig via LFOConfigForGroup() in NewLayer()
  • Layer.AdvanceSample() (layer.go) → applies pitch LFO to oscillator freq, generates sample, applies envelope, applies tremolo
  • OscillatorBank.RenderWindow() (bank.go) → applies per-layer gain + pan, then softLimit() on each stereo frame

Sources

  • Direct code inspection: synth/config.go, synth/bank.go, synth/layer.go, synth/oscillator.go, synth/lfo.go, synth/envelope.go, classify/types.go, classify/rules.go, classify/classifier.go, config/config.go, encode/mp3.go, cmd/netsynth/main.go — HIGH confidence
  • Musical interval theory (detuning, harmonic relationships, pentatonic scales): HIGH confidence — standard acoustic physics
  • Brian Eno incommensurable-period technique: HIGH confidence — well-documented generative music principle
  • v1.2 protocol list: determined from feature research (see FEATURES.md for rationale on which protocols to include)

Architecture research for: NetSynth v1.2 — extended protocol coverage with grouped families Updated: 2026-03-27 — added synthesis layer documentation (ADSR, LFO, pentatonic, soft limiter)