Advanced Topics
Choosing a time-stretch backend
AudioSig has three explicit time-stretch backends. The basic phase vocoder is a
generic numerical reference path; it uses STFT/ISTFT phase propagation and can
sound phasey around transients. WSOLA is the speech-oriented default for
apply_speech_effects and searches for waveform-similar splice points in the
time domain. ESOLA is an experimental speech backend that aligns fixed
waveform frames using detected epochs; it supports 0.5 <= rate <= 2.0 and
requires sample_rate. WSOLA, ESOLA, and TD-PSOLA process every non-sample
dimension as an independent lane, so they do not guarantee a stereo image or
shared pitch/epoch decisions. For stereo speech where image coherence matters,
provide a shared analysis signal and apply its decisions in an application-level
channel wrapper; ordinary batch and arbitrary-axis processing remains supported.
Use WSOLA when processing speech:
from audiosig import apply_speech_effects, time_stretch
stretched = time_stretch(
audio,
rate=1.2,
sample_rate=24_000,
method="wsola",
)
speech = apply_speech_effects(audio, sample_rate=24_000, rate=1.2)
Use ESOLA for an experimental moderate-range speech comparison:
stretched = time_stretch(
audio,
rate=1.25,
sample_rate=24_000,
method="esola",
)
Use the basic phase vocoder for generic array processing or controlled comparisons:
from audiosig import time_stretch
# Control FFT resolution
result = time_stretch(
audio,
rate=1.2,
method="phase_vocoder",
n_fft=4096, # Higher = better frequency resolution, worse time resolution
hop_length=1024, # Controls overlap between frames
)
Backend trade-offs
WSOLA frame/overlap/search: Defaults are 30/10/10 ms and are sample-rate aware; larger searches cost more and can select less stable matches.
Phase-vocoder n_fft: Larger values improve frequency separation but worsen time precision.
Phase-vocoder hop_length: Typically n_fft // 4 for the basic backend.
ESOLA frame/epoch alignment: Uses a fixed 20 ms frame, 50% synthesis overlap, NumPy epoch extraction, and a raised-cosine pairwise crossfade. It is speech-specific and remains experimental until real-speech listening results establish a stable benefit over WSOLA.
The standard compositor plans pitch and rate together using
tsm_rate = rate / pitch_ratio, then resamples once when necessary. Selecting
method="td_psola" uses a separate direct pitch/prosody path instead: it tracks
voicing and pulses, changes synthesis-pulse spacing for voiced speech, and uses
WSOLA for unvoiced duration fallback. Its conservative limits are
-6..+6 semitones and 0.75..1.5 rate. If too few pitch marks are available,
the direct path explicitly falls back. Pure pitch requests use WSOLA plus
resampling for the requested ratio; duration-modification requests preserve
unvoiced material with duration-only WSOLA rather than globally tonalizing it.
The backend does not silently return unchanged audio.
All paths preserve exact output-length and dtype contracts, but TD-PSOLA does
not guarantee formant preservation and should not be treated as a general music
or polyphonic pitch-shifting backend.
TD-PSOLA remains experimental until objective, runtime, and real-speech listening gates pass. See the evaluation protocol.
Resampling Internals
AudioSig uses windowed-sinc interpolation:
from audiosig import resample
# Control filter characteristics
result = resample(
audio,
source_rate=48000,
target_rate=16000,
filter_width=64, # Wider = sharper cutoff, slower
rolloff=0.945, # Anti-aliasing filter rolloff
length_mode='round', # Output length calculation
)
Length Modes
'round': AudioSig’s default, matches historical behavior'ceil': Matches librosa’s output length semantics
from audiosig import resample
# Compare length modes
result_round = resample(audio, source_rate=44100, target_rate=16000, length_mode='round')
result_ceil = resample(audio, source_rate=44100, target_rate=16000, length_mode='ceil')
print(f"Round: {len(result_round)}")
print(f"Ceil: {len(result_ceil)}")
VAD Algorithm Details
Normalized Energy VAD
The normalized energy VAD works by:
Framing the audio into non-overlapping segments
Computing RMS energy per frame
Normalizing to [0, 1] range
Thresholding to detect activity
from audiosig import normalized_energy_vad
# The algorithm in detail
activity = normalized_energy_vad(
audio,
sample_rate=24000,
frame_duration_ms=10, # 10ms frames = 240 samples
energy_threshold=0.15, # 15% of max energy
pad_end=False, # Discard trailing partial frame
)
Relative dB VAD
The relative dB VAD works by:
Computing short-time energy
Finding peak energy
Setting threshold relative to peak
Detecting frames above threshold
from audiosig import relative_db_vad
activity = relative_db_vad(
audio,
frame_length=2048,
hop_length=512,
threshold_db=40.0, # 40 dB below peak
)
Edge Cases and Special Handling
Empty Audio
import numpy as np
from audiosig import trim, split
# Empty arrays are handled gracefully
empty = np.array([], dtype=np.float32)
# trim returns empty array and [0, 0]
trimmed, interval = trim(empty)
assert len(trimmed) == 0
assert np.array_equal(interval, [0, 0])
# split returns empty intervals
intervals = split(empty)
assert len(intervals) == 0
All public transformations, including time_stretch, pitch_shift, gain,
normalization, and resampling, accept an empty sample axis after validating
their parameters and return an empty copy with the original shape and dtype.
Silent Audio
import numpy as np
from audiosig import trim, peak_normalize
# All-zeros audio
silence = np.zeros(24000, dtype=np.float32)
# With the default peak-relative reference, uniform silence remains unchanged.
trimmed, interval = trim(silence, top_db=40.0, ref=np.max)
assert len(trimmed) == 24000
assert np.array_equal(interval, [0, 24000])
# A fixed nonzero reference classifies the all-zero signal as silence.
trimmed, interval = trim(silence, top_db=40.0, ref=1.0)
assert len(trimmed) == 0
assert np.array_equal(interval, [0, 0])
# Normalization preserves silence
normalized = peak_normalize(silence)
assert np.all(normalized == 0)
Very Short Audio
import numpy as np
from audiosig import time_stretch, pitch_shift
# Single sample
single = np.array([0.5], dtype=np.float32)
# Time stretching works with minimum length 1
result = time_stretch(single, rate=2.0)
assert len(result) >= 1
# Pitch shifting preserves length
result = pitch_shift(single, sample_rate=24000, semitones=3.0)
assert len(result) == 1
Multi-Channel and Batch Processing
Stereo Audio
import numpy as np
from audiosig import time_stretch, pitch_shift
# Stereo audio: (2, samples)
stereo = np.random.randn(2, 24000).astype(np.float32)
# Processing works on last axis by default
stretched = time_stretch(stereo, rate=1.1)
assert stretched.shape[0] == 2 # Channels preserved
# Can specify axis explicitly
result = time_stretch(stereo, rate=1.1, axis=-1)
Batch Dimensions
import numpy as np
from audiosig import time_stretch
# Batch of audio: (batch, channels, samples)
batch = np.random.randn(8, 2, 24000).astype(np.float32)
# Processing preserves batch dimensions
result = time_stretch(batch, rate=1.2)
assert result.shape == (8, 2, 20000) # Shorter due to rate > 1
Error Handling Strategies
Fail-Fast vs Fail-Open
from audiosig import (
time_stretch,
AudioSignalError,
InvalidParameterError,
AudioShapeError,
)
def safe_time_stretch(audio, rate, fail_fast=True):
"""Time stretch with error handling."""
try:
return time_stretch(audio, rate=rate)
except InvalidParameterError as e:
if fail_fast:
raise
print(f"Warning: {e}")
return audio # Return original
except AudioShapeError as e:
if fail_fast:
raise
print(f"Warning: Invalid shape: {e}")
return audio
# Fail-fast (default)
result = safe_time_stretch(audio, rate=-1.0, fail_fast=True)
# Fail-open (return original on error)
result = safe_time_stretch(audio, rate=-1.0, fail_fast=False)
Input Validation
import numpy as np
from audiosig import AudioShapeError
def validate_audio_input(audio, expected_sr=None):
"""Validate audio before processing."""
if not isinstance(audio, np.ndarray):
raise TypeError(f"Expected np.ndarray, got {type(audio)}")
if audio.dtype not in (np.float32, np.float64):
raise ValueError(f"Expected float32 or float64, got {audio.dtype}")
if audio.ndim == 0:
raise ValueError("Audio must have at least 1 dimension")
if audio.size == 0:
raise ValueError("Audio array is empty")
return True
Performance Optimization
Memory Layout
import numpy as np
# C-contiguous arrays are faster
audio = np.ascontiguousarray(audio)
# Fortran-contiguous for column-major operations
# (not typically needed for audio)
Chunked Processing
import numpy as np
from audiosig import time_stretch
def process_large_file(audio, rate, chunk_seconds=10, sample_rate=24000):
"""Process large audio in chunks."""
chunk_size = chunk_seconds * sample_rate
results = []
for i in range(0, len(audio), chunk_size):
chunk = audio[i:i + chunk_size]
processed = time_stretch(chunk, rate=rate)
results.append(processed)
return np.concatenate(results)
Type Selection
import numpy as np
# Float32: Good for most applications, lower memory
audio_32 = np.random.randn(24000).astype(np.float32)
# Float64: Higher precision, more memory
audio_64 = np.random.randn(24000).astype(np.float64)
# Processing preserves dtype
from audiosig import time_stretch
result_32 = time_stretch(audio_32, rate=1.1) # Returns float32
result_64 = time_stretch(audio_64, rate=1.1) # Returns float64
Integration with Other Libraries
With librosa
import librosa
from audiosig import resample
# Load with librosa
audio, sr = librosa.load("file.wav", sr=None)
# Resample with AudioSig (NumPy-only)
resampled = resample(audio, source_rate=sr, target_rate=16000)
With soundfile
import soundfile as sf
from audiosig import trim, peak_normalize
# Load audio
audio, sr = sf.read("file.wav")
# Process with AudioSig
trimmed, _ = trim(audio, top_db=40.0)
normalized = peak_normalize(trimmed, peak=0.95)
# Save
sf.write("processed.wav", normalized, sr)
With PyTorch
import torch
import numpy as np
from audiosig import time_stretch
# Convert to numpy
audio_torch = torch.randn(1, 24000)
audio_np = audio_torch.numpy().squeeze()
# Process with AudioSig
processed_np = time_stretch(audio_np, rate=1.1)
# Convert back to torch
processed_torch = torch.from_numpy(processed_np).unsqueeze(0)
Debugging and Profiling
Checking Intermediate Results
import numpy as np
from audiosig import frame_signal, frame_rms
def analyze_frames(audio, frame_length=2048, hop_length=512):
"""Debug frame analysis."""
frames = frame_signal(audio, frame_length=frame_length, hop_length=hop_length)
rms = frame_rms(audio, frame_length=frame_length, hop_length=hop_length)
print(f"Audio length: {len(audio)}")
print(f"Number of frames: {len(frames)}")
print(f"Frame shape: {frames.shape}")
print(f"RMS range: [{np.min(rms):.4f}, {np.max(rms):.4f}]")
return frames, rms
Performance Profiling
import time
import numpy as np
from audiosig import time_stretch
def profile_time_stretch(audio, rate, iterations=100):
"""Profile time_stretch performance."""
times = []
for _ in range(iterations):
start = time.perf_counter()
result = time_stretch(audio, rate=rate)
end = time.perf_counter()
times.append(end - start)
avg_time = np.mean(times)
std_time = np.std(times)
print(f"Average: {avg_time*1000:.2f} ms")
print(f"Std dev: {std_time*1000:.2f} ms")
print(f"Throughput: {len(audio) / avg_time / 1e6:.2f} Msamples/s")
return avg_time, std_time