skills/voice-audio-engineer/references/implementations.md

12 KiB
Raw Blame History

Voice Audio Implementation Reference

Detailed code implementations for voice processing, loudness measurement, and speech analysis.

Biquad Filter (Audio EQ Cookbook)

import numpy as np
from scipy import signal

class AudioFilters:
    """Production-ready filter implementations."""

    @staticmethod
    def biquad_coefficients(filter_type: str, fc: float, fs: float,
                           Q: float = 0.707, gain_db: float = 0) -> tuple:
        """
        Calculate biquad filter coefficients.
        Uses Audio EQ Cookbook formulas (Robert Bristow-Johnson)
        """
        A = 10 ** (gain_db / 40)
        w0 = 2 * np.pi * fc / fs
        cos_w0 = np.cos(w0)
        sin_w0 = np.sin(w0)
        alpha = sin_w0 / (2 * Q)

        if filter_type == 'lowpass':
            b0 = (1 - cos_w0) / 2
            b1 = 1 - cos_w0
            b2 = (1 - cos_w0) / 2
            a0 = 1 + alpha
            a1 = -2 * cos_w0
            a2 = 1 - alpha

        elif filter_type == 'highpass':
            b0 = (1 + cos_w0) / 2
            b1 = -(1 + cos_w0)
            b2 = (1 + cos_w0) / 2
            a0 = 1 + alpha
            a1 = -2 * cos_w0
            a2 = 1 - alpha

        elif filter_type == 'peaking':
            b0 = 1 + alpha * A
            b1 = -2 * cos_w0
            b2 = 1 - alpha * A
            a0 = 1 + alpha / A
            a1 = -2 * cos_w0
            a2 = 1 - alpha / A

        elif filter_type == 'highshelf':
            b0 = A * ((A + 1) + (A - 1) * cos_w0 + 2 * np.sqrt(A) * alpha)
            b1 = -2 * A * ((A - 1) + (A + 1) * cos_w0)
            b2 = A * ((A + 1) + (A - 1) * cos_w0 - 2 * np.sqrt(A) * alpha)
            a0 = (A + 1) - (A - 1) * cos_w0 + 2 * np.sqrt(A) * alpha
            a1 = 2 * ((A - 1) - (A + 1) * cos_w0)
            a2 = (A + 1) - (A - 1) * cos_w0 - 2 * np.sqrt(A) * alpha

        # Normalize
        b = np.array([b0/a0, b1/a0, b2/a0])
        a = np.array([1, a1/a0, a2/a0])
        return b, a

LUFS Loudness Measurement

import numpy as np
from scipy import signal

def measure_lufs(audio: np.ndarray, fs: int) -> float:
    """
    Measure integrated loudness per ITU-R BS.1770-4.
    """
    # Stage 1: K-weighting filter
    b1, a1 = signal.butter(2, 1500 / (fs/2), btype='high')
    b2, a2 = signal.butter(2, 38 / (fs/2), btype='high')

    filtered = signal.lfilter(b1, a1, audio)
    filtered = signal.lfilter(b2, a2, filtered)

    # Stage 2: Mean square with gating
    block_size = int(0.4 * fs)  # 400ms blocks
    hop_size = int(0.1 * fs)    # 100ms overlap

    block_loudness = []
    for i in range(0, len(filtered) - block_size, hop_size):
        block = filtered[i:i+block_size]
        mean_square = np.mean(block ** 2)
        block_loudness.append(-0.691 + 10 * np.log10(mean_square + 1e-10))

    # Absolute threshold gate (-70 LUFS)
    gated = [l for l in block_loudness if l > -70]
    if not gated:
        return -70.0

    # Relative threshold gate (-10 LU below ungated mean)
    ungated_mean = np.mean(gated)
    relative_threshold = ungated_mean - 10
    final_gated = [l for l in gated if l > relative_threshold]

    return np.mean(final_gated) if final_gated else ungated_mean

Compressor Implementation

import numpy as np

class Compressor:
    """Production-quality dynamics compressor."""

    def __init__(self, fs: int):
        self.fs = fs
        self.envelope = 0.0

    def process(self, audio: np.ndarray,
                threshold_db: float = -20,
                ratio: float = 4.0,
                attack_ms: float = 10,
                release_ms: float = 100,
                knee_db: float = 6,
                makeup_db: float = 0) -> np.ndarray:
        """Apply compression to audio signal."""
        attack_coef = np.exp(-1 / (self.fs * attack_ms / 1000))
        release_coef = np.exp(-1 / (self.fs * release_ms / 1000))

        output = np.zeros_like(audio)

        for i in range(len(audio)):
            input_level = 20 * np.log10(abs(audio[i]) + 1e-10)

            # Envelope follower
            if input_level > self.envelope:
                self.envelope = attack_coef * self.envelope + (1 - attack_coef) * input_level
            else:
                self.envelope = release_coef * self.envelope + (1 - release_coef) * input_level

            # Gain computer with soft knee
            over_threshold = self.envelope - threshold_db

            if knee_db > 0 and over_threshold > -knee_db/2 and over_threshold < knee_db/2:
                knee_factor = (over_threshold + knee_db/2) ** 2 / (2 * knee_db)
                gain_db = -knee_factor * (1 - 1/ratio)
            elif over_threshold >= knee_db/2:
                gain_db = -(over_threshold - knee_db/2) * (1 - 1/ratio) - (knee_db/2) * (1 - 1/ratio)
            else:
                gain_db = 0

            gain_linear = 10 ** ((gain_db + makeup_db) / 20)
            output[i] = audio[i] * gain_linear

        return output

De-esser Implementation

import numpy as np
from scipy import signal

class DeEsser:
    """Frequency-selective de-esser for sibilance control."""

    def __init__(self, fs: int):
        self.fs = fs

    def process(self, audio: np.ndarray,
                frequency: float = 6000,
                threshold_db: float = -20,
                reduction_db: float = 6,
                q: float = 2.0) -> np.ndarray:
        """
        Reduce sibilance in voice recordings.

        Args:
            frequency: Center frequency for sibilance detection (5-8kHz typical)
            threshold_db: Level above which de-essing activates
            reduction_db: Maximum gain reduction
            q: Bandwidth (higher = narrower)
        """
        # Create bandpass to detect sibilance
        nyq = self.fs / 2
        low = (frequency - frequency/q) / nyq
        high = (frequency + frequency/q) / nyq
        b, a = signal.butter(2, [low, high], btype='band')

        # Detect sibilance envelope
        sibilance = signal.lfilter(b, a, audio)
        envelope = np.abs(sibilance)

        # Smooth envelope
        smooth_coef = 0.99
        smoothed = np.zeros_like(envelope)
        smoothed[0] = envelope[0]
        for i in range(1, len(envelope)):
            smoothed[i] = smooth_coef * smoothed[i-1] + (1 - smooth_coef) * envelope[i]

        # Calculate gain reduction
        threshold_linear = 10 ** (threshold_db / 20)
        reduction_linear = 10 ** (-reduction_db / 20)

        output = audio.copy()
        for i in range(len(audio)):
            if smoothed[i] > threshold_linear:
                gain = 1.0 - (1.0 - reduction_linear) * (smoothed[i] - threshold_linear) / smoothed[i]
                output[i] *= gain

        return output

Voice Activity Detection (VAD)

import numpy as np

class VoiceActivityDetector:
    """Simple energy-based VAD for voice detection."""

    def __init__(self, fs: int, frame_ms: float = 20):
        self.fs = fs
        self.frame_size = int(fs * frame_ms / 1000)

    def detect(self, audio: np.ndarray,
               energy_threshold_db: float = -40,
               min_speech_ms: float = 100) -> list[tuple[int, int]]:
        """
        Detect speech segments in audio.

        Returns: List of (start_sample, end_sample) tuples
        """
        num_frames = len(audio) // self.frame_size
        is_speech = np.zeros(num_frames, dtype=bool)

        for i in range(num_frames):
            frame = audio[i * self.frame_size:(i + 1) * self.frame_size]
            energy_db = 20 * np.log10(np.sqrt(np.mean(frame ** 2)) + 1e-10)
            is_speech[i] = energy_db > energy_threshold_db

        # Merge short gaps, remove short segments
        min_frames = int(min_speech_ms / (self.frame_size / self.fs * 1000))

        # Simple hangover
        for i in range(1, len(is_speech) - 1):
            if is_speech[i-1] and is_speech[i+1]:
                is_speech[i] = True

        # Extract segments
        segments = []
        in_segment = False
        start = 0

        for i, speech in enumerate(is_speech):
            if speech and not in_segment:
                start = i * self.frame_size
                in_segment = True
            elif not speech and in_segment:
                end = i * self.frame_size
                if (end - start) / self.fs * 1000 >= min_speech_ms:
                    segments.append((start, end))
                in_segment = False

        if in_segment:
            segments.append((start, len(audio)))

        return segments

Audio Analysis Report Generator

import numpy as np
from scipy.fft import rfft, rfftfreq

def analyze_voice_audio(audio: np.ndarray, fs: int) -> dict:
    """Comprehensive voice audio analysis."""

    # Mono for analysis
    if len(audio.shape) > 1:
        mono = np.mean(audio, axis=1)
    else:
        mono = audio

    # Level measurements
    peak_db = 20 * np.log10(np.max(np.abs(mono)) + 1e-10)
    rms_db = 20 * np.log10(np.sqrt(np.mean(mono**2)) + 1e-10)
    crest_factor = peak_db - rms_db
    lufs = measure_lufs(mono, fs)
    dc_offset = np.mean(mono)

    # Spectral analysis
    spectrum = np.abs(rfft(mono))
    freqs = rfftfreq(len(mono), 1/fs)
    spectral_centroid = np.sum(freqs * spectrum) / np.sum(spectrum)

    # Voice-specific metrics
    # Fundamental frequency estimation (simple autocorrelation)
    autocorr = np.correlate(mono[:4096], mono[:4096], mode='full')
    autocorr = autocorr[len(autocorr)//2:]
    # Find first peak after initial decay
    min_lag = int(fs / 500)  # 500Hz max
    max_lag = int(fs / 50)   # 50Hz min
    peak_lag = np.argmax(autocorr[min_lag:max_lag]) + min_lag
    f0_estimate = fs / peak_lag if peak_lag > 0 else 0

    return {
        'peak_db': peak_db,
        'rms_db': rms_db,
        'crest_factor': crest_factor,
        'lufs': lufs,
        'dc_offset': dc_offset,
        'spectral_centroid': spectral_centroid,
        'f0_estimate': f0_estimate,
        'duration_seconds': len(mono) / fs,
        'sample_rate': fs
    }

def generate_recommendations(analysis: dict) -> list[str]:
    """Generate processing recommendations from analysis."""
    recs = []

    if analysis['peak_db'] > -0.5:
        recs.append("Peaks near 0dBFS - risk of clipping; add limiter")
    if analysis['lufs'] > -14:
        recs.append("Too loud for streaming (-14 LUFS target)")
    if analysis['lufs'] < -20:
        recs.append("Consider increasing overall level")
    if analysis['crest_factor'] < 6:
        recs.append("Low crest factor - may sound over-compressed")
    if abs(analysis['dc_offset']) > 0.01:
        recs.append("DC offset detected - apply high-pass filter at 80Hz")
    if analysis['spectral_centroid'] < 1500:
        recs.append("Voice sounds muddy - consider high shelf boost at 3kHz")
    if analysis['spectral_centroid'] > 4000:
        recs.append("Voice sounds harsh - consider reducing 2-4kHz")

    return recs if recs else ["Audio looks good!"]

Loudness Standards Reference

LOUDNESS UNITS (ITU-R BS.1770)

LUFS (Loudness Units Full Scale)
├── Integrated: Average loudness over entire program
├── Short-term: 3-second sliding window
├── Momentary: 400ms sliding window
└── True Peak: Maximum sample value with intersample peaks

DELIVERY STANDARDS
├── Streaming (Spotify, Apple): -14 LUFS, -1 dBTP
├── Broadcast (EBU R128): -23 LUFS ±1, -1 dBTP
├── Broadcast (ATSC A/85): -24 LKFS ±2, -2 dBTP
├── Podcast: -16 to -19 LUFS (dialogue norm)
├── YouTube: -14 LUFS (normalized)
└── Audiobook (ACX): -18 to -23 dBFS RMS, -3 dBFS peak

LOUDNESS RANGE (LRA)
├── Classical: 15-20 LU
├── Film: 10-15 LU
├── Pop music: 5-8 LU
└── Broadcast speech: 3-6 LU

Digital Audio Theory Reference

SAMPLE RATES
├── 44.1kHz: CD standard, captures up to 22.05kHz
├── 48kHz: Video standard (cleaner for frame sync)
├── 96kHz: High-resolution, better for processing headroom
└── Why 44.1kHz? Derived from video: 44100 = 3×3×5×5×7×7×2

BIT DEPTH → DYNAMIC RANGE
├── Dynamic Range (dB) ≈ 6.02 × bits + 1.76
├── 16-bit: ~96 dB (CD quality)
├── 24-bit: ~144 dB (professional)
└── 32-bit float: ~1528 dB (effectively infinite)

DITHERING
├── Required when reducing bit depth (24→16)
├── TPDF (triangular): Standard, mathematically optimal
└── Shaped: Noise pushed above hearing range

Key References

  • ITU-R BS.1770: "Algorithms to measure audio programme loudness"
  • EBU R128: "Loudness normalisation and permitted maximum level"
  • AES-6id: "Personal Sound Exposure"
  • Bristow-Johnson, R.: "Audio EQ Cookbook" (filter formulas)
  • Blauert, J. (1997): Spatial Hearing (MIT Press)