Files
UnrealEngineUWP/Engine/Source/Developer/AudioFormatOgg/Private/AudioFormatOgg.cpp
Matthew Griffin c0d29a957c Multi channel support for Opus
Implemented CookSurround function for AudioFormatOpus, refactoring as much common code from the standard Cook into re-usable functions.
Changed OpusAudioInfo class to use a multistream decoder for all sounds, as 1 and 2 channel sounds can still be decoded by it. Just had to create an array of mappings that would take the internal Opus format and give back PCM data in the order that Unreal expects.
Also made the OpusDecoderWrapper a true wrapper so that you don't have to use any opus functions outside of it and it will not function if WITH_OPUS is not defined.
Added a virtual function to the CompressedAudioInfo interface, to check whether its uncompressed data will be in the Vorbis format. This is to make sure that code currently just checking whether there is a DecompressionState can now be sure that the Vorbis format is being used before routing sound to different speakers.
Made sure that you can't cause any problems by adding or removing a SoundWave from the Audio Streaming Manager more than once. Also rearranged where sounds are added to the manager so that it is only done in one place and it's safe when changing quality triggers recompression/splitting.
Added a global multidimensional array to store all the Vorbis channel orderings as they needed to be used in multiple places.
Added Logging and Memory tracking for the streaming process.

[CL 2217839 by Matthew Griffin in Main branch]
2014-07-15 06:09:13 -04:00

465 lines
13 KiB
C++

// Copyright 1998-2014 Epic Games, Inc. All Rights Reserved.
#include "Core.h"
#include "ModuleInterface.h"
#include "ModuleManager.h"
#include "TargetPlatform.h"
#include "VorbisAudioInfo.h"
#if WITH_OGGVORBIS
#pragma pack(push, 8)
#include "vorbis/vorbisenc.h"
#include "vorbis/vorbisfile.h"
#pragma pack(pop)
#endif
static_assert(WITH_OGGVORBIS, "No point in compiling the OGG compressor if we don't have Vorbis.");
// Vorbis encoded sound is about 15% better quality than XMA - adjust the quality setting to get consistent cross platform sound quality
#define VORBIS_QUALITY_MODIFIER -15
#define SAMPLES_TO_READ 1024
#define SAMPLE_SIZE ( ( uint32 )sizeof( short ) )
static FName NAME_OGG(TEXT("OGG"));
/**
* IAudioFormat, audio compression abstraction
**/
class FAudioFormatOgg : public IAudioFormat
{
enum
{
/** Version for OGG format, this becomes part of the DDC key. */
UE_AUDIO_OGG_VER = 1,
};
public:
virtual bool AllowParallelBuild() const
{
return false;
}
virtual uint16 GetVersion(FName Format) const override
{
check(Format == NAME_OGG);
return UE_AUDIO_OGG_VER;
}
virtual void GetSupportedFormats(TArray<FName>& OutFormats) const
{
OutFormats.Add(NAME_OGG);
}
virtual bool Cook(FName Format, const TArray<uint8>& SrcBuffer, FSoundQualityInfo& QualityInfo, TArray<uint8>& CompressedDataStore) const
{
check(Format == NAME_OGG);
#if WITH_OGGVORBIS
{
short ReadBuffer[SAMPLES_TO_READ * SAMPLE_SIZE * 2];
ogg_stream_state os; // take physical pages, weld into a logical stream of packets
ogg_page og; // one ogg bitstream page. Vorbis packets are inside
ogg_packet op; // one raw packet of data for decode
vorbis_info vi; // struct that stores all the static vorbis bitstream settings
vorbis_comment vc; // struct that stores all the user comments
vorbis_dsp_state vd; // central working state for the packet->PCM decoder
vorbis_block vb; // local working space for packet->PCM decode
uint32 i;
bool eos;
// Create a buffer to store compressed data
CompressedDataStore.Empty();
FMemoryWriter CompressedData( CompressedDataStore );
uint32 BufferOffset = 0;
float CompressionQuality = ( float )( QualityInfo.Quality + VORBIS_QUALITY_MODIFIER ) / 100.0f;
CompressionQuality = FMath::Clamp( CompressionQuality, -0.1f, 1.0f );
vorbis_info_init( &vi );
if( vorbis_encode_init_vbr( &vi, QualityInfo.NumChannels, QualityInfo.SampleRate, CompressionQuality ) )
{
return false;
}
// add a comment
vorbis_comment_init( &vc );
vorbis_comment_add_tag( &vc, "ENCODER", "UnrealEngine4" );
// set up the analysis state and auxiliary encoding storage
vorbis_analysis_init( &vd, &vi );
vorbis_block_init( &vd, &vb );
// set up our packet->stream encoder
ogg_stream_init( &os, 0 );
ogg_packet header;
ogg_packet header_comm;
ogg_packet header_code;
vorbis_analysis_headerout( &vd, &vc, &header, &header_comm, &header_code);
ogg_stream_packetin( &os, &header );
ogg_stream_packetin( &os, &header_comm );
ogg_stream_packetin( &os, &header_code );
// This ensures the actual audio data will start on a new page, as per spec
while( true )
{
int result = ogg_stream_flush( &os, &og );
if( result == 0 )
{
break;
}
CompressedData.Serialize( og.header, og.header_len );
CompressedData.Serialize( og.body, og.body_len );
}
eos = false;
while( !eos )
{
// Read samples
uint32 BytesToRead = FMath::Min( SAMPLES_TO_READ * QualityInfo.NumChannels * SAMPLE_SIZE, QualityInfo.SampleDataSize - BufferOffset );
FMemory::Memcpy( ReadBuffer, SrcBuffer.GetTypedData() + BufferOffset, BytesToRead );
BufferOffset += BytesToRead;
if( BytesToRead == 0)
{
// end of file
vorbis_analysis_wrote( &vd, 0 );
}
else
{
// expose the buffer to submit data
float **buffer = vorbis_analysis_buffer( &vd, SAMPLES_TO_READ );
if( QualityInfo.NumChannels == 1 )
{
for( i = 0; i < BytesToRead / SAMPLE_SIZE; i++ )
{
buffer[0][i] = ( ReadBuffer[i] ) / 32768.0f;
}
}
else
{
for( i = 0; i < BytesToRead / ( SAMPLE_SIZE * 2 ); i++ )
{
buffer[0][i] = ( ReadBuffer[i * 2] ) / 32768.0f;
buffer[1][i] = ( ReadBuffer[i * 2 + 1] ) / 32768.0f;
}
}
// tell the library how many samples we actually submitted
vorbis_analysis_wrote( &vd, i );
}
// vorbis does some data preanalysis, then divvies up blocks for more involved (potentially parallel) processing.
while( vorbis_analysis_blockout( &vd, &vb ) == 1 )
{
// analysis, assume we want to use bitrate management
vorbis_analysis( &vb, NULL );
vorbis_bitrate_addblock( &vb );
while( vorbis_bitrate_flushpacket( &vd, &op ) )
{
// weld the packet into the bitstream
ogg_stream_packetin( &os, &op );
// write out pages (if any)
while( !eos )
{
int result = ogg_stream_pageout( &os, &og );
if( result == 0 )
{
break;
}
CompressedData.Serialize( og.header, og.header_len );
CompressedData.Serialize( og.body, og.body_len );
// this could be set above, but for illustrative purposes, I do it here (to show that vorbis does know where the stream ends)
if( ogg_page_eos( &og ) )
{
eos = true;
}
}
}
}
}
// clean up and exit. vorbis_info_clear() must be called last
ogg_stream_clear( &os );
vorbis_block_clear( &vb );
vorbis_dsp_clear( &vd );
vorbis_comment_clear( &vc );
vorbis_info_clear( &vi );
// ogg_page and ogg_packet structs always point to storage in libvorbis. They're never freed or manipulated directly
}
return CompressedDataStore.Num() > 0;
#else
return false;
#endif // WITH_OGGVOBVIS
}
virtual bool CookSurround(FName Format, const TArray<TArray<uint8> >& SrcBuffers, FSoundQualityInfo& QualityInfo, TArray<uint8>& CompressedDataStore) const
{
check(Format == NAME_OGG);
#if WITH_OGGVORBIS
{
ogg_stream_state os; // take physical pages, weld into a logical stream of packets
ogg_page og; // one ogg bitstream page. Vorbis packets are inside
ogg_packet op; // one raw packet of data for decode
vorbis_info vi; // struct that stores all the static vorbis bitstream settings
vorbis_comment vc; // struct that stores all the user comments
vorbis_dsp_state vd; // central working state for the packet->PCM decoder
vorbis_block vb; // local working space for packet->PCM decode
bool eos;
int j;
// Create a buffer to store compressed data
CompressedDataStore.Empty();
FMemoryWriter CompressedData( CompressedDataStore );
uint32 BufferOffset = 0;
int32 Size = -1;
for (int32 Index = 0; Index < SrcBuffers.Num(); Index++)
{
if (!Index)
{
Size = SrcBuffers[Index].Num();
}
else
{
if (Size != SrcBuffers[Index].Num())
{
return false;
}
}
}
if (Size <= 0)
{
return false;
}
// Extract the relevant info for compression
float CompressionQuality = ( float )( QualityInfo.Quality + VORBIS_QUALITY_MODIFIER ) / 100.0f;
CompressionQuality = FMath::Clamp( CompressionQuality, 0.0f, 1.0f );
vorbis_info_init( &vi );
if( vorbis_encode_init_vbr( &vi, SrcBuffers.Num(), QualityInfo.SampleRate, CompressionQuality ) )
{
return false;
}
// add a comment
vorbis_comment_init( &vc );
vorbis_comment_add_tag( &vc, "ENCODER", "UnrealEngine4" );
// set up the analysis state and auxiliary encoding storage
vorbis_analysis_init( &vd, &vi );
vorbis_block_init( &vd, &vb );
// set up our packet->stream encoder
ogg_stream_init( &os, 0 );
ogg_packet header;
ogg_packet header_comm;
ogg_packet header_code;
vorbis_analysis_headerout( &vd, &vc, &header, &header_comm, &header_code);
ogg_stream_packetin( &os, &header );
ogg_stream_packetin( &os, &header_comm );
ogg_stream_packetin( &os, &header_code );
// This ensures the actual audio data will start on a new page, as per spec
while( true )
{
int result = ogg_stream_flush( &os, &og );
if( result == 0 )
{
break;
}
CompressedData.Serialize( og.header, og.header_len );
CompressedData.Serialize( og.body, og.body_len );
}
TArray<int32> ChannelOrder = GetChannelOrder(SrcBuffers.Num());
eos = false;
while( !eos )
{
// Read samples
uint32 BytesToRead = FMath::Min( SAMPLES_TO_READ * SAMPLE_SIZE, Size - BufferOffset );
if( BytesToRead == 0)
{
// end of file
vorbis_analysis_wrote( &vd, 0 );
}
else
{
// expose the buffer to submit data
float **buffer = vorbis_analysis_buffer( &vd, SAMPLES_TO_READ );
uint32 i = 0;
for( j = 0; j < ChannelOrder.Num(); j++ )
{
short* ReadBuffer = ( short* )( SrcBuffers[ChannelOrder[j]].GetTypedData() + BufferOffset );
for( i = 0; i < BytesToRead / SAMPLE_SIZE; i++ )
{
buffer[j][i] = ( ReadBuffer[i] ) / 32768.0f;
}
}
// tell the library how many samples we actually submitted
vorbis_analysis_wrote( &vd, i );
}
BufferOffset += BytesToRead;
// vorbis does some data preanalysis, then divvies up blocks for more involved (potentially parallel) processing.
while( vorbis_analysis_blockout( &vd, &vb ) == 1 )
{
// analysis, assume we want to use bitrate management
vorbis_analysis( &vb, NULL );
vorbis_bitrate_addblock( &vb );
while( vorbis_bitrate_flushpacket( &vd, &op ) )
{
// weld the packet into the bitstream
ogg_stream_packetin( &os, &op );
// write out pages (if any)
while( !eos )
{
int result = ogg_stream_pageout( &os, &og );
if( result == 0 )
{
break;
}
CompressedData.Serialize( og.header, og.header_len );
CompressedData.Serialize( og.body, og.body_len );
// this could be set above, but for illustrative purposes, I do it here (to show that vorbis does know where the stream ends)
if( ogg_page_eos( &og ) )
{
eos = true;
}
}
}
}
}
// clean up and exit. vorbis_info_clear() must be called last
ogg_stream_clear( &os );
vorbis_block_clear( &vb );
vorbis_dsp_clear( &vd );
vorbis_comment_clear( &vc );
vorbis_info_clear( &vi );
// ogg_page and ogg_packet structs always point to storage in libvorbis. They're never freed or manipulated directly
}
return CompressedDataStore.Num() > 0;
#else
return false;
#endif // WITH_OGGVOBVIS
}
/**
* Put channels into the order expected for a multi-channel ogg vorbis file.
* Ordering taken from http://xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-800004.3.9
* Only concerned with 6 channel version because that seems to apply filtering to LFE.
*/
TArray<int32> GetChannelOrder(int32 NumChannels) const
{
TArray<int32> ChannelOrder;
switch(NumChannels)
{
case 6:
{
// the stream is 5.1 surround. channel order: front left, center, front right, rear left, rear right, LFE
for (int32 i = 0; i < NumChannels; i++)
{
ChannelOrder.Add(VorbisChannelInfo::Order[NumChannels - 1][i]);
}
break;
}
default:
{
// no special ordering, just put all channels into ordered buffer
for( int32 i = 0; i < NumChannels; i++ )
{
ChannelOrder.Add(i);
}
break;
}
}
return ChannelOrder;
}
virtual int32 Recompress(FName Format, const TArray<uint8>& SrcBuffer, FSoundQualityInfo& QualityInfo, TArray<uint8>& OutBuffer) const
{
check(Format == NAME_OGG);
FVorbisAudioInfo AudioInfo;
int32 CompressedSize = -1;
// Cannot quality preview multichannel sounds
if( QualityInfo.NumChannels > 2 )
{
return 0;
}
TArray<uint8> CompressedDataStore;
if( !Cook( Format, SrcBuffer, QualityInfo, CompressedDataStore ) )
{
return 0;
}
// Parse the audio header for the relevant information
if (!AudioInfo.ReadCompressedInfo(CompressedDataStore.GetTypedData(), CompressedDataStore.Num(), &QualityInfo))
{
return 0;
}
// Decompress all the sample data
OutBuffer.Empty(QualityInfo.SampleDataSize);
OutBuffer.AddZeroed(QualityInfo.SampleDataSize);
AudioInfo.ExpandFile(OutBuffer.GetTypedData(), &QualityInfo);
return CompressedDataStore.Num();
}
};
/**
* Module for ogg audio compression
*/
static IAudioFormat* Singleton = NULL;
class FAudioPlatformOggModule : public IAudioFormatModule
{
public:
virtual ~FAudioPlatformOggModule()
{
delete Singleton;
Singleton = NULL;
}
virtual IAudioFormat* GetAudioFormat()
{
if (!Singleton)
{
LoadVorbisLibraries();
Singleton = new FAudioFormatOgg();
}
return Singleton;
}
};
IMPLEMENT_MODULE( FAudioPlatformOggModule, AudioFormatOgg);