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 }