Skip to content

Audio Playback

This tutorial covers audio playback using coremusic, from simple file playback to real-time streaming.

Prerequisites

  • coremusic installed and built
  • Basic Python knowledge
  • Audio files to play (WAV, AIFF, MP3, M4A, etc.)

Simple File Playback

The AudioPlayer class provides the easiest way to play audio files:

import time

from coremusic.base import AudioPlayer

# Create player and load file
player = AudioPlayer()
player.load_file("audio.wav")
player.setup_output()

# Start playback
player.play()

# Wait for playback to complete
while player.is_playing():
    time.sleep(0.1)

print("Playback complete!")

Playback with Progress

Monitor playback progress with a progress bar:

import sys
import time

from coremusic.audio import AudioFile
from coremusic.base import AudioPlayer


def play_with_progress(filepath):
    """Play audio file with progress display."""
    # AudioPlayer does not report duration, so read it from the file
    with AudioFile(filepath) as audio:
        duration = audio.duration

    player = AudioPlayer()
    player.load_file(filepath)
    player.setup_output()

    print(f"Playing: {filepath}")
    print(f"Duration: {duration:.2f}s")

    player.play()

    while player.is_playing():
        progress = player.get_progress()
        current_time = progress * duration

        # Display progress bar
        bar_width = 40
        filled = int(bar_width * progress)
        bar = '=' * filled + '-' * (bar_width - filled)

        sys.stdout.write(f'\r[{bar}] {current_time:.1f}s / {duration:.1f}s')
        sys.stdout.flush()
        time.sleep(0.1)

    print('\nDone!')


play_with_progress("audio.wav")

Looping Playback

For continuous looping:

import time

from coremusic.base import AudioPlayer


def play_looped(filepath, num_loops=3):
    """Play audio file multiple times."""
    player = AudioPlayer()
    player.load_file(filepath)
    player.setup_output()

    for i in range(num_loops):
        print(f"Loop {i + 1}/{num_loops}")
        player.play()

        while player.is_playing():
            time.sleep(0.1)

        # Rewind for the next pass
        player.reset_playback()

    print("Looping complete!")


play_looped("audio.wav", num_loops=2)

Or let the player loop for you, and stop it when you are done:

player = AudioPlayer()
player.load_file("audio.wav")
player.setup_output()

# Let the player loop by itself, and stop it when you have had enough
player.set_looping(True)
player.play()
time.sleep(1.0)
player.stop()

Using the CLI

The coremusic CLI provides quick playback:

# Simple playback
coremusic audio play music.wav

# Looping playback
coremusic audio play music.wav --loop

# List audio devices
coremusic device list

Streaming Playback

AudioPlayer reads the file for you. When you need to produce the samples yourself - a synthesiser, a decoder, a live effect - use AudioOutputStream, which pulls blocks from a generator you supply:

import time

from coremusic.audio import AudioFile
from coremusic.audio.streaming import AudioOutputStream


def make_file_generator(path, channels):
    """Return generator(frame_count) -> bytes, reading a file on demand."""
    audio = AudioFile(path)
    audio.open()
    position = 0
    total = audio.packet_count
    scale = 1.0 / 32768.0

    def generate(frame_count):
        nonlocal position
        import struct

        data, count = audio.read_packets(position, min(frame_count, total - position))
        position += count
        if count == 0:
            return b""

        # The file is 16-bit; the stream wants interleaved float32
        samples = struct.unpack(f"<{len(data) // 2}h", data)
        return struct.pack(f"<{len(samples)}f", *[s * scale for s in samples])

    return generate, audio


generate, audio = make_file_generator("audio.wav", channels=2)

stream = AudioOutputStream(channels=2, sample_rate=44100.0, buffer_size=512)
stream.set_generator(generate)
stream.start()

print(f"Streaming, latency {stream.latency * 1000:.1f}ms")
time.sleep(1.0)

stream.stop()
audio.close()

The generator returns interleaved float32 as raw bytes and must return promptly: it runs on the audio thread, where a slow call becomes an audible dropout. stream.latency reports the round-trip latency of the configured buffer size.

Async Playback

For non-blocking playback in async applications:

import asyncio

from coremusic.audio import AsyncAudioFile


async def async_playback(filepath):
    """Non-blocking audio file reading."""
    async with AsyncAudioFile(filepath) as audio:
        print(f"Duration: {audio.duration:.2f}s")

        # Stream chunks asynchronously
        total = 0
        async for chunk in audio.read_chunks_async(chunk_size=4096):
            # Process each chunk without blocking
            total += len(chunk)
            await asyncio.sleep(0)  # Yield to event loop

        print(f"Read {total:,} bytes")


# Run async playback
asyncio.run(async_playback("audio.wav"))

Playback with Effects

Route audio through AudioUnit effects during playback:

import time

from coremusic.audio import AudioEffectsChain


def run_reverb_chain(seconds):
    """Build reverb -> output and run it."""
    chain = AudioEffectsChain()
    chain.open()

    reverb_node = chain.add_effect_by_name("AUReverb2")
    output_node = chain.add_output()
    chain.connect(reverb_node, output_node)

    chain.initialize()
    try:
        chain.start()
        time.sleep(seconds)
    finally:
        chain.stop()
        chain.dispose()


run_reverb_chain(0.5)

That chain processes live input. To hear a file through an effect, render it through the plugin and play the result:

from coremusic.audio import AudioFile
from coremusic.audio.audiounit_host import AudioUnitPlugin

# To hear a file through an effect rather than a live input, render it:
# read the samples, push them through the plugin, and play or write the result.
with AudioFile("audio.wav") as audio:
    samples = audio.read_as_numpy()

with AudioUnitPlugin.from_name("AUMatrixReverb") as reverb:
    reverb['Dry/Wet Mix'] = 40.0
    block = bytes(512 * 2 * 4)  # one block of float32 stereo silence
    processed = reverb.process(block)
    print(f"processed {len(processed)} bytes")

Device Selection

Play to a specific audio device:

from coremusic.audio import AudioDeviceManager


def list_output_devices():
    """List available output devices."""
    devices = AudioDeviceManager.get_output_devices()

    print("Output Devices:")
    for device in devices:
        default = AudioDeviceManager.get_default_output_device()
        marker = " (default)" if default and device.uid == default.uid else ""
        print(f"  {device.name} [{device.uid}]{marker}")

    return devices


list_output_devices()

CoreAudio routes playback to the default output device, so selecting a device means making it the default for the duration:

from coremusic.audio import AudioDeviceManager


def play_to_device(filepath, device_name):
    """Send playback to a specific device by making it the default."""
    device = AudioDeviceManager.find_device_by_name(device_name)

    if device is None or not device.has_output():
        print(f"Device not found: {device_name}")
        return

    print(f"Playing to: {device.name}")

    previous = AudioDeviceManager.get_default_output_device()
    AudioDeviceManager.set_default_output_device(device)
    try:
        from coremusic.shortcuts import play

        play(filepath)
    finally:
        if previous is not None:
            AudioDeviceManager.set_default_output_device(previous)

Volume Control

Control playback volume:

from coremusic.audio import AudioDeviceManager

device = AudioDeviceManager.get_default_output_device()

# Not every device exposes a software volume control; get_volume() returns
# None when it does not.
level = device.get_volume() if device else None
if level is not None:
    print(f"Output volume: {level:.2f}")
    device.set_volume(level)
else:
    print("This device has no software volume control.")

Error Handling

Handle playback errors gracefully:

import time
from pathlib import Path

from coremusic.base import AudioPlayer
from coremusic.exceptions import AudioFileError, AudioQueueError, CoreAudioError


def safe_play(filepath):
    """Play audio with comprehensive error handling."""
    # Check file exists
    if not Path(filepath).exists():
        print(f"Error: File not found: {filepath}")
        return False

    try:
        player = AudioPlayer()
        player.load_file(filepath)
        player.setup_output()

        player.play()

        while player.is_playing():
            time.sleep(0.1)

        return True

    except AudioFileError as e:
        print(f"Audio file error: {e}")
        return False
    except AudioQueueError as e:
        print(f"Audio queue error: {e}")
        return False
    except CoreAudioError as e:
        # Every coremusic error derives from this one
        print(f"CoreAudio error: {e}")
        return False


# Use with error handling
success = safe_play("audio.wav")
print(f"Playback {'succeeded' if success else 'failed'}")

Complete Example: Music Player

A simple command-line music player:

import sys
import time
from pathlib import Path

from coremusic.audio import AudioFile
from coremusic.base import AudioPlayer


class SimpleMusicPlayer:
    """Simple command-line music player."""

    def __init__(self):
        self.player = AudioPlayer()
        self.duration = 0.0

    def load(self, filepath):
        """Load audio file."""
        if not Path(filepath).exists():
            raise FileNotFoundError(f"File not found: {filepath}")

        with AudioFile(filepath) as audio:
            self.duration = audio.duration

        self.player.load_file(filepath)
        self.player.setup_output()
        print(f"Loaded: {filepath}")
        print(f"Duration: {self.duration:.2f}s")

    def play(self):
        """Start playback."""
        self.player.play()
        print("Playing...")

    def stop(self):
        """Stop playback."""
        self.player.stop()
        print("Stopped")

    def rewind(self):
        """Return to the start of the file."""
        self.player.reset_playback()

    def get_status(self):
        """Get current playback status."""
        progress = self.player.get_progress()
        return {
            'playing': self.player.is_playing(),
            'progress': progress,
            'current_time': progress * self.duration,
            'duration': self.duration,
        }


def main():
    if len(sys.argv) < 2:
        print("Usage: python music_player.py <audio_file>")
        sys.exit(1)

    player = SimpleMusicPlayer()

    try:
        player.load(sys.argv[1])
        player.play()

        # Simple playback loop
        while player.player.is_playing():
            status = player.get_status()
            bar_width = 30
            filled = int(bar_width * status['progress'])
            bar = '=' * filled + '-' * (bar_width - filled)

            sys.stdout.write(
                f"\r[{bar}] {status['current_time']:.1f}s / {status['duration']:.1f}s"
            )
            sys.stdout.flush()
            time.sleep(0.1)

        print("\nPlayback complete!")

    except FileNotFoundError as e:
        print(f"Error: {e}")
        sys.exit(1)
    except KeyboardInterrupt:
        print("\nInterrupted")
        player.stop()


if __name__ == "__main__":
    main()

Next Steps

See Also