Contents

ryanfrancesconi/spfk-audio-conversion

Audio file format conversion library supporting PCM and compressed formats via CoreAudio, AVFoundation, LAME, and libsndfile.

Features

  • Convert between PCM formats (WAV, AIFF, CAF) with sample rate, bit depth, and channel count options
  • Encode to compressed formats (M4A/AAC, MP3, FLAC, OGG Opus)
  • Decode compressed formats to PCM
  • Transcode between compressed formats via intermediate PCM
  • Batch conversion with configurable concurrency and progress reporting
  • Automatic >=2GB file promotion to CAF (64-bit container)
  • Bit depth rule enforcement to prevent unnecessary upsampling

Supported Formats

| Direction | Formats | |-----------|---------| | Input | All AudioFileType cases (WAV, AIFF, CAF, M4A, MP3, MP4, FLAC, OGG, etc.) | | Output | WAV, AIFF, CAF, M4A, MP3, FLAC, OGG Opus |

Usage

Single File Conversion

import SPFKAudioConversion

// Basic conversion (options inferred from output extension)
let converter = AudioFormatConverter(inputURL: inputURL, outputURL: outputURL)
try await converter.start()

// With explicit options
var options = AudioFormatConverterOptions()
options.format = .wav
options.sampleRate = 44100
options.bitsPerChannel = 16
options.channels = 2

let converter = AudioFormatConverter(inputURL: inputURL, outputURL: outputURL, options: options)
try await converter.start()

Conversion Options

// PCM options with bit depth rule
let options = try AudioFormatConverterOptions(
    pcmFormat: .wav,
    sampleRate: 48000,
    bitsPerChannel: 24,
    channels: 2,
    bitDepthRule: .lessThanOrEqual // won't upsample beyond source bit depth
)

// Compressed output with bit rate
var options = AudioFormatConverterOptions(format: .m4a)
options.bitRate = 256_000 // bits per second (clamped to 64k-320k)

Batch Conversion

let sources = inputURLs.map { url in
    AudioFormatConverterSource(
        input: url,
        output: outputDir.appending(component: "\(url.stem).m4a"),
        options: AudioFormatConverterOptions(format: .m4a)
    )
}

let batch = await BatchAudioFormatConverter(inputs: sources)
await batch.update(delegate: self) // optional progress reporting
let results = try await batch.start()

for result in results {
    switch result {
    case .success(let source):
        print("Converted: \(source.output.lastPathComponent)")
    case .failed(let source, let error):
        print("Failed: \(source.input.lastPathComponent) - \(error)")
    }
}

Convenience Functions

// Quick WAV conversion
try await AudioFormatConverter.convertToWave(
    inputURL: inputURL,
    outputURL: outputURL,
    sampleRate: 44100,
    bitDepth: 16
)

// Format detection
let isPCM = AudioFormatConverter.isPCM(url: fileURL)
let isCompressed = AudioFormatConverter.isCompressed(url: fileURL)

Metadata Copying

Metadata is automatically copied from source to output after conversion, controlled by MetadataCopyScheme (default: .copyAll):

| Scheme | Tags | BEXT/iXML | XMP | Markers | Artwork | |--------|:----:|:---------:|:---:|:-------:|:-------:| | .copyAll | yes | yes | yes | yes | yes | | .copyTextAndMarkers | yes | yes | yes | yes | -- | | .copyText | yes | yes | yes | -- | -- | | .copyMarkers | -- | -- | -- | yes | -- | | .ignore | -- | -- | -- | -- | -- |

let source = AudioFormatConverterSource(
    input: inputURL,
    output: outputURL,
    options: options,
    metadataCopyScheme: .copyText // default is .copyAll
)

Marker format mapping — markers are written in the native chapter/cue format of the output:

| Output format | Marker format | |---------------|---------------| | WAV, AIFF | RIFF cue points (AudioToolbox) | | MP3 | ID3v2 CHAP frames | | FLAC, OGG, Opus | Vorbis comment chapters | | M4A, MP4, M4B | Nero chpl chapters |

BEXT and iXML are WAV-to-WAV only. XMP copy is best-effort (silently skipped if not present).

Architecture

AudioFormatConverter.start()
  |-- PCM output        --> convertToPCM()         [CoreAudio ExtAudioFile]
  |-- MP3 output        --> convertToMP3()         [LAME via LameConverter]
  |-- FLAC output       --> convertToFLAC()        [libsndfile via SndFileConverter]
  |-- OGG Opus output   --> convertToOGG()         [libsndfile via SndFileConverter]
  |-- PCM in, M4A out   --> AssetWriter            [AVFoundation]
  |-- Compressed in/out --> convertCompressed()     [intermediate PCM + target encoder]

SPFKAudioConverterC (ObjC target)
  |-- LameConverter      LAME encoder + mpg123 decoder
  |-- SndFileConverter   libsndfile for FLAC/OGG + file info queries

BatchAudioFormatConverter
  |-- Structured concurrency with batchMap
  |-- Sliding window of 8 concurrent conversions
  |-- Per-file progress via BatchAudioFormatConverterDelegate

Dependencies

| Package | Description | |---------|-------------| | spfk-base | Foundation extensions and utilities | | spfk-audio-base | Audio type definitions (AudioFileType, AudioDefaults) | | spfk-metadata | Audio file metadata parsing | | spfk-metadata-xmp | XMP metadata reading/writing | | spfk-lame | LAME + mpg123 xcframeworks for MP3 encoding/decoding | | spfk-utils | General utilities (Entropy, Serializable) | | sndfile-binary-xcframework | libsndfile for FLAC/OGG encoding | | ogg-binary-xcframework | Ogg container library | | flac-binary-xcframework | FLAC codec | | vorbis-binary-xcframework | Vorbis codec | | opus-binary-xcframework | Opus codec |

Requirements

  • Platforms: macOS 13+
  • Swift: 6.2+

About

Spongefork (SPFK) is the personal software projects of Ryan Francesconi. Dedicated to creative sound manipulation, his first application, Spongefork, was released in 1999 for macOS 8. From 2016 to 2025 he was the lead macOS developer at Audio Design Desk.

Package Metadata

Repository: ryanfrancesconi/spfk-audio-conversion

Default branch: main

README: README.md