Skip to content

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:

OO API: 0.0523s
Functional API: 0.0498s
Overhead: 5.0%

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:

  1. Use appropriate buffer sizes (256-512 frames)
  2. Minimize allocations in render callback
  3. Pre-compute expensive operations
  4. Use lock-free data structures for communication
  5. Avoid system calls in callback
  6. 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

Note

Performance characteristics may vary based on:

  • macOS version
  • Hardware specifications
  • Audio format and sample rate
  • System load and background processes