Skip to content

phpmlkit/soundfile

Repository files navigation

PHP SoundFile

Low-level audio I/O and resampling for PHP, backed by libsndfile and libsamplerate

Latest Version GitHub Workflow Status Total Downloads License

Features

  • Read & write audio files — WAV, FLAC, Ogg Vorbis, MP3, AIFF, and 20+ other formats
  • NDArray-native — data flows in and out as [frames × channels] NDArrays, no intermediate buffers
  • Streaming I/O — instance-based SoundFile class with read, write, seek, tell, and block iteration
  • One-shot conveniencesf_read() and sf_write() for simple load/save without managing a handle
  • Sample rate conversionsf_resample() with chunked progressive mode for large files, one-shot simple mode for small signals, and four quality levels
  • Metadata — read and write title, artist, album, track number, and arbitrary tags
  • Type-safe — PHP 8.2+ with backed enums, readonly value objects, and strict types throughout

Installation

composer require phpmlkit/soundfile

Requirements: PHP 8.2 or higher with the FFI extension enabled.

The package ships pre-compiled shared libraries for macOS (arm64, x86_64), Linux (x86_64, arm64), and Windows (x64).

Quick Start

Read and write

use function PhpMlKit\SoundFile\{sf_read, sf_write, sf_info};

// Read a file — returns [NDArray, SfInfo]
[$audio, $info] = sf_read('input.wav');
// $audio shape: [441000] (mono, Float32)

// Write it back (format and subtype auto-detected from extension)
sf_write('output.wav', $audio, $info->sampleRate);

// Probe signal properties without loading data
$info = sf_info('input.wav');
echo "{$info->frames} frames, {$info->channels} channels, {$info->duration()}s";

Resample

use function PhpMlKit\SoundFile\sf_resample;
use PhpMlKit\SoundFile\Enums\ResampleQuality;

// Chunked progressive — safe for large files (default)
$resampled = sf_resample($audio, inputRate: 44100, outputRate: 22050);

// One-shot simple — best for small signals
$resampled = sf_resample($audio, 44100, 16000, chunkSize: null);

// Best quality, explicit chunk size
$resampled = sf_resample(
    $audio, 44100, 8000,
    quality: ResampleQuality::Best,
    chunkSize: 4096,
);

Streaming with SoundFile

use PhpMlKit\SoundFile\SoundFile;
use PhpMlKit\SoundFile\Enums\FileMode;

// Open for reading
$sf = new SoundFile('input.wav', FileMode::Read);

// Seek and read
$sf->seek(44100);
$chunk = $sf->read(512);
echo $sf->tell(); // 44612

// Iterate in blocks
foreach ($sf->blocks(1024) as $block) {
    process($block);
}
$sf->close();

// Open for writing
$out = new SoundFile('output.wav', FileMode::Write,
    sampleRate: 44100, channels: 2,
);
$out->setTitle('My Track');
$out->setArtist('My Artist');
$out->write($stereoData);
$out->close();

See the full documentation for detailed guides, tutorials, and a complete API reference.

Core Concepts

Audio data layout

All NDArrays in this library use the shape [frames × channels] — time first, channel second. A mono file produces [N] or [N, 1] depending on the always2d flag. Stereo files always produce [N, 2].

DType, SampleFormat, and AudioFormat

Three independent choices interact when writing a file:

Concept What it controls Example
NDArray DType How the data is stored in memory Float32, Int16, Float64
SampleFormat The file's encoding subtype Pcm16, Float, Vorbis
AudioFormat The container Wav, Flac, Ogg

When you call sf_write(), the NDArray's dtype is automatically converted to match the target SampleFormat. You never need to call astype() yourself. The combination of AudioFormat and SampleFormat is validated against libsndfile's compatibility table — an incompatible pair (like Ogg + Pcm16) throws a SoundFileException before any data is written.

API Reference

Global Functions

These are importable with use function PhpMlKit\SoundFile\{...} and provide the simplest path for common operations.

sf_read()

Read an audio file into a single NDArray.

function sf_read(
    string $file,
    ?int $start = null,     // First frame (0-based; null = beginning)
    ?int $stop = null,      // One past last frame (null = EOF)
    bool $always2d = false, // If true, mono returns [frames, 1] instead of [frames]
    int $blocksize = 4096,  // Chunk size for internal read loop
): array // [NDArray, SfInfo]

The dtype matches the file's native format. The returned SfInfo object contains the file's signal properties (frames, channels, sample rate, format, etc.). For partial reads, use start and stop to specify a frame range. With $always2d = false (default), mono files return a 1D array for convenience.

[$data, $info] = sf_read('song.wav');
[$part, $info] = sf_read('song.wav', start: 44100, stop: 88200);
[$data, $info] = sf_read('song.wav', always2d: true);

sf_write()

Write an NDArray to an audio file.

function sf_write(
    string $file,
    NDArray $data,               // [frames] or [frames, channels]
    int $sampleRate,             // Sample rate in Hz
    ?AudioFormat $format = null, // Inferred from extension if null
    ?SampleFormat $subtype = null, // Format's default if null
): void

1D arrays are automatically expanded to [frames, 1] before writing. The NDArray dtype is converted to match the target subtype.

sf_write('out.wav', $data, sampleRate: 44100);
sf_write('out.flac', $data, 44100, subtype: SampleFormat::Pcm24);

sf_info()

Read signal properties without loading audio data. Opens the file, reads the header, and closes immediately.

function sf_info(string $file): SfInfo

sf_metadata()

Read string tags (title, artist, album, etc.) without loading audio data.

function sf_metadata(string $file): SfMetadata
$meta = sf_metadata('song.wav');
echo $meta->artist;

sf_check_format()

Validate that a container format and encoding subtype are compatible.

function sf_check_format(AudioFormat $format, SampleFormat $subtype): bool
sf_check_format(AudioFormat::Wav, SampleFormat::Pcm16);  // true
sf_check_format(AudioFormat::Ogg, SampleFormat::Pcm16);  // false

sf_resample()

Convert an NDArray from one sample rate to another.

function sf_resample(
    NDArray $input,                     // [frames, channels]
    int $inputRate,                     // Source sample rate in Hz
    int $outputRate,                    // Target sample rate in Hz
    ResampleQuality $quality = ResampleQuality::Best,
    ?int $chunkSize = 2048,             // null = one-shot, int = chunked progressive
): NDArray                                // [newFrames, channels] Float32

When $chunkSize is non-null (default), the function uses src_process in a loop with a single pre-allocated output buffer — safe for large signals. When null, it uses src_simple for a single-pass conversion.

The output is always Float32 regardless of input dtype. Inputs of other dtypes are automatically converted.

$resampled = sf_resample($data, 44100, 22050);           // chunked progressive
$resampled = sf_resample($data, 44100, 8000, chunkSize: null); // one-shot simple

Classes

SoundFile

An opened audio file handle for streaming read/write.

Constructor:

new SoundFile(
    string $path,
    FileMode $mode = FileMode::Read,
    // Write-mode parameters:
    ?int $sampleRate = null,
    ?int $channels = null,
    ?AudioFormat $format = null,
    ?SampleFormat $subtype = null,
)

In read mode, only $path and $mode are needed — metadata is read from the file header. In write mode, $sampleRate, $channels, $format, and $subtype are required (format defaults to the file extension, subtype defaults to the format's preferred).

Instance methods:

Method Description
read(?int $numFrames): NDArray Read up to N frames from current position. Null = all remaining.
write(NDArray $data): void Write frames. Shape must match the file's channel count.
seek(int $offset, int $whence = SEEK_SET): void Move the read/write position.
tell(): int Current frame position.
eof(): bool Whether the position has reached the end.
blocks(int $size = 4096): Generator Yield NDArrays of up to $size frames.
close(): void Close the handle (called automatically by destructor).
info(): SfInfo Signal properties (frames, channels, sample rate, format).
frames(): int Total frames.
channels(): int Channel count.
sampleRate(): int Sample rate in Hz.

Metadata getters/setters (read/write on open handles):

Getter Setter SF_STR constant
title() setTitle(string) 0x01
copyright() setCopyright(string) 0x02
software() setSoftware(string) 0x03
artist() setArtist(string) 0x04
comment() setComment(string) 0x05
date() setDate(string) 0x06
album() setAlbum(string) 0x07
license() setLicense(string) 0x08
trackNumber() setTrackNumber(string) 0x09
genre() setGenre(string) 0x10

Plus getString(int $strType) and setString(int $strType, string $value) for arbitrary SF_STR constants.

SfInfo

Immutable signal properties describing an audio file.

Factory methods:

Method Description
SfInfo::probe(string $path): self Open file, read header, close.
SfInfo::fromSfInfo(FFI\CData $sfInfo): self Create from a populated libsndfile SF_INFO struct.
SfInfo::forWrite(int $frames, int $channels, int $sampleRate, AudioFormat $format, SampleFormat $subtype): self Create write-ready info.

Properties:

Property Type Description
frames int Total frame count
channels int Number of audio channels
sampleRate int Sample rate in Hz
format AudioFormat Container format
sampleFormat SampleFormat Encoding subtype
sections int Number of data sections
seekable bool Whether the file supports seeking

Derived values:

Method Returns Description
duration() float Duration in seconds
nSamples() int Total sample values (frames × channels)

Immutable builders — return a copy with one field changed:

Method Description
withFrames(int $f): self Change frame count
withChannels(int $c): self Change channel count
withSampleRate(int $sr): self Change sample rate

Enums

AudioFormat

Container formats (25 cases). Values are libsndfile SF_FORMAT_* constants.

AudioFormat::Wav->extension();           // 'wav'
AudioFormat::fromExtension('flac');      // AudioFormat::Flac
AudioFormat::fromPath('song.mp3');       // AudioFormat::Mpeg
AudioFormat::Wav->defaultSampleFormat(); // SampleFormat::Pcm16
AudioFormat::Wav->compatibleSampleFormats(); // [PcmS8, Pcm16, Pcm24, ...]

SampleFormat

Encoding subtypes (20 cases). Values are libsndfile SF_FORMAT_* subtype constants.

SampleFormat::Pcm16->bitDepth();    // 16
SampleFormat::Pcm16->isInteger();   // true
SampleFormat::Float->isPcm();       // false
SampleFormat::Float->toDtype();     // DType::Float32
SampleFormat::fromDtype(DType::Int16); // SampleFormat::Pcm16

FileMode

File open modes.

Case Description
FileMode::Read Open for reading
FileMode::Write Open for writing (creates/truncates)
FileMode::ReadWrite Open for both reading and writing

ResampleQuality

libsamplerate converter quality levels.

Case Description
ResampleQuality::Best Band-limited sinc, highest quality, slowest
ResampleQuality::Medium Band-limited sinc, medium quality
ResampleQuality::Fastest Band-limited sinc, fastest
ResampleQuality::Linear Linear interpolation, fastest but lowest quality

Exceptions

SoundFileException — thrown for I/O errors, invalid format combinations, closed-file operations, and resampling failures. Extends \RuntimeException.

Documentation

Development

# Install PHP dependencies
composer install

# Run tests
composer test

# Run static analysis (PHPStan level 8)
composer lint

# Format code
composer cs:fix

License

MIT

Credits

About

Low-level audio I/O and resampling for PHP

Resources

License

Contributing

Stars

Watchers

Forks

Packages

 
 
 

Contributors