211 lines
6.2 KiB
Go
211 lines
6.2 KiB
Go
//go:build darwin && cgo
|
||
|
||
package aac
|
||
|
||
/*
|
||
#cgo LDFLAGS: -framework AudioToolbox
|
||
#cgo nocallback grdp_aac_converter_new
|
||
#cgo nocallback grdp_aac_decode
|
||
#cgo nocallback AudioConverterDispose
|
||
#cgo noescape grdp_aac_converter_new
|
||
#cgo noescape grdp_aac_decode
|
||
#include <AudioToolbox/AudioToolbox.h>
|
||
#include <stdlib.h>
|
||
|
||
typedef struct {
|
||
const uint8_t *data;
|
||
uint32_t size;
|
||
int consumed;
|
||
} grdp_aac_input_t;
|
||
|
||
// AudioConverter input callback — called by AudioToolbox to pull one AAC
|
||
// packet per invocation. Returns noErr on the first call, then -1 (no data)
|
||
// on subsequent calls to signal end-of-input for the current decode round.
|
||
static OSStatus grdp_aac_input_proc(
|
||
AudioConverterRef inConverter,
|
||
UInt32 *ioNumberDataPackets,
|
||
AudioBufferList *ioData,
|
||
AudioStreamPacketDescription **outDataPacketDescription,
|
||
void *inUserData)
|
||
{
|
||
grdp_aac_input_t *state = (grdp_aac_input_t *)inUserData;
|
||
if (state->consumed || state->size == 0) {
|
||
*ioNumberDataPackets = 0;
|
||
ioData->mBuffers[0].mData = NULL;
|
||
ioData->mBuffers[0].mDataByteSize = 0;
|
||
return -1;
|
||
}
|
||
*ioNumberDataPackets = 1;
|
||
ioData->mBuffers[0].mData = (void *)state->data;
|
||
ioData->mBuffers[0].mDataByteSize = state->size;
|
||
if (outDataPacketDescription != NULL) {
|
||
static AudioStreamPacketDescription desc;
|
||
desc.mStartOffset = 0;
|
||
desc.mDataByteSize = state->size;
|
||
desc.mVariableFramesInPacket = 0;
|
||
*outDataPacketDescription = &desc;
|
||
}
|
||
state->consumed = 1;
|
||
return noErr;
|
||
}
|
||
|
||
// grdp_aac_converter_new creates an AudioConverter that decodes MPEG-4 AAC at
|
||
// the given sample rate / channel count. asc/ascLen is the AudioSpecificConfig
|
||
// stored in the WAVEFORMATEX ExtraData field; it may be NULL/0 for ADTS input.
|
||
static AudioConverterRef grdp_aac_converter_new(
|
||
double sampleRate,
|
||
uint32_t channels,
|
||
const uint8_t *asc,
|
||
uint32_t ascLen,
|
||
OSStatus *outErr)
|
||
{
|
||
AudioStreamBasicDescription inFmt = {
|
||
.mSampleRate = sampleRate,
|
||
.mFormatID = kAudioFormatMPEG4AAC,
|
||
.mChannelsPerFrame = channels,
|
||
};
|
||
AudioStreamBasicDescription outFmt = {
|
||
.mSampleRate = sampleRate,
|
||
.mFormatID = kAudioFormatLinearPCM,
|
||
.mFormatFlags = kLinearPCMFormatFlagIsSignedInteger |
|
||
kLinearPCMFormatFlagIsPacked,
|
||
.mBitsPerChannel = 16,
|
||
.mChannelsPerFrame = channels,
|
||
.mBytesPerFrame = (uint32_t)(2 * channels),
|
||
.mFramesPerPacket = 1,
|
||
.mBytesPerPacket = (uint32_t)(2 * channels),
|
||
};
|
||
AudioConverterRef conv = NULL;
|
||
OSStatus err = AudioConverterNew(&inFmt, &outFmt, &conv);
|
||
if (err != noErr) { *outErr = err; return NULL; }
|
||
if (ascLen > 0 && asc != NULL) {
|
||
err = AudioConverterSetProperty(
|
||
conv,
|
||
kAudioConverterDecompressionMagicCookie,
|
||
ascLen, asc);
|
||
if (err != noErr) {
|
||
AudioConverterDispose(conv);
|
||
*outErr = err;
|
||
return NULL;
|
||
}
|
||
}
|
||
*outErr = noErr;
|
||
return conv;
|
||
}
|
||
|
||
// grdp_aac_decode decodes one AAC packet (inData/inSize) into signed 16-bit
|
||
// little-endian PCM stored in outBuf. *outBytesWritten is set to the number
|
||
// of bytes actually written. Returns noErr on success.
|
||
static OSStatus grdp_aac_decode(
|
||
AudioConverterRef conv,
|
||
const uint8_t *inData,
|
||
uint32_t inSize,
|
||
uint8_t *outBuf,
|
||
uint32_t outBufSize,
|
||
uint32_t *outBytesWritten)
|
||
{
|
||
grdp_aac_input_t state = { inData, inSize, 0 };
|
||
AudioBufferList outList = {
|
||
.mNumberBuffers = 1,
|
||
.mBuffers[0] = {
|
||
.mNumberChannels = 0,
|
||
.mDataByteSize = outBufSize,
|
||
.mData = outBuf,
|
||
},
|
||
};
|
||
// AAC-LC produces 1024 samples/channel per frame; AAC-HE produces 2048.
|
||
// 4096 is large enough for both at up to 2 channels.
|
||
UInt32 numFrames = 4096;
|
||
OSStatus err = AudioConverterFillComplexBuffer(
|
||
conv, grdp_aac_input_proc, &state, &numFrames, &outList, NULL);
|
||
// -1 from the input callback signals "no more data"; that is not an error.
|
||
if (err != noErr && err != -1) {
|
||
*outBytesWritten = 0;
|
||
return err;
|
||
}
|
||
*outBytesWritten = outList.mBuffers[0].mDataByteSize;
|
||
return noErr;
|
||
}
|
||
*/
|
||
import "C"
|
||
|
||
import (
|
||
"fmt"
|
||
"log/slog"
|
||
"runtime"
|
||
"unsafe"
|
||
|
||
"git.zeroonesoft.cn/golib/rdplib/plugin/rdpsnd"
|
||
)
|
||
|
||
// darwinDecoder decodes MPEG-4 AAC audio into signed 16-bit PCM using
|
||
// macOS AudioToolbox (hardware-accelerated on Apple Silicon / Intel iGPU).
|
||
type darwinDecoder struct {
|
||
conv C.AudioConverterRef
|
||
channels int
|
||
outBuf []byte // reusable output scratch buffer
|
||
}
|
||
|
||
func newDecoder(format rdpsnd.AudioFormat) (Decoder, error) {
|
||
var oscErr C.OSStatus
|
||
var ascPtr *C.uint8_t
|
||
ascLen := C.uint32_t(0)
|
||
if len(format.ExtraData) > 0 {
|
||
ascPtr = (*C.uint8_t)(unsafe.Pointer(&format.ExtraData[0]))
|
||
ascLen = C.uint32_t(len(format.ExtraData))
|
||
}
|
||
conv := C.grdp_aac_converter_new(
|
||
C.double(format.SamplesPerSec),
|
||
C.uint32_t(format.Channels),
|
||
ascPtr, ascLen,
|
||
&oscErr)
|
||
if len(format.ExtraData) > 0 {
|
||
runtime.KeepAlive(format.ExtraData)
|
||
}
|
||
if conv == nil {
|
||
return nil, fmt.Errorf("AudioToolbox: AudioConverterNew failed (err=%d)", int32(oscErr))
|
||
}
|
||
slog.Info("AAC decoder created",
|
||
"rate", format.SamplesPerSec,
|
||
"channels", format.Channels,
|
||
"ascLen", len(format.ExtraData))
|
||
// 4096 samples × channels × 2 bytes/sample — large enough for AAC-HE at 2 ch.
|
||
outBufSize := 4096 * int(format.Channels) * 2
|
||
return &darwinDecoder{conv: conv, channels: int(format.Channels), outBuf: make([]byte, outBufSize)}, nil
|
||
}
|
||
|
||
// Decode decodes one raw AAC packet into signed 16-bit little-endian PCM.
|
||
// Returns nil, nil when data is empty.
|
||
func (d *darwinDecoder) Decode(data []byte) ([]byte, error) {
|
||
if len(data) == 0 {
|
||
return nil, nil
|
||
}
|
||
var written C.uint32_t
|
||
err := C.grdp_aac_decode(
|
||
d.conv,
|
||
(*C.uint8_t)(unsafe.Pointer(&data[0])),
|
||
C.uint32_t(len(data)),
|
||
(*C.uint8_t)(unsafe.Pointer(&d.outBuf[0])),
|
||
C.uint32_t(len(d.outBuf)),
|
||
&written)
|
||
runtime.KeepAlive(data)
|
||
runtime.KeepAlive(d.outBuf)
|
||
if err != 0 {
|
||
return nil, fmt.Errorf("AudioToolbox: decode error %d", int32(err))
|
||
}
|
||
if written == 0 {
|
||
return nil, nil
|
||
}
|
||
out := make([]byte, int(written))
|
||
copy(out, d.outBuf[:written])
|
||
return out, nil
|
||
}
|
||
|
||
// Close releases the AudioConverter.
|
||
func (d *darwinDecoder) Close() {
|
||
if d.conv != nil {
|
||
C.AudioConverterDispose(d.conv)
|
||
d.conv = nil
|
||
}
|
||
}
|