Trying to play a Voice Data stream using a ByteArray in Unreal Engine 5

,

Hello! I’m new to programming for FMOD (and just generally not that good at c++ programming) and so far I’ve managed to get the following:

void AProgrammerSoundTest::PlayVoiceAudio(UFMODAudioComponent* AudioComponent, const TArray<uint8>& ByteArray, UVoipAudioComponent* VoipAudioComponent)
{
	UncompressedVoiceData = VoipAudioComponent->GetDecodedVoiceData(ByteArray);
	
	if (AudioComponent)
	{
		const FMOD::Studio::System* System = IFMODStudioModule::Get().GetStudioSystem(EFMODSystemContext::Runtime);
		FMOD::System* CoreSystem = nullptr;
		System->getCoreSystem(&CoreSystem);
		
		FMOD::Sound* Sound = nullptr;

		FMOD_CREATESOUNDEXINFO soundExInfo = { 0 };
		soundExInfo.cbsize = sizeof(soundExInfo);
		soundExInfo.format = FMOD_SOUND_FORMAT_PCM16;
		soundExInfo.defaultfrequency = 16000;
		soundExInfo.numchannels = 1;
		soundExInfo.length = VoipAudioComponent->BytesWrittenPub;
		
		soundExInfo.pcmreadcallback = PCMReadCallback;
		
		const FMOD_RESULT result = CoreSystem->createStream("", FMOD_OPENUSER | FMOD_LOOP_OFF , &soundExInfo, &Sound);
		
		if (result == FMOD_OK)
		{
			AudioComponent->SetProgrammerSound(Sound);
			AudioComponent->Play();
		}
	}
}

FMOD_RESULT F_CALLBACK PCMReadCallback(FMOD_SOUND* Sound, void *data, unsigned int datalen)
{
	FMemory::Memcpy(data, &AProgrammerSoundTest::UncompressedVoiceData[0], datalen);
	return FMOD_OK;
}

After bashing my head against a wall for many hours the above is the result - it manages to play the data but the output is choppy and robotic. Beforehand is was just pure noise so this is a big improvement. I just can’t figure out the next step. Could anyone highlight where I’m going wrong or perhaps some missteps I’ve already made?
For some added context I’m passing this function a compressed byte array and a component. The component (“VoipAudioComponent”) helps me decompress the byte array. I then attempt to use that with fmod via a programmer sound. The byte array and component are passed to this function in blueprints.

Thanks in advance!

Hi,

What version of the FMOD integration are you using?

Would it be possible to get a copy of your project or a stripped-down version displaying the issue uploaded to your profile? Please note you must register a project with us before uploading files.

Hi Connor!
I’m using 2.02.20. I’ve uploaded the project to my profile (Pilgrim_FMOD_Upload.zip).

After more head bashing today I’ve still not figured it out and have distilled my current “working” code to a very simple component called FmodVoice that is in the CH_Character. In the CH_Character there is a voice graph where the PlayVoiceAudioNew function is called.
You can test to see if it’s working when speaking into a mic in the L_VoiceTest map
(found in Temp/Levels)
In C++ the PlayVoiceAudioNew gets the DeCompressed PCM data from the VoipManager component (also in the CH_Character) this is a marketplace plugin that I’m using to handle the voip.
The problem is still that it sounds choppy and robotic. I’ve looked at the various examples in the API both user created sound the record one. But even when trying to recreate what the examples do in my own project I can’t seem to get it to work - I must be too thick to figure it out!
Worth noting too that at one point I added in a _Sleep(200) and that did the trick apart from lagging the game out. So went down that route for a while of introducing buffers and delays. Still no luck!

Thanks a lot for your help :slight_smile:

1 Like

Hi,

Thank you for the project.

An issue may be that you are passing the voice data straight to the sound without any drift compensation, resulting in the popping.

We have an example of Video Playback in Unity which shows how to implement drift compensation. This will have to be slightly modified for C++ and UE.

You will want to replace the SamplesFramesAvailable buffer with the data received from the VOIP.

Let me know how you go implementing drift compensation and if there is anything I can assist with please do not hesitate to ask!

Thanks for taking a look at it!
I’ve combed over the example and tried to implement it. But I still get more of the same crackling/popping and now I also get looped weirdness with the end of the audio repeating. If you wouldn’t mind taking another look at my latest attempt that would be amazing?

#include "FmodVoice.h"


FMOD_RESULT F_CALLBACK PCMReadCallback(FMOD_SOUND* sound, void *data, unsigned int datalen);

const int LATENCY_MS = 50;
const int DRIFT_MS = 1;
const float DRIFT_CORRECTION_PERCENTAGE = 0.5f;

FMOD::Channel* Channel = nullptr;
static inline TArray<uint8> Buffer;
FMOD::Sound* Sound = nullptr;
int SampleRate = 16000;
uint32 DriftThreshold;
uint32 TargetLatency;
uint32 AdjustedLatency;
uint32 ActualLatency;

uint32 TotalSamplesWritten;
uint32 MinimumSamplesWritten = UINT32_MAX;

uint32 LastReadPosition;
uint32 TotalSamplesRead;
const FMOD::Studio::System* System;
FMOD::System* CoreSystem = nullptr;

FMOD_CREATESOUNDEXINFO soundExInfo = { 0 };

UVoipManagerComponent* VOIP;

UFmodVoice::UFmodVoice()
{
	PrimaryComponentTick.bCanEverTick = true;
}

void UFmodVoice::BeginPlay()
{
	Super::BeginPlay();
	System = IFMODStudioModule::Get().GetStudioSystem(EFMODSystemContext::Runtime);
	
	System->getCoreSystem(&CoreSystem);
		
	DriftThreshold = static_cast<uint32>(SampleRate * DRIFT_MS) / 1000;
	TargetLatency = static_cast<uint32>(SampleRate * LATENCY_MS) / 1000;
	AdjustedLatency = TargetLatency;
	ActualLatency = static_cast<int>(TargetLatency);
	
	soundExInfo.cbsize = sizeof(soundExInfo);
	soundExInfo.format = FMOD_SOUND_FORMAT_PCM16;
	soundExInfo.defaultfrequency = SampleRate;
	soundExInfo.numchannels = 1;
	soundExInfo.length = TargetLatency * sizeof(uint8);
	soundExInfo.pcmreadcallback = PCMReadCallback;		
}

void UFmodVoice::BeginDestroy()
{
	Super::BeginDestroy();
	Channel->stop();
	Sound->release();
}

void UFmodVoice::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
{
	Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
	if (VOIP)
	{
		Buffer = VOIP->DecompressedVoiceBuffer;
	
		if (!Channel && TotalSamplesWritten > AdjustedLatency)
		{
			const FMOD_RESULT result = CoreSystem->createSound("", FMOD_OPENUSER | FMOD_LOOP_NORMAL , &soundExInfo, &Sound);
			CoreSystem->playSound(Sound, nullptr, false, &Channel);
		}

		if (Buffer.Num() > 0 && Channel)
		{
			uint32 readPosition;
			Channel->getPosition(&readPosition, FMOD_TIMEUNIT_PCMBYTES);
			UE_LOG(LogTemp, Warning, TEXT("Channel read position: %d"), readPosition);
			/*
			 * Account for wrapping
			 */
			int bytesRead = readPosition - LastReadPosition;
			if (readPosition < LastReadPosition)
			{
				bytesRead += soundExInfo.length;
			}

			if (bytesRead > 0 && Buffer.Num() >= bytesRead)
			{
				/*
				 * Fill previously read data with fresh samples
				 */
				void* ptr1;
				void* ptr2;
				uint32 len1, len2;              

				FMOD_RESULT result = Sound->lock(LastReadPosition, bytesRead, &ptr1, &ptr2, &len1, &len2);
            	
				if (result != FMOD_OK) UE_LOG(LogTemp, Warning, TEXT("VOIP Manager Size: %d"), result);
            	
				int sampleLen1 = static_cast<int>(len1 / sizeof(uint8));
				int sampleLen2 = static_cast<int>(len2 / sizeof(uint8));
				int samplesRead = sampleLen1 + sampleLen2;
				TArray<uint8> tmpBuffer;
            	
				tmpBuffer.Init(0, samplesRead);
				tmpBuffer.SetNum(Buffer.Num(), false);
            	
				// Directly copy the data from Buffer to tmpBuffer using FMemory::Memcpy
				if (Buffer.Num() > 0 && samplesRead <= Buffer.Num())
				{
					FMemory::Memcpy(tmpBuffer.GetData(), Buffer.GetData(), samplesRead);
				}
            	
				// Remove the copied range from mBuffer
				Buffer.RemoveAt(0, tmpBuffer.Num());
            	
				if (len1 > 0)
				{
					// Directly copy from the start of tmpBuffer
					FMemory::Memcpy(ptr1, tmpBuffer.GetData(), sampleLen1);
				}
				if (len2 > 0)
				{
					// Calculate the start index for the second copy by offsetting the source pointer
					// Note: This assumes sampleLen1 is the byte offset to start from for the second copy
					uint8* sourcePtrOffset = tmpBuffer.GetData() + sampleLen1;
					FMemory::Memcpy(ptr2, sourcePtrOffset, sampleLen2);
				}
            	
				result = Sound->unlock(ptr1, ptr2, len1, len2);
            	
				if (result != FMOD_OK) UE_LOG(LogTemp, Warning, TEXT("VOIP Manager Size: %d"), result);
				LastReadPosition = readPosition;
				TotalSamplesRead += static_cast<uint32>(samplesRead);
			}
		}

		//Drift compensation
		uint32 samplesWritten = Buffer.Num();

		TotalSamplesWritten += samplesWritten;

		if (samplesWritten != 0 && samplesWritten < MinimumSamplesWritten)
		{
			MinimumSamplesWritten = samplesWritten;
			AdjustedLatency = FMath::Max(samplesWritten, TargetLatency);
		}

		int32 latency = TotalSamplesWritten - TotalSamplesRead;
		ActualLatency = static_cast<uint32>((0.93f * ActualLatency) + (0.03f * latency));

	
		int32 PlaybackRate = SampleRate;
		if (ActualLatency < (AdjustedLatency - DriftThreshold))
		{
			PlaybackRate = SampleRate - static_cast<int32>(SampleRate * (DRIFT_CORRECTION_PERCENTAGE / 100.0f));
		}
		else if (ActualLatency > (AdjustedLatency + DriftThreshold))
		{
			PlaybackRate = SampleRate + static_cast<int32>(SampleRate * (DRIFT_CORRECTION_PERCENTAGE / 100.0f));
		}
		Channel->setFrequency(PlaybackRate);
	}
}

void UFmodVoice::InitComponent(UFMODAudioComponent* fmodAudioComponent, UVoipManagerComponent* VoipManager)
{
	VOIP = VoipManager;
}

FMOD_RESULT F_CALLBACK PCMReadCallback(FMOD_SOUND* sound, void *data, unsigned int datalen) {
	//FMemory::Memcpy(data, &UFmodVoice::UncompressedVoiceData[0], datalen);
	return FMOD_OK;
}

Thanks again!

1 Like

Hi,

Apologies for the delayed response. Thank you for the updated code, I am working on the issue. Once I have an update I will post it here.

1 Like

Hi Cornnor, no worries! Thank you very much for taking the time to work on the issue. It’s very much appreciated!

1 Like

Hi,

Thank you for your patience. I have other users also experiencing a similar issue so I am working to find a solution that will hopefully help everyone. Once I have some updates I will post them here!

3 Likes

Hi Connor,

Really appreciate the work you’re doing! I’ve still not figured out a solution yet - put it on the back burner whilst I wait :slight_smile: Looking forward to seeing what you come up with!
Thanks Again!

Hi,

Unfortunately, I have not been able to find a solution. However, there is a task to improve this workflow and I have noted your interest. Once there are updates I will post them here. Thank you again for your patience and I apologize that I can not assist further.

Hi Connor, ah that’s quite the shame! Thanks for all your efforts though - were you getting some of the same results I decribed? I’ll have another crack at it and post any results.
I know it’s been done before with this plugin for unity! If only there was one for Unreal.

EDIT:
This is the plugin for realtime voice - the above one is for playback I think

So there must be a way :slight_smile:

1 Like

Hi,

That will be greatly appreciated, any findings you make I will note in the task.

Yes, I was able to reproduce the issue exactly. Your project was vital in being able to do this.

There are a lot of other users experiencing the same issue, as I have noted on the task, if there are any updates I will let you know!

Hi, I will just post this here: Vivox to Unity OnAudioFilterRead to FMOD programmer sound. Stutters/crackling - #15 by dougmolina.

A user may have found a solution working with Vivox which may work with your approach. I have not had time to test it in Unreal Engine but I will continue investigating.

Hi, thanks for linking me this! I’ve had a shot at implementing it for UE in c++ as close as I could and then a whole bunch of other ways - unfortunely it always seems to give the same result as I had before sadly.
Thanks again for your time connor, I hope you have better luck! Really need this to work! There’s no solution at all for Unreal Engine at the moment.
Cheers :slight_smile:

1 Like