1. Caso de Estudio: Voice Volume Tracker para Windows
Proyecto: Detector de Volumen de Voz con Identificación de Hablante
Stack: C# + .NET 8, WPF, NAudio, ML.NET/ONNX, Windows Service
Duración: 12 semanas
Equipo: 1 desarrollador + 1 ML engineer
1.1 📋 Resumen Ejecutivo
Este caso de estudio documenta el desarrollo de una aplicación nativa de Windows que monitorea el volumen de voz del usuario en tiempo real, identifica su voz específica usando Machine Learning, y proporciona feedback visual cuando excede umbrales configurables según el momento del día.
1.1.1 Tecnologías Principales
- Frontend: WPF (Windows Presentation Foundation) + C# 12
- Backend Service: Windows Service + .NET 8
- Audio Processing: NAudio + MathNet.Numerics
- Machine Learning: ML.NET + ONNX Runtime (SpeechBrain embeddings)
- IPC: Named Pipes + SignalR
- Persistencia: SQLite + JSON config
- Alertas: WinUI 3 Notifications + DirectX Overlay
1.1.2 Métricas de Éxito
- ✅ North Star Metric: Tasa de Corrección de Volumen > 70%
- ✅ Latencia de Detección: < 200ms desde que se excede el umbral
- ✅ Precisión de Identificación: > 95% (solo alertar cuando habla el usuario)
- ✅ Tasa de Falsos Positivos: < 5%
- ✅ Consumo de CPU: < 3% en idle, < 8% durante habla
- ✅ Consumo de RAM: < 150 MB
1.2 🎯 1. PRD (Product Requirements Document)
1.2.1 1.1 Contexto y Job To Be Done (JTBD)
Cuando trabajo o convivo en un espacio compartido (oficina, hogar),
Lo contrato para monitorear mi volumen de voz y recibir feedback discreto en tiempo real,
Para así mantener el respeto por el entorno, mejorar mi etiqueta de comunicación, y evitar molestar a otros.
1.2.2 1.2 Requisitos Funcionales
| ID | Requisito | Prioridad | Complejidad |
|---|---|---|---|
| RF-01 | Monitoreo continuo de micrófono en segundo plano | 🔴 Alta | Alta |
| RF-02 | Cálculo de volumen en decibeles (dBFS) en tiempo real | 🔴 Alta | Media |
| RF-03 | Identificación de voz del usuario (Speaker Verification) | 🔴 Alta | Muy Alta |
| RF-04 | Configuración de umbrales de volumen (Día/Noche) | 🔴 Alta | Media |
| RF-05 | Modo Día y Modo Noche con transición automática | 🟠 Media | Media |
| RF-06 | Alerta visual cuando se excede el umbral | 🔴 Alta | Alta |
| RF-07 | Notificación Toast de Windows | 🟠 Media | Baja |
| RF-08 | Overlay de bordes rojos en pantalla | 🟠 Media | Alta |
| RF-09 | Fase de enrollment (registro de voz del usuario) | 🔴 Alta | Alta |
| RF-10 | Configuración de horarios Día/Noche personalizados | 🟢 Baja | Baja |
| RF-11 | Historial de alertas y estadísticas | 🟢 Baja | Media |
| RF-12 | Exportación de datos (CSV) | 🟢 Baja | Baja |
1.2.3 1.3 Requisitos No Funcionales
| ID | Requisito | Métrica | Criticidad |
|---|---|---|---|
| RNF-01 | Latencia: Detección y alerta | < 200ms | 🔴 Crítica |
| RNF-02 | Performance CPU: Uso en idle | < 3% | 🔴 Crítica |
| RNF-03 | Performance CPU: Uso durante habla | < 8% | 🟠 Alta |
| RNF-04 | Performance RAM: Consumo total | < 150 MB | 🟠 Alta |
| RNF-05 | Precisión ML: Identificación correcta | > 95% | 🔴 Crítica |
| RNF-06 | Falsos Positivos: Alertas incorrectas | < 5% | 🔴 Crítica |
| RNF-07 | Disponibilidad: Uptime del servicio | > 99.9% | 🟠 Alta |
| RNF-08 | Seguridad: No almacenar audio crudo | 100% | 🔴 Crítica |
| RNF-09 | Privacidad: Encriptación de embeddings | 100% | 🔴 Crítica |
| RNF-10 | Startup: Tiempo de inicio del servicio | < 3s | 🟢 Media |
1.2.4 1.4 Requisitos de Seguridad y Privacidad
| Requisito | Justificación | Implementación |
|---|---|---|
| No almacenar audio crudo | El audio es PII sensible | Solo guardar embeddings vectoriales |
| Encriptación de embeddings | Proteger identidad vocal | AES-256 en reposo |
| Logs sin PII | Cumplimiento GDPR | Solo registrar timestamps y niveles de dB |
| Permisos de micrófono | Seguridad de Windows | Solicitar permisos explícitos |
| Least Privilege | Minimizar superficie de ataque | Servicio con permisos mínimos |
1.3 🏗️ 2. Decisiones de Arquitectura
1.3.1 2.1 ¿Por qué C# + .NET sobre Python/Electron?
Decisión: Usar C# + .NET 8 nativo
Alternativas Consideradas:
- Python + PyQt/Tkinter
- Electron + Node.js
- C++ nativo
- C# + .NET 8
Justificación:
| Criterio | Python | Electron | C++ | C# + .NET | Ganador |
|---|---|---|---|---|---|
| Performance | ❌ Lento | ❌ Alto overhead | ✅ Excelente | ✅ Muy bueno | C++/C# |
| Acceso bajo nivel (audio) | ⚠️ Limitado | ❌ Difícil | ✅ Total | ✅ Bueno (P/Invoke) | C++/C# |
| Windows Service | ❌ Difícil | ❌ No nativo | ✅ Nativo | ✅ Nativo | C++/C# |
| Ecosistema ML | ✅ Excelente | ⚠️ Limitado | ⚠️ Complejo | ✅ ML.NET + ONNX | Python/C# |
| Curva de aprendizaje | ✅ Baja | ✅ Baja | ❌ Alta | ⚠️ Media | Python/Electron |
| Consumo de RAM | ✅ Bajo | ❌ Alto (>200MB) | ✅ Muy bajo | ✅ Bajo | Python/C++/C# |
| UI nativa Windows | ❌ No | ❌ No | ✅ Sí | ✅ WPF/WinUI | C++/C# |
Resultado: C# + .NET gana 5 a 3
Contexto Específico:
- Necesitamos performance para procesamiento de audio en tiempo real
- Necesitamos acceso de bajo nivel al micrófono de Windows
- Necesitamos crear un Windows Service robusto
- C# ofrece ML.NET + ONNX para inferencia eficiente
- WPF proporciona UI nativa y moderna
Trade-offs:
- ✅ Pro: Excelente performance, acceso nativo a Windows APIs, bajo consumo
- ❌ Contra: Curva de aprendizaje de C# y .NET, menos flexible que Python para ML
1.3.2 2.2 ¿Por qué NAudio para el manejo de Audio?
Decisión: Usar la librería open-source NAudio.
Justificación:
- Madurez: Es el estándar de facto en .NET para audio desde hace >10 años.
- Bajo Nivel: Permite acceso directo a WASAPI (Windows Audio Session API), lo cual es crítico para reducir la latencia a milisegundos.
- Flexibilidad: Soporta captura de audio en buffers de bytes que podemos convertir a floats para el modelo de ML sin overhead innecesario.
Trade-offs:
- ✅ Pro: Control total sobre el dispositivo de entrada y el sample rate (16kHz requerido por ML).
- ❌ Contra: La documentación oficial es escasa; requiere entender conceptos de DSP (Digital Signal Processing).
1.3.3 2.3 Arquitectura Hexagonal (Ports & Adapters)
Decisión: Implementar Arquitectura Hexagonal con separación clara entre UI, Servicio y Core
Estructura del Proyecto:
VoiceVolumeTracker/
├── VoiceTracker.Domain/ # CORE (Lógica de Negocio)
│ ├── Entities/
│ │ ├── VoiceProfile.cs # Perfil de voz del usuario
│ │ ├── VolumeReading.cs # Lectura de volumen
│ │ ├── AlertEvent.cs # Evento de alerta
│ │ └── AppConfig.cs # Configuración
│ ├── ValueObjects/
│ │ ├── Decibel.cs # Value object para dB
│ │ ├── VoiceEmbedding.cs # Embedding de voz
│ │ └── TimeRange.cs # Rango horario (Día/Noche)
│ ├── Repositories/
│ │ ├── IVoiceProfileRepository.cs
│ │ ├── IVolumeHistoryRepository.cs
│ │ └── IConfigRepository.cs
│ ├── Services/
│ │ ├── IAudioProcessor.cs # Interface para procesamiento
│ │ ├── ISpeakerVerifier.cs # Interface para ML
│ │ └── IAlertService.cs # Interface para alertas
│ └── UseCases/
│ ├── EnrollVoiceUseCase.cs # Registrar voz del usuario
│ ├── MonitorVolumeUseCase.cs # Monitorear volumen
│ └── ConfigureThresholdsUseCase.cs
│
├── VoiceTracker.Infrastructure/ # ADAPTERS (Implementaciones)
│ ├── Audio/
│ │ ├── NAudioProcessor.cs # Procesamiento con NAudio
│ │ ├── AudioCapture.cs # Captura de micrófono
│ │ └── DecibelCalculator.cs # Cálculo de dBFS
│ ├── ML/
│ │ ├── ONNXSpeakerVerifier.cs # Verificación con ONNX
│ │ ├── EmbeddingExtractor.cs # Extracción de embeddings
│ │ └── Models/
│ │ └── speechbrain_ecapa.onnx
│ ├── Persistence/
│ │ ├── SQLiteVoiceProfileRepository.cs
│ │ ├── SQLiteVolumeHistoryRepository.cs
│ │ └── JsonConfigRepository.cs
│ ├── Alerts/
│ │ ├── ToastNotificationService.cs
│ │ └── ScreenOverlayService.cs # DirectX overlay
│ └── IPC/
│ └── NamedPipeServer.cs # Comunicación con UI
│
├── VoiceTracker.Service/ # Windows Service
│ ├── VoiceMonitorService.cs # Servicio principal
│ ├── FiniteStateMachine.cs # FSM Día/Noche
│ └── BackgroundWorker.cs # Worker en segundo plano
│
├── VoiceTracker.UI/ # WPF Application
│ ├── Views/
│ │ ├── MainWindow.xaml # Ventana principal
│ │ ├── EnrollmentWindow.xaml # Wizard de enrollment
│ │ ├── ConfigWindow.xaml # Configuración
│ │ └── StatsWindow.xaml # Estadísticas
│ ├── ViewModels/
│ │ ├── MainViewModel.cs
│ │ ├── EnrollmentViewModel.cs
│ │ └── ConfigViewModel.cs
│ └── Services/
│ └── NamedPipeClient.cs # Cliente IPC
│
└── VoiceTracker.Tests/
├── Unit/
├── Integration/
└── Performance/
Beneficios:
- ✅ Separación de responsabilidades: UI, Servicio y Core independientes
- ✅ Testabilidad: Core sin dependencias de infraestructura
- ✅ Mantenibilidad: Cambiar NAudio por otra librería no afecta el Core
- ✅ Escalabilidad: Fácil agregar nuevos adaptadores (ej: alertas por email)
1.3.4 2.4 Finite State Machine (FSM) para Modos Día/Noche
Decisión: Implementar FSM para gestionar estados y transiciones
Estados:
- Idle (Silencio detectado)
- Listening (Detectando audio)
- Speaking (Usuario hablando)
- AlertActive (Alerta mostrada)
- DayMode (Modo Día)
- NightMode (Modo Noche)
Transiciones:
Idle → Listening: Audio detectado
Listening → Speaking: Voz del usuario identificada
Speaking → AlertActive: Volumen > Umbral
AlertActive → Speaking: Volumen < Umbral
Speaking → Idle: Silencio detectado
DayMode ↔ NightMode: Cambio de horario configurado
Implementación:
// VoiceTracker.Service/FiniteStateMachine.cs
public enum SystemState
{
Idle,
Listening,
Speaking,
AlertActive
}
public enum TimeMode
{
Day,
Night
}
public class VoiceTrackerFSM
{
private SystemState _currentState = SystemState.Idle;
private TimeMode _currentMode = TimeMode.Day;
private readonly AppConfig _config;
private readonly IAlertService _alertService;
public SystemState CurrentState => _currentState;
public TimeMode CurrentMode => _currentMode;
public void ProcessAudioFrame(AudioFrame frame, bool isUserVoice, Decibel volume)
{
// Actualizar modo según hora
UpdateTimeMode();
// Transiciones de estado
switch (_currentState)
{
case SystemState.Idle:
if (frame.HasAudio)
TransitionTo(SystemState.Listening);
break;
case SystemState.Listening:
if (isUserVoice)
TransitionTo(SystemState.Speaking);
else if (!frame.HasAudio)
TransitionTo(SystemState.Idle);
break;
case SystemState.Speaking:
var threshold = GetCurrentThreshold();
if (volume > threshold)
TransitionTo(SystemState.AlertActive);
else if (!isUserVoice)
TransitionTo(SystemState.Listening);
break;
case SystemState.AlertActive:
var currentThreshold = GetCurrentThreshold();
if (volume <= currentThreshold)
TransitionTo(SystemState.Speaking);
break;
}
}
private void TransitionTo(SystemState newState)
{
var oldState = _currentState;
_currentState = newState;
OnStateChanged(oldState, newState);
}
private void OnStateChanged(SystemState from, SystemState to)
{
if (to == SystemState.AlertActive)
{
_alertService.ShowAlert(GetCurrentThreshold());
}
else if (from == SystemState.AlertActive && to != SystemState.AlertActive)
{
_alertService.HideAlert();
}
}
private void UpdateTimeMode()
{
var currentHour = DateTime.Now.Hour;
var newMode = currentHour >= _config.NightModeStartHour ||
currentHour < _config.DayModeStartHour
? TimeMode.Night
: TimeMode.Day;
if (newMode != _currentMode)
{
_currentMode = newMode;
OnTimeModeChanged(newMode);
}
}
private Decibel GetCurrentThreshold()
{
return _currentMode == TimeMode.Day
? _config.DayThreshold
: _config.NightThreshold;
}
}
Referencia: Arquitectura y Patrones
1.4 🎤 3. Procesamiento de Audio en Tiempo Real
1.4.1 3.1 Captura de Micrófono con NAudio
Decisión: Usar NAudio para captura de audio de bajo nivel
Implementación:
// VoiceTracker.Infrastructure/Audio/AudioCapture.cs
using NAudio.Wave;
public class AudioCapture : IAudioCapture
{
private WaveInEvent _waveIn;
private readonly int _sampleRate = 16000; // 16 kHz
private readonly int _channels = 1; // Mono
private readonly int _bitsPerSample = 16;
public event EventHandler<AudioFrameEventArgs> AudioFrameReady;
public void Start()
{
_waveIn = new WaveInEvent
{
WaveFormat = new WaveFormat(_sampleRate, _bitsPerSample, _channels),
BufferMilliseconds = 100 // 100ms buffer
};
_waveIn.DataAvailable += OnDataAvailable;
_waveIn.StartRecording();
}
private void OnDataAvailable(object sender, WaveInEventArgs e)
{
// Convertir bytes a floats
var samples = new float[e.BytesRecorded / 2]; // bit = 2 bytes
for (int i = 0; i < samples.Length; i++)
{
short sample = BitConverter.ToInt16(e.Buffer, i * 2);
samples[i] = sample / 32768f; // Normalizar a [-1, 1]
}
var frame = new AudioFrame
{
Samples = samples,
SampleRate = _sampleRate,
Timestamp = DateTime.UtcNow
};
AudioFrameReady?.Invoke(this, new AudioFrameEventArgs(frame));
}
public void Stop()
{
_waveIn?.StopRecording();
_waveIn?.Dispose();
}
}
1.4.2 3.2 Cálculo de Decibeles (dBFS)
Decisión: Calcular dBFS (decibels relative to Full Scale)
Fórmula:
RMS = sqrt(mean(samples^2))
dBFS = 20 * log10(RMS)
Implementación:
// VoiceTracker.Infrastructure/Audio/DecibelCalculator.cs
using System;
using System.Linq;
public class DecibelCalculator
{
public Decibel Calculate(float[] samples)
{
if (samples == null || samples.Length == 0)
return Decibel.Silence;
// Calcular RMS (Root Mean Square)
var sumOfSquares = samples.Select(s => s * s).Sum();
var rms = Math.Sqrt(sumOfSquares / samples.Length);
// Evitar log(0)
if (rms < 1e-10)
return Decibel.Silence;
// Calcular dBFS
var dbfs = 20 * Math.Log10(rms);
return new Decibel(dbfs);
}
public bool HasVoiceActivity(float[] samples, double threshold = -40.0)
{
var db = Calculate(samples);
return db.Value > threshold;
}
}
// VoiceTracker.Domain/ValueObjects/Decibel.cs
public record Decibel
{
public double Value { get; init; }
public static Decibel Silence => new Decibel(-96.0); // Silencio digital
public Decibel(double value)
{
if (value > 0)
throw new ArgumentException("dBFS must be <= 0");
Value = value;
}
public static bool operator >(Decibel a, Decibel b) => a.Value > b.Value;
public static bool operator <(Decibel a, Decibel b) => a.Value < b.Value;
}
1.4.3 3.3 Voice Activity Detection (VAD)
Decisión: Implementar VAD para filtrar silencio y ruido de fondo
Implementación:
// VoiceTracker.Infrastructure/Audio/VoiceActivityDetector.cs
public class VoiceActivityDetector
{
private readonly double _energyThreshold = -40.0; // dBFS
private readonly double _zeroCrossingThreshold = 0.3;
public bool DetectVoice(float[] samples)
{
// Criterio 1: Energía suficiente
var energy = CalculateEnergy(samples);
if (energy < _energyThreshold)
return false;
// Criterio 2: Zero-Crossing Rate (detecta habla vs ruido)
var zcr = CalculateZeroCrossingRate(samples);
if (zcr < _zeroCrossingThreshold)
return false;
return true;
}
private double CalculateEnergy(float[] samples)
{
var calculator = new DecibelCalculator();
return calculator.Calculate(samples).Value;
}
private double CalculateZeroCrossingRate(float[] samples)
{
int crossings = 0;
for (int i = 1; i < samples.Length; i++)
{
if ((samples[i] >= 0 && samples[i - 1] < 0) ||
(samples[i] < 0 && samples[i - 1] >= 0))
{
crossings++;
}
}
return (double)crossings / samples.Length;
}
}
Referencia: Optimización de Performance
1. ⬆️ Volver arriba | ➡️ Parte 2 | 🏠 Volver a Casos de Estudio