Performance Guide¶
Version: 0.1.8
Best practices, benchmarks, and optimization techniques for achieving optimal performance with CoreMusic.
Performance Characteristics¶
Architecture Overview¶
CoreMusic uses a hybrid architecture for optimal performance:
┌─────────────────────────────────────────────┐
│ Python Layer (High-Level OO API) │
│ - Convenience and safety │
│ - Automatic resource management │
│ - ~5-10% overhead │
└─────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────┐
│ Cython Layer (capi.pyx) │
│ - Minimal Python overhead │
│ - Direct C function calls │
│ - ~1-2% overhead │
└─────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────┐
│ CoreAudio C APIs (Apple Frameworks) │
│ - Native performance │
│ - Hardware-accelerated when available │
└─────────────────────────────────────────────┘
Performance Tiers¶
| Operation | API Level | Performance | Use Case |
|---|---|---|---|
| File I/O | OO API | ~5% overhead | Scripts, prototyping |
| File I/O | Functional API | ~1% overhead | Production pipelines |
| Real-time | Cython callback | Native | Live processing |
| Batch | Parallel utils | Linear scaling | Mass conversion |
| MIDI | OO API | Negligible | Composition tools |
API Selection¶
Choosing the Right API¶
Use Object-Oriented API when:
- Development speed is priority
- Code readability matters
- Automatic cleanup is desired
- Overhead is acceptable (<10%)
Use Functional API when:
- Maximum performance is critical
- Processing large files (>100MB)
- Building low-level tools
- Need explicit control
Use Cython callbacks when:
- Real-time audio processing
- Custom DSP implementations
- Latency-sensitive operations
- Need to avoid Python GIL
Performance Comparison¶
import time
from coremusic import capi
from coremusic.audio import AudioFile
test_file = "audio.wav"
# Object-Oriented API
start = time.time()
with AudioFile(test_file) as audio:
data, count = audio.read_packets(0, 1024)
oo_time = time.time() - start
# Functional API
start = time.time()
file_id = capi.audio_file_open_url(test_file)
data, count = capi.audio_file_read_packets(file_id, 0, 1024)
capi.audio_file_close(file_id)
func_time = time.time() - start
print(f"OO API: {oo_time:.4f}s")
print(f"Functional API: {func_time:.4f}s")
print(f"Overhead: {((oo_time / func_time - 1) * 100):.1f}%")
Expected Results:
Hybrid Approach¶
Best of both worlds - use OO for convenience, functional for performance:
from coremusic import capi
from coremusic.audio import AudioFile
# Use OO API for file management
with AudioFile("input.wav") as audio:
format = audio.format # OO API convenience
# Switch to functional API for bulk processing
file_id = audio.object_id
for i in range(0, audio.packet_count, 4096):
# Direct C calls - maximum performance
data, count = capi.audio_file_read_packets(
file_id, i, 4096
)
# Process data...
Memory Management¶
Resource Lifecycle¶
Automatic Cleanup (OO API):
from coremusic.audio import AudioFile
# Good: Automatic cleanup via context manager
with AudioFile("audio.wav") as audio:
data, count = audio.read_packets(0, 1024)
# File automatically closed here
# Also good: Explicit disposal
audio = AudioFile("audio.wav")
audio.open()
try:
data, count = audio.read_packets(0, 1024)
finally:
audio.dispose() # Explicit cleanup
Manual Cleanup (Functional API):
from coremusic import capi
# Must manually clean up
file_id = capi.audio_file_open_url("audio.wav")
try:
data = capi.audio_file_read_packets(file_id, 0, 1024)
finally:
capi.audio_file_close(file_id) # Don't forget!
Memory Pooling¶
Pre-allocate buffers for large operations:
import numpy as np
from coremusic.audio import AudioFile
buffer_size = 4096
with AudioFile("audio.wav") as audio:
total = audio.packet_count
for offset in range(0, total, buffer_size):
data, count = audio.read_packets(offset, min(buffer_size, total - offset))
if count == 0:
break
# Wrap the bytes rather than copying them
samples = np.frombuffer(data, dtype=np.int16).astype(np.float32)
# Process in place to avoid further copies
samples *= 0.5 # Example: reduce volume
Avoiding Memory Leaks¶
from coremusic.midi import MusicPlayer, MusicSequence
# Risky: a raised exception leaves both objects undisposed
player = MusicPlayer()
sequence = MusicSequence()
player.dispose()
sequence.dispose()
# Better: MusicPlayer is a context manager; dispose the sequence in a finally
sequence = MusicSequence()
try:
with MusicPlayer() as player:
player.sequence = sequence
finally:
sequence.dispose()
Buffer Optimization¶
Optimal Buffer Sizes¶
| Use Case | Buffer Size | Rationale |
|---|---|---|
| File I/O | 4096-8192 frames | Balance memory/speed |
| Real-time | 256-512 frames | Low latency |
| Streaming | 8192-16384 | Throughput |
| Batch | 16384-32768 | Maximum speed |
Buffer Size Tuning¶
import time
from coremusic.audio import AudioFile
def benchmark_buffer_size(file_path, buffer_size):
start = time.time()
total_packets = 0
with AudioFile(file_path) as audio:
total = audio.packet_count
while total_packets < total:
to_read = min(buffer_size, total - total_packets)
data, count = audio.read_packets(total_packets, to_read)
if count == 0:
break
total_packets += count
duration = time.time() - start
return total_packets / duration / 1_000_000 # Million packets/sec
# Test different buffer sizes
for size in [512, 1024, 2048, 4096, 8192, 16384]:
throughput = benchmark_buffer_size("audio.wav", size)
print(f"Buffer {size}: {throughput:.2f} Mpackets/sec")
Expected Results:
Buffer 512: 12.5 Mframes/sec
Buffer 1024: 18.2 Mframes/sec
Buffer 2048: 22.3 Mframes/sec
Buffer 4096: 24.8 Mframes/sec <- Sweet spot
Buffer 8192: 25.1 Mframes/sec
Buffer 16384: 25.2 Mframes/sec
Large File Processing¶
Chunked Processing¶
Process large files in manageable chunks:
import numpy as np
from coremusic import capi
from coremusic.audio import AudioFile, AudioFormat, ExtendedAudioFile
def process_large_file(input_path, output_path, chunk_size=8192):
"""Process a large audio file without loading all of it."""
with AudioFile(input_path) as input_file:
source_format = input_file.format
total_packets = input_file.packet_count
# Work in float internally, and write what we actually produced
out_format = AudioFormat.pcm(
source_format.sample_rate,
channels=source_format.channels_per_frame,
bits=32,
is_float=True,
)
with ExtendedAudioFile.create(
output_path, capi.fourchar_to_int('WAVE'), out_format
) as output_file:
processed = 0
while processed < total_packets:
# Read the next chunk - note the offset, not a fixed 0
remaining = min(chunk_size, total_packets - processed)
data, count = input_file.read_packets(processed, remaining)
if count == 0:
break
# Process
samples = np.frombuffer(data, dtype=np.int16).astype(np.float32)
samples /= 32768.0
samples *= 0.8 # Example processing
# Write
output_file.write(count, samples.tobytes())
processed += count
# Progress
print(f"Progress: {processed / total_packets * 100:.1f}%", end='\r')
print()
process_large_file("audio.wav", "processed_large.wav")
Parallel File Processing¶
Process multiple files in parallel:
from concurrent.futures import ProcessPoolExecutor
from pathlib import Path
from coremusic.audio import AudioFile
def convert_file(input_path):
"""Convert single file"""
output_path = input_path.with_suffix('.mp3')
with AudioFile(str(input_path)) as audio:
format = audio.format
# Conversion logic...
return output_path
def batch_convert(input_dir, num_workers=4):
"""Convert all files in directory"""
files = list(Path(input_dir).glob("*.wav"))
with ProcessPoolExecutor(max_workers=num_workers) as executor:
results = executor.map(convert_file, files)
return list(results)
# Convert 100 files using 4 cores
results = batch_convert("audio_files/", num_workers=4)
Real-Time Audio¶
Low-Latency Configuration¶
from coremusic.audio import AudioFormat, AudioUnit
# Create low-latency audio unit
unit = AudioUnit.default_output()
# Configure the format you will feed it. The output scope belongs to the
# device, so the client format goes on the input scope.
audio_format = AudioFormat.pcm(
sample_rate=44100.0, channels=2, bits=32, is_float=True
)
unit.set_stream_format(audio_format, scope="input")
# Smaller slices mean lower latency: 256 frames at 44.1kHz is about 5.8ms
unit.max_frames_per_slice = 256
unit.initialize()
unit.start()
print(f"Latency: {unit.latency * 1000:.2f}ms")
unit.stop()
unit.dispose()
Render Callback Performance¶
# Pure Cython callback for maximum performance
# Defined in capi.pyx
cdef OSStatus render_callback(
void *inRefCon,
AudioUnitRenderActionFlags *ioActionFlags,
const AudioTimeStamp *inTimeStamp,
UInt32 inBusNumber,
UInt32 inNumberFrames,
AudioBufferList *ioData
) nogil:
# No Python overhead
# No GIL held
# Direct memory access
# Native performance
# Fill audio buffers...
return 0
Avoiding Dropouts¶
Best practices for glitch-free real-time audio:
- Use appropriate buffer sizes (256-512 frames)
- Minimize allocations in render callback
- Pre-compute expensive operations
- Use lock-free data structures for communication
- Avoid system calls in callback
- Test under load with other apps running
Benchmarks¶
File I/O Performance¶
Test: Read 100MB audio file (44.1kHz stereo float32)
| API | Time | Throughput |
|---|---|---|
| OO API | 0.423s | 236 MB/s |
| Functional API | 0.401s | 249 MB/s |
| NumPy memmap | 0.387s | 258 MB/s (ref) |
Format Conversion Performance¶
Test: Convert 10 minutes of audio (44.1kHz -> 48kHz)
| Method | Time | Speed Ratio |
|---|---|---|
| ExtAudioFile | 2.13s | 282x realtime |
| AudioConverter | 1.98s | 303x realtime |
| SoX (external) | 3.45s | 174x realtime |
MIDI Processing Performance¶
Test: Generate 10,000 MIDI notes
| Operation | Time | Notes/sec |
|---|---|---|
| MusicTrack add | 0.089s | 112,000 |
| Sequence save | 0.142s | 70,000 |
| File load | 0.067s | 149,000 |
Real-Time Latency¶
Configuration: 44.1kHz, float32, stereo
| Buffer Size | Latency (ms) | CPU Usage |
|---|---|---|
| 128 frames | 2.9ms | 12% |
| 256 frames | 5.8ms | 6% |
| 512 frames | 11.6ms | 3% |
| 1024 frames | 23.2ms | 2% |
Profiling and Debugging¶
Using Python Profiler¶
import cProfile
import pstats
from coremusic.audio import AudioFile
def audio_processing_task():
with AudioFile("audio.wav") as audio:
total = audio.packet_count
for offset in range(0, total, 4096):
data, count = audio.read_packets(offset, min(4096, total - offset))
# Process...
# Profile the code
profiler = cProfile.Profile()
profiler.enable()
audio_processing_task()
profiler.disable()
stats = pstats.Stats(profiler)
stats.strip_dirs()
stats.sort_stats('cumulative')
stats.print_stats(20) # Top 20 functions
Memory Profiling¶
import tracemalloc
from coremusic.audio import AudioFile
def load_files(paths):
"""Hold several decoded files in memory at once."""
loaded = []
for path in paths:
with AudioFile(path) as audio:
data, count = audio.read_packets(0, audio.packet_count)
loaded.append(data)
return loaded
tracemalloc.start()
files = load_files(["audio.wav", "input.wav", "drums.wav"])
current, peak = tracemalloc.get_traced_memory()
tracemalloc.stop()
print(f"Held: {current / 1024 / 1024:.1f} MB")
print(f"Peak: {peak / 1024 / 1024:.1f} MB")
Performance Monitoring¶
import time
import tracemalloc
from coremusic.audio import AudioFile
class PerformanceMonitor:
"""Report elapsed time and allocated memory since construction."""
def __init__(self):
tracemalloc.start()
self.start_time = time.perf_counter()
self.start_memory, _ = tracemalloc.get_traced_memory()
def report(self, label):
elapsed = time.perf_counter() - self.start_time
current, peak = tracemalloc.get_traced_memory()
print(f"{label}:")
print(f" Time: {elapsed:.3f}s")
print(f" Memory: {current / 1024 / 1024:.1f} MB "
f"(+{(current - self.start_memory) / 1024 / 1024:.1f} MB)")
print(f" Peak: {peak / 1024 / 1024:.1f} MB")
# Usage
monitor = PerformanceMonitor()
with AudioFile("audio.wav") as audio:
data, count = audio.read_packets(0, audio.packet_count)
monitor.report("After reading audio")
Best Practices Summary¶
File I/O¶
- Use 4096-8192 frame buffers for optimal throughput
- Reuse buffers when processing multiple chunks
- Use ExtendedAudioFile for format conversion
- Close files promptly to release resources
Real-Time Audio¶
- Target 256-512 frame buffers for low latency
- Implement render callbacks in Cython for best performance
- Avoid memory allocations in audio thread
- Pre-compute lookup tables and coefficients
Memory Management¶
- Always use context managers with OO API
- Dispose objects explicitly when not using context managers
- Pre-allocate buffers for repeated operations
- Use NumPy views instead of copies when possible
Parallel Processing¶
- Use ProcessPoolExecutor for CPU-bound tasks
- Divide work into independent chunks
- Use 1-2x CPU cores for optimal scaling
- Monitor memory usage with multiple processes
API Selection¶
- Start with OO API for prototyping
- Switch to functional API for bottlenecks
- Use Cython callbacks for real-time code
- Profile before optimizing
See Also¶
- Practical recipes
- API reference
- Apple's CoreAudio documentation
Note
Performance characteristics may vary based on:
- macOS version
- Hardware specifications
- Audio format and sample rate
- System load and background processes