6.6 KiB
6.6 KiB
Tuner App - Architecture Documentation
Clean Architecture Diagram
graph TB
subgraph "Presentation Layer"
UI[TunerInterface Component]
Hooks[Custom Hooks<br/>useTuner, useAudioCapture, etc.]
Components[Dumb Components<br/>Button, NoteDisplay, etc.]
end
subgraph "Domain Layer - Pure Business Logic"
Types[types.ts<br/>Core Type Definitions]
NoteConverter[note-converter.ts<br/>Frequency ↔ Note]
TuningCalc[tuning-calculator.ts<br/>Sharp/Flat/InTune Logic]
Instruments[instruments.ts<br/>Instrument Configs]
end
subgraph "Infrastructure Layer - External Services"
AudioCapture[AudioCaptureService<br/>Web Audio API Wrapper]
PitchDetector[PitchDetector<br/>Autocorrelation Algorithm]
Errors[Custom Error Types]
end
subgraph "External APIs"
WebAudio[Web Audio API]
Microphone[Device Microphone]
end
UI --> Hooks
Hooks --> Components
Hooks --> TuningCalc
Hooks --> NoteConverter
Hooks --> Instruments
Hooks --> AudioCapture
Hooks --> PitchDetector
AudioCapture --> WebAudio
WebAudio --> Microphone
TuningCalc --> NoteConverter
TuningCalc --> Types
NoteConverter --> Types
Instruments --> Types
PitchDetector --> Errors
AudioCapture --> Errors
style UI fill:#00bfff,stroke:#0080ff,stroke-width:2px
style Hooks fill:#87ceeb,stroke:#4682b4,stroke-width:2px
style Components fill:#b0e0e6,stroke:#4682b4,stroke-width:2px
style Types fill:#98fb98,stroke:#228b22,stroke-width:2px
style NoteConverter fill:#98fb98,stroke:#228b22,stroke-width:2px
style TuningCalc fill:#98fb98,stroke:#228b22,stroke-width:2px
style Instruments fill:#98fb98,stroke:#228b22,stroke-width:2px
style AudioCapture fill:#ffd700,stroke:#ff8c00,stroke-width:2px
style PitchDetector fill:#ffd700,stroke:#ff8c00,stroke-width:2px
style Errors fill:#ffd700,stroke:#ff8c00,stroke-width:2px
Dependency Flow
The architecture strictly enforces the dependency rule:
Presentation → Domain ← Infrastructure
↓ ↓
→ Infrastructure → External APIs
Key Rules
- Domain has zero dependencies - Pure TypeScript, no React, no external libraries
- Infrastructure depends on external APIs - Web Audio API, browser APIs
- Presentation depends on both - Uses hooks to orchestrate domain + infrastructure
- Components are pure - Only receive props, no business logic
Data Flow Example
User clicks "Start Tuning":
sequenceDiagram
participant User
participant TunerInterface
participant useTuner
participant AudioCapture
participant PitchDetector
participant NoteConverter
participant TuningCalc
User->>TunerInterface: Click "Start"
TunerInterface->>useTuner: start()
useTuner->>AudioCapture: start()
AudioCapture->>Browser: Request mic permission
Browser-->>User: Permission dialog
User-->>Browser: Grant permission
Browser-->>AudioCapture: MediaStream
loop Real-time processing
AudioCapture->>useTuner: Audio samples
useTuner->>PitchDetector: detectPitch(samples)
PitchDetector-->>useTuner: frequency (Hz)
useTuner->>NoteConverter: frequencyToNote(Hz)
NoteConverter-->>useTuner: note {name, octave}
useTuner->>TuningCalc: calculateTuningState()
TuningCalc-->>useTuner: {status, cents, accuracy}
useTuner-->>TunerInterface: Update state
TunerInterface-->>User: Display note & meter
end
Component Composition
The UI is built through composition of small, focused components:
graph TD
TunerInterface[TunerInterface]
TunerInterface --> InstrumentSelector
TunerInterface --> ErrorMessage
TunerInterface --> DisplayPanel[Display Panel]
TunerInterface --> Button
TunerInterface --> StatusIndicator
DisplayPanel --> NoteDisplay
DisplayPanel --> FrequencyDisplay
DisplayPanel --> TuningMeter
style TunerInterface fill:#00bfff,stroke:#0080ff,stroke-width:3px
style InstrumentSelector fill:#87ceeb,stroke:#4682b4
style ErrorMessage fill:#87ceeb,stroke:#4682b4
style DisplayPanel fill:#87ceeb,stroke:#4682b4
style Button fill:#87ceeb,stroke:#4682b4
style StatusIndicator fill:#87ceeb,stroke:#4682b4
style NoteDisplay fill:#b0e0e6,stroke:#4682b4
style FrequencyDisplay fill:#b0e0e6,stroke:#4682b4
style TuningMeter fill:#b0e0e6,stroke:#4682b4
File Organization
src/
├── domain/ # 🟢 Pure Logic (Green)
│ ├── types.ts # Type definitions
│ ├── note-converter.ts # Mathematical conversions
│ ├── tuning-calculator.ts # Tuning logic
│ └── instruments.ts # Configuration data
│
├── infrastructure/ # 🟡 External Services (Gold)
│ ├── audio-capture.ts # Microphone access
│ ├── pitch-detector.ts # Signal processing
│ └── audio-errors.ts # Error handling
│
├── presentation/ # 🔵 UI Layer (Blue)
│ ├── hooks/ # Smart - contain logic
│ │ ├── useAudioCapture.ts
│ │ ├── usePitchDetection.ts
│ │ ├── useInstrument.ts
│ │ └── useTuner.ts
│ │
│ └── components/ # Dumb - only presentation
│ ├── Button.tsx
│ ├── FrequencyDisplay.tsx
│ ├── NoteDisplay.tsx
│ ├── TuningMeter.tsx
│ ├── InstrumentSelector.tsx
│ ├── StatusIndicator.tsx
│ ├── ErrorMessage.tsx
│ └── TunerInterface.tsx
│
├── styles/ # Design system
│ └── components.css
│
├── index.css # Frutiger Aero design tokens
├── App.tsx # Root component
└── main.tsx # Entry point
Benefits of This Architecture
Testability
- Domain logic can be unit tested without React
- Infrastructure can be mocked for testing
- Components can be tested in isolation
Maintainability
- Clear separation makes changes easier
- Each layer has a single responsibility
- Dependencies flow in one direction
Scalability
- Easy to add new instruments (just update domain)
- Can swap pitch detection algorithms (infrastructure)
- Can redesign UI without touching logic (presentation)
Reusability
- Domain logic could be used in a mobile app
- Components can be used in other projects
- Hooks encapsulate reusable behavior