Files
UnrealEngine/Engine/Source/Runtime/SignalProcessing/Private/OggVorbisEncoder.cpp
2025-05-18 13:04:45 +08:00

214 lines
6.1 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "DSP/Encoders/OggVorbisEncoder.h"
#if PLATFORM_SUPPORTS_VORBIS_CODEC
#pragma pack(push, 8)
#include "vorbis/vorbisenc.h"
#include "vorbis/vorbisfile.h"
#pragma pack(pop)
struct FOggVorbisEncoderPrivateState
{
ogg_stream_state StreamState;
ogg_page CurrentPage;
ogg_packet CurrentPacket;
vorbis_info BitstreamSettings;
vorbis_dsp_state DspState;
vorbis_block CurrentBlock;
FOggVorbisEncoderPrivateState(const FSoundQualityInfo& InInfo)
{
vorbis_info_init(&BitstreamSettings);
if (vorbis_encode_init_vbr(&BitstreamSettings, InInfo.NumChannels, InInfo.SampleRate, ((float)InInfo.Quality) / 100.0f))
{
UE_LOG(LogTemp, Warning, TEXT(" Error initializing Ogg Vorbis encoder!"));
}
// Init analyzer:
vorbis_analysis_init(&DspState, &BitstreamSettings);
// Init the current block:
vorbis_block_init(&DspState, &CurrentBlock);
// Init stream encoder with null serial number:
ogg_stream_init(&StreamState, 0);
}
~FOggVorbisEncoderPrivateState()
{
ogg_stream_clear(&StreamState);
vorbis_block_clear(&CurrentBlock);
vorbis_dsp_clear(&DspState);
vorbis_info_clear(&BitstreamSettings);
}
void PushPacket(ogg_packet& InPacket)
{
int32 Result = ogg_stream_packetin(&StreamState, &InPacket);
ensureAlwaysMsgf(Result == 0, TEXT("Pushing packet to the Ogg Stream failed. Make sure Ogg Stream was properly initialized."));
}
// Pop all pages available to DataToAppendTo
void PopPages(TArray<uint8>& DataToAppendTo)
{
// Serialize out ogg pages until we get the EOS page.
do
{
int32 BytesWritten = ogg_stream_pageout(&StreamState, &CurrentPage);
// If ogg_stream_pageout returned 0, there are no pages left to pop.
// Otherwise, append this page to DataToAppendTo.
if (BytesWritten == 0)
{
break;
}
else
{
DataToAppendTo.Append((uint8*)CurrentPage.header, CurrentPage.header_len);
DataToAppendTo.Append((uint8*)CurrentPage.body, CurrentPage.body_len);
}
} while (!ogg_page_eos(&CurrentPage));
}
// Similar to PopPages, but will ensure that the next packet we push will be on
// a fresh page.
void FlushPages(TArray<uint8>& DataToAppendTo)
{
while (true)
{
// Flush stream to page:
int32 BytesPopped = ogg_stream_flush(&StreamState, &CurrentPage);
// If the stream is finished, exit. Otherwise, append the page to DataToAppendTo.
if (!BytesPopped)
{
return;
}
else
{
DataToAppendTo.Append((uint8*)CurrentPage.header, CurrentPage.header_len);
DataToAppendTo.Append((uint8*)CurrentPage.body, CurrentPage.body_len);
}
}
}
private:
FOggVorbisEncoderPrivateState();
};
FOggVorbisEncoder::FOggVorbisEncoder(const FSoundQualityInfo& InInfo, int32 AverageBufferCallbackSize)
: Audio::IAudioEncoder(AverageBufferCallbackSize * 4, 65536 * 4) // Vorbis ogg pages can be relatively large- up to 256 kb.
, NumChannels(InInfo.NumChannels)
, PrivateState(nullptr)
{
Init(InInfo);
}
int32 FOggVorbisEncoder::GetCompressedPacketSize() const
{
// For now, we return 0 because we are not able to chunk Ogg Vorbis streams
// into independent chunks.
return 0;
}
int64 FOggVorbisEncoder::SamplesRequiredPerEncode() const
{
// Based on AudioFormatOgg.cpp- there we typically analyze 1024 samples at a time before encoding.
return 1024;
}
bool FOggVorbisEncoder::StartFile(const FSoundQualityInfo& InQualityInfo, TArray<uint8>& OutFileStart)
{
check(OutFileStart.Num() == 0);
check(PrivateState == nullptr);
// Init all state:
PrivateState = new FOggVorbisEncoderPrivateState(InQualityInfo);
// Create a new comment to insert at the beginning of the file:
vorbis_comment EncoderComment;
vorbis_comment_init(&EncoderComment);
vorbis_comment_add_tag(&EncoderComment, "ENCODER", "UnrealEngine4Runtime");
// Generate headers:
ogg_packet HeaderPacket;
ogg_packet CommHeaderPacket;
ogg_packet CodeHeaderPacket;
vorbis_analysis_headerout(&PrivateState->DspState, &EncoderComment, &HeaderPacket, &CommHeaderPacket, &CodeHeaderPacket);
// Clean up comment.
vorbis_comment_clear(&EncoderComment);
// Push header packets to Ogg stream:
PrivateState->PushPacket(HeaderPacket);
PrivateState->PushPacket(CommHeaderPacket);
PrivateState->PushPacket(CodeHeaderPacket);
// We need to start the actual Vorbis on a fresh page, so
// serialize out the Ogg pages required for the header and then flush:
PrivateState->FlushPages(OutFileStart);
return true;
}
bool FOggVorbisEncoder::EncodeChunk(const TArray<float>& InAudio, TArray<uint8>& OutBytes)
{
check(InAudio.Num() <= 1024);
check(PrivateState != nullptr);
// First, we have to analyze our input buffer:
const int32 NumFrames = InAudio.Num() / NumChannels;
float** AnalysisBuffer = vorbis_analysis_buffer(&PrivateState->DspState, NumFrames);
// Deinterleave for Ogg Vorbis encoder:
for (int32 FrameIndex = 0; FrameIndex < NumFrames; FrameIndex++)
{
for (int32 ChannelIndex = 0; ChannelIndex < NumChannels; ChannelIndex++)
{
AnalysisBuffer[ChannelIndex][FrameIndex] = InAudio[FrameIndex * NumChannels + ChannelIndex];
}
}
vorbis_analysis_wrote(&PrivateState->DspState, NumFrames);
// Separate AnalysisBuffer into separate blocks, then chunk those blocks into Ogg pages.
while (vorbis_analysis_blockout(&PrivateState->DspState, &PrivateState->CurrentBlock) == 1)
{
// Perform actual analysis:
vorbis_analysis(&PrivateState->CurrentBlock, NULL);
// Then determine the bitrate on this block.
vorbis_bitrate_addblock(&PrivateState->CurrentBlock);
// Flush all available vorbis blocks into ogg packets, then append the resulting pages
// to our output buffer:
while (vorbis_bitrate_flushpacket(&PrivateState->DspState, &PrivateState->CurrentPacket))
{
PrivateState->PushPacket(PrivateState->CurrentPacket);
PrivateState->PopPages(OutBytes);
}
}
return true;
}
bool FOggVorbisEncoder::EndFile(TArray<uint8>& OutBytes)
{
check(PrivateState != nullptr);
// Finish handing over any compressed data.
PrivateState->FlushPages(OutBytes);
// Finalize the DSP analyzer:
vorbis_analysis_wrote(&PrivateState->DspState, 0);
// Then clear all resources.
delete PrivateState;
PrivateState = nullptr;
return true;
}
#endif // !PLATFORM_TVOS