nativeaudio

audio playback for Go
Log | Files | Refs | README | LICENSE

audio_macos.go (20259B)


      1 //go:build darwin && cgo
      2 
      3 package nativeaudio
      4 
      5 /*
      6 #cgo CFLAGS: -x objective-c
      7 #cgo LDFLAGS: -framework AudioToolbox -framework AVFoundation
      8 
      9 #include <AudioToolbox/AudioToolbox.h>
     10 #include <AVFoundation/AVFoundation.h>
     11 #include <stdint.h>
     12 
     13 // handleToPointer widens a runtime/cgo handle into the void* these
     14 // AudioToolbox interfaces take for caller data.
     15 //
     16 // The cast belongs on this side. A cgo handle is an integer, and turning
     17 // an integer into a pointer in Go is the very thing the unsafe pointer
     18 // rules forbid.
     19 static void *handleToPointer(uintptr_t handle) {
     20 	return (void *)handle;
     21 }
     22 
     23 // Pre-declare exported Go functions to make them visible in the
     24 // C pseudo package.
     25 
     26 OSStatus InputDataProc(
     27 	AudioConverterRef               inAudioConverter,
     28 	UInt32 *                        ioNumberDataPackets,
     29 	AudioBufferList *               ioData,
     30 	AudioStreamPacketDescription ** outDataPacketDescription,
     31 	void *                          inUserData
     32 );
     33 
     34 OSStatus AudioFileReadProcImpl(
     35 	void *inClientData,
     36 	SInt64 inPosition,
     37 	UInt32 requestCount,
     38 	void *buffer,
     39 	UInt32 *actualCount
     40 );
     41 
     42 SInt64 AudioFileGetSizeProcImpl(
     43 	void *inClientData
     44 );
     45 */
     46 import "C"
     47 import (
     48 	"errors"
     49 	"fmt"
     50 	"io"
     51 	"os"
     52 	"runtime"
     53 	"runtime/cgo"
     54 	"sync"
     55 	"sync/atomic"
     56 	"unsafe"
     57 )
     58 
     59 func start() error {
     60 	return nil
     61 }
     62 
     63 func end() error {
     64 	return nil
     65 }
     66 
     67 // macStream decodes incrementally through an AudioConverter.
     68 //
     69 // The converter was always a pull loop: each FillComplexBuffer asks the
     70 // input proc for as much compressed audio as it needs and hands back a
     71 // chunk of PCM. Buffering the whole decode was only a matter of running
     72 // that loop to completion up front, so streaming is the same loop driven
     73 // one chunk at a time rather than a second pipeline.
     74 type macStream struct {
     75 	mu     sync.Mutex
     76 	pinner runtime.Pinner
     77 
     78 	inputFile *AudioFile
     79 	converter *AudioConverter
     80 	icHandle  cgo.Handle
     81 
     82 	format Format
     83 
     84 	// chunk is the scratch the converter fills. pending is the part of it
     85 	// Read has not handed over yet, so a caller reading in small pieces
     86 	// does not cost a conversion per call.
     87 	chunk   []byte
     88 	pending []byte
     89 
     90 	packetsPerChunk C.UInt32
     91 	drained         bool
     92 	closed          bool
     93 }
     94 
     95 // newMacStream builds the AudioToolbox pipeline over compressed audio
     96 // held in memory. The returned stream owns everything allocated here and
     97 // must be closed.
     98 func newMacStream(buf []byte) (_ *macStream, err error) {
     99 	// Enough output packets to keep the converter busy without holding
    100 	// much: one output packet is a frame, so this is 40KB of stereo, a
    101 	// quarter second at 44.1kHz. The buffered decode used the same number
    102 	// to favour throughput, and at this size it costs nothing to stream.
    103 	s := &macStream{packetsPerChunk: 10000}
    104 
    105 	// Everything below is released by Close, including when the setup
    106 	// fails partway and the caller never receives the stream.
    107 	defer func() {
    108 		if err != nil {
    109 			s.Close()
    110 		}
    111 	}()
    112 
    113 	// Allocate an "AudioFile" backed by a byte slice. The handle inside
    114 	// keeps buf reachable for as long as AudioToolbox can call back, so
    115 	// neither the slice nor its data needs pinning.
    116 	inputFile, err := OpenAudioFileBuffer(buf)
    117 	if err != nil {
    118 		return nil, fmt.Errorf("opening file with callbacks: %w", err)
    119 	}
    120 
    121 	s.inputFile = inputFile
    122 	s.pinner.Pin(inputFile)
    123 
    124 	// Query the input format.
    125 	var inputDescription C.AudioStreamBasicDescription
    126 
    127 	if _, err := inputFile.GetProperty(
    128 		C.kAudioFilePropertyDataFormat,
    129 		C.UInt32(unsafe.Sizeof(inputDescription)),
    130 		unsafe.Pointer(&inputDescription),
    131 	); err != nil {
    132 		return nil, fmt.Errorf("querying for property kAudioFilePropertyDataFormat: %w", err)
    133 	}
    134 
    135 	var inputUsesPacketDescriptions C.Boolean
    136 
    137 	// For variable encodings, the bytes and frames per packet are found
    138 	// during decoding rather than defined globally.
    139 	if inputDescription.mBytesPerPacket == 0 || inputDescription.mFramesPerPacket == 0 {
    140 		inputUsesPacketDescriptions = _true
    141 	}
    142 
    143 	// Define the output format.
    144 	outputDescription := C.AudioStreamBasicDescription{
    145 		mSampleRate:       inputDescription.mSampleRate,
    146 		mChannelsPerFrame: inputDescription.mChannelsPerFrame,
    147 		mFormatID:         C.kAudioFormatLinearPCM,
    148 		mFormatFlags:      C.kAudioFormatFlagIsSignedInteger | C.kAudioFormatFlagIsPacked,
    149 		mBytesPerPacket:   2 * inputDescription.mChannelsPerFrame,
    150 		mFramesPerPacket:  1,
    151 		mBytesPerFrame:    2 * inputDescription.mChannelsPerFrame,
    152 		mBitsPerChannel:   16,
    153 	}
    154 
    155 	// Allocate the AudioConverter with our input and output formats.
    156 	// This handles the conversion between audio formats.
    157 	audioConverter, err := NewAudioConverter(&inputDescription, &outputDescription)
    158 	if err != nil {
    159 		return nil, fmt.Errorf("creating audio converter: %w", err)
    160 	}
    161 
    162 	s.converter = audioConverter
    163 
    164 	magicCookieSize, err := inputFile.GetPropertySize(C.kAudioFilePropertyMagicCookieData)
    165 	if err != nil {
    166 		return nil, fmt.Errorf("getting magic cookie property: %w", err)
    167 	}
    168 
    169 	// If a magic cookie exists in the input, set it on the AudioConverter.
    170 	//
    171 	// The magic cookie is a completely opaque piece of data, written and read only
    172 	// by the codec itself. A magic cookie is only present for codecs that require it;
    173 	// this API will return NULL if one does not exist. This API is specific to audio
    174 	// format descriptions, and will return NULL if called with a non-audio format
    175 	// description.
    176 	//
    177 	// https://developer.apple.com/documentation/coremedia/1489508-cmaudioformatdescriptiongetmagic
    178 	if magicCookieSize > 0 {
    179 		magicCookie := make([]byte, 0, magicCookieSize)
    180 
    181 		s.pinner.Pin(unsafe.SliceData(magicCookie))
    182 
    183 		if _, err := inputFile.GetProperty(
    184 			C.kAudioFilePropertyMagicCookieData,
    185 			magicCookieSize,
    186 			unsafe.Pointer(unsafe.SliceData(magicCookie)),
    187 		); err != nil {
    188 			return nil, fmt.Errorf("getting magic cookie: %w", err)
    189 		}
    190 
    191 		if err := audioConverter.SetProperty(
    192 			C.kAudioConverterDecompressionMagicCookie,
    193 			magicCookieSize,
    194 			unsafe.Pointer(unsafe.SliceData(magicCookie)),
    195 		); err != nil {
    196 			return nil, fmt.Errorf("setting magic cookie: %w", err)
    197 		}
    198 	}
    199 
    200 	var maxInputPacketSize C.UInt32
    201 
    202 	if _, err := inputFile.GetProperty(
    203 		C.kAudioFilePropertyMaximumPacketSize,
    204 		C.UInt32(unsafe.Sizeof(maxInputPacketSize)),
    205 		unsafe.Pointer(&maxInputPacketSize),
    206 	); err != nil {
    207 		return nil, fmt.Errorf("getting maximum packet size from input: %w", err)
    208 	}
    209 
    210 	// Allocate the InputContext.
    211 	// This is a structure that we define and use within the [InputDataProc].
    212 	// It gets passed in as a void pointer.
    213 	ic := NewInputContext(
    214 		inputFile,
    215 		inputDescription,
    216 		maxInputPacketSize,
    217 		inputUsesPacketDescriptions,
    218 	)
    219 
    220 	s.pinner.Pin(unsafe.SliceData(ic.mPacketDescriptions))
    221 
    222 	// The converter keeps this caller data across callbacks, so it goes
    223 	// across as a handle rather than as a Go pointer.
    224 	s.icHandle = cgo.NewHandle(ic)
    225 
    226 	s.chunk = make([]byte, int(s.packetsPerChunk)*int(outputDescription.mBytesPerPacket))
    227 	s.pinner.Pin(unsafe.SliceData(s.chunk))
    228 
    229 	s.format = Format{
    230 		SampleRate:     int(outputDescription.mSampleRate),
    231 		Channels:       int(outputDescription.mChannelsPerFrame),
    232 		BytesPerSample: int(outputDescription.mBitsPerChannel / 8),
    233 	}
    234 
    235 	return s, nil
    236 }
    237 
    238 // Format describes the PCM this stream produces, known before any audio
    239 // is read.
    240 func (s *macStream) Format() Format { return s.format }
    241 
    242 // Read fills p with decoded PCM, returning io.EOF once the input is
    243 // exhausted.
    244 func (s *macStream) Read(p []byte) (int, error) {
    245 	if len(p) == 0 {
    246 		return 0, nil
    247 	}
    248 
    249 	s.mu.Lock()
    250 	defer s.mu.Unlock()
    251 
    252 	if s.closed {
    253 		return 0, io.EOF
    254 	}
    255 
    256 	for len(s.pending) == 0 {
    257 		if s.drained {
    258 			return 0, io.EOF
    259 		}
    260 		if err := s.fill(); err != nil {
    261 			return 0, err
    262 		}
    263 	}
    264 
    265 	n := copy(p, s.pending)
    266 	s.pending = s.pending[n:]
    267 
    268 	return n, nil
    269 }
    270 
    271 // fill runs one turn of the converter loop.
    272 func (s *macStream) fill() error {
    273 	numPackets := s.packetsPerChunk
    274 
    275 	// Initialize AudioBufferList with a single buffer because we are
    276 	// working with interleaved PCM samples. mDataByteSize is an in-out
    277 	// variable, and will contain the number of bytes copied to the
    278 	// buffer after the call to FillComplexBuffer.
    279 	abl := C.AudioBufferList{
    280 		mNumberBuffers: 1,
    281 		mBuffers: [1]C.AudioBuffer{{
    282 			mNumberChannels: C.UInt32(s.format.Channels),
    283 			mDataByteSize:   C.UInt32(len(s.chunk)), // in: capacity, out: length
    284 			mData:           unsafe.Pointer(unsafe.SliceData(s.chunk)),
    285 		}},
    286 	}
    287 
    288 	if err := s.converter.FillComplexBuffer(
    289 		(C.AudioConverterComplexInputDataProc)(C.InputDataProc),
    290 		C.handleToPointer(C.uintptr_t(s.icHandle)),
    291 		&numPackets,
    292 		&abl,
    293 		nil,
    294 	); err != nil {
    295 		return fmt.Errorf("filling buffer: %w", err)
    296 	}
    297 
    298 	s.pending = s.chunk[:abl.mBuffers[0].mDataByteSize]
    299 
    300 	// A short fill means the input ran out. This is the same signal the
    301 	// buffered loop used to decide it had reached the end, and the chunk
    302 	// carrying it still holds audio, so it is served before io.EOF.
    303 	if numPackets < s.packetsPerChunk {
    304 		s.drained = true
    305 	}
    306 
    307 	return nil
    308 }
    309 
    310 // Close releases the pipeline. It is safe to call more than once, and
    311 // abandoning a stream before io.EOF is fine as long as it is closed.
    312 func (s *macStream) Close() error {
    313 	s.mu.Lock()
    314 	defer s.mu.Unlock()
    315 
    316 	if s.closed {
    317 		return nil
    318 	}
    319 
    320 	s.closed = true
    321 	s.pending = nil
    322 
    323 	// Order matters here. The converter can call back into the input
    324 	// file, so it is torn down first; the handle those callbacks resolve
    325 	// stays valid until it cannot be reached; and nothing is unpinned
    326 	// while AudioToolbox could still be holding a pointer to it.
    327 	//
    328 	// AudioFile.Dispose is not idempotent, which is what the closed flag
    329 	// above is guarding.
    330 	if s.converter != nil {
    331 		s.converter.Dispose()
    332 	}
    333 	if s.inputFile != nil {
    334 		s.inputFile.Dispose()
    335 	}
    336 	if s.icHandle != 0 {
    337 		s.icHandle.Delete()
    338 	}
    339 
    340 	s.pinner.Unpin()
    341 
    342 	return nil
    343 }
    344 
    345 const (
    346 	_false = C.Boolean(0)
    347 	_true  = C.Boolean(1)
    348 )
    349 
    350 // AudioConverter is responsible for converting between audio formats.
    351 type AudioConverter struct {
    352 	ref      C.AudioConverterRef
    353 	disposed atomic.Bool
    354 }
    355 
    356 func NewAudioConverter(inSourceFormat, inDestinationFormat *C.AudioStreamBasicDescription) (*AudioConverter, error) {
    357 	ac := &AudioConverter{}
    358 	if err := C.AudioConverterNew(inSourceFormat, inDestinationFormat, &ac.ref); err != C.noErr {
    359 		return nil, fmt.Errorf("AudioConverterNew: %v", err)
    360 	}
    361 	return ac, nil
    362 }
    363 
    364 func (ac *AudioConverter) FillComplexBuffer(
    365 	inInputDataProc C.AudioConverterComplexInputDataProc,
    366 	inInputDataProcUserData unsafe.Pointer,
    367 	ioOutputDataPacketSize *C.UInt32,
    368 	outOutputData *C.AudioBufferList,
    369 	outPacketDescription *C.AudioStreamPacketDescription,
    370 ) error {
    371 	if err := C.AudioConverterFillComplexBuffer(
    372 		ac.ref,
    373 		inInputDataProc,
    374 		inInputDataProcUserData,
    375 		ioOutputDataPacketSize,
    376 		outOutputData,
    377 		outPacketDescription,
    378 	); err != C.noErr {
    379 		return fmt.Errorf("AudioConverterFillComplexBuffer: %v", err)
    380 	}
    381 	return nil
    382 }
    383 
    384 func (ac *AudioConverter) SetProperty(
    385 	inPropertyID C.AudioFilePropertyID,
    386 	inDataSize C.UInt32,
    387 	inPropertyData unsafe.Pointer,
    388 ) error {
    389 	if err := C.AudioConverterSetProperty(ac.ref, inPropertyID, inDataSize, inPropertyData); err != C.noErr {
    390 		return fmt.Errorf("AudioConverterSetProperty: %v", err)
    391 	}
    392 	return nil
    393 }
    394 
    395 func (ac *AudioConverter) Dispose() {
    396 	if ac.disposed.Swap(true) {
    397 		return
    398 	}
    399 	if ac.ref != nil {
    400 		C.AudioConverterDispose(ac.ref)
    401 	}
    402 }
    403 
    404 type AudioFile struct {
    405 	id         C.AudioFileID
    406 	nextPacket C.SInt64
    407 
    408 	// data holds the Go buffer this file reads from, when it was opened
    409 	// from one. AudioToolbox keeps the caller data across callbacks, so
    410 	// it cannot be a Go pointer; a handle is an opaque integer the
    411 	// runtime resolves back for us. Disposing the file releases it.
    412 	data cgo.Handle
    413 }
    414 
    415 func OpenAudioFile(path string) (*AudioFile, error) {
    416 	inputFileURL := C.CFURLCreateFromFileSystemRepresentation(
    417 		C.kCFAllocatorDefault,
    418 		(*C.UInt8)(unsafe.Pointer(unsafe.StringData(path))),
    419 		C.long(len(path)),
    420 		_false,
    421 	)
    422 
    423 	runtime.KeepAlive(path)
    424 
    425 	var fileID C.AudioFileID
    426 
    427 	if err := C.AudioFileOpenURL(inputFileURL, C.kAudioFileReadPermission, 0, &fileID); err != C.noErr {
    428 		return nil, fmt.Errorf("AudioFileOpenURL: %v", err)
    429 	}
    430 
    431 	C.CFRelease(C.CFTypeRef(inputFileURL))
    432 
    433 	af := &AudioFile{id: fileID}
    434 
    435 	return af, nil
    436 }
    437 
    438 // OpenAudioFileBuffer allocates an AudioFile that operates on a
    439 // set of callbacks instead of a true file. This can be used to supply
    440 // data from arbitrary sources.
    441 //
    442 // The particular implementation here simply wraps a Go byte slice and
    443 // copies the data into the out buffer.
    444 //
    445 // This could be made lazy by wrapping an [io.Reader] instead.
    446 //
    447 // Make sure that [buf] is pinned.
    448 func OpenAudioFileBuffer(buf []byte) (*AudioFile, error) {
    449 	var outAudioFile C.AudioFileID
    450 
    451 	handle := cgo.NewHandle(buf)
    452 
    453 	// WriteProc and SetSizeProc must be nil, otherwise AudioToolbox considers
    454 	// it a writeable file, which restricts what formats it can accept.
    455 	if err := C.AudioFileOpenWithCallbacks(
    456 		C.handleToPointer(C.uintptr_t(handle)),
    457 		(C.AudioFile_ReadProc)(C.AudioFileReadProcImpl),
    458 		nil,
    459 		(C.AudioFile_GetSizeProc)(C.AudioFileGetSizeProcImpl),
    460 		nil,
    461 		0,
    462 		&outAudioFile,
    463 	); err != C.noErr {
    464 		handle.Delete()
    465 		return nil, fmt.Errorf("AudioFileOpenWithCallbacks: %v", err)
    466 	}
    467 
    468 	return &AudioFile{id: outAudioFile, data: handle}, nil
    469 }
    470 
    471 func (af *AudioFile) ID() C.AudioFileID {
    472 	return af.id
    473 }
    474 
    475 func (af *AudioFile) Dispose() {
    476 	C.AudioFileClose(af.id)
    477 	if af.data != 0 {
    478 		af.data.Delete()
    479 		af.data = 0
    480 	}
    481 }
    482 
    483 func (af *AudioFile) NextPacket() C.SInt64 {
    484 	return af.nextPacket
    485 }
    486 
    487 func (af *AudioFile) ReadPackets(
    488 	ioNumBytes *C.UInt32,
    489 	outPacketDescriptions *C.AudioStreamPacketDescription,
    490 	ioNumPackets *C.UInt32,
    491 	outBuffer unsafe.Pointer,
    492 ) error {
    493 	if err := C.AudioFileReadPacketData(
    494 		af.id,
    495 		_false,
    496 		ioNumBytes,
    497 		outPacketDescriptions,
    498 		af.nextPacket,
    499 		ioNumPackets,
    500 		outBuffer,
    501 	); err != C.noErr {
    502 		return fmt.Errorf("AudioFileReadPacketData: %v", err)
    503 	}
    504 	af.nextPacket += C.SInt64(*ioNumPackets)
    505 	return nil
    506 }
    507 
    508 func (af *AudioFile) WritePackets(
    509 	inNumBytes C.UInt32,
    510 	inPacketDescriptions *C.AudioStreamPacketDescription,
    511 	inNumPackets C.UInt32,
    512 	inBuffer unsafe.Pointer,
    513 ) error {
    514 	if err := C.AudioFileWritePackets(
    515 		af.id,
    516 		_false,
    517 		inNumBytes,
    518 		inPacketDescriptions,
    519 		af.nextPacket,
    520 		&inNumPackets,
    521 		inBuffer,
    522 	); err != C.noErr {
    523 		return fmt.Errorf("AudioFileWritePackets: %v", err)
    524 	}
    525 	af.nextPacket += C.SInt64(inNumPackets)
    526 	return nil
    527 }
    528 
    529 func (af *AudioFile) GetProperty(
    530 	inPropertyID C.AudioFilePropertyID,
    531 	inDataSize C.UInt32,
    532 	outPropertyData unsafe.Pointer,
    533 ) (C.UInt32, error) {
    534 	dataSize := inDataSize
    535 	if err := C.AudioFileGetProperty(af.id, inPropertyID, &dataSize, outPropertyData); err != C.noErr {
    536 		return 0, fmt.Errorf("AudioFileGetProperty: %v", err)
    537 	}
    538 	return dataSize, nil
    539 }
    540 
    541 func (af *AudioFile) GetPropertySize(inPropertyID C.AudioFilePropertyID) (C.UInt32, error) {
    542 	var (
    543 		size       C.UInt32
    544 		isWritable C.UInt32
    545 	)
    546 	if err := C.AudioFileGetPropertyInfo(af.id, inPropertyID, &size, &isWritable); err != C.noErr {
    547 		if err == C.kAudioFileUnsupportedPropertyError {
    548 			return 0, nil
    549 		}
    550 		return 0, fmt.Errorf("AudioFileGetPropertyInfo: %v", err)
    551 	}
    552 	return size, nil
    553 }
    554 
    555 // InputContext is smuggled into the [InputDataProc].
    556 type InputContext struct {
    557 	mInputFile                   *AudioFile
    558 	mMaxInputPacketSize          C.UInt32
    559 	mInputUsesPacketDescriptions C.Boolean
    560 	mInputDescription            C.AudioStreamBasicDescription
    561 	mPacketDescriptions          []C.AudioStreamPacketDescription
    562 }
    563 
    564 func NewInputContext(
    565 	inputFile *AudioFile,
    566 	inputDescription C.AudioStreamBasicDescription,
    567 	maxInputPacketSize C.UInt32,
    568 	inputUsesPacketDescriptions C.Boolean,
    569 ) *InputContext {
    570 	return &InputContext{
    571 		mInputFile:                   inputFile,
    572 		mInputDescription:            inputDescription,
    573 		mMaxInputPacketSize:          maxInputPacketSize,
    574 		mInputUsesPacketDescriptions: inputUsesPacketDescriptions,
    575 		mPacketDescriptions:          make([]C.AudioStreamPacketDescription, 0, 8),
    576 	}
    577 }
    578 
    579 func (ic *InputContext) PacketsRead() C.SInt64 {
    580 	return ic.mInputFile.NextPacket()
    581 }
    582 
    583 // InputDataProc reads audio packets from the input file.
    584 //
    585 //export InputDataProc
    586 func InputDataProc(
    587 	inAudioConverter C.AudioConverterRef,
    588 	ioNumberDataPackets *C.UInt32,
    589 	ioData *C.AudioBufferList,
    590 	outDataPacketDescription **C.AudioStreamPacketDescription,
    591 	inUserData unsafe.Pointer,
    592 ) C.OSStatus {
    593 	ic := cgo.Handle(uintptr(inUserData)).Value().(*InputContext)
    594 
    595 	// Only variable bitrate input carries packet descriptions. Constant
    596 	// bitrate input has none, and there the out-parameter arrives
    597 	// uninitialised, so reading it back to pass along, as this used to,
    598 	// hands the file reader whatever happened to be on the stack.
    599 	//
    600 	// Nothing caught it because the only fixture was AAC, which is
    601 	// variable. The first constant bitrate input, a plain WAV, wedged the
    602 	// converter until the test timeout.
    603 	var packetDescriptions *C.AudioStreamPacketDescription
    604 
    605 	if ic.mInputUsesPacketDescriptions == _true {
    606 		// Cap the number of data packets to the capacity of the slice.
    607 		if int(*ioNumberDataPackets) > cap(ic.mPacketDescriptions) {
    608 			*ioNumberDataPackets = C.UInt32(cap(ic.mPacketDescriptions))
    609 		}
    610 		packetDescriptions = unsafe.SliceData(ic.mPacketDescriptions)
    611 	}
    612 
    613 	if outDataPacketDescription != nil {
    614 		*outDataPacketDescription = packetDescriptions
    615 	}
    616 
    617 	if err := ic.mInputFile.ReadPackets(
    618 		&ioData.mBuffers[0].mDataByteSize,
    619 		packetDescriptions,
    620 		ioNumberDataPackets,
    621 		ioData.mBuffers[0].mData,
    622 	); err != nil {
    623 		return unwrapOSStatus(err)
    624 	}
    625 
    626 	return C.noErr
    627 }
    628 
    629 // AudioFileReadProcImpl copies data from a Go byte slice to the
    630 // output buffer.
    631 //
    632 //export AudioFileReadProcImpl
    633 func AudioFileReadProcImpl(
    634 	inClientData unsafe.Pointer,
    635 	inPosition C.SInt64,
    636 	requestCount C.UInt32,
    637 	buffer unsafe.Pointer,
    638 	actualCount *C.UInt32,
    639 ) C.OSStatus {
    640 	pos := int(inPosition)
    641 	req := int(requestCount)
    642 	end := pos + req
    643 
    644 	inBuf := cgo.Handle(uintptr(inClientData)).Value().([]byte)
    645 
    646 	// Assuming the the out buffer is sized to contain the requested number of bytes.
    647 	// This is not memory we control.
    648 	outBuf := unsafe.Slice((*byte)(buffer), req)
    649 
    650 	dst := outBuf[:req]
    651 
    652 	// The requested amount is allowed to exceed the actual size of the
    653 	// audio data, and a read can start beyond the end of it, so clamp
    654 	// both ends. Clamping only the end would slice with the start past
    655 	// the finish and panic inside a C callback, which takes the process
    656 	// with it.
    657 	if pos < 0 {
    658 		pos = 0
    659 	}
    660 	if pos > len(inBuf) {
    661 		pos = len(inBuf)
    662 	}
    663 	if end > len(inBuf) {
    664 		end = len(inBuf)
    665 	}
    666 	if end < pos {
    667 		end = pos
    668 	}
    669 
    670 	src := inBuf[pos:end]
    671 
    672 	n := copy(dst, src)
    673 
    674 	*actualCount = C.UInt32(n)
    675 
    676 	return C.noErr
    677 }
    678 
    679 //export AudioFileGetSizeProcImpl
    680 func AudioFileGetSizeProcImpl(
    681 	inClientData unsafe.Pointer,
    682 ) C.SInt64 {
    683 	inBuf := cgo.Handle(uintptr(inClientData)).Value().([]byte)
    684 
    685 	// The length, not the capacity. A slice built by append or returned
    686 	// by io.ReadAll usually has room to spare, and reporting that as the
    687 	// file size tells AudioToolbox there is more audio than there is. It
    688 	// then keeps asking for data past the end, gets short reads reported
    689 	// as success, and never stops: a generated WAV hung the decode for
    690 	// five minutes before this was found.
    691 	return C.SInt64(len(inBuf))
    692 }
    693 
    694 // unwrapOSStatus extracts the [OSStatus] from an [error] for conforming to C
    695 // function signatures.
    696 func unwrapOSStatus(err error) C.OSStatus {
    697 	var errOSStatus ErrOSStatus
    698 	if errors.As(err, &errOSStatus) {
    699 		return C.OSStatus(errOSStatus)
    700 	}
    701 	panic(fmt.Errorf("unwrapping OSStatus from %v", err))
    702 }
    703 
    704 type ErrOSStatus C.OSStatus
    705 
    706 func (e ErrOSStatus) Error() string {
    707 	return fmt.Sprintf("%v", C.OSStatus(e))
    708 }
    709 
    710 // openStream decodes incrementally through AudioToolbox.
    711 func openStream(by []byte) (*Stream, error) {
    712 	ms, err := newMacStream(by)
    713 	if err != nil {
    714 		return nil, err
    715 	}
    716 	return &Stream{r: ms, format: ms.Format()}, nil
    717 }
    718 
    719 // openStreamFile buffers the compressed file, which is small next to the
    720 // PCM the stream avoids holding, then decodes it incrementally.
    721 func openStreamFile(path string) (*Stream, error) {
    722 	by, err := os.ReadFile(path)
    723 	if err != nil {
    724 		return nil, fmt.Errorf("reading input file: %w", err)
    725 	}
    726 	return openStream(by)
    727 }