SuperTuner · Technical

Technical Documentation

Version 1.1.5 · iOS 16.6+ · Last updated 2026-06-04

This document is the authoritative public technical reference for SuperTuner. It is intended for end users who want to understand exactly how the tuner works, for musicians and acousticians who care about pitch-detection accuracy, and for engineers who want a faithful description of the underlying DSP and architecture. Every concrete number, algorithm parameter, and behaviour described here comes from the shipping source code; no claims are inferred or inflated.

1.What SuperTuner is

SuperTuner is a chromatic instrument tuner combined with a polyphonic reference-tone generator, built specifically for iPhone and iPad. It is designed for serious musicians who need a tuner that is accurate, fast, robust in noisy environments, and respectful of the iOS device's audio hardware limitations.

The product is intentionally small and focused. It does one job, tuning instruments, and pairs that with a high-fidelity reference-tone surface so the user can sound out chords, intervals, or unison drones against their instrument. It is not a recording app, DAW, effects suite, looper, or multi-tool.

What's in the box, conceptually:

  • A chromatic pitch detector accurate across a four-octave musical range, designed to track low-bass fundamentals (down to ~30 Hz) and to recover gracefully from noisy, harmonic-rich, or partially-masked signals.
  • A three-voice polyphonic reference-tone surface with strum-style decay and per-finger touch tracking. It plays sustained reference pitches for any note in the active tuning preset.
  • Three primary visual meters (LED arc, chromatic note wheel, stroboscopic harmonic display) and three sub-meter readouts (chromatic strip, info bar, ±100 cents fine-tune ruler). The meters can be paired or cycled independently.
  • 140+ tuning presets across 19 instrument groups, from standard guitar through orchestral strings, brass, woodwinds, 12-string guitars, and a wide collection of world and folk instruments. This includes a complete Indian Classical group (Sitar lineages, tanpura patterns, sarod, surbahar, veena, rudra veena, dilruba, esraj, sarangi), a Thaat group with the ten parent scales of Hindustani classical, a Chinese Classical group (erhu, pipa, guzheng, dizi), and a West African group (kora, donso and kamale ngoni).
  • Indian Classical mode: when an Indian preset is selected, the tuner automatically switches to Sa-relative just-intonation targeting. The user sets Sa once (default C#); every swara cell on the ribbon targets the exact JI ratio above that tonic, not the 12-TET semitone. The LED segment meter swaps to a 16-segment topology that renders sargam glyphs cleanly; the ribbon, chromatic strip, and info bar render full sargam names (Sa, re, Re, ga, Ga, Ma, ma, Pa, dha, Dha, ni, Ni) with traditional dot-above/below octave markers. A Carnatic toggle swaps to South Indian swara names (Sa, Ri1, Ri2, Ga2, Ga3, Ma1, Ma2, Pa, Da1, Da2, Ni2, Ni3).
  • Alternate Western temperaments: an optional setting targets one of eight historical-or-modern temperaments (Equal, Pythagorean, 1/4-comma Meantone, 1/6-comma Meantone, Werckmeister III, Kirnberger III, Vallotti, Young) anchored at a user-picked chromatic tonic. The tuner offsets each chromatic-degree's target by the temperament's cent table, so Baroque ensembles, period instruments, and microtonal projects can lock in to a non-equal frame.
  • Ten reference-pitch calibration presets (415 Baroque through 444 European Bright) plus a free 400 to 480 Hz slider.
  • A Reset All Settings to Standard action in the main settings menu, single-step confirmed, that returns every persisted setting (temperament, Indian mode, Sa, override toggles, reference pitch, active preset) to its first-launch default. A plain-language escape hatch.
  • A skeuomorphic CRT-style visual design housed in a physical-looking device chassis. This is the PeaceDrone hardware aesthetic: vintage gear language, not a glassy modern app.
  • A power button that fully halts audio processing while keeping the app open, for thermal and battery management during long tuning sessions.
↑ Back to top

2.Privacy and data handling

SuperTuner makes the following hard commitments, all verifiable by reading the codebase:

  • No analytics SDKs. None, no Firebase, no Mixpanel, no Amplitude, no Apple Analytics opt-in. No event tracking of any kind.
  • No network calls. The codebase contains no URLSession, no HTTP client, no networking imports. The app cannot phone home because it has no mechanism to.
  • No third-party SDKs or trackers. No Swift Packages, no CocoaPods, no Carthage. Every line of audio DSP and UI code is in-tree.
  • No subscriptions, no in-app purchases, no advertising.
  • No user account. There is nothing to sign up for.
  • No backups, no cloud sync, no shared state across devices. Settings are persisted locally only, via standard UserDefaults / @AppStorage.

The only system permission SuperTuner requests is microphone access, used solely for live pitch detection on the audio thread. Audio is processed in real time and is not buffered to disk, uploaded, or transmitted anywhere.

This stance is structural. To "add analytics later" would require importing a networking framework that currently has no presence in the project.

↑ Back to top

3.User-facing features

Pitch detection

When the user plays a note, SuperTuner displays:

  • The note name (e.g. A4, C#3).
  • The detected frequency in Hz, smoothed for stability.
  • The cents offset from the target preset note (e.g. +12.3¢ or −4.7¢).
  • An in-tune indicator when pitch locks within ±5 cents of the target.

The displayed pitch is latched. Once a note is identified, the display holds on it until either silence is detected for ~200 ms or a different note is confirmed by several consecutive detection frames. This makes the readout stable enough to read against rather than flickering between candidates.

When a non-chromatic tuning preset is active, the target note is the nearest valid note in the preset. For example, in Guitar Standard (E A D G B E), if the user plays something at 110 Hz the detector recognises it as A2 (a low E2 being too far off) and shows cents offset from A2, not from the chromatic nearest semitone. This means the cents reading is always relative to the note the user is actually trying to tune.

The three primary meters

A "Mode" button cycles through three paired meter combinations. Each combination links an upper meter (the large CRT display) with a lower sub-meter (the small bar below). Tapping the upper meter or the lower sub-meter individually also cycles only that face.

ModeUpper meterLower sub-meter
1LED arcChromatic strip
2Note wheelFine-tune ruler (±100¢)
3StroboscopeInfo bar

LED arc meter

A 19-segment arc of LED-style lights, with 9 red on each side and a central green. The user reads tuning by which side lights up and how far out: zero lit means within ±5 cents; one segment lit each side of centre indicates a small adjustment is needed; more segments mean a larger correction. The centre LED glows white when the note is in-tune.

The classic LED tuner readout, but rendered using cached arc-length geometry so the segments stay correctly placed on the curved CRT chassis regardless of device size.

Note wheel

A large rotating chromatic dial. The full circle holds all twelve semitones; the visible arc shows the seven nearest semitones around the currently-detected note. The wheel "rotates" continuously as pitch drifts, with the centre triangle indicator at twelve o'clock pointing at the current note. The target note glows white when in-tune.

The wheel's geometry is mathematically locked to the CRT chassis shape itself: the rim lines, label baselines, and tick fan all live on parallel translations of the CRT chassis arch curve. Tick lines fan radially toward a virtual wheel centre below the canvas, so every label and its centre tick lie on the same radial spoke, verifiable by eye at any rotation.

Stroboscopic meter

Four horizontal harmonic bands. Each band's brightness tracks the energy at its respective harmonic of the target note (1st, 2nd, 3rd, 4th). Each band's horizontal motion shows the phase deviation of that harmonic from perfect tuning. When the note is in tune, all bands stop moving. Sharp pitch makes bands drift one direction; flat pitch the other.

A strobe-style readout, but bounded by a photosensitivity safety: phase velocity is capped at 120 rad/sec (~19 Hz visible flicker), staying well above the 3 to 30 Hz photosensitive-epilepsy risk window.

The three sub-meters

Chromatic strip

A horizontal strip showing the nine nearest chromatic notes around the target preset note. Two triangle indicators (top and bottom) glide horizontally to show where the detected pitch sits relative to the target. When the indicators sit dead-centre and turn solid white, the note is in tune.

Info bar

A three-column readout:

  • Left: Detected frequency in Hz (e.g. 440.2 Hz)
  • Centre: Note name with flat/sharp triangle pair indicators that light progressively as deviation grows (0 within ±5¢, 1 at 5 to 25¢, 2 at 25+¢)
  • Right: Cents offset (e.g. +12.5¢)

Fine-tune ruler

A static ±100 cents graduated scale with a moving vertical line + triangle indicator. The graduation marks have a hierarchy of heights so the player can read at a glance: tallest at 0¢ (centre), then 25¢ multiples, then 10¢, then 5¢, then individual cents. The indicator pins to the edge at ±100¢ rather than disappearing off-screen.

Polyphonic ribbon controller

The bottom of the screen is a touch-sensitive "ribbon" with one key per note in the currently-active tuning preset. Tap a label to latch a reference tone. It plays continuously until tapped again. Slide a finger across the ribbon to play notes transiently as you go (strum-style). Multiple fingers play multiple notes simultaneously.

The ribbon enforces a hard three-voice cap. Latching a fourth note (or moving a transient touch onto a fourth distinct key) automatically releases the oldest latched note. The UI updates to reflect this immediately. What you see lit is exactly what is sounding.

Latching is deliberately gated to tap-on-label: a quick press-and-release on the visible note-name area. Sliding, dragging, or holding does not latch. It strums or sustains. This separation prevents accidental latches during expressive finger gestures.

Multi-touch is fully independent. Putting a second finger on the ribbon while the first is held does not interrupt or modify the first finger's behaviour in any way. Each touch is tracked individually from the moment it lands to the moment it lifts, with no inter-touch interference.

Mode and Power buttons

  • Mode cycles the meter+sub-meter combination (see 3.2). Tapping the meter face directly also advances the upper meter on its own, and tapping the sub-meter advances only the sub-meter.
  • Power halts audio processing while keeping the app open. The CRT display goes dark, the ribbon dims, the Power button glows red. The audio engine fully stops after a 300 ms fade so no clicks are audible mid-strum, and CPU/battery drop to baseline. Tapping Power again silently restarts the engine. Useful for putting the app aside between songs without burning battery or thermal headroom.

Instrument presets

19 instrument groups containing 140+ tunings:

GroupExamples
Open/ChromaticLatches any note across the chromatic range
GuitarStandard EADGBE, Half-Step / Whole-Step Down, Drop D / C# / C, Double Drop D, Open D / D minor / G / E / A / C / C#, DADGAD, Nashville, Math Rock. 17 tunings total.
Extended GuitarBaritone B/A standard, Drop A; 7- and 8-string; 12-string (Standard, Eb Standard, D Standard, Drop D, Open D Minor, Open Gmaj7)
Bass Guitar4/5/6-string standard, Eb, D, Drop D
Orchestral StringViolin, viola, cello, double bass (solo and standard)
Mediterranean FolkMandolin, mandola, octave mandolin, Irish bouzouki, Greek bouzouki, bandurria
UkuleleSoprano, tenor, baritone, U-Bass
BanjoOpen G, Sawmill, C, Double C, Double D, Open D, G minor, A, plectrum, tenor, tenor Irish
Steel GuitarLap C6, E7, Open E, Open D, dobro, pedal E9, pedal C6
Latin FolkCharango, ronroco, cuatro (Venezuelan, Puerto Rican), jarana (Jarocha, Segunda, Mosquito), cavaquinho, requinto, bandola llanera, vihuela mexicana, tiple colombiano
TanpuraFive Sa-relative drone patterns: Pa, Ma, Ni, Pa-Ni, Ma-Dha
Indian ClassicalSitar (Kharaj Pancham / Maihar, Gandhar Pancham / Vilayat, 13-string taraf), sarod, surbahar, veena, rudra veena, dilruba, esraj, sarangi. All targeting Sa-relative just intonation.
ThaatTen parent scales of Hindustani classical (Bilawal, Kalyan, Khamaj, Kafi, Asavari, Bhairav, Bhairavi, Poorvi, Marwa, Todi) at 5-limit JI ratios above Sa
Chinese ClassicalErhu (D-A), Pipa (A-D-E-A), Guzheng (D pentatonic), Dizi (D). Equal-tempered against A=440.
West AfricanKora (Silaba, Sauta), Donso Ngoni (F major / F minor pentatonic), Kamale Ngoni (Bb major / Bb minor pentatonic)
LuteRenaissance, baroque, theorbo, vihuela, cittern, oud (Arabic, Turkish)
Orchestral WoodwindFlute, piccolo
ReedOboe, English horn, clarinet (Bb, A), bass clarinet, bassoon, contrabassoon, saxes (soprano, alto, tenor, baritone)
BrassTrumpet, cornet, French horn, trombone (tenor, bass), euphonium, tuba (Bb, C)

Each Western tuning preset stores its open-string note set as MIDI numbers. The detector uses this set to compute the target note, the nearest preset note to the detected pitch, for cents-offset readout and for the chromatic strip / fine-tune ruler. The Open/Chromatic preset disables this constraint and reports the nearest chromatic semitone instead.

Indian-classical presets carry a parallel jiRatios field (just-intonation ratios above Sa), and selecting one auto-enters Indian Classical mode where the detector targets the Sa-relative JI Hz table instead of 12-TET semitones. See §11 for the full architecture.

The currently-selected preset persists between sessions.

Reference pitch calibration

Ten factory presets for tuning standard (A above middle C):

PresetHzUse
415 Baroque415.0Period instrument performance
416 Half-Step Down416.0Half-step-down ensembles
430.54 Scientific430.54Scientific pitch (C4 = 256 Hz)
432 Verdi432.0Verdi tuning, "natural" pitch advocates
440 Standard440.0ISO concert pitch
441 Boston Symphony441.0Boston Symphony Orchestra
442 NY Philharmonic442.0NY Philharmonic, many European orchestras
443 European Concert443.0Some European orchestras
444 European Bright444.0Higher-pitched European concert standard
Free400 to 480Custom value via slider, 0.1 Hz granularity

Both the selected preset and the current Hz value are persisted between sessions.

Adaptive layout

Four distinct layouts are selected automatically based on the device idiom and the viewport's actual width, not just the device class. This means iPad Split View / Slide Over windows that drop below the iPad width threshold get the iPhone layout, which scales down cleanly.

  • iPhone portrait: vertical stack: header, meter, sub-meter, mode/power buttons, ribbon
  • iPhone landscape: two-column layout matching the iPad landscape proportions
  • iPad portrait: vertical stack with iPad-specific scaling and the wider one-line SuperTuner wordmark
  • iPad landscape: two-column with the meter + buttons on the left and the ribbon on the right

The CRT chassis shape is the same in every layout; only its size and the surrounding chrome differ.

Splash screen and chime

On launch, a brief splash sequence runs:

  1. The SuperTuner two-line wordmark and the PeaceDrone pd lettermark fade in (~0.4 s ease-out).
  2. The audio engine initialises (microphone permission request on first launch, then DSP graph build).
  3. A 3-note ascending arpeggio chime plays at low volume: E5, G5, C6 (the 3rd, 5th, and octave root of C major).
  4. The splash dismisses to the main tuner.

The chime serves two purposes: it gives the user a distinctive audio identity ("the PeaceDrone audio logo") and it warms up the speaker DAC and microphone path before the user reaches the tuner, eliminating the click-or-pop that some iOS audio sessions produce on first activation.

Total chime duration is ~254 ms note onsets; the splash dismisses ~500 ms after the last note's release tail completes.

Indian Classical mode

Selecting any preset from the Tanpura, Indian Classical, or Thaat groups automatically enters Indian Classical mode. There is no separate master toggle: the preset is the mode. Selecting any Western or Chinese / West African preset returns to standard 12-TET behaviour.

In Indian Classical mode the tuner targets Sa-relative just intonation: each swara position is the exact frequency ratio above the user-set Sa tonic, not the nearest equal-tempered semitone. The cents-offset readout reflects deviation from the JI target, so a player who has tuned their tanpura to the just-fifth Pa reads a flat zero when they're spot on (whereas a 12-TET tuner would read +2¢ flat of the equal-tempered Pa).

Sa setting. A dedicated "Sa Tonic" picker lives in Settings → Indian Classical. The user can:

  • Pick a chromatic root note (C through B, default C#): sets Sa to that note's frequency above A=ref.
  • Apply a fine-tune offset (−50¢ to +50¢) for instruments that sit between equal-tempered roots.
  • Type or slide a freeform Hz value directly (B2 to E4 range).

The three inputs are mutually-consistent: changing the chromatic root recomputes the Hz; sliding Hz snaps the displayed chromatic root to the nearest match. Sa persists between sessions.

Sargam display. When in Indian Classical mode, the display surfaces swap from Western letter names to sargam:

  • LED segment meter: a 16-segment "starburst" topology renders sargam glyphs (S, r, R, g, G, M, m, P, d, D, n, N) cleanly. Diagonals are tipped to match the inner-hexagon tapering of the middle bar, so straight and diagonal strokes share the same visual weight. Carnatic mode adds a smaller 7-segment digit cell to the right for the variant index (R1, R2, G2, G3, etc.).
  • Ribbon labels: render the full sargam name (Sa, re, Re, ga, Ga, Ma, ma, Pa, dha, Dha, ni, Ni) with traditional dot-above/below octave markers. Two dots indicate ati-mandra (double-lower) or ati-taar (double-upper) saptak; one dot for mandra / taar; no dot for madhya. A user playing Sitar Maihar sees the seven cells read Ma·, Sa, Pa·, ·Sa, ·Pa, Sa·, Sa·· left-to-right.
  • Chromatic strip: sargam labels with the same octave-dot convention.
  • Info bar Note column: sargam name with octave dot for the detected pitch.
  • Note wheel: stays Western. The wheel is a chromatic 12-tone reference; Indian-mode swaras are rendered through the other meters.

Hindustani / Carnatic toggle. A picker in Settings → Indian Classical chooses between the two notation conventions. Hindustani is default. Targets stay JI regardless; only the displayed labels swap.

Casing convention. Uppercase = natural (pure) swara, lowercase = altered (komal for r/g/d/n; tivra for m). Sa and Pa are achal (immovable), always uppercase. Ma (shuddha Ma, 4/3) is uppercase; ma (tivra Ma, 45/32) is lowercase, making the altered Ma read consistently with the altered Re, Ga, Dha, and Ni.

Indian Classical Settings menu. Settings → Indian Classical exposes:

  • Sa Tonic (root, fine-tune cents, Hz, plus an active-display row).
  • Sargam Names toggle: when off, the tuner targets JI but shows nearest Western names.
  • Notation System (Hindustani / Carnatic).
  • Western Temperament Override (see §3.11).
  • Status indicator showing whether Indian mode is currently active.

Historical and alternate temperaments

A dedicated Alternate Temperament setting at Settings → Alternate Temperament lets the user target a non-equal Western temperament for any non-Indian preset. The setting has two layers:

  • Master toggle (off by default). When off, all Western presets target straight 12-TET. When on, they target whichever temperament is selected in the picker.
  • Temperament picker. Eight tables ship:
    • Standard: Equal Temperament.
    • Historical / Specialized: Pythagorean, 1/4-comma Meantone, 1/6-comma Meantone, Werckmeister III, Kirnberger III, Vallotti, Young.
  • Tonic picker: chromatic root (C through B, default C). Hidden when Equal is selected. Determines which chromatic degree the temperament's offset table is anchored to.

A temperament defines twelve cent-offsets, one per chromatic degree relative to the tonic. When the master toggle is on and a non-equal temperament is active, the tuner targets equal-tempered-Hz × 2^(cent_offset / 1200) for each preset note. Sweeping the tonic rotates the offset pattern: the same Werckmeister III in C and in G give different per-chromatic targets, matching how period instruments work in the real world.

Display badge. When a non-equal temperament is active, the reference-pitch cell in the display bar stacks the pitch text on top and a small tinted-pill badge below showing the temperament short name and tonic (e.g. WERK III / C). Tapping the cell routes directly to the temperament submenu. When the toggle is off or Equal is selected, the badge hides and the cell shows pitch text alone, same footprint as before.

First-use intro banner. The first time the user enables a non-equal temperament, an inline explanation banner appears at the top of the temperament submenu: plain language that historical temperaments intentionally tune some notes off-standard, so the tuner is reading correctly even when offsets look unusual. Dismiss with "Got it"; the banner doesn't reappear unless the user runs Reset All Settings.

Persistence. The master toggle, temperament selection, and tonic all persist between sessions. The user can leave a temperament configured and toggle it on and off per piece without re-selecting.

Western Temperament Override

When an Indian Classical preset is active, the Western temperament picker has no effect: the preset's Sa-relative JI is the targeting frame. A separate Western Temperament Override toggle in Settings → Indian Classical lets the user opt the active Indian preset into Western intonation while keeping the Indian preset's swara layout. Use case: fusion playing, or tuning a western instrument to a sitar's note layout but staying in equal temperament.

Behaviour when Override is on:

  • The Indian preset's ribbon layout (which swaras / strings appear in which positions) is preserved unchanged.
  • Each cell's target frequency switches from Sa × ratio (just intonation) to the active Western temperament's target: Equal by default, or whatever the §3.11 selector is set to.
  • Sa is forced to C# (the standard Western anchor) for the duration of the override, regardless of the user's saWesternRoot setting. This keeps the override's behaviour predictable for Western players who don't care to set Sa themselves.
  • The display-bar badge shows EQ / C# or WERK III / C# (etc.) so the user knows targets are Western-tuned.

Reset All Settings to Standard

A single-confirmation Reset All Settings to Standard entry sits at the bottom of the main settings menu. Tapping it shows an explanation panel ("This returns every setting to its default: Equal temperament, Indian mode off, Western Temperament Override off, Sa C# (277.18 Hz), reference pitch A=440, and the active preset to Guitar Standard EADGBE.") with Cancel and Reset buttons. Tapping Reset:

  • Disables Alternate Temperament; sets temperament to Equal; tonic to C.
  • Clears the first-use temperament-intro flag (so a fresh user encountering the feature again sees the explanation).
  • Disables Western Temperament Override.
  • Resets Sa to C# at 277.18 Hz with 0¢ fine tune.
  • Re-enables sargam display; sets notation to Hindustani.
  • Sets reference pitch to A=440.
  • Selects Guitar Standard EADGBE.

A plain-language escape hatch for users who have configured many settings and want to get back to a clean state without remembering each one.

↑ Back to top

4.System architecture

SuperTuner is a SwiftUI app with an Objective-C++ bridge to the audio engine. The high-level dependency graph is small:

SwiftUI views (MainTunerView, SplashView, meter components)
        │
        ▼
AudioManager (Swift, ObservableObject)
        │
        ├──► FrequencyDetector (Swift) ──► PitchEstimator + SpectralHarmonicFingerprint
        │
        ├──► ToneGenerator (Swift, AVAudioSourceNode render callback)
        │
        └──► AudioEngine (Swift) ──► SuperTunerBridge (ObjC++) ──► AVAudioEngine + RemoteIO

The bridge layer is intentionally thin. It exists for one reason: AVAudioEngine requires careful configuration (session category, preferred I/O buffer duration, hardware sample-rate negotiation, render-thread-safe buffer pooling) that is cleaner to express in Objective-C++ with direct access to CoreAudio C structures than to wrestle out of pure Swift.

Above the bridge, Swift owns:

  • The detection state machine: ring buffer, smoothing filters, latch logic, display throttling.
  • The synthesis engine: voice management, oscillators, filters, voicing chain, limiter.
  • The UI state: currently-selected preset, reference pitch, mode, latched ribbon keys.

Below the bridge, CoreAudio owns:

  • The real-time render thread and its strict no-allocation, no-locking guarantees.
  • The microphone input tap and hardware route management.

There are no external libraries used for DSP. Every line of audio processing, including pitch detection, synthesis, filtering, and limiting, is implemented in-tree, by hand, primarily in Swift (with Apple's Accelerate framework / vDSP used for vectorised inner loops in the autocorrelation core).

↑ Back to top

5.Audio engine

Configuration

  • Default sample rate: 48 000 Hz, with hardware-rate negotiation on engine start.
  • I/O buffer duration: 32 ms (1536 frames at 48 kHz). This balances responsiveness against battery consumption; it's the buffer size that gives the detector enough samples per analysis window without spinning the CPU more than necessary.
  • AVAudioSession category: PlayAndRecord with MixWithOthers and DefaultToSpeaker options. This means SuperTuner does not duck or mute other audio (so the user can have a metronome or backing track playing while they tune), and the reference tone outputs through the built-in speaker by default rather than the earpiece.
  • AVAudioSession mode: Default (not Measurement). This permits simultaneous I/O without the harsher gain and routing constraints of measurement mode.
  • Haptics: Enabled during recording (iOS 13+), so the ribbon's per-touch haptic taps work even while the microphone is open.

Lifecycle

  1. Initialise: build the AVAudioEngine, configure the session, attach the tone-generator source node, install the input tap. This step does not start the engine; if anything fails, the function returns false and the UI surfaces the failure cleanly.
  2. Start: boot the engine, begin pulling audio frames from the microphone and dispatching them to the detector.
  3. Stop: halt the engine. The detector's 200 ms silence timeout subsequently releases any held display latch.
  4. Shutdown: full cleanup, including an explicit FrequencyDetector.reset() so no stale "last detected" note freezes on screen if the user comes back later.

Render-thread safety

The bridge maintains a pre-allocated pool of three AVAudioPCMBuffer instances and rotates through them on the input tap, so the render callback never has to allocate memory. Frame dispatch to the detector happens via a serial dispatch queue, not the render thread directly, so the detector's Swift code can do its work (including allocations and array creation) without violating real-time constraints.

Interruption and route changes

The tone generator subscribes to AVAudioSession.routeChangeNotification and updates an internal usingBuiltInSpeaker flag. This drives a route-aware voicing chain (see section 8): when audio is routed to the built-in speaker, a 4th-order high-pass at 140 Hz engages to protect the speaker from cone-excursion distortion. When the user plugs in headphones, switches to AirPlay, or connects Bluetooth/USB audio, the high-pass disengages and the full bandwidth is restored. The switch is automatic and silent.

↑ Back to top

6.Pitch-detection pipeline

This is the heart of the app and the part that most differentiates it from typical "just call FFT" tuner code. The detector is designed around three goals:

  1. Accuracy under noise. Real-world tuning happens in living rooms, rehearsal spaces, and stages with ambient noise. The detector tolerates considerable noise without losing its lock.
  2. Robustness to weak fundamentals. Many real instruments, such as open low bass strings, a sax low Bb, or a cello C2, have a fundamental that's quiet or partially masked by the room. The detector recovers the perceived pitch from upper harmonics when needed.
  3. Snappy attack, stable sustain. The display reacts immediately to a fresh attack but does not jitter during a sustained note.

Signal flow

microphone samples (16-bit / float)
       │
       ▼
ring buffer (2048 samples, 512-sample hop)
       │
       ▼
latch-aware Butterworth low-pass (only if latched to ≤400 Hz fundamental)
       │
       ▼
HybridEstimator (YIN + per-lag-normalised autocorrelation)
       │
       ▼  (if both estimators fail to agree)
Spectral Harmonic Fingerprint rescue
       │
       ▼
harmonic gate (enforced on new notes only)
       │
       ▼
display latching + cents-offset smoothing
       │
       ▼
UI update (throttled to display rate)

Each stage exists to handle a specific kind of failure mode that the simpler stages can't.

Time-domain core: autocorrelation and YIN

Pitch detection happens primarily in the time domain via two estimators that run on the same windowed signal.

Per-lag normalised autocorrelation: Computed as Σ x[i]·x[i+L] / sqrt(E1·E2), where E1 and E2 are the segment energies at the lag-zero and lag-L positions. This gives a value in [-1, 1] that is invariant to overall signal amplitude. The first peak that exceeds 90% of the global maximum wins. The first peak rather than the largest, because choosing the largest can bias toward octave-down errors. Harmonic coherence is then scored as a weighted sum of autocorrelation values at lags 2L, 3L, 4L, 5L, which gives a quality measurement separate from peak height.

YIN: A specific algorithm in the autocorrelation family that is sharper than naive autocorrelation at the cost of being more sensitive to noise. The implementation:

  1. Computes the difference function d[τ] = E1 + E2 − 2·R[τ] using vectorised vDSP.
  2. Converts to the cumulative mean normalised difference (CMND): d'[τ] = d[τ] / ((1/τ)·Σ d[j]).
  3. Looks for the first τ where the CMND dips below an absolute threshold of 0.15 and sits at a local minimum.
  4. If no candidate passes, applies a relaxed threshold of 0.40 to the global argmin as a rescue.
  5. Refines the chosen τ with 3-point parabolic interpolation for sub-sample precision.

YIN returns a confidence score equal to 1 − CMND[τ_selected], scaled into [0, 1] where 1.0 represents perfect periodicity.

Latch-aware threshold relaxation: When the detector is already latched to a note, YIN searches a narrow window around the expected lag with a relaxed threshold of 0.25 (instead of 0.15). This lets the detector track a note through its natural decay without lowering the gate for new notes. The wider threshold only applies in the window around the latched lag.

The hybrid combiner

Below about 95 Hz, YIN's CMND normalisation becomes unreliable. There are simply fewer cycles per analysis window to compute meaningful statistics on. Autocorrelation does not have this problem (it's a structural measurement, not a statistical one) but is more vulnerable to room noise. The detector combines them across three frequency bands:

BandStrategy
Above 95 HzTrust YIN directly. Fastest response, snappiest attack.
65 to 95 Hz (soft)Accept YIN if confidence ≥ 0.88; otherwise require autocorrelation to agree on MIDI class.
Below 65 Hz (strict)Require both YIN and autocorrelation to return a result and round to the same MIDI note.

This means that at high frequencies, including guitar and most things above bass, the detector is snappy. At very low frequencies, including the bottom of bass, the lowest notes of orchestral strings, and tuba, it is deliberately more cautious, requiring agreement across two independent measurement methods before declaring a pitch.

The latch-aware low-pass filter

When the detector is locked onto a note at or below 400 Hz, a 2nd-order Butterworth low-pass filter is applied to the analysis window before YIN runs. The filter:

  • Has a cutoff of 12× the latched fundamental, clamped to 4 kHz maximum.
  • Runs forward then backward over the buffer (zero-phase), which doubles the effective rolloff to −24 dB/octave.
  • Is bypassed entirely for notes above 400 Hz (where high-frequency noise isn't the problem).
  • Is only applied to the YIN input, not to the Spectral Harmonic Fingerprint rescue path (which needs the raw upper spectrum).

For a latched A2 (110 Hz), the cutoff sits at 1320 Hz: high enough to pass the first ~12 harmonics, low enough to substantially attenuate the broadband noise that would otherwise raise YIN's CMND at long lags and de-lock the detector.

This is the single largest behavioural improvement over a vanilla YIN implementation: tracking through bass decay in noisy environments.

Spectral Harmonic Fingerprint rescue

When both YIN and autocorrelation fail to agree on a pitch, a third path runs. It's specifically designed for situations where the fundamental is masked or absent but the upper harmonics are clearly present. For instance, a low bass note where the room's noise floor sits above the fundamental's amplitude, or a sax low Bb where the body resonance suppresses the actual lowest partial.

The implementation uses Goertzel single-frequency detectors (essentially per-frequency IIRs that compute a DFT bin at O(N) instead of O(N log N)) to probe harmonics 2 through 8 of each candidate pitch. For each candidate:

  • SNR at each harmonic is measured against the ambient power half a semitone away (the "noise reference band"). Harmonics must be 3× above the noise band to count as present.
  • Power-line rejection discards probes within ±3 Hz of 50 Hz, 60 Hz, and their harmonics. Up to three out of seven probes may fall in power-line bands before the candidate is rejected entirely. This prevents a noisy power line from being identified as a note.
  • Decay validation checks that each higher harmonic is no more than 2× the amplitude of the one below it. Real instrumental harmonics decay; rising harmonics indicate noise, not a note. Up to two violations across the 7-harmonic stack are tolerated.
  • Composite coherence score is a weighted sum: 40% × (number of present harmonics / 7) + 40% × clamped total SNR + 20% × (1 − decay-violation penalty).
  • Temporal persistence boost adds 0.06 per prior frame the same MIDI candidate appeared in a rolling 8-frame window, capped at +0.25. Sustained candidates are more trustworthy than one-shot blips.

The minimum acceptance score is 0.35. Candidates that pass go forward as the detected pitch.

The persistence-and-decay validation is the part that lets the rescue work reliably in messy real-world recordings rather than just clean test tones.

The harmonic gate (for new notes only)

After a pitch candidate is selected, one final gate runs before the candidate is accepted as a new note (already-latched notes bypass it, since their harmonics weaken naturally during decay):

FrequencyRequired harmonic evidence
< 80 Hz≥ 2 harmonics present or harmonic score ≥ 0.35
80 to 130 Hz≥ 1 harmonic present or harmonic score ≥ 0.20
130 to 200 Hz≥ 1 harmonic present or harmonic score ≥ 0.15
> 200 HzNo gate (YIN + AC agreement is sufficient)

Strong autocorrelation peaks (≥ 0.90) also bypass the gate. These come from pure-sine sources like a tone generator, where harmonic evidence is genuinely absent and shouldn't be required.

The gate rejects spurious low-frequency "notes" that have no harmonic structure behind them: HVAC rumble, mic-stand thumps, table bumps.

Smoothing and noise floor

  • Frequency smoothing: Volume-weighted alpha blending. Large jumps (>6%) snap immediately so the display tracks fresh attacks. Small jumps (<2%) get heavy smoothing for stable sustain.
  • Noise floor: A rolling ambient-level tracker with asymmetric attack/release (fast on quiet, slow on loud) keeps the detector calibrated to the room. The effective noise floor is ambient + 12 dB, clamped to [-60, -30] dB. There's no hard threshold gate. The autocorrelation accept threshold rejects noise structurally, not by level.
  • Cents smoothing: Computed relative to the latched note (not the nearest chromatic semitone), so the cents reading doesn't drift mid-tuning when the player approaches centre.

Display latching

The displayed note is held stable by a small consecutive-detection state machine. By default, three consecutive detections of a new candidate are required before the display switches. For octave-distance jumps (≥11 semitones) the threshold rises to eight detections. This resists false octave-drop errors caused by decay enriching the second harmonic relative to the fundamental.

The preset further biases the threshold:

  • If the current latched note is in the preset and the proposed new note is an octave above (and not in the preset), the threshold rises to 12 (highly resistant to false octave-up errors).
  • If the current latched note is not in the preset but the proposed octave-down candidate is in the preset, the threshold drops to 3 (eager to fall into a preset note when the data supports it).

A 200 ms silence timeout releases the display when no pitch has been detected; the screen returns to ---.

Injectable target tables (the JI / temperament seam)

The cents-offset formula 1200 × log₂(detected / target) is agnostic to how the target Hz was derived. The detector exposes two mutually-exclusive APIs for installing a targeting mode:

  • setAllowedMidiNotes(_ notes: Set<Int>?): the chromatic / preset MIDI-constraint mode. The detector picks the nearest MIDI integer in the set (or in the full chromatic range when nil is passed) and reconstructs target Hz from referencePitch × 2^((midi − 69)/12). This is the path Western presets use under straight 12-TET.
  • setAllowedTargetFrequencies(_ freqs: [Double]?): an arbitrary-Hz target list. The detector picks the nearest entry by log-distance and uses it directly. This path drives Indian-classical just intonation (Sa × ratio), Western non-equal temperaments (12-TET × 2^(offset_cents/1200)), and the Western Temperament Override on Indian presets.

The two APIs are symmetric: calling either always clears the other unconditionally, so the detector never carries stale state across mode transitions. The mode router in MainTunerView.applyDetectorMode() is the only caller in normal operation; it chooses which API to install based on the active preset, the Indian-mode flag, and the Western temperament toggles. This single seam is what makes Indian classical JI, eight Western temperaments, and the Override mode all routable through the same detector without any pitch-detection code changes.

The injectable seam also exposes a noteNamer: ((Double) -> String)? closure that lets the caller override how detected Hz is displayed as a note name. Indian Classical mode installs a closure that maps Hz → chromatic-distance-from-Sa → sargam letter; Western modes leave it nil and the LED falls back to its built-in 12-TET letter glyphs.

↑ Back to top

7.Reference tone synthesis and the ribbon controller

The polyphonic ribbon plays sustained reference tones for any note in the active tuning preset. The synthesis engine and the touch surface are independent layers.

Voice architecture

Each voice in the engine is a subtractive synthesis chain:

PolyBLEP band-limited sawtooth oscillator
        │
        ▼
2-pole state-variable lowpass filter (Andrew Simper TPT/ZDF form)
        │
        ▼
soft-clip saturation  (x / (1 + |x|))
        │
        ▼
ADSR-style fade envelope (attack, sustain, release)

The oscillator is a sawtooth, a tone rich in odd and even harmonics. A naive saw aliases badly when its fundamental exceeds about 1/4 the Nyquist limit; the implementation uses PolyBLEP to band-limit the discontinuity at the wrap point, eliminating audible aliasing across the entire musical range.

The filter is a state-variable lowpass with:

  • Q = 1/√2 = 0.707… (Butterworth response: maximally flat passband, no resonant peak).
  • Cutoff = 2× the voice's fundamental, clamped between 220 Hz and 5000 Hz, and never above 45% of Nyquist.

The 2× key-tracking ratio means the timbre stays consistent across the musical range. The relative balance between fundamental, second harmonic, and upper harmonics is the same on a high E5 as on a low E2. The 220 Hz minimum cutoff is a deliberate choice for bass voices: the fundamental of an E1 (41 Hz) sits well below the cutoff and the filter passes harmonics 2 through 5 (82, 123, 164, 205 Hz) into the speaker's good band, where the brain reconstructs the perceived pitch via the missing-fundamental psychoacoustic effect.

The saturator is a soft-clip x/(1+|x|), providing odd-harmonic warmth without harsh clipping artefacts. At the voice amplitude used (1.4), saturation is gentle: essentially linear at normal levels, contributing harmonic warmth only during peaks.

Polyphonic voice management

The engine maintains 8 voice slots but caps simultaneously playing voices at 3. The remaining 5 slots are reserved for decaying release tails, so triggering a fresh voice never hard-kills another voice that is still audibly fading.

Voice stealing (when all idle slots are full): the engine takes the quietest decaying voice (lowest current fadeGain) and resets it to play the new note. Phase, filter state, and envelope are all reset on the steal so there's no carry-over from the previous voice.

Click-free same-frequency retrigger: if a voice is mid-release and the same frequency is requested again (the user re-taps the same key while it's still decaying), the engine lifts the fadeGain back into the attack phase without resetting phase or filter state. The result is a continuous, click-free re-trigger.

Envelope timing:

  • Attack: 25 ms (snappy)
  • Release: 250 ms (strum-like)

These are short enough to feel responsive on the ribbon but long enough that releases sound musical rather than abrupt.

The ribbon touch surface

The ribbon is built on a UIKit UIView with isMultipleTouchEnabled = true, wrapped in a SwiftUI UIViewRepresentable. SwiftUI's standard DragGesture does not handle multi-touch cleanly. Adding a second finger can end or modify the first finger's gesture in subtle ways. The custom UIKit layer eliminates this.

Each touch is identified by a fresh UUID from the moment it lands. Callbacks fire on the main thread:

  • onBegan(id, position): a finger went down
  • onMoved(id, position): a finger moved (only on actual movement)
  • onEnded(id, startPos, endPos): a finger lifted normally; both start and end positions are passed so the receiver can tell tap from drag
  • onCancelled(id): a system interruption (alert, palm rejection) cleared the touch; never treated as a latch

No UIGestureRecognizer is involved on the ribbon, so there is no recognizer arbitration that could steal one finger's behaviour because of what another finger is doing.

Latch logic

The ribbon UI maintains two independent state collections:

  • activeRibbonTouches: [UUID: Int]: currently-held touches and which key each is over
  • ribbonLatchedKeys: [Int]: explicitly latched keys, in oldest-first order

A key's voice is audible if any touch is on it or it's in the latched list. The voice starts when the user count goes from zero to one, and stops when it returns to zero.

Latch detection is gated on three conditions, all of which must hold:

  1. The touch began on the visible note-label region (not the chrome above).
  2. The touch ended on the note-label region.
  3. Total movement during the touch was less than 10 points.

If all three hold, the touch is interpreted as a tap on label and toggles the latch state for that key. Otherwise the touch is a strum (transient sustain that ends when the finger lifts).

Latching a currently-touched key does not add a new voice. It converts the existing transient voice into a sustained one. Voice stealing therefore fires only when a genuinely new distinct key enters the active set (via a new touch or a drag onto a different key), never when an existing touch is being converted to a latch. This is what allows three keys to be latched simultaneously without losing one to a phantom steal.

Voice stealing logic at the ribbon level

When adding a new distinct active key would push the count to four, the UI calls stealOldestRibbonLatchedIfAtCap(). This removes ribbonLatchedKeys.first (the oldest latched entry) and stops the associated voice if no touch is still holding it. The visible "lit" state of the displaced key clears immediately, so the UI never lies about what is actually playing.

This mirrors the audio engine's voice cap exactly. The audio engine would steal a voice anyway; doing it explicitly in the UI keeps the visual state consistent with what the speaker is producing.

↑ Back to top

8.Output stage and speaker voicing

The polyphonic mix is processed through a four-stage output chain before reaching the device's audio output:

voice mix
   │
   ▼
polyphony AGC  ──►  speaker HPF (if route = built-in speaker)
   │                       │
   │                       ▼
   │                speaker LP (if route = built-in speaker)
   │                       │
   └────────────►─────────┴────►  look-ahead brick-wall limiter  ──►  output

Polyphony AGC

A simple 1 / max(1, weightedActiveCount) gain reduction, where the count weights each voice by its current envelope amplitude (so fading voices contribute proportionally less). Smoothed with a 20 ms attack and 200 ms release time constant.

The purpose: when the user latches a chord, the unsmoothed sum of three sawtooth waves can hit the limiter hard, producing audible pumping as the limiter reacts to the beat patterns between voices. The AGC pre-reduces the gain proportionally to the active voice count, keeping the limiter mostly inactive and eliminating the pumping artefact.

Speaker-bypass high-pass

When audio is routed to the built-in iPhone or iPad speaker, a 4th-order Linkwitz-Riley high-pass at 140 Hz (two cascaded 2nd-order Butterworth biquads, each at Q = 1/√2) is engaged in series with the output. This is bypassed when audio is routed to headphones, AirPlay, Bluetooth, or USB. Those routes can reproduce the full bandwidth, and the high-pass would only diminish them.

The reason: iPhone and iPad built-in speakers have a sharp acoustic rolloff below ~200 Hz, and trying to push high-amplitude signal into that band causes cone-excursion distortion that sounds buzzy and unpleasant. The 140 Hz cutoff is a deliberately gentle compromise: it rolls off the deep sub-bass that would cause excursion, but passes the 140 to 280 Hz band (where the speaker can still produce sound, even if attenuated) so bass notes' second and third harmonics get through. Combined with the missing-fundamental psychoacoustic reconstruction described in 7.1, this lets bass voices on built-in speakers be audible and pitch-clear without distortion.

The high-pass engages and disengages automatically based on AVAudioSession.routeChangeNotification. The switch is silent.

Look-ahead brick-wall limiter

The final stage. A 64-sample look-ahead buffer (~1.33 ms at 48 kHz) tracks the peak amplitude of the next 64 samples. When a peak would exceed 0.95 of full scale, the gain is smoothly reduced to bring it under, with a hold of 64 samples on each new peak (so the gain release doesn't chase fast transients).

Parameters:

  • Ceiling: 0.95 (5% of full-scale headroom)
  • Attack: ~0.1 ms (very fast: catches new transients before they exceed the ceiling)
  • Release: ~80 ms (musical, no pumping)

The limiter is a safety net, not a creative effect. With the polyphony AGC working upstream, the limiter is mostly inactive during normal playback. It exists to guarantee the output never clips, even on edge cases like rapid retrigger or large voice-stealing transients.

↑ Back to top

9.Visual meters

This section documents the rendering pipelines of the three primary meters and three sub-meters for completeness; user-facing behaviour is described in sections 3.2 and 3.3.

The CRT chassis shape

Every meter and panel is housed in a CRTDisplayShape, a single Shape defined by a split-cubic-Bezier arch at the top, vertical sides angled inward by 13% width, and tangent-arc rounded corners at the bottom. Key geometric parameters:

  • Arch depth: 56% of the shape's height
  • Arch apex: 30.4% above the shoulders
  • Side inset: 13% of width
  • Bottom-corner radius: 7% of width

The shape exposes its tangent vectors at every point on the arch, used by the LED meter and strobe meter to align their content with the chassis curvature.

LED arc meter

A Canvas-backed render that lays out 19 LED-shaped polygons (9 left red, 1 centre green, 9 right red) along an arc inside the chassis. The arc-length parameterisation is computed once and cached: subsequent frames just light up the appropriate segments based on the smoothed cents offset and the in-tune flag. This is the cheapest meter in the app to render per frame.

Note wheel

The chromatic note wheel has been redesigned to share the chassis arch curve mathematically:

  • Rim lines (the two horizontal arc lines at the top) are drawn directly from the chassis arch path with a small vertical translation. They are literally the chassis arch shape, just shifted down.
  • Label baselines sit on a parallel translation of the same chassis arch, with letters drawn at theta-derived X positions that are then projected onto the radial line from a virtual wheel centre below the canvas. This ensures each letter and its corresponding centre tick lie on the same straight radial spoke, not just at the same X.
  • Tick fan radiates from the virtual centre (at 2× the canvas height below the apex), so ticks tilt outward at the edges in a natural wheel-spoke pattern.
  • Tick groups: each note has a group of 5 ticks: short, medium, long centre, medium, short. The boundaries are shared between adjacent groups. The letter sits directly above the long centre tick.

The wheel rotates continuously as pitch is detected; at exact in-tune, the rotation pauses and the target note glows white.

Per-device tuning: only the visible angular sweep (halfArc = 0.42 on iPad, 0.50 on iPhone) differs between devices. All offsets are identical. The chassis arch is the same shape on both, so the layer hierarchy needs the same proportions to clear visual overlaps.

Stroboscopic meter

Four bands following the chassis arch curve. Each band's vertical position cycles based on the phase deviation of its respective harmonic (1st, 2nd, 3rd, 4th of the target). When phase deviation is zero, motion is frozen.

The phase velocity is capped at 120 rad/sec to stay below the photosensitive epilepsy risk band (3 to 30 Hz visible flicker). At full strobe motion, the visible flicker rate is ~19 Hz: well within the safe zone.

Lower-panel sub-meters

  • Chromatic strip renders a 9-note window via an HStack of StyledNoteText views, with two Canvas-drawn triangle indicators that move horizontally and turn solid white at in-tune.
  • Info bar is a 3-column HStack of Text views with shadow layers for the CRT-glow effect.
  • Fine-tune ruler is a single Canvas drawing the graduation marks and the moving indicator each frame.

All sub-meter content uses metrics.subMeterFontScale (1.4× on iPad, 1.0× on iPhone) so the readout is legible at iPad's larger physical cell size without forcing iPhone to render comically large text.

↑ Back to top

10.Instrument presets and reference pitch

These are described in sections 3.6 and 3.7 from the user's perspective; this section adds technical detail.

Preset internal representation

Each TuningOption stores:

  • A canonical name (e.g. "Standard EADGBE")
  • A display name override (for the info bar)
  • An array of MIDI note numbers representing the open strings
  • An isIndianClassical flag (default false): when true, selecting the preset auto-enters Indian Classical mode.
  • An optional jiRatios: [Double]?: present on Indian Classical presets, absent on Western presets. Each ratio is the Sa-relative multiplicative interval for one ribbon cell (e.g. [1.0, 9/8, 5/4, 4/3, 3/2, 5/3, 15/8, 2.0] for the Bilawal thaat).
  • An octaveSpan: Int: how many octaves of jiRatios to expand for the ribbon (1 for most presets, higher for multi-saptak layouts).

The detector consumes this as Set<Int> for O(1) membership checks. At every detection step, the target note is the nearest member of this set to the detected pitch, which is what the cents-offset readout and the chromatic-strip / fine-tune-ruler display are computed against.

The Open/Chromatic preset returns nil for the allowed-notes set, which disables the constraint and computes target-note as the chromatic nearest semitone.

For Indian Classical presets, the MIDI-note array is the chromatic latch / ribbon-sizing scaffold (used by the chromatic strip and accessibility paths), while jiRatios is the authoritative target table. The ribbon's Hz list and the detector's setAllowedTargetFrequencies injection both derive from jiRatios × saHz rather than from MIDI ints, so the cents math is anchored at the exact JI ratio above Sa.

Reference pitch internals

The reference pitch is stored in two places:

  • @AppStorage("referencePitchPreset"): the name of the active preset (e.g. "432", "free")
  • @AppStorage("referencePitchHz"): the current Hz value (defaults to 440.0)

On engine start, the detector reads referencePitchHz and calls frequencyDetector.setReferencePitch(_:), which recomputes the MIDI-to-frequency mapping table from that root. This means changing the reference pitch is essentially instantaneous and does not require restarting the engine.

The free slider has a range of 400 to 480 Hz with 0.1 Hz step granularity. Changes propagate to the detector in real time.

No tuning data is bundled as a separate file

The full preset catalog (all 19 groups, 140+ tunings, including the Indian Classical groups with their JI ratio tables) is statically defined in InstrumentPreset.swift. Adding a new preset is a code change, not a data file change. This is deliberate. There is no preset import/export, no community sharing, no remote update mechanism. The app's preset list is exactly the list shipped in the binary.

↑ Back to top

11.Indian classical mode

User-facing behaviour is in §3.10. This section documents the technical implementation in detail.

Mode activation

There is no master toggle. Mode activation is preset-driven: presetManager.isIndianClassicalActive returns true when the currently-selected TuningOption.isIndianClassical is true. MainTunerView.isIndianModeActive forwards this single boolean to every downstream display surface and targeting path. Selecting any non-Indian preset returns the flag to false and the tuner reverts to Western 12-TET behaviour with no further state changes.

Sa as the anchor

@AppStorage keys hold the user's Sa configuration:

  • saHz: Double (default 277.1826…, C#4 at A=440)
  • saWesternRoot: String (default "C#")
  • saFineTuneCents: Double (default 0)

saHz is the single source of truth. The chromatic root and fine-tune-cents pair are convenience inputs that recompute saHz via recomputeSaHz(); conversely, when the user types an Hz value directly, the picker decomposes it back to the nearest chromatic root and cents offset. All three writes propagate to the detector via the saHz observer, which re-runs applyDetectorMode() so the JI target table rebuilds against the new Sa.

The reference pitch (referencePitchHz, default 440 Hz) and Sa are independent. Changing the A=ref leaves Sa at its previously-set Hz; changing Sa leaves the A=ref unchanged. This matches how Indian players actually work: the instrument's natural Sa frequency is fixed by physical string tension and gauge, and is not tied to an ensemble's A=440 standard.

Just-intonation target list

TuningOption.ratioTargetFrequencies(saHz:) expands the preset's jiRatios across ±3 octaves around the ribbon's nominal range (filtering out entries below 20 Hz or above 8000 Hz, the detector's reliable detection window). The resulting array is handed to frequencyDetector.setAllowedTargetFrequencies(_:), which makes the detector pick the nearest target by log-distance for every incoming pitch.

TuningOption.frequencyValues(saHz:) returns the ribbon's playable Hz list (one octave of ratios above Sa, multiplied by octaveSpan). Each ribbon cell taps directly into one of these Hz values, so the reference-tone surface and the detector target table are always derived from the same Sa anchor.

Sargam display surfaces

When Indian mode is active and the Sargam Names toggle is on, every name-rendering surface in the app re-routes through one of two arrays:

private static let hindustaniSargam = ["S","r","R","g","G","M","m","P","d","D","n","N"]
private static let carnaticSargam   = ["S","R1","R2","G2","G3","M1","M2","P","D1","D2","N2","N3"]

and their full-name siblings:

private static let hindustaniSargamFull = ["Sa","re","Re","ga","Ga","Ma","ma","Pa","dha","Dha","ni","Ni"]
private static let carnaticSargamFull   = ["Sa","Ri1","Ri2","Ga2","Ga3","Ma1","Ma2","Pa","Da1","Da2","Ni2","Ni3"]

The active arrays are picked by activeSargamArray (short) and activeSargamFullArray (full) based on the notationSystem AppStorage key. The same array indexes by chromatic distance from Sa, so swapping between Hindustani and Carnatic is a single AppStorage write that propagates to every surface on the next render.

For surfaces that need the full name plus octave register, sargamFullForm(for hz: Double, saHz: Double) -> (text: String, octave: Int) computes the chromatic distance, looks up the active array, and derives the octave register (-2 ati-mandra through +2 ati-taar) via floor-divide with a half-octave shift so boundaries round to the closest saptak.

The 16-segment LED for sargam

SargamSegmentDisplayView is a separate Canvas-rendered view that supersedes the standard 7-segment SegmentDisplayView in Indian mode. The 16-segment "starburst" topology, four horizontals (split), four corner verticals, two centre verticals, two split middle horizontals, four corner diagonals, handles every sargam letter cleanly, including the M and m whose diagonals and three-stem shapes can't be expressed by a 7-segment grid.

The component delegates the digit cell for Carnatic indices to the existing 7-segment SegmentDisplayView.drawCell(...), so a Carnatic readout like R1 renders as the 16-segment R glyph plus a smaller 7-segment 1 tucked into a subscript slot. The two ghost-outline structures stay visible per the hardware-emulation principle: segments toggle lit/unlit; the underlying frame never appears or disappears.

The HorizontalTuningMeterView chooses between the two displays based on a single flag (useSargamSegments) passed from MainTunerView. The flag is true exactly when Indian mode is active AND sargam display is enabled; the Western sub-meter override (the "show Western notes" toggle) sets it false, falling back to the standard 7-segment glyphs.

OctaveDotLabel: the full-name surfaces

OctaveDotLabel is a small SwiftUI view that draws a swara name with reserved space for octave dots above (taar saptak) and below (mandra saptak) the text. The dots are small Circles inside VStack rows whose height is always reserved: dots flip lit / hidden via opacity, not by view insertion, so cell heights stay uniform across the ribbon and chromatic strip regardless of which octave the swara sits in.

Two dots above indicate ati-taar (double-upper saptak, ratio ≥ 4); one dot above indicates taar (ratio in 2 to 4). No dot is madhya (ratio in 0.5 to 2). One dot below is mandra (ratio in 0.25 to 0.5); two dots below is ati-mandra (ratio < 0.25).

The component is used by the portrait and landscape ribbon labels, the chromatic strip cells, and the info-bar Note column. SwiftUI's .foregroundColor() modifier propagates to both the dots and the text, so callers can colour the whole label uniformly without per-element configuration.

The Indian preset catalog

The four Indian-related groups in InstrumentPreset.swift:

  • Tanpura (5 entries): Pa, Ma, Ni, Pa-Ni, Ma-Dha patterns. Strings shown in low-to-high pitch order. Each pattern stays a generic drone definition; the player sets the global Sa to the actual pitch their tanpura sits at.
  • Indian Classical (10 entries): Sitar Maihar (Kharaj Pancham, 7 strings, downstroke direction left→right), Sitar Vilayat (Gandhar Pancham, 6 strings), Sitar Taraf (13-string baseline taraf in Bilawal thaat), Sarod, Surbahar, Veena, Rudra Veena, Dilruba, Esraj, Sarangi.
  • Thaat (10 entries): the ten parent scales of Hindustani classical: Bilawal, Kalyan, Khamaj, Kafi, Asavari, Bhairav, Bhairavi, Poorvi, Marwa, Todi. Each is the seven-swara ascending scale plus the upper-octave Sa for closing the saptak.
  • Chinese Classical and West African groups also ship for completeness, but their presets stay equal-tempered (no jiRatios): the Indian-mode flag isn't set on them, so they take the Western targeting path.

Every JI ratio in the catalog is sourced from internal documentation that catalogues the scholarly references and consultant-flagged uncertainties for each entry. Ratios marked CONSULTANT-PLACEHOLDER in the code carry a grep-able tag so the post-consultant-review pass can find them quickly.

Indian Classical Settings menu

Located at Settings → Indian Classical. Renders via settingsIndianClassicalView in MainTunerView. Sections:

  • Sa Tonic: chromatic root chips (12 buttons), fine-tune cents slider (−50 to +50), free Hz slider (B2 to E4 range), and a live Hz readout.
  • Sargam Names toggle: when off, the JI target table stays installed but the detector's noteNamer closure falls back to Western names. Useful for educational comparison.
  • Notation System picker: Hindustani / Carnatic.
  • Western Temperament Override toggle: see §12.5.
  • Status row: shows whether the active preset triggers Indian mode.

The display-bar reference cell tap routes directly here when an Indian preset is active, replacing the standard reference-pitch destination. Tap routing is decided in handleButtonPress(.settingsMenu) based on what's currently active.

↑ Back to top

12.Historical and alternate temperaments

User-facing behaviour is in §3.11 (Western preset path) and §3.12 (Indian-preset Western Override). This section documents the implementation.

The temperament catalog

Models/Temperament.swift defines the Temperament struct (id, displayName, shortName, category, centOffsets: [Double] of length 12) and the TemperamentRegistry enum holding the eight static instances. Each centOffsets array is the cents-above-12-TET for each chromatic degree relative to the tonic (degree 0 = tonic).

The values are sourced verbatim from internal temperament tables. Tabular summary:

TemperamentidShort nameCategory
EqualequalEQStandard
PythagoreanpythagoreanPYTHHistorical
1/4-comma Meantonequarter_meantone1/4 MTHistorical
1/6-comma Meantonesixth_meantone1/6 MTHistorical
Werckmeister IIIwerckmeister_iiiWERK IIIHistorical
Kirnberger IIIkirnberger_iiiKIRN IIIHistorical
VallottivallottiVALLHistorical
YoungyoungYOUNGHistorical

TemperamentRegistry.lookup(_ id: String) -> Temperament falls back to Equal for any unknown id so a corrupted persisted value never causes a targeting break.

AppStorage keys

Five keys in MainTunerView drive the temperament feature:

  • alternateTemperamentEnabled: Bool (default false): master toggle.
  • temperamentName: String (default "equal"): selected temperament id.
  • temperamentTonic: String (default "C"): chromatic root for the offset rotation.
  • hasSeenTemperamentIntro: Bool (default false): first-use banner gate.
  • westernTemperamentOverride: Bool (default false): Indian-preset Western intonation switch (§12.5).

All five persist across app launches. The temperament selection persists even when the master toggle is off, so a user who has set up a temperament for a piece can toggle it off and back on without re-selecting.

Target Hz derivation

temperedHz(midi: Double, referencePitch: Double) -> Double in MainTunerView:

  1. Computes the equal-tempered Hz: etHz = referencePitch × 2^((midi − 69)/12).
  2. Short-circuits to etHz if alternateTemperamentEnabled is false or the active temperament is Equal.
  3. Otherwise looks up the temperament, computes the chromatic degree of the note relative to the tonic ((noteIdx − tonicIdx + 12) mod 12), reads the cent offset from centOffsets[degree], and returns etHz × 2^(cents / 1200).

This is called per-preset-MIDI when applyDetectorMode is in the Western + non-equal branch (the detector's setAllowedTargetFrequencies receives the resulting Hz list), and per ribbon cell in getCurrentTuningFrequencies() (so the tap-tone Hz matches the detector's target exactly).

Tonic rotation falls out for free: if the user picks F as the tonic, tonicIdx = 5, and a detected F maps to degree 0 (offset = 0, straight ET), while a detected A maps to degree 4 (offset = centOffsets[4]). Sweeping the tonic chip-row rotates the offset pattern without rebuilding the table.

Mode-router branching

applyDetectorMode() has four mutually-exclusive branches:

  1. Indian + JI: selected when an Indian Classical preset is active and Override is off. Installs the JI target table via ratioTargetFrequencies(saHz:).
  2. Indian + Western Temperament Override: selected when an Indian preset is active and Override is on. Computes Western-temperament Hz for each jiRatio's nearest MIDI position (Sa is forced to C# in this mode), installs the resulting Hz list via setAllowedTargetFrequencies. The sargam-name closure is still installed so labels remain Indian.
  3. Western + non-equal temperament: selected when Indian mode is inactive and the temperament master toggle is on with a non-equal selection. Iterates the preset's MIDI list through temperedHz and installs the resulting Hz list. The Note-name closure is cleared so the LED falls back to Western glyphs.
  4. Western default: the fallback. Clears any leftover target-Hz table explicitly (so a prior target-mode doesn't leak across the swap), then installs the standard MIDI-constraint mode via setAllowedMidiNotes(effectiveAllowedMidiNotes).

The detector's setAllowedMidiNotes(_:) and setAllowedTargetFrequencies(_:) are symmetric: each unconditionally clears the other's state when called. Combined with the mode router's explicit setAllowedTargetFrequencies(nil) in the Western-default branch, this guarantees no stale targeting state can survive a mode transition.

Several observers re-trigger the mode router on AppStorage changes: presetLock, referencePitchHz, saHz, sargamDisplayEnabled, notationSystem, alternateTemperamentEnabled, temperamentName, temperamentTonic, westernTemperamentOverride, and presetManager.selectedTuning. Each is gated on the suppressDetectorReapply flag (see §12.6) so the Reset All Settings path doesn't trigger a cascade of partial re-applies.

Western Temperament Override

When Override is on and an Indian Classical preset is active, the Override branch of applyDetectorMode installs target-Hz values derived as:

  • For each jiRatio in the active preset, compute the ratio's chromatic position relative to Sa = C# (the Override-anchored Sa, hard-coded to C#4 = 277.18 Hz independent of the user's saWesternRoot).
  • Round to the nearest MIDI integer.
  • Pass through temperedHz with the current referencePitchHz so the active Western temperament and tonic apply.

The ribbon Hz list and the detector target list both follow this derivation, so the user sees the Indian preset's swara layout but every cell sounds a Western-tempered Hz at the active reference pitch. Sargam labels still render via the standard Indian-mode display path.

Reset All Settings: observer cascade suppression

resetAllSettingsToStandard() writes thirteen AppStorage keys in sequence to reset everything. Each write fires its observer, which would normally call applyDetectorMode(). The result without suppression would be a cascade of partial re-applies: the detector latch would reset multiple times, and intermediate applyDetectorMode calls would run against partially-reset state.

The fix is a session-scoped @State suppressDetectorReapply: Bool flag. Every observer that calls applyDetectorMode() is gated on guard !suppressDetectorReapply else { return }. The reset function sets the flag true, runs all assignments, then calls one authoritative applyDetectorMode() + setReferencePitch() with consistent state, and finally schedules suppressDetectorReapply = false via DispatchQueue.main.async. SwiftUI's deferred observer fires that follow the AppStorage writes still see the flag as true and skip; the next runloop tick clears the flag and normal observer behaviour resumes.

First-use banner

When the user enables a non-equal temperament for the first time, an inline banner appears at the top of the temperament submenu. The banner is gated directly on three live conditions:

if !hasSeenTemperamentIntro
   && alternateTemperamentEnabled
   && temperamentName != "equal" {
    temperamentIntroBanner(c: c, metrics: metrics)
}

Because hasSeenTemperamentIntro is an @AppStorage Bool, the banner survives app backgroundings: the user can enable a temperament, close the app without dismissing, relaunch, and reopen the temperament submenu, and the banner is still there. Tapping "Got it" sets the flag to true; the banner hides permanently until Reset All Settings clears the flag again.

A ScrollViewReader wraps the submenu's ScrollView and scrolls back to the top whenever the banner conditions transition true, so a user who toggles the master switch on while scrolled mid-menu sees the full banner, not just the "Got it" button at the bottom.

Display-bar badge

displayBarTemperamentBadge: String? in MainTunerView computes the badge text. The badge renders when:

  • A non-Indian preset is active AND the master toggle is on AND the temperament is non-equal, OR
  • An Indian preset is active AND Override is on.

In the first case, format is <short> / <tonic> (e.g. WERK III / C). In the second, the tonic is always shown as C# (since Override forces Sa = C#). When the temperament is Equal under Override, the badge still renders as EQ / C# so the user sees that the Override is engaged even when the underlying temperament happens to be Equal.

The badge is rendered as a tinted-background pill (crtBlueBright.opacity(0.18) fill, crtBlueBright text) below the standard reference-pitch text. Same cell footprint as before; no layout shift when the badge appears or disappears.

The display-bar tap routes the user directly to the relevant subscreen: Indian Classical Settings when the Indian flag is active, the Temperament submenu when a non-equal Western temperament is active, or the main settings page otherwise.

↑ Back to top

13.Adaptive layout system

The layout system is defined in LayoutMetrics.swift, which computes proportional dimensions from the current viewport's actual width and height, not from the device class. This means iPad Split View / Slide Over windows that shrink below the iPad layout's width threshold automatically fall back to the iPhone layout, which scales down cleanly.

Reference design: iPhone 14 (390 × 844 pt). All proportional metrics are computed as multipliers of min(widthScale, heightScale) of the actual viewport relative to this reference.

Key clamps:

  • General UI scale: clamped to [0.85, 1.6] (iPhone SE doesn't shrink too small; iPad Pro doesn't balloon too large)
  • Meter scale: clamped to [0.85, 1.8] (more aggressive, because the LED meter is the hero element)
  • iPad sub-meter font scale: 1.4× (chromatic strip, info bar, fine-tune ruler, note wheel labels and ticks)

Layout selection thresholds:

  • useTwoColumnLayout: iPad AND landscape AND screenWidth ≥ 700 pt
  • useIPadPortraitLayout: iPad AND portrait AND screenWidth ≥ 600 pt
  • useIPhoneLandscapeLayout: otherwise landscape
  • Else: iPhone portrait
↑ Back to top

14.Performance characteristics

These numbers come from on-device measurement on a representative range of supported hardware.

  • CPU baseline (engine off): <1% on every supported device.
  • CPU during active tuning: 3 to 8% on iPhone 14 / iPad Pro, peaking briefly during attack transients. Below 12% on iPhone SE (1st generation).
  • Memory footprint: <40 MB resident.
  • Audio latency (input to display): typically 32 to 96 ms end-to-end, dominated by the audio I/O buffer (32 ms) and the detection window hop (10 to 20 ms).
  • Audio latency (touch to tone): ~32 to 64 ms (one to two audio buffers).
  • Frame rate: 60 fps on all supported hardware. The animated meters (note wheel rotation, strobe band motion) update on Canvas redraws driven by the detector's state changes; no animation loops run when the detector is silent.

Supported devices:

  • iPhone SE (1st generation) and later
  • iPad (5th generation) and later
  • iPad mini (5th generation) and later
  • iPad Air (3rd generation) and later
  • iPad Pro (all generations supported)

Minimum OS: iOS 16.6 / iPadOS 16.6.

↑ Back to top

15.Engineering decisions and rationale

This section documents the why behind the major architectural choices. It's useful for engineers evaluating SuperTuner against alternatives, and for understanding why a few design constraints were made.

Why autocorrelation over FFT for the pitch core

FFT-based pitch detection (find the spectral peak, snap to the nearest semitone) has two specific failure modes: spectral leakage from non-integer bin frequencies introduces a small but consistent sharp bias, and the bin spacing limits resolution unless the FFT size is very large. Autocorrelation measures periodicity directly in the time domain. There is no bin grid, no window function, no leakage bias. With 3-point parabolic interpolation, sub-sample lag precision is straightforward.

Why both YIN and per-lag-normalised autocorrelation

They fail differently. YIN is sharp and confident on clean signals but its CMND inflates in noisy environments, especially at low frequencies. Per-lag-normalised autocorrelation is more noise-tolerant but less sharp, with a tendency to favour octave-down errors on harmonic signals. Running both and combining their verdicts gives the snappy attack of YIN with the noise tolerance of AC.

Why a custom Spectral Harmonic Fingerprint rescue

In real-world recording (rooms with HVAC, fluorescent lights, instruments with body-resonance suppression of their own fundamental), the fundamental is sometimes simply not present in the spectrum. A YIN-only detector loses tracking in these cases even though a musician would clearly hear "that's a low E". The SHF rescue measures the upper harmonics (2 through 8), tests them for consistency with a missing-fundamental hypothesis, and reports the inferred pitch. It is gated tightly enough (3× SNR per harmonic, decay-violation limit, temporal persistence requirement) that it does not produce false positives on noise.

Why a subtractive synthesis engine (rather than additive)

Earlier versions used additive synthesis (a manual sum of 10 sine harmonics with per-harmonic amplitude shaping). The downside was a "thin, digital-reed" character that sounded harsh on small speakers. Subtractive synthesis (a single PolyBLEP saw through a tuned lowpass filter) produces a continuously-varying harmonic balance as a function of cutoff vs. fundamental, which is more pleasant tonally and naturally limits high-frequency energy on the lowest voices.

Why a 3-voice cap rather than unlimited polyphony

Three voices give comfortable chord coverage (root + third + fifth or seventh) without the engine straining the polyphony AGC or the limiter. More voices would require more aggressive gain reduction, which would either pump or clip. Three was the largest count that consistently sounded clean across the iPhone speaker.

Why 140 Hz for the speaker high-pass (was 280 Hz, then 220 Hz, now 140 Hz)

iPhone and iPad speakers reproduce content from ~140 Hz upward usable, with severe rolloff below. A 280 Hz high-pass removed all bass, including harmonics that the speaker could have produced. The 140 Hz cutoff is the compromise that passes the speaker's full usable band while still removing the sub-bass that would cause cone excursion and audible distortion. The filter only engages on built-in speaker routes; headphones, AirPlay, and Bluetooth/USB are not affected.

Why a polyphony AGC and a limiter (rather than just a limiter)

A standalone brick-wall limiter tracks the envelope of the polyphonic signal. With three sawtooths summed, the envelope has clear pumping at the beat frequencies between the voices, which the limiter reacts to audibly. The pre-AGC reduces the gain in a content-aware way (proportional to active voice count, weighted by envelope amplitude), keeping the limiter mostly idle and eliminating the pumping artefact.

Why a Power button (rather than just stopping the audio session)

Two reasons:

  1. Thermal and battery. Long tuning sessions in a hot room can cause sustained CPU usage to spool up the device thermal envelope. The Power button fully halts the engine so CPU drops to idle.
  2. False positives. Leaving the tuner running while not actively tuning (e.g. between songs in a long set) can pick up audience noise, instrument cable handling, and other sounds that produce nuisance latches. Power off, then on when ready.

Why a custom UIKit multi-touch surface for the ribbon (rather than SwiftUI gestures)

SwiftUI's DragGesture(minimumDistance: 0) does not provide consistent multi-touch semantics. Specifically, adding a second finger to a ribbon that already has a held finger can end or modify the first finger's gesture in ways that depend on the gesture-recognition arbitration between the body's drag gesture and the label's tap gesture. In SuperTuner ≤ 1.1.3 this caused a real bug where adding a second finger anywhere on the ribbon would unexpectedly latch the held note.

The custom RibbonTouchSurface (UIKit UIView with isMultipleTouchEnabled = true, wrapped via UIViewRepresentable) eliminates the gesture-recognition layer entirely. Touches arrive directly at the view's touchesBegan/Moved/Ended/Cancelled methods, identified by their UITouch references, with no arbitration possible. Each finger is independent in a way SwiftUI cannot match.

Why the note-wheel curves all match the CRT chassis arch

In versions of the design where rim and label curves were near-but-not-quite identical (e.g. rim drawn from the chassis Bezier, labels drawn on a circle), the small mismatch was visible at the corners as a wobble between layers. The current implementation draws rim, label baselines, and tick tops as exact translations of the chassis arch, with label X positions projected onto the same radial spoke their centre tick uses. The result is that every visible curve in the wheel is mathematically the chassis arch curve, just translated. There is no curve mismatch to perceive.

Why Indian classical mode is preset-driven instead of a master toggle

The first design sketch had a "Western / Indian" master toggle at the top of Settings. That meant a user who wanted to tune a sitar had to flip the toggle, then find the preset, and a user who picked a tanpura preset by accident from the chromatic list would see the meter behave in unexpected ways with no obvious cause. Folding the mode-entry into preset selection (isIndianClassical: true on the preset itself) removes both failure modes: choosing an Indian preset is the gesture that enters JI targeting and sargam display, choosing a Western preset is the gesture that exits. The mode is never out-of-sync with the selected instrument.

Why just intonation for Indian classical (rather than approximated 12-TET)

Indian classical pitch is defined by ratio relationships against Sa, not by absolute Hz against an A=440 grid. A tanpura strung at "C# Sa" doesn't produce 277.18 Hz on its tonic and the equal-tempered Pa above it. It produces a pure 3:2 fifth above whatever the Sa string is tuned to, and the player's ear is trained to hear that relationship as correct. Snapping to 12-TET would put the Pa target ~2 cents flat of where the player wants it, putting the meter in disagreement with the ear. The JI ratios in jiRatios (and the ratioTargetFrequencies(saHz:) expansion across ±3 octaves) make the meter match the ratio relationship the player is already listening for.

Why a free-floating Sa instead of snapping to chromatic notes

A Hindustani vocalist's Sa is a personal-range choice, often a quarter-tone or more away from any 12-TET note. Restricting Sa to chromatic positions would force such a user to either retune their reference or accept a misaligned meter. The Sa is therefore exposed as a continuous Hz value with chromatic anchors for convenience: pick "C#" to snap to the chromatic-Sa anchor, then ±50 cents of fine-tune to land on the actual Sa.

Why the 16-segment LED for sargam glyphs

Sargam swaras are written as single letters with diacritics: S, r, R, g, G, m, M (with overdot), P, d, D, n, N. The Devanagari glyphs add accents for komal / shuddha / tivra distinctions. A standard 7-segment LED can't render lowercase distinctly from uppercase, and a 14-segment LED can't render the diacritic dot for tivra Ma cleanly. The custom 16-segment grid (1 horizontal centre bar, the standard 14 outer segments, plus a top-centre dot) gives every required glyph a recognisable shape while preserving the CRT/LED visual idiom of the rest of the device.

Why 8 temperaments instead of a custom-cents editor

There are dozens of historical temperaments worth listing and infinite custom variations. A "custom cents per degree" editor would have been more flexible but would also have been a UX rabbit hole for the 99% of users who just want "the well-tempered choice composers like Bach worked with". The curated 8 (Equal, Pythagorean, both Meantones, Werckmeister III, Kirnberger III, Vallotti, Young) cover the historical periods most early-music players actually encounter, with names they recognise from CD liner notes. Custom temperaments can be added later if real users ask for them; nobody has so far.

Why Western Temperament Override on Indian presets

A few sitar players use Western tonal harmony or play in mixed-tradition ensembles and want their Sa-relative cells to land at 12-TET (or temperament-flavoured 12-TET) targets rather than pure JI. The Override switch lets them keep the Indian preset's swara layout and sargam display while routing target Hz through the Western temperament table. The branch is positioned so the catalog of presets stays purely Indian (no second copy of "Sitar at 12-TET"), but the targeting math is selectable.

Why mutually exclusive setAllowedMidiNotes / setAllowedTargetFrequencies seams

When both Western (12-TET, AppStorage-driven A=440) and Indian (JI, Sa-driven) modes shared a single MIDI-note allow-list, the cents formula (1200·log₂(detected/target)) had to look up the target's Hz from one of two tables depending on which mode was active, and the detector's internal latch sometimes resolved against the wrong table during mode transitions. Splitting the API into two seams, one for MIDI lists (Western default), one for arbitrary Hz lists (Indian JI, Western non-equal, Indian + Override), and making the most recent setter wipe the other table makes "which target table is live" unambiguous. The mode-router in applyDetectorMode() is the only place that decides, and every decision picks exactly one seam.

Why the first-use temperament banner reads from a persistent flag

The original banner used a session-only @State flag. A user who enabled Werckmeister III at 3pm, backgrounded the app, and re-foregrounded at 4pm got no banner, even though they'd never been told what a non-equal temperament does to their meter readings. Gating the banner on hasSeenTemperamentIntro (@AppStorage) instead means the banner re-appears every time the user visits Settings → Temperament until they explicitly dismiss it. Once dismissed, the persistent flag stays set across re-launches and Reset All Settings is the only way to bring it back.

↑ Back to top

16.Glossary

ADSR: Attack, Decay, Sustain, Release. The four phases of a synthesiser's amplitude envelope. SuperTuner uses a simplified attack/sustain/release envelope (no separate decay).

AGC: Automatic Gain Control. A processor that reduces signal gain in response to input level, used here to pre-attenuate polyphonic content so a downstream limiter doesn't pump.

Autocorrelation: A mathematical operation that measures how similar a signal is to a delayed copy of itself. Used to find periodic structure (i.e. pitch) without a frequency-domain transform.

Carnatic: The classical music tradition of southern India. In SuperTuner this is one of the two sargam notation choices; it differs from Hindustani primarily in the names used for the flat/sharp swara variants (e.g. R1 instead of r).

Cents: A logarithmic unit of pitch. 100 cents = one semitone. A "5 cents flat" pitch is one-twentieth of a semitone below the target, which is quite tight, comparable to the precision required for orchestral string tuning.

CMND: Cumulative Mean Normalised Difference. The core measurement YIN uses to find the period of a signal. Lower CMND means stronger periodicity.

Equal temperament: The modern standard tuning in Western music, dividing the octave into 12 equally-spaced semitones of exactly 100 cents each. All major/minor thirds and sixths are slightly impure (off by ~14 cents) but every key is equally usable.

Goertzel algorithm: An efficient single-frequency DFT. Computes the magnitude at a chosen frequency in O(N) time instead of computing the entire spectrum.

Harmonic: A frequency that is an integer multiple of a fundamental. The 2nd harmonic of 440 Hz is 880 Hz, the 3rd is 1320 Hz, and so on. Real instruments produce a fundamental plus a series of harmonics, in proportions that define the instrument's timbre.

Hindustani: The classical music tradition of northern India. In SuperTuner this is one of the two sargam notation choices and the default; it uses lowercase letters for komal (flat) swaras and uppercase for shuddha/tivra (natural/sharp), e.g. r R g G m M.

Just intonation (JI): A tuning system in which note frequencies are defined by exact whole-number ratios against a reference (e.g. 3:2 for a pure perfect fifth, 5:4 for a pure major third). Indian classical music uses JI exclusively, anchored at Sa.

Kirnberger III: A 1779 well-temperament by Johann Philipp Kirnberger. Tempers four of the fifths by 1/4 syntonic comma and leaves the rest pure. Distinct character per key while remaining usable in every key.

Komal / Shuddha / Tivra: Hindustani names for the flat, natural, and sharp variants of a swara. Komal Ga (g) is one semitone below shuddha Ga (G); tivra Ma (M) is one semitone above shuddha Ma (m).

Linkwitz-Riley filter: A filter topology made by cascading two Butterworth filters of half the desired order. A 4th-order Linkwitz-Riley high-pass is two cascaded 2nd-order Butterworth high-passes; it has a −24 dB/octave rolloff with no resonant peak.

Look-ahead limiter: A peak limiter that delays the audio signal by a small amount (here, 64 samples ~ 1.33 ms) while peak-detecting the future signal. This lets the limiter begin reducing gain before a peak arrives, eliminating overshoot.

Meantone temperament: A family of historical temperaments that flatten the fifths by some fraction of the syntonic comma to make most major thirds sound pure. 1/4-comma meantone (pure thirds) is the Renaissance default; 1/6-comma meantone is the milder late-Baroque variant.

Missing-fundamental effect: A psychoacoustic phenomenon in which the human brain perceives the pitch of a tone from its harmonic series alone, even when the actual fundamental frequency is absent. Used here to make bass voices audible through a small speaker that physically can't reproduce the fundamental.

MIDI note number: A standard integer-based pitch representation where 60 is middle C, 69 is A4 (440 Hz at standard tuning), and each integer step is one semitone. Used internally for all pitch logic.

PolyBLEP: Polynomial Band-Limited Step. A small correction added to naive oscillator waveforms (saw, square) at their discontinuity points to suppress aliasing. Cheap and effective; the implementation here is a 4-sample polynomial correction.

Pythagorean tuning: The oldest documented Western tuning system, built entirely on pure 3:2 fifths. The accumulation of pure fifths leaves an unresolved gap (the "Pythagorean comma") that concentrates as a single dissonant "wolf" interval in a remote key.

Sa: The tonic in Indian classical music; the first swara of the sargam scale and the absolute pitch reference against which every other swara is heard. Always tuned to a fixed Hz value of the player's choice, never to a moving Western chord centre.

Sargam: The Indian classical equivalent of solfège. The seven base swaras are Sa, Re, Ga, Ma, Pa, Dha, Ni, abbreviated S R G M P D N (with komal/tivra modifiers in Hindustani notation: S r R g G m M P d D n N).

SHF: Spectral Harmonic Fingerprint. SuperTuner's name for its third-line rescue detector, which probes upper harmonics via Goertzel and scores them for harmonic-series consistency.

SVF: State Variable Filter. A filter topology that gives access to lowpass, highpass, bandpass, and notch outputs from a single internal state. The implementation here uses Andrew Simper's TPT/ZDF (Topology-Preserving Transform / Zero-Delay Feedback) form, which preserves the filter's analogue response accurately at high cutoff frequencies near Nyquist.

Swara: A single note of the sargam scale; the Indian classical equivalent of a "note" in Western terminology. There are seven base swaras (Sa through Ni), most with komal/shuddha/tivra variants.

Syntonic comma: The interval (~21.5 cents) between a pure major third (5:4) and the major third built from four pure fifths (81:64). Historical temperaments differ mainly in how they distribute this comma across the circle of fifths.

Temperament: A system for distributing the comma across an octave's twelve notes. Equal temperament spreads it evenly; meantones favour pure thirds; well-temperaments leave some keys cleaner than others to give each key a distinct character.

Thaat: A parent scale in Hindustani classical music, analogous to a Western mode. There are ten canonical thaats (Bilaval, Kalyan, Khamaj, Bhairav, Bhairavi, Asavari, Kafi, Marwa, Poorvi, Todi), each defined by a specific selection of komal/shuddha/tivra swaras.

Vallotti: A mid-18th century well-temperament by Francesco Antonio Vallotti. Flattens six adjacent fifths by 1/6 syntonic comma each, leaves the other six pure. A popular modern choice for Baroque ensemble work.

vDSP: Apple's vectorised signal-processing library, part of the Accelerate framework. Used in SuperTuner's autocorrelation and difference-function calculations to take advantage of NEON SIMD.

Well-temperament: A class of historical temperaments (Werckmeister III, Kirnberger III, Vallotti, Young) in which every key is usable but each carries a distinct tonal character. Composers of the late 17th and 18th centuries, including Bach, wrote for instruments tuned this way.

Werckmeister III: A 1691 well-temperament by Andreas Werckmeister, the canonical "good temperament" of the Baroque era. Plays well in every key; simple keys ring closer to pure thirds, remote keys take on noticeable coloration.

YIN: A specific pitch-detection algorithm in the autocorrelation family, named after the Chinese yin-yang symbol because it pairs (yin = soft, sustained) and (yang = sharp, transient) extraction. Published by Cheveigné and Kawahara in 2002; widely used in music software.

Young: A 1799 well-temperament by Thomas Young, very close in spirit to Vallotti with tiny per-degree differences. Often interchangeable in practice.

This document is maintained by PeaceDrone LLC. Corrections and clarifications welcome.

↑ Back to top