Move toju-app into own its folder
This commit is contained in:
97
toju-app/src/app/domains/voice-connection/README.md
Normal file
97
toju-app/src/app/domains/voice-connection/README.md
Normal file
@@ -0,0 +1,97 @@
|
||||
# Voice Connection Domain
|
||||
|
||||
Bridges the application layer to the low-level realtime infrastructure for voice calls. Provides speaking detection via Web Audio analysis and per-peer volume control for playback. The actual WebRTC plumbing lives in `infrastructure/realtime`; this domain wraps it with a clean facade.
|
||||
|
||||
## Module map
|
||||
|
||||
```
|
||||
voice-connection/
|
||||
├── application/
|
||||
│ ├── voice-connection.facade.ts Proxy to RealtimeSessionFacade for voice signals and methods
|
||||
│ ├── voice-activity.service.ts RMS-based speaking detection via AnalyserNode (per-user signals)
|
||||
│ └── voice-playback.service.ts Per-peer GainNode chain, 0-200% volume, deafen support
|
||||
│
|
||||
├── domain/
|
||||
│ └── voice-connection.models.ts Re-exports LatencyProfile, VoiceStateSnapshot from shared-kernel / realtime
|
||||
│
|
||||
└── index.ts Barrel exports
|
||||
```
|
||||
|
||||
## Service relationships
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
VCF[VoiceConnectionFacade]
|
||||
VAS[VoiceActivityService]
|
||||
VPS[VoicePlaybackService]
|
||||
RSF[RealtimeSessionFacade]
|
||||
Models[voice-connection.models]
|
||||
|
||||
VCF --> RSF
|
||||
VAS --> VCF
|
||||
VPS --> VCF
|
||||
|
||||
click VCF "application/voice-connection.facade.ts" "Proxy to RealtimeSessionFacade" _blank
|
||||
click VAS "application/voice-activity.service.ts" "RMS-based speaking detection" _blank
|
||||
click VPS "application/voice-playback.service.ts" "Per-peer GainNode volume chain" _blank
|
||||
click RSF "../../infrastructure/realtime/realtime-session.service.ts" "Low-level WebRTC composition root" _blank
|
||||
click Models "domain/voice-connection.models.ts" "Re-exported types" _blank
|
||||
```
|
||||
|
||||
## Voice connection facade
|
||||
|
||||
`VoiceConnectionFacade` exposes signals and methods from `RealtimeSessionFacade` without leaking infrastructure details into feature components. It covers:
|
||||
|
||||
- Connection state: `isVoiceConnected`, `isMuted`, `isDeafened`, `hasConnectionError`
|
||||
- Stream access: `getRemoteVoiceStream`, `getLocalStream`, `getRawMicStream`
|
||||
- Controls: `enableVoice`, `disableVoice`, `toggleMute`, `toggleDeafen`, `toggleNoiseReduction`
|
||||
- Audio tuning: `setOutputVolume`, `setInputVolume`, `setAudioBitrate`, `setLatencyProfile`
|
||||
- Peer events: `onRemoteStream`, `onPeerConnected`, `onPeerDisconnected`
|
||||
- Heartbeat: `startVoiceHeartbeat`, `stopVoiceHeartbeat`
|
||||
|
||||
## Speaking detection
|
||||
|
||||
`VoiceActivityService` monitors audio levels for local and remote streams using the Web Audio API. Each tracked stream gets its own `AudioContext` with an `AnalyserNode`. A single `requestAnimationFrame` loop polls all analysers.
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
Stream[MediaStream] --> Ctx[AudioContext]
|
||||
Ctx --> Src[MediaStreamAudioSourceNode]
|
||||
Src --> Analyser[AnalyserNode<br/>fftSize = 256]
|
||||
Analyser --> Poll[rAF poll loop]
|
||||
Poll --> RMS{RMS >= 0.015?}
|
||||
RMS -- yes --> Speaking[speakingSignal = true]
|
||||
RMS -- no, 8 frames --> Silent[speakingSignal = false]
|
||||
|
||||
click Stream "application/voice-activity.service.ts" "VoiceActivityService.trackStream()" _blank
|
||||
click Poll "application/voice-activity.service.ts" "VoiceActivityService.poll()" _blank
|
||||
```
|
||||
|
||||
| Parameter | Value |
|
||||
|---|---|
|
||||
| FFT size | 256 samples |
|
||||
| Speaking threshold | RMS >= 0.015 |
|
||||
| Silent grace period | 8 consecutive frames below threshold |
|
||||
|
||||
The service exposes `isSpeaking(userId)` and `volume(userId)` as Angular signals. It automatically tracks remote peers via the `onRemoteStream` and `onPeerDisconnected` observables. Local mic tracking is started explicitly by calling `trackLocalMic(userId, stream)`.
|
||||
|
||||
A reactive `speakingMap` signal (a `Map<string, boolean>`) is published whenever any user's speaking state changes, so components can bind directly.
|
||||
|
||||
## Voice playback
|
||||
|
||||
`VoicePlaybackService` handles audio output for remote peers. Each peer gets an independent Web Audio pipeline:
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
Remote[Remote stream] --> Src[MediaStreamAudioSourceNode]
|
||||
Src --> Gain[GainNode<br/>0 - 200%]
|
||||
Gain --> Dest[MediaStreamAudioDestinationNode]
|
||||
Dest --> Audio[HTMLAudioElement<br/>.play]
|
||||
|
||||
click Remote "application/voice-playback.service.ts" "VoicePlaybackService.setupPeer()" _blank
|
||||
click Gain "application/voice-playback.service.ts" "VoicePlaybackService.setUserVolume()" _blank
|
||||
```
|
||||
|
||||
Volume per peer is stored in localStorage and restored on reconnect. The range is 0% to 200% (gain values 0.0 to 2.0). When the user deafens, all gain nodes are set to zero; undeafening restores the previous values.
|
||||
|
||||
A Chrome workaround attaches a muted `<audio>` element to keep the `AudioContext` from suspending when no audible output is detected.
|
||||
Reference in New Issue
Block a user