diff --git a/client/audiodevs/PipeWire/pipewire.c b/client/audiodevs/PipeWire/pipewire.c index 92791086..5ce064a1 100644 --- a/client/audiodevs/PipeWire/pipewire.c +++ b/client/audiodevs/PipeWire/pipewire.c @@ -156,6 +156,7 @@ static enum spa_audio_format pipewire_sampleFormat( case LG_AUDIO_FMT_S24_LE : return SPA_AUDIO_FORMAT_S24_LE; case LG_AUDIO_FMT_S32_LE : return SPA_AUDIO_FORMAT_S32_LE; case LG_AUDIO_FMT_F32_LE : return SPA_AUDIO_FORMAT_F32_LE; + case LG_AUDIO_FMT_F32_NE : return SPA_AUDIO_FORMAT_F32; case LG_AUDIO_FMT_F64_LE : return SPA_AUDIO_FORMAT_F64_LE; } @@ -170,7 +171,8 @@ static int pipewire_sampleSize(LG_AudioSampleFormat format) case LG_AUDIO_FMT_S16_LE : return 2; case LG_AUDIO_FMT_S24_LE : return 3; case LG_AUDIO_FMT_S32_LE : - case LG_AUDIO_FMT_F32_LE : return 4; + case LG_AUDIO_FMT_F32_LE : + case LG_AUDIO_FMT_F32_NE : return 4; case LG_AUDIO_FMT_F64_LE : return 8; } @@ -278,15 +280,15 @@ static inline void pipewire_updatePlaybackLatency(void) } #if PW_CHECK_VERSION(1, 4, 0) -static bool pipewire_playbackSetRate(double ratio) +static bool pipewire_playbackSetRate(double * ratio) { - if (ratio <= 0.0 || + if (!ratio || *ratio <= 0.0 || !pw.playback.resamplerEnabled) return false; - if (ratio == pw.playback.appliedResampleRatio) + if (*ratio == pw.playback.appliedResampleRatio) return true; - const int result = pw_stream_set_rate(pw.playback.stream, ratio); + const int result = pw_stream_set_rate(pw.playback.stream, *ratio); if (result < 0) { int expected = 0; @@ -296,7 +298,7 @@ static bool pipewire_playbackSetRate(double ratio) return false; } - pw.playback.appliedResampleRatio = ratio; + pw.playback.appliedResampleRatio = *ratio; return true; } @@ -507,6 +509,12 @@ static bool pipewire_playbackSetup(const LG_AudioFormat * format, const int channels = format->channelCount; const int sampleRate = format->sampleRate; + const int sampleSize = + pipewire_sampleSize(format->sampleFormat); + const enum spa_audio_format sampleFormat = + pipewire_sampleFormat(format->sampleFormat); + if (!sampleSize || sampleFormat == SPA_AUDIO_FORMAT_UNKNOWN) + return false; const struct spa_pod * params[1]; uint8_t buffer[1024]; @@ -541,7 +549,7 @@ static bool pipewire_playbackSetup(const LG_AudioFormat * format, requestedPeriodFrames, sampleRate); pw.playback.format = *format; - pw.playback.stride = sizeof(float) * channels; + pw.playback.stride = sampleSize * channels; pw.playback.pullFn = pullFn; pw_thread_loop_lock(pw.thread); @@ -627,7 +635,7 @@ static bool pipewire_playbackSetup(const LG_AudioFormat * format, *startFrames = pw.playback.startFrames; struct spa_audio_info_raw info = - pipewire_audioInfo(format, SPA_AUDIO_FORMAT_F32); + pipewire_audioInfo(format, sampleFormat); params[0] = spa_format_audio_raw_build( &b, SPA_PARAM_EnumFormat, &info); diff --git a/client/audiodevs/PulseAudio/pulseaudio.c b/client/audiodevs/PulseAudio/pulseaudio.c index 59c1c3d6..06d4c5dc 100644 --- a/client/audiodevs/PulseAudio/pulseaudio.c +++ b/client/audiodevs/PulseAudio/pulseaudio.c @@ -40,10 +40,17 @@ struct PulseAudio bool sinkCorked; bool sinkMuted; bool sinkStarting; + bool sinkResamplerEnabled; + bool sinkRateFailed; int sinkMaxPeriodFrames; int sinkStartFrames; LG_AudioFormat sinkFormat; int sinkStride; + uint32_t sinkNominalRate; + uint32_t sinkAppliedRate; + uint32_t sinkPendingRate; + uint32_t sinkRequestedRate; + pa_operation * sinkRateOperation; LG_AudioPullFn sinkPullFn; _Atomic(int64_t) sinkPresentationDeadline; }; @@ -61,6 +68,23 @@ static bool pulseaudio_audioFormatEqual(const LG_AudioFormat * a, a->channelCount * sizeof(*a->channels)) == 0; } +static pa_sample_format_t pulseaudio_sampleFormat( + LG_AudioSampleFormat format) +{ + switch (format) + { + case LG_AUDIO_FMT_U8 : return PA_SAMPLE_U8; + case LG_AUDIO_FMT_S16_LE : return PA_SAMPLE_S16LE; + case LG_AUDIO_FMT_S24_LE : return PA_SAMPLE_S24LE; + case LG_AUDIO_FMT_S32_LE : return PA_SAMPLE_S32LE; + case LG_AUDIO_FMT_F32_LE : return PA_SAMPLE_FLOAT32LE; + case LG_AUDIO_FMT_F32_NE : return PA_SAMPLE_FLOAT32; + case LG_AUDIO_FMT_F64_LE : return PA_SAMPLE_INVALID; + } + + return PA_SAMPLE_INVALID; +} + static pa_channel_position_t pulseaudio_channel( LG_AudioChannel channel, uint8_t index) { @@ -118,6 +142,54 @@ static void pulseaudio_unrefOperation(pa_operation * operation) pa_operation_unref(operation); } +static bool pulseaudio_submitRateUpdate(void); + +static void pulseaudio_rateUpdate_cb(pa_stream * stream, int success, + void * userdata) +{ + if (stream != pa.sink || !pa.sinkRateOperation) + return; + + pa_operation * operation = pa.sinkRateOperation; + pa.sinkRateOperation = NULL; + pa_operation_unref(operation); + + if (!success) + { + DEBUG_ERROR("Failed to update PulseAudio sample rate: %s", + pa_strerror(pa_context_errno(pa.context))); + pa.sinkRateFailed = true; + return; + } + + pa.sinkAppliedRate = pa.sinkPendingRate; + if (pa.sinkCorked) + pulseaudio_submitRateUpdate(); +} + +static bool pulseaudio_submitRateUpdate(void) +{ + if (pa.sinkRateFailed) + return false; + + if (pa.sinkRateOperation || + pa.sinkRequestedRate == pa.sinkAppliedRate) + return true; + + pa.sinkPendingRate = pa.sinkRequestedRate; + pa.sinkRateOperation = pa_stream_update_sample_rate(pa.sink, + pa.sinkPendingRate, pulseaudio_rateUpdate_cb, NULL); + if (!pa.sinkRateOperation) + { + DEBUG_ERROR("Failed to request a PulseAudio sample rate update: %s", + pa_strerror(pa_context_errno(pa.context))); + pa.sinkRateFailed = true; + return false; + } + + return true; +} + static void pulseaudio_sink_input_cb(pa_context *c, const pa_sink_input_info *i, int eol, void *userdata) { @@ -268,9 +340,22 @@ static void pulseaudio_sink_close_nl(void) pa_stream_set_write_callback(pa.sink, NULL, NULL); pa_stream_set_underflow_callback(pa.sink, NULL, NULL); pa_stream_set_overflow_callback(pa.sink, NULL, NULL); + if (pa.sinkRateOperation) + { + pa_operation * operation = pa.sinkRateOperation; + pa.sinkRateOperation = NULL; + pa_operation_cancel(operation); + pa_operation_unref(operation); + } pulseaudio_unrefOperation(pa_stream_flush(pa.sink, NULL, NULL)); pa_stream_unref(pa.sink); pa.sink = NULL; + pa.sinkResamplerEnabled = false; + pa.sinkRateFailed = false; + pa.sinkNominalRate = 0; + pa.sinkAppliedRate = 0; + pa.sinkPendingRate = 0; + pa.sinkRequestedRate = 0; atomic_store_explicit( &pa.sinkPresentationDeadline, 0, memory_order_release); } @@ -340,6 +425,11 @@ static void pulseaudio_write_cb(pa_stream * p, size_t nbytes, void * userdata) return; } + /* Queue rate changes after the current audio block. This keeps the rate + * reported by the preceding pull aligned with the block PulseAudio has + * already received. */ + pulseaudio_submitRateUpdate(); + pa_usec_t latency; int negative; if (pa_stream_get_latency(p, &latency, &negative) == 0) @@ -370,23 +460,24 @@ static bool pulseaudio_setup(const LG_AudioFormat * format, const int channels = format->channelCount; const int sampleRate = format->sampleRate; - if (pa.sink && pulseaudio_audioFormatEqual(&pa.sinkFormat, format)) - { - *maxPeriodFrames = pa.sinkMaxPeriodFrames; - *startFrames = pa.sinkStartFrames; - return true; - } + const pa_sample_format_t sampleFormat = + pulseaudio_sampleFormat(format->sampleFormat); + if (sampleFormat == PA_SAMPLE_INVALID) + return false; pa_sample_spec spec = { - .format = PA_SAMPLE_FLOAT32, + .format = sampleFormat, .rate = sampleRate, .channels = channels }; + if (!pa_sample_spec_valid(&spec)) + return false; + pa_channel_map channelMap = { .channels = channels }; for (uint8_t i = 0; i < format->channelCount; ++i) channelMap.map[i] = pulseaudio_channel(format->channels[i], i); - int stride = channels * sizeof(float); + const int stride = (int)pa_frame_size(&spec); int bufferSize = requestedPeriodFrames * 2 * stride; pa_buffer_attr attribs = { @@ -397,13 +488,36 @@ static bool pulseaudio_setup(const LG_AudioFormat * format, }; pa_threaded_mainloop_lock(pa.loop); + /* pa_stream_update_sample_rate requires protocol version 12. */ + const bool enableResampler = requestResampler && + pa_context_get_server_protocol_version(pa.context) >= 12; + if (pa.sink && !pa.sinkRateFailed && + pa.sinkResamplerEnabled == enableResampler && + !pa.sinkRateOperation && + pa.sinkAppliedRate == pa.sinkNominalRate && + pa.sinkRequestedRate == pa.sinkNominalRate && + pulseaudio_audioFormatEqual(&pa.sinkFormat, format)) + { + *resamplerEnabled = pa.sinkResamplerEnabled; + *maxPeriodFrames = pa.sinkMaxPeriodFrames; + *startFrames = pa.sinkStartFrames; + pa_threaded_mainloop_unlock(pa.loop); + return true; + } + pulseaudio_sink_close_nl(); - pa.sinkFormat = *format; - pa.sinkStride = stride; - pa.sinkPullFn = pullFn; - pa.sinkCorked = true; - pa.sinkStarting = false; + pa.sinkFormat = *format; + pa.sinkStride = stride; + pa.sinkPullFn = pullFn; + pa.sinkCorked = true; + pa.sinkStarting = false; + pa.sinkResamplerEnabled = enableResampler; + pa.sinkRateFailed = false; + pa.sinkNominalRate = sampleRate; + pa.sinkAppliedRate = sampleRate; + pa.sinkPendingRate = sampleRate; + pa.sinkRequestedRate = sampleRate; pa.sink = pa_stream_new( pa.context, "Looking Glass", &spec, &channelMap); @@ -424,7 +538,8 @@ static bool pulseaudio_setup(const LG_AudioFormat * format, PA_STREAM_START_CORKED | PA_STREAM_ADJUST_LATENCY | PA_STREAM_INTERPOLATE_TIMING | - PA_STREAM_AUTO_TIMING_UPDATE; + PA_STREAM_AUTO_TIMING_UPDATE | + (enableResampler ? PA_STREAM_VARIABLE_RATE : 0); if (pa_stream_connect_playback( pa.sink, NULL, &attribs, flags, NULL, NULL) < 0) { @@ -464,8 +579,9 @@ static bool pulseaudio_setup(const LG_AudioFormat * format, pa.sinkMaxPeriodFrames = actualMinRequest; pa.sinkStartFrames = actualTarget; - *maxPeriodFrames = pa.sinkMaxPeriodFrames; - *startFrames = pa.sinkStartFrames; + *maxPeriodFrames = pa.sinkMaxPeriodFrames; + *startFrames = pa.sinkStartFrames; + *resamplerEnabled = pa.sinkResamplerEnabled; atomic_store_explicit( &pa.sinkPresentationDeadline, 0, memory_order_release); @@ -504,6 +620,11 @@ static void pulseaudio_stop(void) pulseaudio_unrefOperation(pa_stream_cork(pa.sink, 1, NULL, NULL)); pa.sinkCorked = true; pa.sinkStarting = false; + if (pa.sinkResamplerEnabled) + { + pa.sinkRequestedRate = pa.sinkNominalRate; + pulseaudio_submitRateUpdate(); + } atomic_store_explicit( &pa.sinkPresentationDeadline, 0, memory_order_release); @@ -539,6 +660,26 @@ static void pulseaudio_mute(bool mute) pa_threaded_mainloop_unlock(pa.loop); } +static bool pulseaudio_setRate(double * ratio) +{ + if (!pa.sink || !pa.sinkResamplerEnabled || + pa.sinkRateFailed || !ratio || !(*ratio > 0.0)) + return false; + + const double requestedRate = pa.sinkNominalRate / *ratio; + if (requestedRate < 1.0 || requestedRate > PA_RATE_MAX) + return false; + + pa.sinkRequestedRate = (uint32_t)llround(requestedRate); + const uint32_t scheduledRate = pa.sinkRateOperation ? + pa.sinkPendingRate : pa.sinkRequestedRate; + if (!scheduledRate) + return false; + + *ratio = (double)pa.sinkNominalRate / scheduledRate; + return true; +} + static uint64_t pulseaudio_latency(void) { const int64_t deadline = atomic_load_explicit( @@ -556,11 +697,12 @@ struct LG_AudioDevOps LGAD_PulseAudio = .free = pulseaudio_free, .playback = { - .setup = pulseaudio_setup, - .start = pulseaudio_start, - .stop = pulseaudio_stop, - .volume = pulseaudio_volume, - .mute = pulseaudio_mute, + .setup = pulseaudio_setup, + .start = pulseaudio_start, + .stop = pulseaudio_stop, + .volume = pulseaudio_volume, + .mute = pulseaudio_mute, + .setRate = pulseaudio_setRate, .latency = pulseaudio_latency } }; diff --git a/client/include/interface/audio.h b/client/include/interface/audio.h index 189de240..90025c86 100644 --- a/client/include/interface/audio.h +++ b/client/include/interface/audio.h @@ -37,6 +37,8 @@ typedef enum LG_AudioSampleFormat /* IEEE 754 little-endian floating-point samples. */ LG_AUDIO_FMT_F32_LE, LG_AUDIO_FMT_F64_LE, + /* Native-endian floating point for client-side processing. */ + LG_AUDIO_FMT_F32_NE, } LG_AudioSampleFormat; diff --git a/client/include/interface/audiodev.h b/client/include/interface/audiodev.h index 1d30c816..548dfec0 100644 --- a/client/include/interface/audiodev.h +++ b/client/include/interface/audiodev.h @@ -46,12 +46,11 @@ struct LG_AudioDevOps struct { - /* setup the stream for playback but don't start it yet, returning false - * if the stream could not be configured. If backend resampling is + /* Setup the stream for playback but don't start it yet, returning false + * if the stream could not be configured. The pull function returns the + * exact interleaved format supplied here. If backend resampling is * requested, resamplerEnabled reports whether it was activated for this - * stream. - * Note: the pull function returns f32 samples - */ + * stream. */ bool (*setup)(const LG_AudioFormat * format, int requestedPeriodFrames, bool requestResampler, bool * resamplerEnabled, int * maxPeriodFrames, int * startFrames, LG_AudioPullFn pullFn); @@ -69,8 +68,10 @@ struct LG_AudioDevOps void (*mute)(bool mute); /* [optional] update the active backend resampler's output/input ratio. - * Called from the backend's playback callback and must be realtime safe. */ - bool (*setRate)(double ratio); + * The backend replaces ratio with the value it selected for the next + * pull. Called from the backend's playback callback and must be realtime + * safe. */ + bool (*setRate)(double * ratio); /* return the current total playback latency in microseconds */ uint64_t (*latency)(void); diff --git a/client/src/audio.c b/client/src/audio.c index 690a826c..f54c2ce0 100644 --- a/client/src/audio.c +++ b/client/src/audio.c @@ -217,6 +217,7 @@ typedef struct int channels; int sampleRate; int stride; + bool convertToFloat; int deviceMaxPeriodFrames; int deviceStartFrames; int targetStartFrames; @@ -288,7 +289,8 @@ static size_t audioSampleSize(LG_AudioSampleFormat format) case LG_AUDIO_FMT_S16_LE: return 2; case LG_AUDIO_FMT_S24_LE: return 3; case LG_AUDIO_FMT_S32_LE: - case LG_AUDIO_FMT_F32_LE: return 4; + case LG_AUDIO_FMT_F32_LE: + case LG_AUDIO_FMT_F32_NE: return 4; case LG_AUDIO_FMT_F64_LE: return 8; } @@ -398,6 +400,10 @@ static bool audioConvertToFloat(float * dst, const void * src, return true; } + case LG_AUDIO_FMT_F32_NE: + memcpy(dst, src, samples * sizeof(*dst)); + return true; + case LG_AUDIO_FMT_F64_LE: { const uint8_t * in = src; @@ -923,7 +929,7 @@ static int playbackPullFrames(uint8_t * dst, int frames) { nextRatio = atomic_load_explicit( &audio.playback.backendResampleRatio, memory_order_acquire); - if (!audio.audioDev->playback.setRate(nextRatio)) + if (!audio.audioDev->playback.setRate(&nextRatio)) { atomic_store_explicit( &audio.playback.backendResamplerFailed, true, @@ -1038,6 +1044,22 @@ static int playbackPullFrames(uint8_t * dst, int frames) return frames; } +static bool playbackSetupDevice(const LG_AudioFormat * format, + int requestedPeriodFrames, bool requestResampler) +{ + audio.playback.backendResampler = false; + audio.playback.deviceMaxPeriodFrames = 0; + audio.playback.deviceStartFrames = 0; + + return audio.audioDev->playback.setup(format, + requestedPeriodFrames, requestResampler, + &audio.playback.backendResampler, + &audio.playback.deviceMaxPeriodFrames, + &audio.playback.deviceStartFrames, playbackPullFrames) && + audio.playback.deviceMaxPeriodFrames > 0 && + audio.playback.deviceStartFrames >= 0; +} + static void playbackStart(const LG_AudioFormat * format, const LG_AudioClock * sourceClock) { @@ -1076,19 +1098,12 @@ static void playbackStart(const LG_AudioFormat * format, if (state != STREAM_STATE_STOP) playbackStop(); - const int bufferFrames = sampleRate; - audio.playback.buffer = ringbuffer_newUnbounded(bufferFrames, - channels * sizeof(float)); - if (!audio.playback.buffer) - return; - audio.playback.format = *format; audio.playback.lastFormat = *format; audio.playback.lastFormatValid = true; audio.playback.channels = channels; audio.playback.sampleRate = sampleRate; - audio.playback.stride = channels * sizeof(float); playbackSetState(STREAM_STATE_SETUP_SOURCE); audio.playback.deviceData.nextPosition = 0; @@ -1142,8 +1157,6 @@ static void playbackStart(const LG_AudioFormat * format, const int requestedPeriodFrames = g_params.audioPeriodSize > 0 ? clamp(g_params.audioPeriodSize, 1, sampleRate) : max(sampleRate / 100, 1); - audio.playback.deviceMaxPeriodFrames = 0; - audio.playback.deviceStartFrames = 0; audio.playback.targetStartFrames = 0; audio.playback.startupLowWaterFrames = 0; audio.playback.startupPacketDeadline = 0; @@ -1151,20 +1164,48 @@ static void playbackStart(const LG_AudioFormat * format, const bool requestBackendResampler = g_params.audioResampler != AUDIO_RESAMPLER_LIBSAMPLERATE; LG_AudioFormat deviceFormat = *format; - deviceFormat.sampleFormat = LG_AUDIO_FMT_F32_LE; - if (!audio.audioDev->playback.setup(&deviceFormat, - requestedPeriodFrames, requestBackendResampler, - &audio.playback.backendResampler, - &audio.playback.deviceMaxPeriodFrames, - &audio.playback.deviceStartFrames, playbackPullFrames) || - audio.playback.deviceMaxPeriodFrames <= 0 || - audio.playback.deviceStartFrames < 0) + + /* The ring generates zero-filled silence. Keep unsigned PCM on the float + * path because its silence level is biased rather than zero. */ + if (!requestBackendResampler || + deviceFormat.sampleFormat == LG_AUDIO_FMT_U8) + deviceFormat.sampleFormat = LG_AUDIO_FMT_F32_NE; + + bool deviceConfigured = playbackSetupDevice( + &deviceFormat, requestedPeriodFrames, requestBackendResampler); + + /* Native samples require the backend to own rate correction. If it cannot, + * reconnect using float samples for the libsamplerate path. This also + * provides a float fallback for formats unsupported by the backend. */ + if ((!deviceConfigured || !audio.playback.backendResampler) && + deviceFormat.sampleFormat != LG_AUDIO_FMT_F32_NE) + { + deviceFormat.sampleFormat = LG_AUDIO_FMT_F32_NE; + deviceConfigured = playbackSetupDevice( + &deviceFormat, requestedPeriodFrames, requestBackendResampler); + } + + if (!deviceConfigured) { DEBUG_ERROR("Failed to configure audio playback device"); playbackStop(); return; } + audio.playback.stride = channels * + audioSampleSize(deviceFormat.sampleFormat); + audio.playback.convertToFloat = + !audio.playback.backendResampler || + deviceFormat.sampleFormat != format->sampleFormat; + + audio.playback.buffer = ringbuffer_newUnbounded( + sampleRate, audio.playback.stride); + if (!audio.playback.buffer) + { + playbackStop(); + return; + } + if (g_params.audioResampler == AUDIO_RESAMPLER_BACKEND && !audio.playback.backendResampler) DEBUG_WARN("%s could not activate backend resampling; " @@ -1291,10 +1332,11 @@ static double computeDevicePosition(int64_t curTime) static bool playbackEnsureConversionBuffers( PlaybackSourceData * sourceData, int frames) { - if (frames > sourceData->framesInSize) + if (audio.playback.convertToFloat && + frames > sourceData->framesInSize) { float * framesIn = realloc(sourceData->framesIn, - (size_t)frames * audio.playback.stride); + (size_t)frames * audio.playback.channels * sizeof(float)); if (!framesIn) { DEBUG_ERROR("Failed to grow playback input buffer"); @@ -1312,7 +1354,7 @@ static bool playbackEnsureConversionBuffers( if (framesOut > sourceData->framesOutSize) { float * output = realloc(sourceData->framesOut, - (size_t)framesOut * audio.playback.stride); + (size_t)framesOut * audio.playback.channels * sizeof(float)); if (!output) { DEBUG_ERROR("Failed to grow playback output buffer"); @@ -1418,13 +1460,19 @@ static void playbackData(const void * data, size_t frameCount, playbackStop(); return; } - if (!audioConvertToFloat(sourceData->framesIn, data, - (size_t)frames * audio.playback.channels, - audio.playback.format.sampleFormat)) + + const void * inputFrames = data; + if (audio.playback.convertToFloat) { - DEBUG_ERROR("Failed to convert playback samples"); - playbackStop(); - return; + if (!audioConvertToFloat(sourceData->framesIn, data, + (size_t)frames * audio.playback.channels, + audio.playback.format.sampleFormat)) + { + DEBUG_ERROR("Failed to convert playback samples"); + playbackStop(); + return; + } + inputFrames = sourceData->framesIn; } bool discontinuity = sourceClock && sourceClock->discontinuity; @@ -1769,7 +1817,7 @@ static void playbackData(const void * data, size_t frameCount, &audio.playback.backendResampleRatio, ratio, memory_order_release); const int outputFrames = - playbackAppendFrames(sourceData, sourceData->framesIn, frames); + playbackAppendFrames(sourceData, inputFrames, frames); sourceData->outputPosition += outputFrames; } else diff --git a/client/src/audio_spice.c b/client/src/audio_spice.c index e7a9f719..310bb88e 100644 --- a/client/src/audio_spice.c +++ b/client/src/audio_spice.c @@ -252,6 +252,7 @@ static size_t sampleSize(LG_AudioSampleFormat format) case LG_AUDIO_FMT_S24_LE: return 3; case LG_AUDIO_FMT_S32_LE: return 4; case LG_AUDIO_FMT_F32_LE: return 4; + case LG_AUDIO_FMT_F32_NE: return 4; case LG_AUDIO_FMT_F64_LE: return 8; }