Merge pull request #19053 from hrydgard/remove-ffmpeg-from-sceatrac

Remove ffmpeg use from the sceAtrac HLE module
This commit is contained in:
Henrik Rydgård 2024-04-13 09:03:18 +02:00 committed by GitHub
commit c3485d7f6a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 74 additions and 348 deletions

View File

@ -29,6 +29,7 @@
#include "Core/Debugger/MemBlockInfo.h" #include "Core/Debugger/MemBlockInfo.h"
#include "Core/HW/MediaEngine.h" #include "Core/HW/MediaEngine.h"
#include "Core/HW/BufferQueue.h" #include "Core/HW/BufferQueue.h"
#include "Core/HW/Atrac3Standalone.h"
#include "Core/HLE/sceKernel.h" #include "Core/HLE/sceKernel.h"
#include "Core/HLE/sceUtility.h" #include "Core/HLE/sceUtility.h"
@ -120,19 +121,6 @@ const size_t overAllocBytes = 16384;
static const int atracDecodeDelay = 2300; static const int atracDecodeDelay = 2300;
#ifdef USE_FFMPEG
extern "C" {
#include "libavformat/avformat.h"
#include "libswresample/swresample.h"
#include "libavutil/samplefmt.h"
#include "libavcodec/avcodec.h"
#include "libavutil/version.h"
}
#include "Core/FFMPEGCompat.h"
#endif // USE_FFMPEG
enum AtracDecodeResult { enum AtracDecodeResult {
ATDECODE_FAILED = -1, ATDECODE_FAILED = -1,
ATDECODE_FEEDME = 0, ATDECODE_FEEDME = 0,
@ -190,9 +178,8 @@ struct Atrac {
} }
void ResetData() { void ResetData() {
#ifdef USE_FFMPEG delete decoder_;
ReleaseFFMPEGContext(); decoder_ = nullptr;
#endif // USE_FFMPEG
if (dataBuf_) if (dataBuf_)
delete [] dataBuf_; delete [] dataBuf_;
@ -391,6 +378,10 @@ struct Atrac {
} }
int atracID_ = -1; int atracID_ = -1;
// TODO: Save the internal state of this, now technically possible.
AudioDecoder *decoder_ = nullptr;
u8 *dataBuf_ = nullptr; u8 *dataBuf_ = nullptr;
u32 decodePos_ = 0; u32 decodePos_ = 0;
@ -430,48 +421,10 @@ struct Atrac {
PSPPointer<SceAtracId> context_; PSPPointer<SceAtracId> context_;
#ifdef USE_FFMPEG
AVCodecContext *codecCtx_ = nullptr;
SwrContext *swrCtx_ = nullptr;
AVFrame *frame_ = nullptr;
AVPacket *packet_ = nullptr;
#endif // USE_FFMPEG
#ifdef USE_FFMPEG
void ReleaseFFMPEGContext() {
// All of these allow null pointers.
av_freep(&frame_);
swr_free(&swrCtx_);
#if LIBAVCODEC_VERSION_INT >= AV_VERSION_INT(55, 52, 0)
// If necessary, extradata is automatically freed.
avcodec_free_context(&codecCtx_);
#else
// Future versions may add other things to free, but avcodec_free_context didn't exist yet here.
// Some old versions crash when we try to free extradata and subtitle_header, so let's not. A minor
// leak is better than a segfault.
// av_freep(&codecCtx_->extradata);
// av_freep(&codecCtx_->subtitle_header);
avcodec_close(codecCtx_);
av_freep(&codecCtx_);
#endif
#if LIBAVCODEC_VERSION_INT >= AV_VERSION_INT(57, 12, 100)
av_packet_free(&packet_);
#else
av_free_packet(packet_);
delete packet_;
packet_ = nullptr;
#endif
}
#endif // USE_FFMPEG
void ForceSeekToSample(int sample) { void ForceSeekToSample(int sample) {
#ifdef USE_FFMPEG if (decoder_) {
avcodec_flush_buffers(codecCtx_); decoder_->FlushBuffers();
}
// Discard any pending packet data.
packet_->size = 0;
#endif
currentSample_ = sample; currentSample_ = sample;
} }
@ -480,18 +433,14 @@ struct Atrac {
} }
void SeekToSample(int sample) { void SeekToSample(int sample) {
#ifdef USE_FFMPEG
// Discard any pending packet data.
packet_->size = 0;
// It seems like the PSP aligns the sample position to 0x800...? // It seems like the PSP aligns the sample position to 0x800...?
const u32 offsetSamples = firstSampleOffset_ + FirstOffsetExtra(); const u32 offsetSamples = firstSampleOffset_ + FirstOffsetExtra();
const u32 unalignedSamples = (offsetSamples + sample) % SamplesPerFrame(); const u32 unalignedSamples = (offsetSamples + sample) % SamplesPerFrame();
int seekFrame = sample + offsetSamples - unalignedSamples; int seekFrame = sample + offsetSamples - unalignedSamples;
if ((sample != currentSample_ || sample == 0) && codecCtx_ != nullptr) { if ((sample != currentSample_ || sample == 0) && decoder_ != nullptr) {
// Prefill the decode buffer with packets before the first sample offset. // Prefill the decode buffer with packets before the first sample offset.
avcodec_flush_buffers(codecCtx_); decoder_->FlushBuffers();
int adjust = 0; int adjust = 0;
if (sample == 0) { if (sample == 0) {
@ -501,17 +450,11 @@ struct Atrac {
const u32 off = FileOffsetBySample(sample + adjust); const u32 off = FileOffsetBySample(sample + adjust);
const u32 backfill = bytesPerFrame_ * 2; const u32 backfill = bytesPerFrame_ * 2;
const u32 start = off - dataOff_ < backfill ? dataOff_ : off - backfill; const u32 start = off - dataOff_ < backfill ? dataOff_ : off - backfill;
for (u32 pos = start; pos < off; pos += bytesPerFrame_) {
av_init_packet(packet_);
packet_->data = BufferStart() + pos;
packet_->size = bytesPerFrame_;
packet_->pos = pos;
// Process the packet, we don't care about success. for (u32 pos = start; pos < off; pos += bytesPerFrame_) {
DecodePacket(); decoder_->Decode(BufferStart() + pos, bytesPerFrame_, nullptr, nullptr, nullptr);
} }
} }
#endif // USE_FFMPEG
currentSample_ = sample; currentSample_ = sample;
} }
@ -525,85 +468,6 @@ struct Atrac {
return 0; return 0;
} }
bool FillPacket(int adjust = 0) {
u32 off = FileOffsetBySample(currentSample_ + adjust);
if (off < first_.size) {
#ifdef USE_FFMPEG
av_init_packet(packet_);
packet_->data = BufferStart() + off;
packet_->size = std::min((u32)bytesPerFrame_, first_.size - off);
packet_->pos = off;
#endif // USE_FFMPEG
return true;
} else {
return false;
}
return true;
}
AtracDecodeResult DecodeLowLevelPacket(u8 *ptr) {
#ifdef USE_FFMPEG
av_init_packet(packet_);
packet_->data = ptr;
packet_->size = bytesPerFrame_;
packet_->pos = 0;
#endif // USE_FFMPEG
return DecodePacket();
}
AtracDecodeResult DecodePacket() {
#ifdef USE_FFMPEG
if (codecCtx_ == nullptr) {
return ATDECODE_FAILED;
}
int got_frame = 0;
#if LIBAVCODEC_VERSION_INT >= AV_VERSION_INT(57, 48, 101)
if (packet_->size != 0) {
int err = avcodec_send_packet(codecCtx_, packet_);
if (err < 0) {
ERROR_LOG_REPORT(ME, "avcodec_send_packet: Error decoding audio %d / %08x", err, err);
failedDecode_ = true;
return ATDECODE_FAILED;
}
}
int err = avcodec_receive_frame(codecCtx_, frame_);
int bytes_read = 0;
if (err >= 0) {
bytes_read = frame_->pkt_size;
got_frame = 1;
} else if (err != AVERROR(EAGAIN)) {
bytes_read = err;
}
#else
int bytes_read = avcodec_decode_audio4(codecCtx_, frame_, &got_frame, packet_);
#endif
#if LIBAVCODEC_VERSION_INT >= AV_VERSION_INT(57, 12, 100)
av_packet_unref(packet_);
#else
av_free_packet(packet_);
#endif
if (bytes_read == AVERROR_PATCHWELCOME) {
ERROR_LOG(ME, "Unsupported feature in ATRAC audio.");
// Let's try the next packet.
packet_->size = 0;
return ATDECODE_BADFRAME;
} else if (bytes_read < 0) {
ERROR_LOG_REPORT(ME, "avcodec_decode_audio4: Error decoding audio %d / %08x", bytes_read, bytes_read);
failedDecode_ = true;
return ATDECODE_FAILED;
}
return got_frame ? ATDECODE_GOTFRAME : ATDECODE_FEEDME;
#else
return ATDECODE_BADFRAME;
#endif // USE_FFMPEG
}
void CalculateStreamInfo(u32 *readOffset); void CalculateStreamInfo(u32 *readOffset);
u32 StreamBufferEnd() const { u32 StreamBufferEnd() const {
@ -663,15 +527,6 @@ void __AtracInit() {
atracIDTypes[3] = PSP_MODE_AT_3; atracIDTypes[3] = PSP_MODE_AT_3;
atracIDTypes[4] = 0; atracIDTypes[4] = 0;
atracIDTypes[5] = 0; atracIDTypes[5] = 0;
#ifdef USE_FFMPEG
#if LIBAVFORMAT_VERSION_INT < AV_VERSION_INT(58, 18, 100)
avcodec_register_all();
#endif
#if LIBAVFORMAT_VERSION_INT < AV_VERSION_INT(58, 12, 100)
av_register_all();
#endif
#endif // USE_FFMPEG
} }
void __AtracLoadModule(int version, u32 crc) { void __AtracLoadModule(int version, u32 crc) {
@ -1252,29 +1107,26 @@ u32 _AtracDecodeData(int atracID, u8 *outbuf, u32 outbufPtr, u32 *SamplesNum, u3
atrac->SeekToSample(atrac->currentSample_); atrac->SeekToSample(atrac->currentSample_);
AtracDecodeResult res = ATDECODE_FEEDME; AtracDecodeResult res = ATDECODE_FEEDME;
while (atrac->FillPacket(-skipSamples)) { u32 off = atrac->FileOffsetBySample(atrac->currentSample_ - skipSamples);
uint32_t packetAddr = atrac->CurBufferAddress(-skipSamples); if (off < atrac->first_.size) {
#ifdef USE_FFMPEG uint8_t *indata = atrac->BufferStart() + off;
int packetSize = atrac->packet_->size; int bytesConsumed = 0;
#endif // USE_FFMPEG int outBytes = 0;
res = atrac->DecodePacket(); if (!atrac->decoder_->Decode(indata, atrac->bytesPerFrame_, &bytesConsumed,
if (res == ATDECODE_FEEDME) { outbuf, &outBytes)) {
continue; // Decode failed.
} else if (res == ATDECODE_FAILED) {
*SamplesNum = 0; *SamplesNum = 0;
*finish = 1; *finish = 1;
return ATRAC_ERROR_ALL_DATA_DECODED; return ATRAC_ERROR_ALL_DATA_DECODED;
} else if (res == ATDECODE_BADFRAME) {
// Retry next time.
break;
} }
_dbg_assert_(res == ATDECODE_GOTFRAME); res = ATDECODE_GOTFRAME;
#ifdef USE_FFMPEG
// got a frame
int skipped = std::min(skipSamples, atrac->frame_->nb_samples);
skipSamples -= skipped;
numSamples = atrac->frame_->nb_samples - skipped;
numSamples = outBytes / 4;
uint32_t packetAddr = atrac->CurBufferAddress(-skipSamples);
// got a frame
int skipped = std::min((u32)skipSamples, numSamples);
skipSamples -= skipped;
numSamples = numSamples - skipped;
// If we're at the end, clamp to samples we want. It always returns a full chunk. // If we're at the end, clamp to samples we want. It always returns a full chunk.
numSamples = std::min(maxSamples, numSamples); numSamples = std::min(maxSamples, numSamples);
@ -1283,38 +1135,15 @@ u32 _AtracDecodeData(int atracID, u8 *outbuf, u32 outbufPtr, u32 *SamplesNum, u3
res = ATDECODE_FEEDME; res = ATDECODE_FEEDME;
} }
if (outbuf != NULL && numSamples != 0) { if (packetAddr != 0 && MemBlockInfoDetailed()) {
int inbufOffset = 0; char tagData[128];
if (skipped != 0) { size_t tagSize = FormatMemWriteTagAt(tagData, sizeof(tagData), "AtracDecode/", packetAddr, atrac->bytesPerFrame_);
AVSampleFormat fmt = (AVSampleFormat)atrac->frame_->format; NotifyMemInfo(MemBlockFlags::READ, packetAddr, atrac->bytesPerFrame_, tagData, tagSize);
// We want the offset per channel. NotifyMemInfo(MemBlockFlags::WRITE, outbufPtr, outBytes, tagData, tagSize);
inbufOffset = av_samples_get_buffer_size(NULL, 1, skipped, fmt, 1); } else {
} NotifyMemInfo(MemBlockFlags::WRITE, outbufPtr, outBytes, "AtracDecode");
u8 *out = outbuf;
const u8 *inbuf[2] = {
atrac->frame_->extended_data[0] + inbufOffset,
atrac->frame_->extended_data[1] + inbufOffset,
};
int avret = swr_convert(atrac->swrCtx_, &out, numSamples, inbuf, numSamples);
if (outbufPtr != 0) {
u32 outBytes = numSamples * atrac->outputChannels_ * sizeof(s16);
if (packetAddr != 0 && MemBlockInfoDetailed()) {
char tagData[128];
size_t tagSize = FormatMemWriteTagAt(tagData, sizeof(tagData), "AtracDecode/", packetAddr, packetSize);
NotifyMemInfo(MemBlockFlags::READ, packetAddr, packetSize, tagData, tagSize);
NotifyMemInfo(MemBlockFlags::WRITE, outbufPtr, outBytes, tagData, tagSize);
} else {
NotifyMemInfo(MemBlockFlags::WRITE, outbufPtr, outBytes, "AtracDecode");
}
}
if (avret < 0) {
ERROR_LOG(ME, "swr_convert: Error while converting %d", avret);
}
} }
// We only want one frame per call, let's continue the next time. // We only want one frame per call, let's continue the next time.
break;
#endif // USE_FFMPEG
} }
if (res != ATDECODE_GOTFRAME && atrac->currentSample_ < atrac->endSample_) { if (res != ATDECODE_GOTFRAME && atrac->currentSample_ < atrac->endSample_) {
@ -1812,116 +1641,33 @@ static u32 sceAtracResetPlayPosition(int atracID, int sample, int bytesWrittenFi
return hleDelayResult(hleLogSuccessInfoI(ME, 0), "reset play pos", 3000); return hleDelayResult(hleLogSuccessInfoI(ME, 0), "reset play pos", 3000);
} }
#ifdef USE_FFMPEG // extraData should be 14 zeroed bytes.
static int __AtracUpdateOutputMode(Atrac *atrac, int wanted_channels) { void InitAT3ExtraData(Atrac *atrac, uint8_t *extraData) {
if (atrac->swrCtx_ && atrac->outputChannels_ == wanted_channels) // We don't pull this from the RIFF so that we can support OMA also.
return 0;
atrac->outputChannels_ = wanted_channels;
int64_t wanted_channel_layout = av_get_default_channel_layout(wanted_channels);
int64_t dec_channel_layout = av_get_default_channel_layout(atrac->channels_);
atrac->swrCtx_ = // The only thing that changes are the jointStereo_ values.
swr_alloc_set_opts extraData[0] = 1;
( extraData[3] = atrac->channels_ << 3;
atrac->swrCtx_, extraData[6] = atrac->jointStereo_;
wanted_channel_layout, extraData[8] = atrac->jointStereo_;
AV_SAMPLE_FMT_S16, extraData[10] = 1;
atrac->codecCtx_->sample_rate,
dec_channel_layout,
atrac->codecCtx_->sample_fmt,
atrac->codecCtx_->sample_rate,
0,
NULL
);
if (!atrac->swrCtx_) {
ERROR_LOG(ME, "swr_alloc_set_opts: Could not allocate resampler context");
return -1;
}
if (swr_init(atrac->swrCtx_) < 0) {
ERROR_LOG(ME, "swr_init: Failed to initialize the resampling context");
return -1;
}
return 0;
} }
#endif // USE_FFMPEG
int __AtracSetContext(Atrac *atrac) { int __AtracSetContext(Atrac *atrac) {
#ifdef USE_FFMPEG if (atrac->decoder_) {
InitFFmpeg(); delete atrac->decoder_;
}
AVCodecID ff_codec; // First, init the standalone decoder. Only used for low-level-decode initially, but simple.
if (atrac->codecType_ == PSP_MODE_AT_3) { if (atrac->codecType_ == PSP_MODE_AT_3) {
ff_codec = AV_CODEC_ID_ATRAC3; uint8_t extraData[14]{};
} else if (atrac->codecType_ == PSP_MODE_AT_3_PLUS) { InitAT3ExtraData(atrac, extraData);
ff_codec = AV_CODEC_ID_ATRAC3P; atrac->decoder_ = CreateAtrac3Audio(atrac->channels_, atrac->bytesPerFrame_, extraData, sizeof(extraData));
} else { } else {
return hleReportError(ME, ATRAC_ERROR_UNKNOWN_FORMAT, "unknown codec type in set context"); atrac->decoder_ = CreateAtrac3PlusAudio(atrac->channels_, atrac->bytesPerFrame_);
} }
if (atrac->codecCtx_) {
// Shouldn't happen, but just in case.
atrac->ReleaseFFMPEGContext();
}
AVCodec *codec = avcodec_find_decoder(ff_codec);
atrac->codecCtx_ = avcodec_alloc_context3(codec);
if (atrac->codecType_ == PSP_MODE_AT_3) {
// For ATRAC3, we need the "extradata" in the RIFF header.
atrac->codecCtx_->extradata = (uint8_t *)av_mallocz(14);
atrac->codecCtx_->extradata_size = 14;
// We don't pull this from the RIFF so that we can support OMA also.
// The only thing that changes are the jointStereo_ values.
atrac->codecCtx_->extradata[0] = 1;
atrac->codecCtx_->extradata[3] = atrac->channels_ << 3;
atrac->codecCtx_->extradata[6] = atrac->jointStereo_;
atrac->codecCtx_->extradata[8] = atrac->jointStereo_;
atrac->codecCtx_->extradata[10] = 1;
}
// Appears we need to force mono in some cases. (See CPkmn's comments in issue #4248)
if (atrac->channels_ == 1) {
atrac->codecCtx_->channels = 1;
atrac->codecCtx_->channel_layout = AV_CH_LAYOUT_MONO;
} else if (atrac->channels_ == 2) {
atrac->codecCtx_->channels = 2;
atrac->codecCtx_->channel_layout = AV_CH_LAYOUT_STEREO;
} else {
return hleReportError(ME, ATRAC_ERROR_UNKNOWN_FORMAT, "unknown channel layout in set context");
}
// Explicitly set the block_align value (needed by newer FFmpeg versions, see #5772.)
if (atrac->codecCtx_->block_align == 0) {
atrac->codecCtx_->block_align = atrac->bytesPerFrame_;
}
// Only one supported, it seems?
atrac->codecCtx_->sample_rate = 44100;
atrac->codecCtx_->request_sample_fmt = AV_SAMPLE_FMT_S16;
int ret;
if ((ret = avcodec_open2(atrac->codecCtx_, codec, nullptr)) < 0) {
// This can mean that the frame size is wrong or etc.
return hleLogError(ME, ATRAC_ERROR_BAD_CODEC_PARAMS, "failed to open decoder %d", ret);
}
if ((ret = __AtracUpdateOutputMode(atrac, atrac->outputChannels_)) < 0)
return hleLogError(ME, ret, "failed to set the output mode");
// alloc audio frame
atrac->frame_ = av_frame_alloc();
#if LIBAVCODEC_VERSION_INT >= AV_VERSION_INT(57, 12, 100)
atrac->packet_ = av_packet_alloc();
#else
atrac->packet_ = new AVPacket;
av_init_packet(atrac->packet_);
atrac->packet_->data = nullptr;
atrac->packet_->size = 0;
#endif
// reinit decodePos, because ffmpeg had changed it. // reinit decodePos, because ffmpeg had changed it.
atrac->decodePos_ = 0; atrac->decodePos_ = 0;
#endif
return 0; return 0;
} }
@ -2321,11 +2067,6 @@ static int sceAtracSetAA3DataAndGetID(u32 buffer, u32 bufferSize, u32 fileSize,
int _AtracGetIDByContext(u32 contextAddr) { int _AtracGetIDByContext(u32 contextAddr) {
int atracID = (int)Memory::Read_U32(contextAddr + 0xfc); int atracID = (int)Memory::Read_U32(contextAddr + 0xfc);
#ifdef USE_FFMPEG
Atrac *atrac = getAtrac(atracID);
if (atrac)
__AtracUpdateOutputMode(atrac, 1);
#endif // USE_FFMPEG
return atracID; return atracID;
} }
@ -2466,9 +2207,6 @@ static int sceAtracLowLevelInitDecoder(int atracID, u32 paramsAddr) {
atrac->first_.size = 0; atrac->first_.size = 0;
atrac->first_.filesize = atrac->bytesPerFrame_; atrac->first_.filesize = atrac->bytesPerFrame_;
atrac->bufferState_ = ATRAC_STATUS_LOW_LEVEL; atrac->bufferState_ = ATRAC_STATUS_LOW_LEVEL;
atrac->dataBuf_ = new u8[atrac->first_.filesize + overAllocBytes];
memset(atrac->dataBuf_, 0, atrac->first_.filesize + overAllocBytes);
atrac->currentSample_ = 0; atrac->currentSample_ = 0;
int ret = __AtracSetContext(atrac); int ret = __AtracSetContext(atrac);
@ -2500,30 +2238,13 @@ static int sceAtracLowLevelDecode(int atracID, u32 sourceAddr, u32 sourceBytesCo
} }
int numSamples = (atrac->codecType_ == PSP_MODE_AT_3_PLUS ? ATRAC3PLUS_MAX_SAMPLES : ATRAC3_MAX_SAMPLES); int numSamples = (atrac->codecType_ == PSP_MODE_AT_3_PLUS ? ATRAC3PLUS_MAX_SAMPLES : ATRAC3_MAX_SAMPLES);
int bytesConsumed = 0;
int bytesWritten = 0;
atrac->decoder_->Decode(srcp, atrac->bytesPerFrame_, &bytesConsumed, outp, &bytesWritten);
*srcConsumed = bytesConsumed;
*outWritten = bytesWritten;
if (!atrac->failedDecode_) { NotifyMemInfo(MemBlockFlags::WRITE, samplesAddr, bytesWritten, "AtracLowLevelDecode");
AtracDecodeResult res = atrac->DecodeLowLevelPacket(srcp);
if (res == ATDECODE_GOTFRAME) {
#ifdef USE_FFMPEG
// got a frame
numSamples = atrac->frame_->nb_samples;
u8 *out = outp;
int avret = swr_convert(atrac->swrCtx_, &out, numSamples,
(const u8**)atrac->frame_->extended_data, numSamples);
u32 outBytes = numSamples * atrac->outputChannels_ * sizeof(s16);
NotifyMemInfo(MemBlockFlags::WRITE, samplesAddr, outBytes, "AtracLowLevelDecode");
if (avret < 0) {
ERROR_LOG(ME, "swr_convert: Error while converting %d", avret);
}
#endif // USE_FFMPEG
} else {
// TODO: Error code otherwise?
}
}
*outWritten = numSamples * atrac->outputChannels_ * sizeof(s16);
*srcConsumed = atrac->bytesPerFrame_;
return hleLogDebug(ME, hleDelayResult(0, "low level atrac decode data", atracDecodeDelay)); return hleLogDebug(ME, hleDelayResult(0, "low level atrac decode data", atracDecodeDelay));
} }

View File

@ -75,20 +75,25 @@ public:
*outbytes = 0; *outbytes = 0;
return false; return false;
} }
*inbytesConsumed = result; if (inbytesConsumed) {
*inbytesConsumed = result;
}
outSamples_ = nb_samples; outSamples_ = nb_samples;
if (nb_samples > 0) { if (nb_samples > 0) {
*outbytes = nb_samples * 2 * 2; if (outbytes) {
*outbytes = nb_samples * 2 * 2;
// Convert frame to outbuf. TODO: Very SIMDable, though hardly hot. }
for (int channel = 0; channel < 2; channel++) { if (outbuf) {
int16_t *output = (int16_t *)outbuf; // Convert frame to outbuf. TODO: Very SIMDable, though hardly hot.
for (int i = 0; i < nb_samples; i++) { for (int channel = 0; channel < 2; channel++) {
output[i * 2] = clamp16(buffers_[0][i]); int16_t *output = (int16_t *)outbuf;
output[i * 2 + 1] = clamp16(buffers_[1][i]); for (int i = 0; i < nb_samples; i++) {
output[i * 2] = clamp16(buffers_[0][i]);
output[i * 2 + 1] = clamp16(buffers_[1][i]);
}
} }
} }
} else { } else if (outbytes) {
*outbytes = 0; *outbytes = 0;
} }
return true; return true;