Files
UnrealEngineUWP/Engine/Source/Developer/AudioFormatOpus/Private/AudioFormatOpus.cpp
phil popp 204e5eef67 Fix audio derived data invalidating old data.
This fix reverts the logic so that old sounds don't go through a pcm->float->pcm conversion.
#rb Jimmy.Smith, Dan.Thompson
#preflight 636ae824dc30a4ce96c34f99

[CL 23069835 by phil popp in ue5-main branch]
2022-11-09 21:04:08 -05:00

579 lines
18 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "AudioFormatOpus.h"
#include "Audio.h"
#include "Serialization/MemoryWriter.h"
#include "Modules/ModuleManager.h"
#include "Interfaces/IAudioFormat.h"
#include "Interfaces/IAudioFormatModule.h"
#include "OpusAudioInfo.h"
#include "VorbisAudioInfo.h"
// Need to define this so that resampler.h compiles - probably a way around this somehow
#define OUTSIDE_SPEEX
THIRD_PARTY_INCLUDES_START
#include "opus_multistream.h"
#include "speex_resampler.h"
THIRD_PARTY_INCLUDES_END
/** Use UE memory allocation or Opus */
#define USE_UE_MEM_ALLOC 1
#define SAMPLE_SIZE ( ( uint32 )sizeof( short ) )
static FName NAME_OPUS(TEXT("OPUS"));
/**
* IAudioFormat, audio compression abstraction
**/
class FAudioFormatOpus : public IAudioFormat
{
enum
{
/** Version for OPUS format, this becomes part of the DDC key. */
UE_AUDIO_OPUS_VER = 8,
};
public:
virtual bool AllowParallelBuild() const override
{
return false;
}
virtual uint16 GetVersion(FName Format) const override
{
check(Format == NAME_OPUS);
return UE_AUDIO_OPUS_VER;
}
virtual void GetSupportedFormats(TArray<FName>& OutFormats) const override
{
OutFormats.Add(NAME_OPUS);
}
virtual bool Cook(FName Format, const TArray<uint8>& SrcBuffer, FSoundQualityInfo& QualityInfo, TArray<uint8>& CompressedDataStore) const override
{
TRACE_CPUPROFILER_EVENT_SCOPE(FAudioFormatOpus::Cook);
check(Format == NAME_OPUS);
// Get best compatible sample rate
const uint16 kOpusSampleRate = GetBestOutputSampleRate(QualityInfo.SampleRate);
// Frame size must be one of 2.5, 5, 10, 20, 40 or 60 ms
const int32 kOpusFrameSizeMs = 60;
// Calculate frame size required by Opus
const int32 kOpusFrameSizeSamples = (kOpusSampleRate * kOpusFrameSizeMs) / 1000;
const uint32 kSampleStride = SAMPLE_SIZE * QualityInfo.NumChannels;
const int32 kBytesPerFrame = kOpusFrameSizeSamples * kSampleStride;
// Check whether source has compatible sample rate
TArray<uint8> SrcBufferCopy;
if (QualityInfo.SampleRate != kOpusSampleRate)
{
if (!ResamplePCM(QualityInfo.NumChannels, SrcBuffer, QualityInfo.SampleRate, SrcBufferCopy, kOpusSampleRate))
{
return false;
}
}
else
{
// Take a copy of the source regardless
SrcBufferCopy = SrcBuffer;
}
// Initialise the Opus encoder
OpusEncoder* Encoder = NULL;
int32 EncError = 0;
#if USE_UE_MEM_ALLOC
int32 EncSize = opus_encoder_get_size(QualityInfo.NumChannels);
Encoder = (OpusEncoder*)FMemory::Malloc(EncSize);
EncError = opus_encoder_init(Encoder, kOpusSampleRate, QualityInfo.NumChannels, OPUS_APPLICATION_AUDIO);
#else
Encoder = opus_encoder_create(kOpusSampleRate, QualityInfo.NumChannels, OPUS_APPLICATION_AUDIO, &EncError);
#endif
if (EncError != OPUS_OK)
{
Destroy(Encoder);
return false;
}
int32 BitRate = GetBitRateFromQuality(QualityInfo);
opus_encoder_ctl(Encoder, OPUS_SET_BITRATE(BitRate));
// Create a buffer to store compressed data
CompressedDataStore.Empty();
FMemoryWriter CompressedData(CompressedDataStore);
int32 SrcBufferOffset = 0;
// Calc frame and sample count
int32 FramesToEncode = SrcBufferCopy.Num() / kBytesPerFrame;
uint32 TrueSampleCount = SrcBufferCopy.Num() / kSampleStride;
// Pad the end of data with zeroes if it isn't exactly the size of a frame.
if (SrcBufferCopy.Num() % kBytesPerFrame != 0)
{
int32 FrameDiff = kBytesPerFrame - (SrcBufferCopy.Num() % kBytesPerFrame);
SrcBufferCopy.AddZeroed(FrameDiff);
FramesToEncode++;
}
check(QualityInfo.NumChannels <= MAX_uint8);
check(FramesToEncode <= MAX_uint16);
SerializeHeaderData(CompressedData, kOpusSampleRate, TrueSampleCount, QualityInfo.NumChannels, FramesToEncode);
// Temporary storage with more than enough to store any compressed frame
TArray<uint8> TempCompressedData;
TempCompressedData.AddUninitialized(kBytesPerFrame);
while (SrcBufferOffset < SrcBufferCopy.Num())
{
int32 CompressedLength = opus_encode(Encoder, (const opus_int16*)(SrcBufferCopy.GetData() + SrcBufferOffset), kOpusFrameSizeSamples, TempCompressedData.GetData(), TempCompressedData.Num());
if (CompressedLength < 0)
{
const char* ErrorStr = opus_strerror(CompressedLength);
UE_LOG(LogAudio, Warning, TEXT("Failed to encode: [%d] %s"), CompressedLength, ANSI_TO_TCHAR(ErrorStr));
Destroy(Encoder);
CompressedDataStore.Empty();
return false;
}
else
{
// Store frame length and copy compressed data before incrementing pointers
check(CompressedLength < MAX_uint16);
SerialiseFrameData(CompressedData, TempCompressedData.GetData(), CompressedLength);
SrcBufferOffset += kBytesPerFrame;
}
}
Destroy(Encoder);
return CompressedDataStore.Num() > 0;
}
virtual bool CookSurround(FName Format, const TArray<TArray<uint8> >& SrcBuffers, FSoundQualityInfo& QualityInfo, TArray<uint8>& CompressedDataStore) const override
{
TRACE_CPUPROFILER_EVENT_SCOPE(FAudioFormatOpus::CookSurround);
check(Format == NAME_OPUS);
// Get best compatible sample rate
const uint16 kOpusSampleRate = GetBestOutputSampleRate(QualityInfo.SampleRate);
// Frame size must be one of 2.5, 5, 10, 20, 40 or 60 ms
const int32 kOpusFrameSizeMs = 60;
// Calculate frame size required by Opus
const int32 kOpusFrameSizeSamples = (kOpusSampleRate * kOpusFrameSizeMs) / 1000;
const uint32 kSampleStride = SAMPLE_SIZE * QualityInfo.NumChannels;
const int32 kBytesPerFrame = kOpusFrameSizeSamples * kSampleStride;
// Check whether source has compatible sample rate
TArray<TArray<uint8>> SrcBufferCopies;
if (QualityInfo.SampleRate != kOpusSampleRate)
{
for (int32 Index = 0; Index < SrcBuffers.Num(); Index++)
{
TArray<uint8>& NewCopy = *new (SrcBufferCopies) TArray<uint8>;
if (!ResamplePCM(1, SrcBuffers[Index], QualityInfo.SampleRate, NewCopy, kOpusSampleRate))
{
return false;
}
}
}
else
{
SrcBufferCopies.Reset();
SrcBufferCopies.AddDefaulted(SrcBuffers.Num());
// Take a copy of the source regardless
for (int32 Index = 0; Index < SrcBuffers.Num(); Index++)
{
SrcBufferCopies[Index] = SrcBuffers[Index];
}
}
// Ensure that all channels are the same length
int32 SourceSize = -1;
for (int32 Index = 0; Index < SrcBufferCopies.Num(); Index++)
{
if (!Index)
{
SourceSize = SrcBufferCopies[Index].Num();
}
else
{
if (SourceSize != SrcBufferCopies[Index].Num())
{
return false;
}
}
}
if (SourceSize <= 0)
{
return false;
}
// Initialise the Opus multistream encoder
OpusMSEncoder* Encoder = NULL;
int32 EncError = 0;
int32 streams = 0;
int32 coupled_streams = 0;
// mapping_family not documented but figured out: 0 = 1 or 2 channels, 1 = 1 to 8 channel surround sound, 255 = up to 255 channels with no surround processing
int32 mapping_family = 1;
TArray<uint8> mapping;
mapping.AddUninitialized(QualityInfo.NumChannels);
#if USE_UE_MEM_ALLOC
int32 EncSize = opus_multistream_surround_encoder_get_size(QualityInfo.NumChannels, mapping_family);
Encoder = (OpusMSEncoder*)FMemory::Malloc(EncSize);
EncError = opus_multistream_surround_encoder_init(Encoder, kOpusSampleRate, QualityInfo.NumChannels, mapping_family, &streams, &coupled_streams, mapping.GetData(), OPUS_APPLICATION_AUDIO);
#else
Encoder = opus_multistream_surround_encoder_create(kOpusSampleRate, QualityInfo.NumChannels, mapping_family, &streams, &coupled_streams, mapping.GetData(), OPUS_APPLICATION_AUDIO, &EncError);
#endif
if (EncError != OPUS_OK)
{
Destroy(Encoder);
return false;
}
int32 BitRate = GetBitRateFromQuality(QualityInfo);
opus_multistream_encoder_ctl(Encoder, OPUS_SET_BITRATE(BitRate));
// Create a buffer to store compressed data
CompressedDataStore.Empty();
FMemoryWriter CompressedData(CompressedDataStore);
int32 SrcBufferOffset = 0;
// Calc frame and sample count
int32 FramesToEncode = SourceSize / (kOpusFrameSizeSamples * SAMPLE_SIZE);
uint32 TrueSampleCount = SourceSize / SAMPLE_SIZE;
// Add another frame if Source does not divide into an equal number of frames
if (SourceSize % (kOpusFrameSizeSamples * SAMPLE_SIZE) != 0)
{
FramesToEncode++;
}
check(QualityInfo.NumChannels <= MAX_uint8);
check(FramesToEncode <= MAX_uint16);
SerializeHeaderData(CompressedData, kOpusSampleRate, TrueSampleCount, QualityInfo.NumChannels, FramesToEncode);
// Temporary storage for source data in an interleaved format
TArray<uint8> TempInterleavedSrc;
TempInterleavedSrc.AddUninitialized(kBytesPerFrame);
// Temporary storage with more than enough to store any compressed frame
TArray<uint8> TempCompressedData;
TempCompressedData.AddUninitialized(kBytesPerFrame);
while (SrcBufferOffset < SourceSize)
{
// Read a frames worth of data from the source and pack it into interleaved temporary storage
for (int32 SampleIndex = 0; SampleIndex < kOpusFrameSizeSamples; ++SampleIndex)
{
int32 CurrSrcOffset = SrcBufferOffset + SampleIndex*SAMPLE_SIZE;
int32 CurrInterleavedOffset = SampleIndex*kSampleStride;
if (CurrSrcOffset < SourceSize)
{
check(QualityInfo.NumChannels <= 8); // Static analysis fix: warning C6385: Reading invalid data from 'Order': the readable size is '256' bytes, but '8160' bytes may be read.
for (uint32 ChannelIndex = 0; ChannelIndex < QualityInfo.NumChannels; ++ChannelIndex)
{
// Interleave the channels in the Vorbis format, so that the correct channel is used for LFE
int32 OrderedChannelIndex = VorbisChannelInfo::Order[QualityInfo.NumChannels - 1][ChannelIndex];
int32 CurrInterleavedIndex = CurrInterleavedOffset + ChannelIndex*SAMPLE_SIZE;
// Copy both bytes that make up a single sample
TempInterleavedSrc[CurrInterleavedIndex] = SrcBufferCopies[OrderedChannelIndex][CurrSrcOffset];
TempInterleavedSrc[CurrInterleavedIndex + 1] = SrcBufferCopies[OrderedChannelIndex][CurrSrcOffset + 1];
}
}
else
{
// Zero the rest of the temp buffer to make it an exact frame
FMemory::Memzero(TempInterleavedSrc.GetData() + CurrInterleavedOffset, kBytesPerFrame - CurrInterleavedOffset);
SampleIndex = kOpusFrameSizeSamples;
}
}
int32 CompressedLength = opus_multistream_encode(Encoder, (const opus_int16*)(TempInterleavedSrc.GetData()), kOpusFrameSizeSamples, TempCompressedData.GetData(), TempCompressedData.Num());
if (CompressedLength < 0)
{
const char* ErrorStr = opus_strerror(CompressedLength);
UE_LOG(LogAudio, Warning, TEXT("Failed to encode: [%d] %s"), CompressedLength, ANSI_TO_TCHAR(ErrorStr));
Destroy(Encoder);
CompressedDataStore.Empty();
return false;
}
else
{
// Store frame length and copy compressed data before incrementing pointers
check(CompressedLength < MAX_uint16);
SerialiseFrameData(CompressedData, TempCompressedData.GetData(), CompressedLength);
SrcBufferOffset += kOpusFrameSizeSamples * SAMPLE_SIZE;
}
}
Destroy(Encoder);
return CompressedDataStore.Num() > 0;
}
virtual int32 Recompress(FName Format, const TArray<uint8>& SrcBuffer, FSoundQualityInfo& QualityInfo, TArray<uint8>& OutBuffer) const override
{
check(Format == NAME_OPUS);
FOpusAudioInfo AudioInfo;
// Cannot quality preview multichannel sounds
if( QualityInfo.NumChannels > 2 )
{
return 0;
}
TArray<uint8> CompressedDataStore;
if( !Cook( Format, SrcBuffer, QualityInfo, CompressedDataStore ) )
{
return 0;
}
// Parse the opus header for the relevant information
if( !AudioInfo.ReadCompressedInfo( CompressedDataStore.GetData(), CompressedDataStore.Num(), &QualityInfo ) )
{
return 0;
}
// Decompress all the sample data
OutBuffer.Empty(QualityInfo.SampleDataSize);
OutBuffer.AddZeroed(QualityInfo.SampleDataSize);
AudioInfo.ExpandFile( OutBuffer.GetData(), &QualityInfo );
return CompressedDataStore.Num();
}
virtual int32 GetMinimumSizeForInitialChunk(FName Format, const TArray<uint8>& SrcBuffer) const override
{
// Since UE uses it's own version of the header, we hardcode the size of that here. See SplitDataForStreaming below to see our initial info.
return FCStringAnsi::Strlen(OPUS_ID_STRING) + 1 // Format identifier
+ sizeof(uint16) // Sample Rate
+ sizeof(uint32) // True Sample Count
+ sizeof(uint8) // Number of Channels
+ sizeof(uint16); // Serialized Frames
}
virtual bool SplitDataForStreaming(const TArray<uint8>& SrcBuffer, TArray<TArray<uint8>>& OutBuffers, const int32 MaxInitialChunkSize, const int32 MaxChunkSize) const override
{
if (SrcBuffer.Num() == 0)
{
return false;
}
uint32 ReadOffset = 0;
uint32 WriteOffset = 0;
uint16 ProcessedFrames = 0;
const uint8* LockedSrc = SrcBuffer.GetData();
// Read Identifier, True Sample Count, Number of channels and Frames to Encode first
if (FCStringAnsi::Strcmp((char*)LockedSrc, OPUS_ID_STRING) != 0)
{
return false;
}
ReadOffset += FCStringAnsi::Strlen(OPUS_ID_STRING) + 1;
uint16 SampleRate = *((uint16*)(LockedSrc + ReadOffset));
ReadOffset += sizeof(uint16);
uint32 TrueSampleCount = *((uint32*)(LockedSrc + ReadOffset));
ReadOffset += sizeof(uint32);
uint8 NumChannels = *(LockedSrc + ReadOffset);
ReadOffset += sizeof(uint8);
uint16 SerializedFrames = *((uint16*)(LockedSrc + ReadOffset));
ReadOffset += sizeof(uint16);
// Should always be able to store basic info in a single chunk
check(ReadOffset - WriteOffset <= (uint32)MaxInitialChunkSize);
int32 ChunkSize = MaxInitialChunkSize;
while (ProcessedFrames < SerializedFrames)
{
uint16 FrameSize = *((uint16*)(LockedSrc + ReadOffset));
if ( (ReadOffset + sizeof(uint16) + FrameSize) - WriteOffset >= ChunkSize)
{
WriteOffset += AddDataChunk(OutBuffers, LockedSrc + WriteOffset, ReadOffset - WriteOffset);
}
ReadOffset += sizeof(uint16) + FrameSize;
ProcessedFrames++;
ChunkSize = MaxChunkSize;
}
if (WriteOffset < ReadOffset)
{
WriteOffset += AddDataChunk(OutBuffers, LockedSrc + WriteOffset, ReadOffset - WriteOffset);
}
return true;
}
/**
* Calculate the best sample rate for the output opus data
*/
static uint16 GetBestOutputSampleRate(int32 SampleRate)
{
static const uint16 ValidSampleRates[] =
{
0, // not really valid, but simplifies logic below
8000,
12000,
16000,
24000,
48000,
};
// look for the next highest valid rate
for (int32 Index = UE_ARRAY_COUNT(ValidSampleRates) - 2; Index >= 0; Index--)
{
if (SampleRate > ValidSampleRates[Index])
{
return ValidSampleRates[Index + 1];
}
}
// this should never get here!
check(0);
return 0;
}
bool ResamplePCM(uint32 NumChannels, const TArray<uint8>& InBuffer, uint32 InSampleRate, TArray<uint8>& OutBuffer, uint32 OutSampleRate) const
{
// Initialize resampler to convert to desired rate for Opus
int32 err = 0;
SpeexResamplerState* resampler = speex_resampler_init(NumChannels, InSampleRate, OutSampleRate, SPEEX_RESAMPLER_QUALITY_DESKTOP, &err);
if (err != RESAMPLER_ERR_SUCCESS)
{
speex_resampler_destroy(resampler);
return false;
}
// Calculate extra space required for sample rate
const uint32 SampleStride = SAMPLE_SIZE * NumChannels;
const float Duration = (float)InBuffer.Num() / (InSampleRate * SampleStride);
const int32 SafeCopySize = (Duration + 1) * OutSampleRate * SampleStride;
OutBuffer.Empty(SafeCopySize);
OutBuffer.AddUninitialized(SafeCopySize);
uint32 InSamples = InBuffer.Num() / SampleStride;
uint32 OutSamples = OutBuffer.Num() / SampleStride;
// Do resampling and check results
if (NumChannels == 1)
{
err = speex_resampler_process_int(resampler, 0, (const short*)(InBuffer.GetData()), &InSamples, (short*)(OutBuffer.GetData()), &OutSamples);
}
else
{
err = speex_resampler_process_interleaved_int(resampler, (const short*)(InBuffer.GetData()), &InSamples, (short*)(OutBuffer.GetData()), &OutSamples);
}
speex_resampler_destroy(resampler);
if (err != RESAMPLER_ERR_SUCCESS)
{
return false;
}
// reduce the size of Out Buffer if more space than necessary was allocated
const int32 WrittenBytes = (int32)(OutSamples * SampleStride);
if (WrittenBytes < OutBuffer.Num())
{
OutBuffer.SetNum(WrittenBytes, true);
}
return true;
}
int32 GetBitRateFromQuality(FSoundQualityInfo& QualityInfo) const
{
// There is no perfect way to map Vorbis' Quality setting to an Opus bitrate but this
// will use it as a multiplier to decide how much smaller than the original the
// compressed data should be
int32 OriginalBitRate = QualityInfo.SampleRate * QualityInfo.NumChannels * SAMPLE_SIZE * 8;
return (float)OriginalBitRate * FMath::GetMappedRangeValueClamped(FVector2f(1, 100), FVector2f(0.04, 0.25), (float)QualityInfo.Quality);
}
void SerializeHeaderData(FMemoryWriter& CompressedData, uint16 SampleRate, uint32 TrueSampleCount, uint8 NumChannels, uint16 NumFrames) const
{
const char* OpusIdentifier = OPUS_ID_STRING;
CompressedData.Serialize((void*)OpusIdentifier, FCStringAnsi::Strlen(OpusIdentifier) + 1);
CompressedData.Serialize(&SampleRate, sizeof(uint16));
CompressedData.Serialize(&TrueSampleCount, sizeof(uint32));
CompressedData.Serialize(&NumChannels, sizeof(uint8));
CompressedData.Serialize(&NumFrames, sizeof(uint16));
}
void SerialiseFrameData(FMemoryWriter& CompressedData, uint8* FrameData, uint16 FrameSize) const
{
CompressedData.Serialize(&FrameSize, sizeof(uint16));
CompressedData.Serialize(FrameData, FrameSize);
}
void Destroy(OpusEncoder* Encoder) const
{
#if USE_UE_MEM_ALLOC
FMemory::Free(Encoder);
#else
opus_encoder_destroy(Encoder);
#endif
}
void Destroy(OpusMSEncoder* Encoder) const
{
#if USE_UE_MEM_ALLOC
FMemory::Free(Encoder);
#else
opus_multistream_encoder_destroy(Encoder);
#endif
}
/**
* Adds a new chunk of data to the array
*
* @param OutBuffers Array of buffers to add to
* @param ChunkData Pointer to chunk data
* @param ChunkSize How much data to write
* @return How many bytes were written
*/
int32 AddDataChunk(TArray<TArray<uint8>>& OutBuffers, const uint8* ChunkData, int32 ChunkSize) const
{
TArray<uint8>& NewBuffer = *new (OutBuffers) TArray<uint8>;
NewBuffer.Empty(ChunkSize);
NewBuffer.AddUninitialized(ChunkSize);
FMemory::Memcpy(NewBuffer.GetData(), ChunkData, ChunkSize);
return ChunkSize;
}
};
/**
* Module for opus audio compression
*/
static IAudioFormat* Singleton = NULL;
class FAudioPlatformOpusModule : public IAudioFormatModule
{
public:
virtual ~FAudioPlatformOpusModule()
{
delete Singleton;
Singleton = NULL;
}
virtual IAudioFormat* GetAudioFormat()
{
if (!Singleton)
{
Singleton = new FAudioFormatOpus();
}
return Singleton;
}
};
IMPLEMENT_MODULE( FAudioPlatformOpusModule, AudioFormatOpus);