Add Games File

This commit is contained in:
Ivan Anikin
2026-05-19 13:42:41 +07:00
parent 7908479e0c
commit 12e7e22c1a
3672 changed files with 125922 additions and 0 deletions
@@ -0,0 +1,19 @@
{
"FileVersion": 3,
"Version": 1,
"VersionName": "1.0.0",
"FriendlyName": "Android Radio Player",
"Description": "Blueprint radio playback for Android using AndroidX Media3 ExoPlayer.",
"Category": "Media Players",
"CreatedBy": "FunnyGirl",
"CanContainContent": false,
"IsBetaVersion": true,
"EnabledByDefault": true,
"Modules": [
{
"Name": "AndroidRadioPlayer",
"Type": "Runtime",
"LoadingPhase": "Default"
}
]
}
@@ -0,0 +1,51 @@
<?xml version="1.0" encoding="utf-8"?>
<root xmlns:android="http://schemas.android.com/apk/res/android">
<init>
<log text="AndroidRadioPlayer init"/>
</init>
<androidManifestUpdates>
<addPermission android:name="android.permission.INTERNET"/>
<addAttribute tag="application" name="android:usesCleartextTraffic" value="true"/>
</androidManifestUpdates>
<buildGradleAdditions>
<insert>
dependencies {
implementation('androidx.media3:media3-exoplayer:1.3.1')
implementation('androidx.media3:media3-exoplayer-hls:1.3.1')
}
</insert>
</buildGradleAdditions>
<prebuildCopies>
<log text="Copying AndroidRadioPlayer Java source"/>
<copyFile src="$S(PluginDir)/Java/AndroidRadioPlayer.java" dst="$S(BuildDir)/src/com/epicgames/unreal/AndroidRadioPlayer.java"/>
</prebuildCopies>
<proguardAdditions>
<insert>
-keep class com.epicgames.unreal.AndroidRadioPlayer {
public *;
}
</insert>
</proguardAdditions>
<gameActivityOnPauseAdditions>
<insert>
AndroidRadioPlayer.handleActivityPause();
</insert>
</gameActivityOnPauseAdditions>
<gameActivityOnResumeAdditions>
<insert>
AndroidRadioPlayer.handleActivityResume();
</insert>
</gameActivityOnResumeAdditions>
<gameActivityOnDestroyAdditions>
<insert>
AndroidRadioPlayer.releaseRadio();
</insert>
</gameActivityOnDestroyAdditions>
</root>
@@ -0,0 +1,274 @@
package com.epicgames.unreal;
import android.app.Activity;
import android.net.Uri;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import androidx.media3.common.AudioAttributes;
import androidx.media3.common.C;
import androidx.media3.common.MediaItem;
import androidx.media3.common.MimeTypes;
import androidx.media3.common.PlaybackException;
import androidx.media3.common.Player;
import androidx.media3.exoplayer.DefaultLoadControl;
import androidx.media3.exoplayer.ExoPlayer;
public final class AndroidRadioPlayer {
private static final String TAG = "AndroidRadioPlayer";
private static final int STATE_IDLE = 0;
private static final int STATE_BUFFERING = 1;
private static final int STATE_READY = 2;
private static final int STATE_ENDED = 3;
private static final int STATE_ERROR = 4;
private static ExoPlayer player;
private static volatile int playbackState = STATE_IDLE;
private static volatile String lastError = "";
private static volatile boolean isPlaying = false;
private static volatile boolean shouldResumeAfterActivityPause = false;
private static volatile long cachedDurationMillis = 0;
private static volatile long cachedCurrentPositionMillis = 0;
private static float pendingVolume = 1.0f;
private static final Handler mainHandler = new Handler(Looper.getMainLooper());
private AndroidRadioPlayer() {
}
public static boolean playRadio(final Activity activity, final String url, final float volume) {
if (activity == null) {
lastError = "Activity is null";
playbackState = STATE_ERROR;
return false;
}
if (url == null || url.trim().isEmpty()) {
lastError = "Radio URL is empty";
playbackState = STATE_ERROR;
return false;
}
pendingVolume = clampVolume(volume);
activity.runOnUiThread(new Runnable() {
@Override
public void run() {
try {
ensurePlayer(activity);
lastError = "";
playbackState = STATE_BUFFERING;
player.stop();
player.clearMediaItems();
isPlaying = false;
player.setVolume(pendingVolume);
player.setMediaItem(buildMediaItem(url.trim()));
player.prepare();
player.play();
} catch (Throwable error) {
lastError = error.toString();
playbackState = STATE_ERROR;
Log.e(TAG, "Failed to play radio", error);
}
}
});
return true;
}
public static void stopRadio() {
runOnPlayerThread(new Runnable() {
@Override
public void run() {
releasePlayer();
}
});
}
public static void releaseRadio() {
runOnPlayerThread(new Runnable() {
@Override
public void run() {
releasePlayer();
}
});
}
public static void pauseRadio() {
runOnPlayerThread(new Runnable() {
@Override
public void run() {
if (player != null) {
player.pause();
}
}
});
}
public static void resumeRadio() {
runOnPlayerThread(new Runnable() {
@Override
public void run() {
if (player != null) {
player.play();
}
}
});
}
public static void setRadioVolume(final float volume) {
pendingVolume = clampVolume(volume);
runOnPlayerThread(new Runnable() {
@Override
public void run() {
if (player != null) {
player.setVolume(pendingVolume);
}
}
});
}
public static void handleActivityPause() {
runOnPlayerThread(new Runnable() {
@Override
public void run() {
shouldResumeAfterActivityPause = player != null && player.isPlaying();
if (player != null && shouldResumeAfterActivityPause) {
player.pause();
}
}
});
}
public static void handleActivityResume() {
runOnPlayerThread(new Runnable() {
@Override
public void run() {
if (player != null && shouldResumeAfterActivityPause) {
shouldResumeAfterActivityPause = false;
player.play();
}
}
});
}
public static boolean isRadioPlaying() {
return isPlaying;
}
public static int getPlaybackState() {
return playbackState;
}
public static String getLastError() {
return lastError == null ? "" : lastError;
}
public static long getDurationMillis() {
return cachedDurationMillis;
}
public static long getCurrentPositionMillis() {
return cachedCurrentPositionMillis;
}
private static void ensurePlayer(Activity activity) {
if (player != null) {
return;
}
DefaultLoadControl loadControl = new DefaultLoadControl.Builder()
.setBufferDurationsMs(5000, 15000, 1000, 2000)
.build();
player = new ExoPlayer.Builder(activity.getApplicationContext())
.setLoadControl(loadControl)
.setHandleAudioBecomingNoisy(true)
.build();
AudioAttributes audioAttributes = new AudioAttributes.Builder()
.setUsage(C.USAGE_MEDIA)
.setContentType(C.AUDIO_CONTENT_TYPE_MUSIC)
.build();
player.setAudioAttributes(audioAttributes, true);
player.setVolume(pendingVolume);
player.addListener(new Player.Listener() {
@Override
public void onPlaybackStateChanged(int state) {
if (state == Player.STATE_BUFFERING) {
playbackState = STATE_BUFFERING;
} else if (state == Player.STATE_READY) {
playbackState = STATE_READY;
long duration = player.getDuration();
cachedDurationMillis = duration == C.TIME_UNSET ? 0 : duration;
} else if (state == Player.STATE_ENDED) {
playbackState = STATE_ENDED;
} else {
playbackState = STATE_IDLE;
}
}
@Override
public void onIsPlayingChanged(boolean playing) {
isPlaying = playing;
}
@Override
public void onPlayerError(PlaybackException error) {
lastError = error == null ? "Unknown ExoPlayer error" : error.toString();
playbackState = STATE_ERROR;
isPlaying = false;
Log.e(TAG, "Playback error", error);
}
});
}
private static void releasePlayer() {
if (player != null) {
try {
player.stop();
player.clearMediaItems();
player.release();
} catch (Throwable error) {
Log.w(TAG, "Failed to release radio player cleanly", error);
}
player = null;
}
shouldResumeAfterActivityPause = false;
isPlaying = false;
cachedDurationMillis = 0;
cachedCurrentPositionMillis = 0;
playbackState = STATE_IDLE;
}
private static MediaItem buildMediaItem(String url) {
MediaItem.Builder builder = new MediaItem.Builder().setUri(Uri.parse(url));
String lowerUrl = url.toLowerCase();
if (lowerUrl.contains(".m3u8") || lowerUrl.contains("m3u8")) {
builder.setMimeType(MimeTypes.APPLICATION_M3U8);
} else if (lowerUrl.contains("aacp") || lowerUrl.contains(".aac") || lowerUrl.contains("audio/aac")) {
builder.setMimeType(MimeTypes.AUDIO_AAC);
} else if (lowerUrl.contains(".mp3") || lowerUrl.contains("mp3")) {
builder.setMimeType(MimeTypes.AUDIO_MPEG);
} else if (lowerUrl.contains(".ogg") || lowerUrl.contains("opus")) {
builder.setMimeType(MimeTypes.AUDIO_OGG);
}
return builder.build();
}
private static void runOnPlayerThread(Runnable runnable) {
mainHandler.post(runnable);
}
private static float clampVolume(float volume) {
if (volume < 0.0f) {
return 0.0f;
}
if (volume > 1.0f) {
return 1.0f;
}
return volume;
}
}
@@ -0,0 +1,36 @@
using System.IO;
using UnrealBuildTool;
public class AndroidRadioPlayer : ModuleRules
{
public AndroidRadioPlayer(ReadOnlyTargetRules Target) : base(Target)
{
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
PublicDependencyModuleNames.AddRange(new string[]
{
"Core",
"CoreUObject",
"Engine",
"Media",
"MediaAssets"
});
PrivateDependencyModuleNames.AddRange(new string[]
{
"Projects"
});
if (Target.Platform == UnrealTargetPlatform.Android)
{
PrivateDependencyModuleNames.AddRange(new string[]
{
"ApplicationCore",
"Launch"
});
string PluginPath = Utils.MakePathRelativeTo(ModuleDirectory, Target.RelativeEnginePath);
AdditionalPropertiesForReceipt.Add("AndroidPlugin", Path.Combine(PluginPath, "Android", "AndroidRadioPlayer_UPL.xml"));
}
}
}
@@ -0,0 +1,207 @@
#include "AndroidRadioJavaBridge.h"
#if PLATFORM_ANDROID
#include "Android/AndroidApplication.h"
#include "Android/AndroidJavaEnv.h"
#include "Android/AndroidJNI.h"
#endif
namespace AndroidRadioJavaBridge
{
#if PLATFORM_ANDROID
jclass GetJavaClass(JNIEnv* Env)
{
static jclass RadioPlayerClass = FAndroidApplication::FindJavaClassGlobalRef("com/epicgames/unreal/AndroidRadioPlayer");
if (RadioPlayerClass == nullptr)
{
FAndroidApplication::CheckJavaException();
UE_LOG(LogTemp, Error, TEXT("AndroidRadioPlayer Java class was not found."));
}
return RadioPlayerClass;
}
jmethodID GetStaticMethod(JNIEnv* Env, const ANSICHAR* MethodName, const ANSICHAR* Signature)
{
jclass RadioPlayerClass = GetJavaClass(Env);
if (RadioPlayerClass == nullptr)
{
return nullptr;
}
jmethodID Method = FJavaWrapper::FindStaticMethod(Env, RadioPlayerClass, MethodName, Signature, false);
if (Method == nullptr)
{
FAndroidApplication::CheckJavaException();
UE_LOG(LogTemp, Error, TEXT("AndroidRadioPlayer Java method was not found: %s"), ANSI_TO_TCHAR(MethodName));
}
return Method;
}
#endif
bool Play(const FString& Url, float Volume)
{
#if PLATFORM_ANDROID
if (Url.IsEmpty())
{
return false;
}
JNIEnv* Env = FAndroidApplication::GetJavaEnv();
if (Env == nullptr)
{
return false;
}
jclass RadioPlayerClass = GetJavaClass(Env);
jmethodID Method = GetStaticMethod(Env, "playRadio", "(Landroid/app/Activity;Ljava/lang/String;F)Z");
if (RadioPlayerClass == nullptr || Method == nullptr)
{
return false;
}
auto JavaUrl = FJavaHelper::ToJavaString(Env, Url);
const jfloat ClampedVolume = FMath::Clamp(Volume, 0.0f, 1.0f);
const jboolean Result = Env->CallStaticBooleanMethod(RadioPlayerClass, Method, FJavaWrapper::GameActivityThis, *JavaUrl, ClampedVolume);
FAndroidApplication::CheckJavaException();
return Result == JNI_TRUE;
#else
return false;
#endif
}
void Stop()
{
#if PLATFORM_ANDROID
JNIEnv* Env = FAndroidApplication::GetJavaEnv();
jclass RadioPlayerClass = Env ? GetJavaClass(Env) : nullptr;
jmethodID Method = Env ? GetStaticMethod(Env, "stopRadio", "()V") : nullptr;
if (RadioPlayerClass != nullptr && Method != nullptr)
{
Env->CallStaticVoidMethod(RadioPlayerClass, Method);
FAndroidApplication::CheckJavaException();
}
#endif
}
void Pause()
{
#if PLATFORM_ANDROID
JNIEnv* Env = FAndroidApplication::GetJavaEnv();
jclass RadioPlayerClass = Env ? GetJavaClass(Env) : nullptr;
jmethodID Method = Env ? GetStaticMethod(Env, "pauseRadio", "()V") : nullptr;
if (RadioPlayerClass != nullptr && Method != nullptr)
{
Env->CallStaticVoidMethod(RadioPlayerClass, Method);
FAndroidApplication::CheckJavaException();
}
#endif
}
void Resume()
{
#if PLATFORM_ANDROID
JNIEnv* Env = FAndroidApplication::GetJavaEnv();
jclass RadioPlayerClass = Env ? GetJavaClass(Env) : nullptr;
jmethodID Method = Env ? GetStaticMethod(Env, "resumeRadio", "()V") : nullptr;
if (RadioPlayerClass != nullptr && Method != nullptr)
{
Env->CallStaticVoidMethod(RadioPlayerClass, Method);
FAndroidApplication::CheckJavaException();
}
#endif
}
void SetVolume(float Volume)
{
#if PLATFORM_ANDROID
JNIEnv* Env = FAndroidApplication::GetJavaEnv();
jclass RadioPlayerClass = Env ? GetJavaClass(Env) : nullptr;
jmethodID Method = Env ? GetStaticMethod(Env, "setRadioVolume", "(F)V") : nullptr;
if (RadioPlayerClass != nullptr && Method != nullptr)
{
Env->CallStaticVoidMethod(RadioPlayerClass, Method, FMath::Clamp(Volume, 0.0f, 1.0f));
FAndroidApplication::CheckJavaException();
}
#endif
}
bool IsPlaying()
{
#if PLATFORM_ANDROID
JNIEnv* Env = FAndroidApplication::GetJavaEnv();
jclass RadioPlayerClass = Env ? GetJavaClass(Env) : nullptr;
jmethodID Method = Env ? GetStaticMethod(Env, "isRadioPlaying", "()Z") : nullptr;
if (RadioPlayerClass != nullptr && Method != nullptr)
{
const jboolean Result = Env->CallStaticBooleanMethod(RadioPlayerClass, Method);
FAndroidApplication::CheckJavaException();
return Result == JNI_TRUE;
}
#endif
return false;
}
int32 GetPlaybackState()
{
#if PLATFORM_ANDROID
JNIEnv* Env = FAndroidApplication::GetJavaEnv();
jclass RadioPlayerClass = Env ? GetJavaClass(Env) : nullptr;
jmethodID Method = Env ? GetStaticMethod(Env, "getPlaybackState", "()I") : nullptr;
if (RadioPlayerClass != nullptr && Method != nullptr)
{
const jint State = Env->CallStaticIntMethod(RadioPlayerClass, Method);
FAndroidApplication::CheckJavaException();
return static_cast<int32>(State);
}
#endif
return -1;
}
FString GetLastError()
{
#if PLATFORM_ANDROID
JNIEnv* Env = FAndroidApplication::GetJavaEnv();
jclass RadioPlayerClass = Env ? GetJavaClass(Env) : nullptr;
jmethodID Method = Env ? GetStaticMethod(Env, "getLastError", "()Ljava/lang/String;") : nullptr;
if (RadioPlayerClass != nullptr && Method != nullptr)
{
jstring Error = static_cast<jstring>(Env->CallStaticObjectMethod(RadioPlayerClass, Method));
FAndroidApplication::CheckJavaException();
return FJavaHelper::FStringFromLocalRef(Env, Error);
}
#endif
return FString();
}
int64 GetDurationMillis()
{
#if PLATFORM_ANDROID
JNIEnv* Env = FAndroidApplication::GetJavaEnv();
jclass RadioPlayerClass = Env ? GetJavaClass(Env) : nullptr;
jmethodID Method = Env ? GetStaticMethod(Env, "getDurationMillis", "()J") : nullptr;
if (RadioPlayerClass != nullptr && Method != nullptr)
{
const jlong Duration = Env->CallStaticLongMethod(RadioPlayerClass, Method);
FAndroidApplication::CheckJavaException();
return static_cast<int64>(Duration);
}
#endif
return 0;
}
int64 GetCurrentPositionMillis()
{
#if PLATFORM_ANDROID
JNIEnv* Env = FAndroidApplication::GetJavaEnv();
jclass RadioPlayerClass = Env ? GetJavaClass(Env) : nullptr;
jmethodID Method = Env ? GetStaticMethod(Env, "getCurrentPositionMillis", "()J") : nullptr;
if (RadioPlayerClass != nullptr && Method != nullptr)
{
const jlong Position = Env->CallStaticLongMethod(RadioPlayerClass, Method);
FAndroidApplication::CheckJavaException();
return static_cast<int64>(Position);
}
#endif
return 0;
}
}
@@ -0,0 +1,17 @@
#pragma once
#include "CoreMinimal.h"
namespace AndroidRadioJavaBridge
{
bool Play(const FString& Url, float Volume);
void Stop();
void Pause();
void Resume();
void SetVolume(float Volume);
bool IsPlaying();
int32 GetPlaybackState();
FString GetLastError();
int64 GetDurationMillis();
int64 GetCurrentPositionMillis();
}
@@ -0,0 +1,365 @@
#include "AndroidRadioMediaPlayer.h"
#include "AndroidRadioJavaBridge.h"
#include "IMediaEventSink.h"
#include "Misc/Guid.h"
namespace
{
constexpr int32 JavaStateIdle = 0;
constexpr int32 JavaStateBuffering = 1;
constexpr int32 JavaStateReady = 2;
constexpr int32 JavaStateEnded = 3;
constexpr int32 JavaStateError = 4;
}
FAndroidRadioMediaPlayer::FAndroidRadioMediaPlayer(IMediaEventSink& InEventSink)
: EventSink(InEventSink)
, CurrentState(EMediaState::Closed)
, CurrentStatus(EMediaStatus::None)
, CurrentRate(0.0f)
, CurrentVolume(1.0f)
, bLooping(false)
, bHasSentOpenEvent(false)
, bHasSentBufferingEvent(false)
, bHasSentEndEvent(false)
{
}
FAndroidRadioMediaPlayer::~FAndroidRadioMediaPlayer()
{
Close();
}
void FAndroidRadioMediaPlayer::Close()
{
if (CurrentState != EMediaState::Closed)
{
AndroidRadioJavaBridge::Stop();
CurrentUrl.Empty();
CurrentState = EMediaState::Closed;
CurrentStatus = EMediaStatus::None;
CurrentRate = 0.0f;
bHasSentOpenEvent = false;
bHasSentBufferingEvent = false;
bHasSentEndEvent = false;
EventSink.ReceiveMediaEvent(EMediaEvent::MediaClosed);
}
}
IMediaCache& FAndroidRadioMediaPlayer::GetCache()
{
return *this;
}
IMediaControls& FAndroidRadioMediaPlayer::GetControls()
{
return *this;
}
FString FAndroidRadioMediaPlayer::GetInfo() const
{
return FString::Printf(TEXT("Android Radio Player\nUrl: %s\nState: %d\nLast error: %s"), *CurrentUrl, static_cast<int32>(CurrentState), *AndroidRadioJavaBridge::GetLastError());
}
FGuid FAndroidRadioMediaPlayer::GetPlayerPluginGUID() const
{
static FGuid PlayerPluginGUID(0x2547a7af, 0xe9504c2f, 0xa090e1b7, 0x26c7d716);
return PlayerPluginGUID;
}
IMediaSamples& FAndroidRadioMediaPlayer::GetSamples()
{
return *this;
}
FString FAndroidRadioMediaPlayer::GetStats() const
{
return FString();
}
IMediaTracks& FAndroidRadioMediaPlayer::GetTracks()
{
return *this;
}
FString FAndroidRadioMediaPlayer::GetUrl() const
{
return CurrentUrl;
}
IMediaView& FAndroidRadioMediaPlayer::GetView()
{
return *this;
}
bool FAndroidRadioMediaPlayer::Open(const FString& Url, const IMediaOptions* Options)
{
Close();
if (!Url.StartsWith(TEXT("http://")) && !Url.StartsWith(TEXT("https://")))
{
CurrentState = EMediaState::Error;
return false;
}
CurrentUrl = Url;
CurrentState = EMediaState::Preparing;
CurrentStatus = EMediaStatus::Connecting | EMediaStatus::Buffering;
CurrentRate = 1.0f;
bHasSentOpenEvent = false;
bHasSentBufferingEvent = false;
bHasSentEndEvent = false;
EventSink.ReceiveMediaEvent(EMediaEvent::MediaConnecting);
if (!AndroidRadioJavaBridge::Play(Url, CurrentVolume))
{
CurrentState = EMediaState::Error;
CurrentStatus = EMediaStatus::None;
CurrentRate = 0.0f;
EventSink.ReceiveMediaEvent(EMediaEvent::MediaOpenFailed);
return false;
}
return true;
}
bool FAndroidRadioMediaPlayer::Open(const TSharedRef<FArchive, ESPMode::ThreadSafe>& Archive, const FString& OriginalUrl, const IMediaOptions* Options)
{
return false;
}
void FAndroidRadioMediaPlayer::TickInput(FTimespan DeltaTime, FTimespan Timecode)
{
UpdateStateFromJava();
}
bool FAndroidRadioMediaPlayer::SetNativeVolume(float Volume)
{
CurrentVolume = FMath::Clamp(Volume, 0.0f, 1.0f);
AndroidRadioJavaBridge::SetVolume(CurrentVolume);
return true;
}
bool FAndroidRadioMediaPlayer::CanControl(EMediaControl Control) const
{
return Control == EMediaControl::Pause || Control == EMediaControl::Resume;
}
FTimespan FAndroidRadioMediaPlayer::GetDuration() const
{
const int64 DurationMillis = AndroidRadioJavaBridge::GetDurationMillis();
return DurationMillis > 0 ? FTimespan::FromMilliseconds(DurationMillis) : FTimespan::Zero();
}
float FAndroidRadioMediaPlayer::GetRate() const
{
return CurrentRate;
}
EMediaState FAndroidRadioMediaPlayer::GetState() const
{
return CurrentState;
}
EMediaStatus FAndroidRadioMediaPlayer::GetStatus() const
{
return CurrentStatus;
}
TRangeSet<float> FAndroidRadioMediaPlayer::GetSupportedRates(EMediaRateThinning Thinning) const
{
TRangeSet<float> Result;
Result.Add(TRange<float>(0.0f));
Result.Add(TRange<float>(1.0f));
return Result;
}
FTimespan FAndroidRadioMediaPlayer::GetTime() const
{
return FTimespan::FromMilliseconds(AndroidRadioJavaBridge::GetCurrentPositionMillis());
}
bool FAndroidRadioMediaPlayer::IsLooping() const
{
return bLooping;
}
bool FAndroidRadioMediaPlayer::Seek(const FTimespan& Time)
{
return false;
}
bool FAndroidRadioMediaPlayer::SetLooping(bool Looping)
{
bLooping = Looping;
return true;
}
bool FAndroidRadioMediaPlayer::SetRate(float Rate)
{
if (CurrentState == EMediaState::Closed || CurrentState == EMediaState::Error)
{
return false;
}
if (FMath::IsNearlyZero(Rate))
{
AndroidRadioJavaBridge::Pause();
CurrentRate = 0.0f;
CurrentState = EMediaState::Paused;
EventSink.ReceiveMediaEvent(EMediaEvent::PlaybackSuspended);
return true;
}
if (FMath::IsNearlyEqual(Rate, 1.0f))
{
AndroidRadioJavaBridge::Resume();
CurrentRate = 1.0f;
CurrentState = EMediaState::Playing;
EventSink.ReceiveMediaEvent(EMediaEvent::PlaybackResumed);
return true;
}
return false;
}
bool FAndroidRadioMediaPlayer::GetAudioTrackFormat(int32 TrackIndex, int32 FormatIndex, FMediaAudioTrackFormat& OutFormat) const
{
if (TrackIndex == 0 && FormatIndex == 0)
{
OutFormat.BitsPerSample = 16;
OutFormat.NumChannels = 2;
OutFormat.SampleRate = 44100;
OutFormat.TypeName = TEXT("External");
return true;
}
return false;
}
int32 FAndroidRadioMediaPlayer::GetNumTracks(EMediaTrackType TrackType) const
{
return TrackType == EMediaTrackType::Audio ? 1 : 0;
}
int32 FAndroidRadioMediaPlayer::GetNumTrackFormats(EMediaTrackType TrackType, int32 TrackIndex) const
{
return TrackType == EMediaTrackType::Audio && TrackIndex == 0 ? 1 : 0;
}
int32 FAndroidRadioMediaPlayer::GetSelectedTrack(EMediaTrackType TrackType) const
{
return TrackType == EMediaTrackType::Audio ? 0 : INDEX_NONE;
}
FText FAndroidRadioMediaPlayer::GetTrackDisplayName(EMediaTrackType TrackType, int32 TrackIndex) const
{
return TrackType == EMediaTrackType::Audio && TrackIndex == 0 ? NSLOCTEXT("AndroidRadioMediaPlayer", "AudioTrackName", "Radio") : FText::GetEmpty();
}
int32 FAndroidRadioMediaPlayer::GetTrackFormat(EMediaTrackType TrackType, int32 TrackIndex) const
{
return TrackType == EMediaTrackType::Audio && TrackIndex == 0 ? 0 : INDEX_NONE;
}
FString FAndroidRadioMediaPlayer::GetTrackLanguage(EMediaTrackType TrackType, int32 TrackIndex) const
{
return TEXT("und");
}
FString FAndroidRadioMediaPlayer::GetTrackName(EMediaTrackType TrackType, int32 TrackIndex) const
{
return TrackType == EMediaTrackType::Audio && TrackIndex == 0 ? TEXT("Radio") : FString();
}
bool FAndroidRadioMediaPlayer::GetVideoTrackFormat(int32 TrackIndex, int32 FormatIndex, FMediaVideoTrackFormat& OutFormat) const
{
return false;
}
bool FAndroidRadioMediaPlayer::SelectTrack(EMediaTrackType TrackType, int32 TrackIndex)
{
return TrackType == EMediaTrackType::Audio && TrackIndex == 0;
}
bool FAndroidRadioMediaPlayer::SetTrackFormat(EMediaTrackType TrackType, int32 TrackIndex, int32 FormatIndex)
{
return TrackType == EMediaTrackType::Audio && TrackIndex == 0 && FormatIndex == 0;
}
bool FAndroidRadioMediaPlayer::PeekVideoSampleTime(FMediaTimeStamp& TimeStamp)
{
return false;
}
void FAndroidRadioMediaPlayer::UpdateStateFromJava()
{
if (CurrentState == EMediaState::Closed || CurrentUrl.IsEmpty())
{
return;
}
const int32 JavaState = AndroidRadioJavaBridge::GetPlaybackState();
const bool bIsPlaying = AndroidRadioJavaBridge::IsPlaying();
if (JavaState == JavaStateError)
{
if (CurrentState != EMediaState::Error)
{
CurrentState = EMediaState::Error;
CurrentStatus = EMediaStatus::None;
CurrentRate = 0.0f;
EventSink.ReceiveMediaEvent(bHasSentOpenEvent ? EMediaEvent::PlaybackSuspended : EMediaEvent::MediaOpenFailed);
}
return;
}
if (JavaState == JavaStateBuffering)
{
CurrentState = EMediaState::Preparing;
CurrentStatus = EMediaStatus::Buffering;
if (!bHasSentBufferingEvent)
{
bHasSentBufferingEvent = true;
EventSink.ReceiveMediaEvent(EMediaEvent::MediaBuffering);
}
return;
}
if (JavaState == JavaStateReady)
{
if (!bHasSentOpenEvent)
{
bHasSentOpenEvent = true;
EventSink.ReceiveMediaEvent(EMediaEvent::MediaOpened);
EventSink.ReceiveMediaEvent(EMediaEvent::TracksChanged);
EventSink.ReceiveMediaEvent(EMediaEvent::MediaBufferingComplete);
}
CurrentStatus = EMediaStatus::None;
CurrentState = bIsPlaying ? EMediaState::Playing : EMediaState::Paused;
CurrentRate = bIsPlaying ? 1.0f : 0.0f;
bHasSentBufferingEvent = false;
return;
}
if (JavaState == JavaStateEnded)
{
CurrentState = EMediaState::Stopped;
CurrentStatus = EMediaStatus::None;
CurrentRate = 0.0f;
if (!bHasSentEndEvent)
{
bHasSentEndEvent = true;
EventSink.ReceiveMediaEvent(EMediaEvent::PlaybackEndReached);
}
return;
}
if (JavaState == JavaStateIdle)
{
CurrentState = CurrentUrl.IsEmpty() ? EMediaState::Closed : EMediaState::Stopped;
CurrentStatus = EMediaStatus::None;
CurrentRate = 0.0f;
}
}
@@ -0,0 +1,78 @@
#pragma once
#include "IMediaCache.h"
#include "IMediaControls.h"
#include "IMediaPlayer.h"
#include "IMediaSamples.h"
#include "IMediaTracks.h"
#include "IMediaView.h"
class IMediaEventSink;
class FAndroidRadioMediaPlayer
: public IMediaPlayer
, protected IMediaCache
, protected IMediaControls
, protected IMediaSamples
, protected IMediaTracks
, protected IMediaView
{
public:
explicit FAndroidRadioMediaPlayer(IMediaEventSink& InEventSink);
virtual ~FAndroidRadioMediaPlayer() override;
virtual void Close() override;
virtual IMediaCache& GetCache() override;
virtual IMediaControls& GetControls() override;
virtual FString GetInfo() const override;
virtual FGuid GetPlayerPluginGUID() const override;
virtual IMediaSamples& GetSamples() override;
virtual FString GetStats() const override;
virtual IMediaTracks& GetTracks() override;
virtual FString GetUrl() const override;
virtual IMediaView& GetView() override;
virtual bool Open(const FString& Url, const IMediaOptions* Options) override;
virtual bool Open(const TSharedRef<FArchive, ESPMode::ThreadSafe>& Archive, const FString& OriginalUrl, const IMediaOptions* Options) override;
virtual void TickInput(FTimespan DeltaTime, FTimespan Timecode) override;
virtual bool SetNativeVolume(float Volume) override;
virtual bool CanControl(EMediaControl Control) const override;
virtual FTimespan GetDuration() const override;
virtual float GetRate() const override;
virtual EMediaState GetState() const override;
virtual EMediaStatus GetStatus() const override;
virtual TRangeSet<float> GetSupportedRates(EMediaRateThinning Thinning) const override;
virtual FTimespan GetTime() const override;
virtual bool IsLooping() const override;
virtual bool Seek(const FTimespan& Time) override;
virtual bool SetLooping(bool Looping) override;
virtual bool SetRate(float Rate) override;
virtual bool GetAudioTrackFormat(int32 TrackIndex, int32 FormatIndex, FMediaAudioTrackFormat& OutFormat) const override;
virtual int32 GetNumTracks(EMediaTrackType TrackType) const override;
virtual int32 GetNumTrackFormats(EMediaTrackType TrackType, int32 TrackIndex) const override;
virtual int32 GetSelectedTrack(EMediaTrackType TrackType) const override;
virtual FText GetTrackDisplayName(EMediaTrackType TrackType, int32 TrackIndex) const override;
virtual int32 GetTrackFormat(EMediaTrackType TrackType, int32 TrackIndex) const override;
virtual FString GetTrackLanguage(EMediaTrackType TrackType, int32 TrackIndex) const override;
virtual FString GetTrackName(EMediaTrackType TrackType, int32 TrackIndex) const override;
virtual bool GetVideoTrackFormat(int32 TrackIndex, int32 FormatIndex, FMediaVideoTrackFormat& OutFormat) const override;
virtual bool SelectTrack(EMediaTrackType TrackType, int32 TrackIndex) override;
virtual bool SetTrackFormat(EMediaTrackType TrackType, int32 TrackIndex, int32 FormatIndex) override;
virtual bool PeekVideoSampleTime(FMediaTimeStamp& TimeStamp) override;
private:
void UpdateStateFromJava();
IMediaEventSink& EventSink;
FString CurrentUrl;
EMediaState CurrentState;
EMediaStatus CurrentStatus;
float CurrentRate;
float CurrentVolume;
bool bLooping;
bool bHasSentOpenEvent;
bool bHasSentBufferingEvent;
bool bHasSentEndEvent;
};
@@ -0,0 +1,74 @@
#include "AndroidRadioPlayerBPLibrary.h"
#include "AndroidRadioJavaBridge.h"
bool UAndroidRadioPlayerBPLibrary::OpenStreamMediaSourceUrl(UMediaPlayer* MediaPlayer, UStreamMediaSource* StreamMediaSource, const FString& Url)
{
if (MediaPlayer == nullptr || StreamMediaSource == nullptr || !Url.Contains(TEXT("://")))
{
return false;
}
StreamMediaSource->StreamUrl = Url;
MediaPlayer->Close();
return MediaPlayer->OpenSource(StreamMediaSource);
}
bool UAndroidRadioPlayerBPLibrary::PlayRadio(const FString& Url, float Volume)
{
return AndroidRadioJavaBridge::Play(Url, Volume);
}
void UAndroidRadioPlayerBPLibrary::StopRadio()
{
AndroidRadioJavaBridge::Stop();
}
void UAndroidRadioPlayerBPLibrary::PauseRadio()
{
AndroidRadioJavaBridge::Pause();
}
void UAndroidRadioPlayerBPLibrary::ResumeRadio()
{
AndroidRadioJavaBridge::Resume();
}
void UAndroidRadioPlayerBPLibrary::SetRadioVolume(float Volume)
{
AndroidRadioJavaBridge::SetVolume(Volume);
}
bool UAndroidRadioPlayerBPLibrary::IsRadioPlaying()
{
return AndroidRadioJavaBridge::IsPlaying();
}
EAndroidRadioPlaybackState UAndroidRadioPlayerBPLibrary::GetRadioPlaybackState()
{
switch (AndroidRadioJavaBridge::GetPlaybackState())
{
case 0:
return EAndroidRadioPlaybackState::Idle;
case 1:
return EAndroidRadioPlaybackState::Buffering;
case 2:
return EAndroidRadioPlaybackState::Ready;
case 3:
return EAndroidRadioPlaybackState::Ended;
case 4:
return EAndroidRadioPlaybackState::Error;
default:
return EAndroidRadioPlaybackState::Unsupported;
}
}
FString UAndroidRadioPlayerBPLibrary::GetRadioLastError()
{
return AndroidRadioJavaBridge::GetLastError();
}
bool UAndroidRadioPlayerBPLibrary::IsAndroidRadioPlayerSupported()
{
return PLATFORM_ANDROID;
}
@@ -0,0 +1,115 @@
#include "AndroidRadioMediaPlayer.h"
#include "IMediaModule.h"
#include "IMediaPlayerFactory.h"
#include "Misc/Paths.h"
#include "Modules/ModuleManager.h"
#define LOCTEXT_NAMESPACE "FAndroidRadioPlayerModule"
class FAndroidRadioPlayerModule
: public IModuleInterface
, public IMediaPlayerFactory
{
public:
virtual void StartupModule() override
{
SupportedPlatforms.Add(TEXT("Android"));
SupportedUriSchemes.Add(TEXT("http"));
SupportedUriSchemes.Add(TEXT("https"));
if (IMediaModule* MediaModule = FModuleManager::LoadModulePtr<IMediaModule>("Media"))
{
MediaModule->RegisterPlayerFactory(*this);
}
}
virtual void ShutdownModule() override
{
if (IMediaModule* MediaModule = FModuleManager::GetModulePtr<IMediaModule>("Media"))
{
MediaModule->UnregisterPlayerFactory(*this);
}
}
virtual bool CanPlayUrl(const FString& Url, const IMediaOptions* Options, TArray<FText>* OutWarnings, TArray<FText>* OutErrors) const override
{
return GetPlayabilityConfidenceScore(Url, Options, OutWarnings, OutErrors) > 0;
}
virtual int32 GetPlayabilityConfidenceScore(const FString& Url, const IMediaOptions* Options, TArray<FText>* OutWarnings, TArray<FText>* OutErrors) const override
{
FString Scheme;
FString Location;
if (!Url.Split(TEXT("://"), &Scheme, &Location, ESearchCase::CaseSensitive))
{
if (OutErrors != nullptr)
{
OutErrors->Add(LOCTEXT("NoSchemeFound", "No URI scheme found"));
}
return 0;
}
if (!SupportedUriSchemes.Contains(Scheme))
{
if (OutErrors != nullptr)
{
OutErrors->Add(FText::Format(LOCTEXT("SchemeNotSupported", "The URI scheme '{0}' is not supported"), FText::FromString(Scheme)));
}
return 0;
}
const FString Extension = FPaths::GetExtension(Location, false).ToLower();
if (Extension.IsEmpty() || Extension == TEXT("mp3") || Extension == TEXT("aac") || Extension == TEXT("m3u8") || Extension == TEXT("pls") || Extension == TEXT("ogg") || Extension == TEXT("opus"))
{
return 90;
}
if (OutWarnings != nullptr)
{
OutWarnings->Add(FText::Format(LOCTEXT("ExtensionProbablyNotRadio", "The file extension '{0}' is not a known radio stream extension"), FText::FromString(Extension)));
}
return 20;
}
virtual TSharedPtr<IMediaPlayer, ESPMode::ThreadSafe> CreatePlayer(IMediaEventSink& EventSink) override
{
return MakeShared<FAndroidRadioMediaPlayer, ESPMode::ThreadSafe>(EventSink);
}
virtual FText GetDisplayName() const override
{
return LOCTEXT("MediaPlayerDisplayName", "Android Radio Player");
}
virtual FName GetPlayerName() const override
{
static FName PlayerName(TEXT("AndroidRadioPlayer"));
return PlayerName;
}
virtual FGuid GetPlayerPluginGUID() const override
{
static FGuid PlayerPluginGUID(0x2547a7af, 0xe9504c2f, 0xa090e1b7, 0x26c7d716);
return PlayerPluginGUID;
}
virtual const TArray<FString>& GetSupportedPlatforms() const override
{
return SupportedPlatforms;
}
virtual bool SupportsFeature(EMediaFeature Feature) const override
{
return Feature == EMediaFeature::AudioTracks;
}
private:
TArray<FString> SupportedPlatforms;
TArray<FString> SupportedUriSchemes;
};
IMPLEMENT_MODULE(FAndroidRadioPlayerModule, AndroidRadioPlayer)
#undef LOCTEXT_NAMESPACE
@@ -0,0 +1,54 @@
#pragma once
#include "Kismet/BlueprintFunctionLibrary.h"
#include "MediaPlayer.h"
#include "StreamMediaSource.h"
#include "AndroidRadioPlayerBPLibrary.generated.h"
UENUM(BlueprintType)
enum class EAndroidRadioPlaybackState : uint8
{
Idle UMETA(DisplayName = "Idle"),
Buffering UMETA(DisplayName = "Buffering"),
Ready UMETA(DisplayName = "Ready"),
Ended UMETA(DisplayName = "Ended"),
Error UMETA(DisplayName = "Error"),
Unsupported UMETA(DisplayName = "Unsupported")
};
UCLASS()
class ANDROIDRADIOPLAYER_API UAndroidRadioPlayerBPLibrary : public UBlueprintFunctionLibrary
{
GENERATED_BODY()
public:
UFUNCTION(BlueprintCallable, Category = "Android Radio Player|Media")
static bool OpenStreamMediaSourceUrl(UMediaPlayer* MediaPlayer, UStreamMediaSource* StreamMediaSource, const FString& Url);
UFUNCTION(BlueprintCallable, Category = "Android Radio Player")
static bool PlayRadio(const FString& Url, float Volume = 1.0f);
UFUNCTION(BlueprintCallable, Category = "Android Radio Player")
static void StopRadio();
UFUNCTION(BlueprintCallable, Category = "Android Radio Player")
static void PauseRadio();
UFUNCTION(BlueprintCallable, Category = "Android Radio Player")
static void ResumeRadio();
UFUNCTION(BlueprintCallable, Category = "Android Radio Player")
static void SetRadioVolume(float Volume);
UFUNCTION(BlueprintPure, Category = "Android Radio Player")
static bool IsRadioPlaying();
UFUNCTION(BlueprintPure, Category = "Android Radio Player")
static EAndroidRadioPlaybackState GetRadioPlaybackState();
UFUNCTION(BlueprintPure, Category = "Android Radio Player")
static FString GetRadioLastError();
UFUNCTION(BlueprintPure, Category = "Android Radio Player")
static bool IsAndroidRadioPlayerSupported();
};
@@ -0,0 +1,19 @@
{
"FileVersion": 3,
"Version": 1,
"VersionName": "1.0.0",
"FriendlyName": "Android Speech To Text",
"Description": "Blueprint Android speech recognition using the native SpeechRecognizer API.",
"Category": "Input",
"CreatedBy": "FunnyGirl",
"CanContainContent": false,
"IsBetaVersion": true,
"EnabledByDefault": true,
"Modules": [
{
"Name": "AndroidSpeechToText",
"Type": "Runtime",
"LoadingPhase": "Default"
}
]
}
@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<root xmlns:android="http://schemas.android.com/apk/res/android">
<init>
<log text="AndroidSpeechToText init"/>
</init>
<androidManifestUpdates>
<addPermission android:name="android.permission.RECORD_AUDIO"/>
</androidManifestUpdates>
<prebuildCopies>
<log text="Copying AndroidSpeechToText Java source"/>
<copyFile src="$S(PluginDir)/Java/AndroidSpeechToText.java" dst="$S(BuildDir)/src/com/epicgames/unreal/AndroidSpeechToText.java"/>
</prebuildCopies>
<proguardAdditions>
<insert>
-keep class com.epicgames.unreal.AndroidSpeechToText {
public *;
}
</insert>
</proguardAdditions>
<gameActivityOnDestroyAdditions>
<insert>
AndroidSpeechToText.destroyRecognizer();
</insert>
</gameActivityOnDestroyAdditions>
<gameActivityOnRequestPermissionsResultAdditions>
<insert>
AndroidSpeechToText.handlePermissionResult(this, requestCode, permissions, grantResults);
</insert>
</gameActivityOnRequestPermissionsResultAdditions>
</root>
@@ -0,0 +1,389 @@
package com.epicgames.unreal;
import android.Manifest;
import android.app.Activity;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.speech.RecognitionListener;
import android.speech.RecognizerIntent;
import android.speech.SpeechRecognizer;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Locale;
public class AndroidSpeechToText {
private static final int REQUEST_RECORD_AUDIO = 52031;
private static final long RESTART_DELAY_MS = 250;
private static SpeechRecognizer recognizer = null;
private static boolean isListening = false;
private static boolean enablePartialResults = true;
private static boolean keepListening = false;
private static boolean recognizerActive = false;
private static boolean hasPendingPermissionStart = false;
private static String pendingLanguageTag = "";
private static boolean pendingPreferOffline = false;
private static boolean pendingPartialResults = true;
private static boolean pendingKeepListening = true;
private static String activeLanguageTag = "";
private static boolean activePreferOffline = false;
private static boolean activePartialResults = true;
private static WeakReference<Activity> activeActivity = new WeakReference<Activity>(null);
private static String lastFinalText = "";
private static String lastPartialText = "";
private static String lastErrorMessage = "";
private static final Handler mainHandler = new Handler(Looper.getMainLooper());
public static native void nativeOnReady();
public static native void nativeOnBeginningOfSpeech();
public static native void nativeOnEndOfSpeech();
public static native void nativeOnPartialText(String text);
public static native void nativeOnFinalText(String text);
public static native void nativeOnError(int errorCode, String message);
public static boolean startListening(final Activity activity, final String languageTag, final boolean preferOffline, final boolean partialResults, final boolean shouldKeepListening) {
if (activity == null) {
keepListening = false;
setError(SpeechRecognizer.ERROR_CLIENT, "Android activity is not available.");
return false;
}
if (!SpeechRecognizer.isRecognitionAvailable(activity)) {
keepListening = false;
setError(1000, "Speech recognition is not available on this device.");
return false;
}
if (!hasRecordAudioPermission(activity)) {
hasPendingPermissionStart = true;
pendingLanguageTag = languageTag != null ? languageTag : "";
pendingPreferOffline = preferOffline;
pendingPartialResults = partialResults;
pendingKeepListening = shouldKeepListening;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
activity.requestPermissions(new String[] { Manifest.permission.RECORD_AUDIO }, REQUEST_RECORD_AUDIO);
}
setError(SpeechRecognizer.ERROR_INSUFFICIENT_PERMISSIONS, "Microphone permission is required.");
return false;
}
enablePartialResults = partialResults;
keepListening = shouldKeepListening;
activeLanguageTag = languageTag != null ? languageTag : "";
activePreferOffline = preferOffline;
activePartialResults = partialResults;
activeActivity = new WeakReference<Activity>(activity);
activity.runOnUiThread(new Runnable() {
@Override
public void run() {
try {
ensureRecognizer(activity);
lastFinalText = "";
lastPartialText = "";
lastErrorMessage = "";
Intent intent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH);
intent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, RecognizerIntent.LANGUAGE_MODEL_FREE_FORM);
intent.putExtra(RecognizerIntent.EXTRA_PARTIAL_RESULTS, partialResults);
intent.putExtra(RecognizerIntent.EXTRA_CALLING_PACKAGE, activity.getPackageName());
intent.putExtra(RecognizerIntent.EXTRA_PREFER_OFFLINE, preferOffline);
if (languageTag != null && languageTag.length() > 0) {
intent.putExtra(RecognizerIntent.EXTRA_LANGUAGE, languageTag);
} else {
intent.putExtra(RecognizerIntent.EXTRA_LANGUAGE, Locale.getDefault().toLanguageTag());
}
isListening = true;
recognizerActive = true;
recognizer.startListening(intent);
} catch (Exception exception) {
isListening = false;
recognizerActive = false;
setError(SpeechRecognizer.ERROR_CLIENT, exception.getMessage());
}
}
});
return true;
}
public static void stopListening() {
keepListening = false;
if (recognizer == null) {
isListening = false;
recognizerActive = false;
return;
}
runRecognizerCommand(new Runnable() {
@Override
public void run() {
try {
recognizer.stopListening();
} catch (Exception exception) {
isListening = false;
recognizerActive = false;
setError(SpeechRecognizer.ERROR_CLIENT, exception.getMessage());
}
}
});
}
public static void cancelListening() {
keepListening = false;
if (recognizer == null) {
isListening = false;
recognizerActive = false;
return;
}
runRecognizerCommand(new Runnable() {
@Override
public void run() {
try {
recognizer.cancel();
} catch (Exception exception) {
setError(SpeechRecognizer.ERROR_CLIENT, exception.getMessage());
}
isListening = false;
recognizerActive = false;
}
});
}
public static void destroyRecognizer() {
keepListening = false;
if (recognizer == null) {
isListening = false;
recognizerActive = false;
return;
}
runRecognizerCommand(new Runnable() {
@Override
public void run() {
try {
recognizer.destroy();
} catch (Exception ignored) {
}
recognizer = null;
isListening = false;
recognizerActive = false;
}
});
}
public static boolean isListening() {
return isListening || keepListening;
}
public static boolean isSpeechRecognizerAvailable(Activity activity) {
return activity != null && SpeechRecognizer.isRecognitionAvailable(activity);
}
public static void handlePermissionResult(Activity activity, int requestCode, String[] permissions, int[] grantResults) {
if (requestCode != REQUEST_RECORD_AUDIO || !hasPendingPermissionStart) {
return;
}
boolean granted = grantResults != null
&& grantResults.length > 0
&& grantResults[0] == PackageManager.PERMISSION_GRANTED;
if (!granted) {
hasPendingPermissionStart = false;
setError(SpeechRecognizer.ERROR_INSUFFICIENT_PERMISSIONS, "Microphone permission was denied.");
return;
}
String languageTag = pendingLanguageTag;
boolean preferOffline = pendingPreferOffline;
boolean partialResults = pendingPartialResults;
boolean shouldKeepListening = pendingKeepListening;
hasPendingPermissionStart = false;
startListening(activity, languageTag, preferOffline, partialResults, shouldKeepListening);
}
public static String getLastFinalText() {
return lastFinalText;
}
public static String getLastPartialText() {
return lastPartialText;
}
public static String getLastErrorMessage() {
return lastErrorMessage;
}
private static void ensureRecognizer(Activity activity) {
if (recognizer != null) {
recognizer.cancel();
recognizer.destroy();
}
recognizer = SpeechRecognizer.createSpeechRecognizer(activity.getApplicationContext());
recognizer.setRecognitionListener(new RecognitionListener() {
@Override
public void onReadyForSpeech(Bundle params) {
nativeOnReady();
}
@Override
public void onBeginningOfSpeech() {
nativeOnBeginningOfSpeech();
}
@Override
public void onRmsChanged(float rmsdB) {
}
@Override
public void onBufferReceived(byte[] buffer) {
}
@Override
public void onEndOfSpeech() {
recognizerActive = false;
isListening = keepListening;
nativeOnEndOfSpeech();
}
@Override
public void onError(int error) {
recognizerActive = false;
isListening = keepListening;
setError(error, errorToMessage(error));
if (shouldRestartAfterError(error)) {
scheduleRestart();
}
}
@Override
public void onResults(Bundle results) {
recognizerActive = false;
isListening = keepListening;
String text = firstRecognitionResult(results);
lastFinalText = text;
nativeOnFinalText(text);
scheduleRestart();
}
@Override
public void onPartialResults(Bundle partialResults) {
if (!enablePartialResults) {
return;
}
String text = firstRecognitionResult(partialResults);
lastPartialText = text;
nativeOnPartialText(text);
}
@Override
public void onEvent(int eventType, Bundle params) {
}
});
}
private static String firstRecognitionResult(Bundle results) {
if (results == null) {
return "";
}
ArrayList<String> matches = results.getStringArrayList(SpeechRecognizer.RESULTS_RECOGNITION);
if (matches == null || matches.isEmpty()) {
return "";
}
String first = matches.get(0);
return first != null ? first : "";
}
private static boolean hasRecordAudioPermission(Activity activity) {
return Build.VERSION.SDK_INT < Build.VERSION_CODES.M
|| activity.checkSelfPermission(Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED;
}
private static void runRecognizerCommand(Runnable command) {
mainHandler.post(command);
}
private static void scheduleRestart() {
if (!keepListening) {
return;
}
mainHandler.postDelayed(new Runnable() {
@Override
public void run() {
if (!keepListening || recognizerActive) {
return;
}
Activity activity = activeActivity.get();
if (activity == null) {
keepListening = false;
isListening = false;
setError(SpeechRecognizer.ERROR_CLIENT, "Android activity is not available.");
return;
}
startListening(activity, activeLanguageTag, activePreferOffline, activePartialResults, true);
}
}, RESTART_DELAY_MS);
}
private static boolean shouldRestartAfterError(int error) {
return keepListening
&& error != SpeechRecognizer.ERROR_INSUFFICIENT_PERMISSIONS
&& error != SpeechRecognizer.ERROR_CLIENT
&& error != 1000;
}
private static void setError(int errorCode, String message) {
lastErrorMessage = message != null ? message : errorToMessage(errorCode);
nativeOnError(errorCode, lastErrorMessage);
}
private static String errorToMessage(int error) {
switch (error) {
case SpeechRecognizer.ERROR_NETWORK_TIMEOUT:
return "Network timeout.";
case SpeechRecognizer.ERROR_NETWORK:
return "Network error.";
case SpeechRecognizer.ERROR_AUDIO:
return "Audio recording error.";
case SpeechRecognizer.ERROR_SERVER:
return "Recognition server error.";
case SpeechRecognizer.ERROR_CLIENT:
return "Recognition client error.";
case SpeechRecognizer.ERROR_SPEECH_TIMEOUT:
return "No speech input.";
case SpeechRecognizer.ERROR_NO_MATCH:
return "Speech was not recognized.";
case SpeechRecognizer.ERROR_RECOGNIZER_BUSY:
return "Speech recognizer is busy.";
case SpeechRecognizer.ERROR_INSUFFICIENT_PERMISSIONS:
return "Insufficient microphone permissions.";
case 10:
return "Too many recognition requests.";
case 11:
return "Recognition server disconnected.";
case 12:
return "Language is not supported.";
case 13:
return "Language is unavailable.";
case 1000:
return "Speech recognition is not available on this device.";
default:
return "Unknown speech recognition error: " + error;
}
}
}
@@ -0,0 +1,34 @@
using System.IO;
using UnrealBuildTool;
public class AndroidSpeechToText : ModuleRules
{
public AndroidSpeechToText(ReadOnlyTargetRules Target) : base(Target)
{
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
PublicDependencyModuleNames.AddRange(new string[]
{
"Core",
"CoreUObject",
"Engine"
});
PrivateDependencyModuleNames.AddRange(new string[]
{
"Projects"
});
if (Target.Platform == UnrealTargetPlatform.Android)
{
PrivateDependencyModuleNames.AddRange(new string[]
{
"ApplicationCore",
"Launch"
});
string PluginPath = Utils.MakePathRelativeTo(ModuleDirectory, Target.RelativeEnginePath);
AdditionalPropertiesForReceipt.Add("AndroidPlugin", Path.Combine(PluginPath, "Android", "AndroidSpeechToText_UPL.xml"));
}
}
}
@@ -0,0 +1,140 @@
#include "AndroidSpeechToTextComponent.h"
#include "AndroidSpeechToTextEvents.h"
#include "AndroidSpeechToTextJavaBridge.h"
UAndroidSpeechToTextComponent::UAndroidSpeechToTextComponent()
{
PrimaryComponentTick.bCanEverTick = false;
}
void UAndroidSpeechToTextComponent::BeginPlay()
{
Super::BeginPlay();
AndroidSpeechToTextEvents::OnReady.AddUObject(this, &UAndroidSpeechToTextComponent::HandleSpeechReady);
AndroidSpeechToTextEvents::OnBeginning.AddUObject(this, &UAndroidSpeechToTextComponent::HandleSpeechStarted);
AndroidSpeechToTextEvents::OnEnd.AddUObject(this, &UAndroidSpeechToTextComponent::HandleSpeechEnded);
AndroidSpeechToTextEvents::OnPartialText.AddUObject(this, &UAndroidSpeechToTextComponent::HandlePartialText);
AndroidSpeechToTextEvents::OnFinalText.AddUObject(this, &UAndroidSpeechToTextComponent::HandleFinalText);
AndroidSpeechToTextEvents::OnError.AddUObject(this, &UAndroidSpeechToTextComponent::HandleSpeechError);
}
void UAndroidSpeechToTextComponent::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
AndroidSpeechToTextEvents::OnReady.RemoveAll(this);
AndroidSpeechToTextEvents::OnBeginning.RemoveAll(this);
AndroidSpeechToTextEvents::OnEnd.RemoveAll(this);
AndroidSpeechToTextEvents::OnPartialText.RemoveAll(this);
AndroidSpeechToTextEvents::OnFinalText.RemoveAll(this);
AndroidSpeechToTextEvents::OnError.RemoveAll(this);
Super::EndPlay(EndPlayReason);
}
bool UAndroidSpeechToTextComponent::StartListening(const FString& LanguageTag, bool bPreferOffline, bool bEnablePartialResults, bool bKeepListening)
{
return AndroidSpeechToTextJavaBridge::StartListening(LanguageTag, bPreferOffline, bEnablePartialResults, bKeepListening);
}
void UAndroidSpeechToTextComponent::StopListening()
{
AndroidSpeechToTextJavaBridge::StopListening();
}
void UAndroidSpeechToTextComponent::CancelListening()
{
AndroidSpeechToTextJavaBridge::CancelListening();
}
bool UAndroidSpeechToTextComponent::IsListening() const
{
return AndroidSpeechToTextJavaBridge::IsListening();
}
FString UAndroidSpeechToTextComponent::GetLastFinalText() const
{
return AndroidSpeechToTextJavaBridge::GetLastFinalText();
}
FString UAndroidSpeechToTextComponent::GetLastPartialText() const
{
return AndroidSpeechToTextJavaBridge::GetLastPartialText();
}
FString UAndroidSpeechToTextComponent::GetLastErrorMessage() const
{
return AndroidSpeechToTextJavaBridge::GetLastErrorMessage();
}
bool UAndroidSpeechToTextComponent::IsAndroidSpeechToTextSupported()
{
return AndroidSpeechToTextJavaBridge::IsSupported();
}
void UAndroidSpeechToTextComponent::HandleSpeechReady()
{
OnSpeechReady.Broadcast();
}
void UAndroidSpeechToTextComponent::HandleSpeechStarted()
{
OnSpeechStarted.Broadcast();
}
void UAndroidSpeechToTextComponent::HandleSpeechEnded()
{
OnSpeechEnded.Broadcast();
}
void UAndroidSpeechToTextComponent::HandlePartialText(const FString& Text)
{
OnPartialText.Broadcast(Text);
}
void UAndroidSpeechToTextComponent::HandleFinalText(const FString& Text)
{
OnFinalText.Broadcast(Text);
}
void UAndroidSpeechToTextComponent::HandleSpeechError(int32 ErrorCode, const FString& Message)
{
OnSpeechError.Broadcast(ConvertErrorCode(ErrorCode), Message);
}
EAndroidSpeechToTextError UAndroidSpeechToTextComponent::ConvertErrorCode(int32 ErrorCode)
{
switch (ErrorCode)
{
case 1:
return EAndroidSpeechToTextError::NetworkTimeout;
case 2:
return EAndroidSpeechToTextError::Network;
case 3:
return EAndroidSpeechToTextError::Audio;
case 4:
return EAndroidSpeechToTextError::Server;
case 5:
return EAndroidSpeechToTextError::Client;
case 6:
return EAndroidSpeechToTextError::SpeechTimeout;
case 7:
return EAndroidSpeechToTextError::NoMatch;
case 8:
return EAndroidSpeechToTextError::RecognizerBusy;
case 9:
return EAndroidSpeechToTextError::InsufficientPermissions;
case 10:
return EAndroidSpeechToTextError::TooManyRequests;
case 11:
return EAndroidSpeechToTextError::ServerDisconnected;
case 12:
return EAndroidSpeechToTextError::LanguageNotSupported;
case 13:
return EAndroidSpeechToTextError::LanguageUnavailable;
case 1000:
return EAndroidSpeechToTextError::Unsupported;
default:
return EAndroidSpeechToTextError::Unknown;
}
}
@@ -0,0 +1,17 @@
#pragma once
#include "CoreMinimal.h"
DECLARE_MULTICAST_DELEGATE(FAndroidSpeechToTextNativeSimpleDelegate);
DECLARE_MULTICAST_DELEGATE_OneParam(FAndroidSpeechToTextNativeTextDelegate, const FString&);
DECLARE_MULTICAST_DELEGATE_TwoParams(FAndroidSpeechToTextNativeErrorDelegate, int32, const FString&);
namespace AndroidSpeechToTextEvents
{
extern FAndroidSpeechToTextNativeSimpleDelegate OnReady;
extern FAndroidSpeechToTextNativeSimpleDelegate OnBeginning;
extern FAndroidSpeechToTextNativeSimpleDelegate OnEnd;
extern FAndroidSpeechToTextNativeTextDelegate OnPartialText;
extern FAndroidSpeechToTextNativeTextDelegate OnFinalText;
extern FAndroidSpeechToTextNativeErrorDelegate OnError;
}
@@ -0,0 +1,239 @@
#include "AndroidSpeechToTextJavaBridge.h"
#include "AndroidSpeechToTextEvents.h"
#include "Async/Async.h"
#if PLATFORM_ANDROID
#include "Android/AndroidApplication.h"
#include "Android/AndroidJavaEnv.h"
#include "Android/AndroidJNI.h"
#endif
namespace AndroidSpeechToTextEvents
{
FAndroidSpeechToTextNativeSimpleDelegate OnReady;
FAndroidSpeechToTextNativeSimpleDelegate OnBeginning;
FAndroidSpeechToTextNativeSimpleDelegate OnEnd;
FAndroidSpeechToTextNativeTextDelegate OnPartialText;
FAndroidSpeechToTextNativeTextDelegate OnFinalText;
FAndroidSpeechToTextNativeErrorDelegate OnError;
}
namespace
{
#if PLATFORM_ANDROID
jclass GetJavaClass(JNIEnv* Env)
{
static jclass SpeechClass = FAndroidApplication::FindJavaClassGlobalRef("com/epicgames/unreal/AndroidSpeechToText");
if (SpeechClass == nullptr)
{
FAndroidApplication::CheckJavaException();
UE_LOG(LogTemp, Error, TEXT("AndroidSpeechToText Java class was not found."));
}
return SpeechClass;
}
jmethodID GetStaticMethod(JNIEnv* Env, const ANSICHAR* MethodName, const ANSICHAR* Signature)
{
jclass SpeechClass = GetJavaClass(Env);
if (SpeechClass == nullptr)
{
return nullptr;
}
jmethodID Method = FJavaWrapper::FindStaticMethod(Env, SpeechClass, MethodName, Signature, false);
if (Method == nullptr)
{
FAndroidApplication::CheckJavaException();
UE_LOG(LogTemp, Error, TEXT("AndroidSpeechToText Java method was not found: %s"), ANSI_TO_TCHAR(MethodName));
}
return Method;
}
FString JavaStringResult(const ANSICHAR* MethodName)
{
JNIEnv* Env = FAndroidApplication::GetJavaEnv();
jclass SpeechClass = Env ? GetJavaClass(Env) : nullptr;
jmethodID Method = Env ? GetStaticMethod(Env, MethodName, "()Ljava/lang/String;") : nullptr;
if (SpeechClass != nullptr && Method != nullptr)
{
jstring Result = static_cast<jstring>(Env->CallStaticObjectMethod(SpeechClass, Method));
FAndroidApplication::CheckJavaException();
return FJavaHelper::FStringFromLocalRef(Env, Result);
}
return FString();
}
#endif
void BroadcastSimpleOnGameThread(FAndroidSpeechToTextNativeSimpleDelegate& Delegate)
{
AsyncTask(ENamedThreads::GameThread, [&Delegate]()
{
Delegate.Broadcast();
});
}
void BroadcastTextOnGameThread(FAndroidSpeechToTextNativeTextDelegate& Delegate, FString Text)
{
AsyncTask(ENamedThreads::GameThread, [&Delegate, Text]()
{
Delegate.Broadcast(Text);
});
}
}
namespace AndroidSpeechToTextJavaBridge
{
bool StartListening(const FString& LanguageTag, bool bPreferOffline, bool bEnablePartialResults, bool bKeepListening)
{
#if PLATFORM_ANDROID
JNIEnv* Env = FAndroidApplication::GetJavaEnv();
jclass SpeechClass = Env ? GetJavaClass(Env) : nullptr;
jmethodID Method = Env ? GetStaticMethod(Env, "startListening", "(Landroid/app/Activity;Ljava/lang/String;ZZZ)Z") : nullptr;
if (SpeechClass == nullptr || Method == nullptr)
{
return false;
}
auto JavaLanguageTag = FJavaHelper::ToJavaString(Env, LanguageTag);
const jboolean Result = Env->CallStaticBooleanMethod(
SpeechClass,
Method,
FJavaWrapper::GameActivityThis,
*JavaLanguageTag,
bPreferOffline ? JNI_TRUE : JNI_FALSE,
bEnablePartialResults ? JNI_TRUE : JNI_FALSE,
bKeepListening ? JNI_TRUE : JNI_FALSE);
FAndroidApplication::CheckJavaException();
return Result == JNI_TRUE;
#else
return false;
#endif
}
void StopListening()
{
#if PLATFORM_ANDROID
JNIEnv* Env = FAndroidApplication::GetJavaEnv();
jclass SpeechClass = Env ? GetJavaClass(Env) : nullptr;
jmethodID Method = Env ? GetStaticMethod(Env, "stopListening", "()V") : nullptr;
if (SpeechClass != nullptr && Method != nullptr)
{
Env->CallStaticVoidMethod(SpeechClass, Method);
FAndroidApplication::CheckJavaException();
}
#endif
}
void CancelListening()
{
#if PLATFORM_ANDROID
JNIEnv* Env = FAndroidApplication::GetJavaEnv();
jclass SpeechClass = Env ? GetJavaClass(Env) : nullptr;
jmethodID Method = Env ? GetStaticMethod(Env, "cancelListening", "()V") : nullptr;
if (SpeechClass != nullptr && Method != nullptr)
{
Env->CallStaticVoidMethod(SpeechClass, Method);
FAndroidApplication::CheckJavaException();
}
#endif
}
bool IsListening()
{
#if PLATFORM_ANDROID
JNIEnv* Env = FAndroidApplication::GetJavaEnv();
jclass SpeechClass = Env ? GetJavaClass(Env) : nullptr;
jmethodID Method = Env ? GetStaticMethod(Env, "isListening", "()Z") : nullptr;
if (SpeechClass != nullptr && Method != nullptr)
{
const jboolean Result = Env->CallStaticBooleanMethod(SpeechClass, Method);
FAndroidApplication::CheckJavaException();
return Result == JNI_TRUE;
}
#endif
return false;
}
FString GetLastFinalText()
{
#if PLATFORM_ANDROID
return JavaStringResult("getLastFinalText");
#else
return FString();
#endif
}
FString GetLastPartialText()
{
#if PLATFORM_ANDROID
return JavaStringResult("getLastPartialText");
#else
return FString();
#endif
}
FString GetLastErrorMessage()
{
#if PLATFORM_ANDROID
return JavaStringResult("getLastErrorMessage");
#else
return FString();
#endif
}
bool IsSupported()
{
#if PLATFORM_ANDROID
JNIEnv* Env = FAndroidApplication::GetJavaEnv();
jclass SpeechClass = Env ? GetJavaClass(Env) : nullptr;
jmethodID Method = Env ? GetStaticMethod(Env, "isSpeechRecognizerAvailable", "(Landroid/app/Activity;)Z") : nullptr;
if (SpeechClass != nullptr && Method != nullptr)
{
const jboolean Result = Env->CallStaticBooleanMethod(SpeechClass, Method, FJavaWrapper::GameActivityThis);
FAndroidApplication::CheckJavaException();
return Result == JNI_TRUE;
}
#endif
return false;
}
}
#if PLATFORM_ANDROID
extern "C"
{
JNIEXPORT void JNICALL Java_com_epicgames_unreal_AndroidSpeechToText_nativeOnReady(JNIEnv* Env, jclass Class)
{
BroadcastSimpleOnGameThread(AndroidSpeechToTextEvents::OnReady);
}
JNIEXPORT void JNICALL Java_com_epicgames_unreal_AndroidSpeechToText_nativeOnBeginningOfSpeech(JNIEnv* Env, jclass Class)
{
BroadcastSimpleOnGameThread(AndroidSpeechToTextEvents::OnBeginning);
}
JNIEXPORT void JNICALL Java_com_epicgames_unreal_AndroidSpeechToText_nativeOnEndOfSpeech(JNIEnv* Env, jclass Class)
{
BroadcastSimpleOnGameThread(AndroidSpeechToTextEvents::OnEnd);
}
JNIEXPORT void JNICALL Java_com_epicgames_unreal_AndroidSpeechToText_nativeOnPartialText(JNIEnv* Env, jclass Class, jstring Text)
{
BroadcastTextOnGameThread(AndroidSpeechToTextEvents::OnPartialText, FJavaHelper::FStringFromParam(Env, Text));
}
JNIEXPORT void JNICALL Java_com_epicgames_unreal_AndroidSpeechToText_nativeOnFinalText(JNIEnv* Env, jclass Class, jstring Text)
{
BroadcastTextOnGameThread(AndroidSpeechToTextEvents::OnFinalText, FJavaHelper::FStringFromParam(Env, Text));
}
JNIEXPORT void JNICALL Java_com_epicgames_unreal_AndroidSpeechToText_nativeOnError(JNIEnv* Env, jclass Class, jint ErrorCode, jstring Message)
{
const FString ErrorMessage = FJavaHelper::FStringFromParam(Env, Message);
AsyncTask(ENamedThreads::GameThread, [ErrorCode = static_cast<int32>(ErrorCode), ErrorMessage]()
{
AndroidSpeechToTextEvents::OnError.Broadcast(ErrorCode, ErrorMessage);
});
}
}
#endif
@@ -0,0 +1,15 @@
#pragma once
#include "CoreMinimal.h"
namespace AndroidSpeechToTextJavaBridge
{
bool StartListening(const FString& LanguageTag, bool bPreferOffline, bool bEnablePartialResults, bool bKeepListening);
void StopListening();
void CancelListening();
bool IsListening();
FString GetLastFinalText();
FString GetLastPartialText();
FString GetLastErrorMessage();
bool IsSupported();
}
@@ -0,0 +1,7 @@
#include "Modules/ModuleManager.h"
class FAndroidSpeechToTextModule : public IModuleInterface
{
};
IMPLEMENT_MODULE(FAndroidSpeechToTextModule, AndroidSpeechToText)
@@ -0,0 +1,94 @@
#pragma once
#include "Components/ActorComponent.h"
#include "AndroidSpeechToTextComponent.generated.h"
UENUM(BlueprintType)
enum class EAndroidSpeechToTextError : uint8
{
None UMETA(DisplayName = "None"),
NetworkTimeout UMETA(DisplayName = "Network Timeout"),
Network UMETA(DisplayName = "Network"),
Audio UMETA(DisplayName = "Audio"),
Server UMETA(DisplayName = "Server"),
Client UMETA(DisplayName = "Client"),
SpeechTimeout UMETA(DisplayName = "Speech Timeout"),
NoMatch UMETA(DisplayName = "No Match"),
RecognizerBusy UMETA(DisplayName = "Recognizer Busy"),
InsufficientPermissions UMETA(DisplayName = "Insufficient Permissions"),
TooManyRequests UMETA(DisplayName = "Too Many Requests"),
ServerDisconnected UMETA(DisplayName = "Server Disconnected"),
LanguageNotSupported UMETA(DisplayName = "Language Not Supported"),
LanguageUnavailable UMETA(DisplayName = "Language Unavailable"),
Unsupported UMETA(DisplayName = "Unsupported"),
Unknown UMETA(DisplayName = "Unknown")
};
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FAndroidSpeechToTextSimpleDelegate);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FAndroidSpeechToTextTextDelegate, const FString&, Text);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FAndroidSpeechToTextErrorDelegate, EAndroidSpeechToTextError, Error, const FString&, Message);
UCLASS(ClassGroup = (Android), meta = (BlueprintSpawnableComponent))
class ANDROIDSPEECHTOTEXT_API UAndroidSpeechToTextComponent : public UActorComponent
{
GENERATED_BODY()
public:
UAndroidSpeechToTextComponent();
UPROPERTY(BlueprintAssignable, Category = "Android Speech To Text")
FAndroidSpeechToTextSimpleDelegate OnSpeechReady;
UPROPERTY(BlueprintAssignable, Category = "Android Speech To Text")
FAndroidSpeechToTextSimpleDelegate OnSpeechStarted;
UPROPERTY(BlueprintAssignable, Category = "Android Speech To Text")
FAndroidSpeechToTextSimpleDelegate OnSpeechEnded;
UPROPERTY(BlueprintAssignable, Category = "Android Speech To Text")
FAndroidSpeechToTextTextDelegate OnPartialText;
UPROPERTY(BlueprintAssignable, Category = "Android Speech To Text")
FAndroidSpeechToTextTextDelegate OnFinalText;
UPROPERTY(BlueprintAssignable, Category = "Android Speech To Text")
FAndroidSpeechToTextErrorDelegate OnSpeechError;
UFUNCTION(BlueprintCallable, Category = "Android Speech To Text")
bool StartListening(const FString& LanguageTag = TEXT(""), bool bPreferOffline = false, bool bEnablePartialResults = true, bool bKeepListening = true);
UFUNCTION(BlueprintCallable, Category = "Android Speech To Text")
void StopListening();
UFUNCTION(BlueprintCallable, Category = "Android Speech To Text")
void CancelListening();
UFUNCTION(BlueprintPure, Category = "Android Speech To Text")
bool IsListening() const;
UFUNCTION(BlueprintPure, Category = "Android Speech To Text")
FString GetLastFinalText() const;
UFUNCTION(BlueprintPure, Category = "Android Speech To Text")
FString GetLastPartialText() const;
UFUNCTION(BlueprintPure, Category = "Android Speech To Text")
FString GetLastErrorMessage() const;
UFUNCTION(BlueprintPure, Category = "Android Speech To Text")
static bool IsAndroidSpeechToTextSupported();
protected:
virtual void BeginPlay() override;
virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override;
private:
void HandleSpeechReady();
void HandleSpeechStarted();
void HandleSpeechEnded();
void HandlePartialText(const FString& Text);
void HandleFinalText(const FString& Text);
void HandleSpeechError(int32 ErrorCode, const FString& Message);
static EAndroidSpeechToTextError ConvertErrorCode(int32 ErrorCode);
};
+13
View File
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<TpsData xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<Name> FidelityFX-FSR </Name>
<Location> //UE5/Main/Engine/Shaders/Private/Mobile/FSR <Location>
<Function> The implementation here is HLSL shader source code that will be distributed to the general public with all versions of Unreal Engine (Github., Epic Games Launcher, Perforce) in the Engine/Shaders folder. All of our Unreal-engine products such as Fortnite will also ship a compiled version of this code.It's neither statically or dynamically linked, because it's not CPU code, it's shader code that</Function>
<Eula> https://github.com/GPUOpen-Effects/FidelityFX-FSR/blob/master/license.txt </Eula>
<RedistributeTo>
<EndUserGroup>Licensees</EndUserGroup>
<EndUserGroup>Git</EndUserGroup>
<EndUserGroup>P4</EndUserGroup>
</RedistributeTo>
<LicenseFolder></LicenseFolder>
</TpsData>
+28
View File
@@ -0,0 +1,28 @@
{
"FileVersion": 1,
"Version": 1,
"VersionName": "1.0",
"FriendlyName": "Mobile FSR",
"Description": "Mobile FSR 1.0",
"Category": "Rendering",
"CreatedBy": "Epic Games, Inc.",
"CreatedByURL": "https://epicgames.com",
"DocsURL": "",
"MarketplaceURL": "",
"SupportURL": "",
"EnabledByDefault": false,
"CanContainContent": true,
"IsBetaVersion": false,
"Installed": false,
"SupportedTargetPlatforms": [
"IOS",
"Android"
],
"Modules": [
{
"Name": "MobileFSR",
"Type": "Runtime",
"LoadingPhase": "PostConfigInit"
}
]
}
@@ -0,0 +1,154 @@
// Copyright Epic Games, Inc. All Rights Reserved.
//------------------------------------------------------------------------------
// Copyright (c) 2021 Advanced Micro Devices, Inc. All rights reserved.
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files(the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions :
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//------------------------------------------------------------------------------
// !!!
// FidelityFX Super Resolution - Common
// This header should be included after ffx_a.ush
// !!!
// =====================================================================================
//
// EOTF Conversions
//
// =====================================================================================
// FSR output device definitions : matching EFSR_OutputDevice enum in PostProcessFSR.cpp
#define FSR_SRGB 0
#define FSR_LINEAR 1
#define FSR_ST2084 2
#define FSR_scRGB 3
#define MAX_ODT_NITS_ENUM 0
#define CONVERT_TO_OUTPUT_DEVICE 0
#include "/Engine/Private/ACES/ACESCommon.ush"
#include "/Engine/Private/GammaCorrectionCommon.ush"
float4 LinearToGamma2(float4 Color) { return sqrt(Color); }
float3 LinearToGamma2(float3 Color) { return sqrt(Color); }
float2 LinearToGamma2(float2 Color) { return sqrt(Color); }
float LinearToGamma2(float Color) { return sqrt(Color); }
float4 Gamma2ToLinear(float4 Color) { return Color * Color; }
float3 Gamma2ToLinear(float3 Color) { return Color * Color; }
float2 Gamma2ToLinear(float2 Color) { return Color * Color; }
float Gamma2ToLinear(float Color) { return Color * Color; }
#if ENABLE_FP16
AH4 LinearToGamma2(AH4 Color) { return sqrt(Color); }
AH3 LinearToGamma2(AH3 Color) { return sqrt(Color); }
AH2 LinearToGamma2(AH2 Color) { return sqrt(Color); }
AH1 LinearToGamma2(AH1 Color) { return sqrt(Color); }
AH4 Gamma2ToLinear(AH4 Color) { return Color * Color; }
AH3 Gamma2ToLinear(AH3 Color) { return Color * Color; }
AH2 Gamma2ToLinear(AH2 Color) { return Color * Color; }
AH1 Gamma2ToLinear(AH1 Color) { return Color * Color; }
#endif
//-----------------------------------------------------------------
// only float3 ST2084ToLinear() is defined in GammaCorrectionCommon.ush
float ST2084ToLinear_F(float pq)
{
const float m1 = 0.1593017578125; // = 2610. / 4096. * .25;
const float m2 = 78.84375; // = 2523. / 4096. * 128;
const float c1 = 0.8359375; // = 2392. / 4096. * 32 - 2413./4096.*32 + 1;
const float c2 = 18.8515625; // = 2413. / 4096. * 32;
const float c3 = 18.6875; // = 2392. / 4096. * 32;
const float C = 10000.;
float Np = pow(pq, 1. / m2);
float L = Np - c1;
L = max(0., L);
L = L / (c2 - c3 * Np);
L = pow(L, 1. / m1);
float P = L * C;
return P;
}
float ST2084ToGamma2(float pq) { return LinearToGamma2(ST2084ToLinear_F(pq)); }
float2 ST2084ToGamma2(float2 pq) { return LinearToGamma2(float2(ST2084ToLinear_F(pq.r), ST2084ToLinear_F(pq.g))); }
float3 ST2084ToGamma2(float3 pq) { return LinearToGamma2(ST2084ToLinear(pq)); }
float4 ST2084ToGamma2(float4 pq) { return LinearToGamma2(float4(ST2084ToLinear(pq.rgb), ST2084ToLinear_F(pq.a))); }
//-----------------------------------------------------------------
// Either 1000 or 2000 depending on UE4 Tonemapper enum,
// should be provided by the engine
#if MAX_ODT_NITS_ENUM == 1
#define HDR_MAX_ODT_NITS 2000
#else
#define HDR_MAX_ODT_NITS 1000
#endif
#define HDR_MAX_NITS 10000
#define HDR_MAX_NITS_INV 0.0001f
// matches white point in ST2084ToScRGB()
#define HDR_WHITE_POINT 80.0f
float3 Gamma2ToScRGB(float3 Color)
{
const float3x3 Rec2020_2_sRGB = mul(XYZ_2_sRGB_MAT, Rec2020_2_XYZ_MAT);
const float MaxODTNits = HDR_MAX_ODT_NITS;
const float scRGBScale = MaxODTNits / HDR_WHITE_POINT;
// Gamma2 -> Linear encoding
float3 LinearColor_Rec2020 = Gamma2ToLinear(Color);
// Rec2020 -> sRGB primaries
float3 LinearColor_sRGB = mul(Rec2020_2_sRGB, LinearColor_Rec2020);
// Linear_sRGB -> scRGB
return LinearColor_sRGB * scRGBScale;
}
float3 scRGBToGamma2(float3 Color_scRGB)
{
const float3x3 sRGB_2_Rec2020 = mul(XYZ_2_Rec2020_MAT, sRGB_2_XYZ_MAT);
const float MaxODTNits = HDR_MAX_ODT_NITS;
const float scRGBScale = MaxODTNits / HDR_WHITE_POINT;
const float scRGBScaleInv = HDR_WHITE_POINT / MaxODTNits;
// normalize to [0-1]
Color_scRGB *= scRGBScaleInv;
// convert primaries sRGB -> Rec2020
float3 Color_Rec2020 = mul(sRGB_2_Rec2020, Color_scRGB);
// clamp negatives
Color_Rec2020 = max(Color_Rec2020, 0);
// Gamma2-encode
return LinearToGamma2(Color_Rec2020);
}
float3 LinearToScRGB(float3 LinearColor_Rec2020)
{
const float3x3 Rec2020_2_sRGB = mul(XYZ_2_sRGB_MAT, Rec2020_2_XYZ_MAT);
const float MaxODTNits = HDR_MAX_ODT_NITS;
const float scRGBScale = MaxODTNits / HDR_WHITE_POINT;
// Rec2020 -> sRGB primaries
float3 LinearColor_sRGB = mul(Rec2020_2_sRGB, LinearColor_Rec2020);
// Linear_sRGB -> scRGB
return LinearColor_sRGB * scRGBScale;
}
@@ -0,0 +1,162 @@
// Copyright Epic Games, Inc. All Rights Reserved.
//------------------------------------------------------------------------------
//
// Copyright (c) 2021 Advanced Micro Devices, Inc. All rights reserved.
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files(the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions :
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//------------------------------------------------------------------------------
#include "/Engine/Private/Common.ush"
#include "/Engine/Private/ScreenPass.ush"
// =====================================================================================
//
// SHADER RESOURCES
//
// =====================================================================================
uint4 Const0;
uint4 Const1;
uint4 Const2;
uint4 Const3;
SamplerState samLinearClamp;
// UE4 GrainIntensity
float4 GrainRandomFull; // zw:unused
float4 GrainScaleBiasJitter; // x:Scale y:Bias z:Jitter
float2 VPColor_ExtentInverse;
float2 VPColor_ViewportMin;
// =====================================================================================
//
// FIDELITYFX SETUP
//
// =====================================================================================
#define A_GPU 1
#define A_HLSL 1
// Note: Input is expected to be in Gamma2-encoding.
// LDR: This is already the case.
// HDR: A ColorConversionCS runs before FSR to provide Gamma2 input to FSR passes
#if ENABLE_FP16
#define A_HALF
#include "ffx_a.ush"
Texture2D<AH4> InputTexture;
RWTexture2D<AH4> OutputTexture;
#define FSR_EASU_H 1
AH4 FsrEasuRH(AF2 p) { AH4 res = InputTexture.GatherRed (samLinearClamp, p, ASU2(0, 0)); return res; }
AH4 FsrEasuGH(AF2 p) { AH4 res = InputTexture.GatherGreen(samLinearClamp, p, ASU2(0, 0)); return res; }
AH4 FsrEasuBH(AF2 p) { AH4 res = InputTexture.GatherBlue (samLinearClamp, p, ASU2(0, 0)); return res; }
AH4 FsrEasuAH(AF2 p) { AH4 res = InputTexture.GatherAlpha(samLinearClamp, p, ASU2(0, 0)); return res; }
AH4 FsrEasuRHO(AF2 p, ASW2 o) { AH4 res = InputTexture.GatherRed (samLinearClamp, p, o); return res; }
AH4 FsrEasuGHO(AF2 p, ASW2 o) { AH4 res = InputTexture.GatherGreen(samLinearClamp, p, o); return res; }
AH4 FsrEasuBHO(AF2 p, ASW2 o) { AH4 res = InputTexture.GatherBlue (samLinearClamp, p, o); return res; }
AH4 FsrEasuAHO(AF2 p, ASW2 o) { AH4 res = InputTexture.GatherAlpha(samLinearClamp, p, o); return res; }
AH4 FsrEasuSampleH(AF2 p) { AH4 res = InputTexture.SampleLevel(samLinearClamp, p, 0); return res; }
#else
#include "ffx_a.ush"
Texture2D InputTexture;
RWTexture2D<float4> OutputTexture;
#define FSR_EASU_F 1
AF4 FsrEasuRF(AF2 p) { AF4 res = InputTexture.GatherRed (samLinearClamp, p, ASU2(0, 0)); return res; }
AF4 FsrEasuGF(AF2 p) { AF4 res = InputTexture.GatherGreen(samLinearClamp, p, ASU2(0, 0)); return res; }
AF4 FsrEasuBF(AF2 p) { AF4 res = InputTexture.GatherBlue (samLinearClamp, p, ASU2(0, 0)); return res; }
AF4 FsrEasuAF(AF2 p) { AF4 res = InputTexture.GatherAlpha(samLinearClamp, p, ASU2(0, 0)); return res; }
#endif
// should be included after ffx_a.ush, defines FSR_OUTPUTDEVICE
#include "PostProcessMobileFFX_Common.ush"
#include "ffx_fsr1.ush"
// =====================================================================================
//
// FIDELITYFX SUPER RESOLUTION
//
// =====================================================================================
#if ENABLE_FP16
AH4 FidelityFXSuperResolution(AU2 gxy)
#else
AF4 FidelityFXSuperResolution(AU2 gxy)
#endif
{
//
// FSR / EASU()
//
#if ENABLE_FP16
AH4 Gamma2Color = AH4(0,0,0,0);
#if ES3_1_PROFILE
FsrEasuL(Gamma2Color, gxy, Const0, Const1, Const2, Const3);
#else
FsrEasuH(Gamma2Color, gxy, Const0, Const1, Const2, Const3);
#endif
#else
AF4 Gamma2Color = AF4(0,0,0,0);
FsrEasuF(Gamma2Color, gxy, Const0, Const1, Const2, Const3);
#endif // ENABLE_FP16
#if !USE_PASSTHROUGH_ALPHA
Gamma2Color.a = 1;
#endif // !USE_PASSTHROUGH_ALPHA
return Gamma2Color;
}
// =====================================================================================
//
// ENTRY POINTS
//
// =====================================================================================
#if COMPUTE_SHADER
[numthreads(THREADGROUP_SIZEX, THREADGROUP_SIZEY, THREADGROUP_SIZEZ)]
void MainCS(uint3 LocalThreadId : SV_GroupThreadID, uint3 WorkGroupId : SV_GroupID, uint3 Dtid : SV_DispatchThreadID)
{
// Do remapping of local xy in workgroup for a more PS-like swizzle pattern.
// Threadgroup of 64 threads, each working on a cache-friendly 2x2 region (= 16x16 workgroup if 1px/thread assumed)
AU2 gxy = ARmp8x8(LocalThreadId.x) + AU2(WorkGroupId.x << 4u, WorkGroupId.y << 4u);
OutputTexture[gxy + VPColor_ViewportMin] = FidelityFXSuperResolution(gxy);
gxy.x += 8u;
OutputTexture[gxy + VPColor_ViewportMin] = FidelityFXSuperResolution(gxy);
gxy.y += 8u;
OutputTexture[gxy + VPColor_ViewportMin] = FidelityFXSuperResolution(gxy);
gxy.x -= 8u;
OutputTexture[gxy + VPColor_ViewportMin] = FidelityFXSuperResolution(gxy);
}
#endif // COMPUTE SHADER
#if ENABLE_FP16
void MainPS(noperspective float4 UVAndScreenPos : TEXCOORD0, float4 SvPosition : SV_POSITION, out AH4 OutColor : SV_Target0)
{
AU2 gxy = AU2(SvPosition.xy - VPColor_ViewportMin);
OutColor = FidelityFXSuperResolution(gxy);
}
#else
void MainPS(noperspective float4 UVAndScreenPos : TEXCOORD0, float4 SvPosition : SV_POSITION, out float4 OutColor : SV_Target0)
{
AU2 gxy = AU2(SvPosition.xy - VPColor_ViewportMin);
OutColor = FidelityFXSuperResolution(gxy);
}
#endif
@@ -0,0 +1,190 @@
// Copyright Epic Games, Inc. All Rights Reserved.
//------------------------------------------------------------------------------
// Copyright (c) 2021 Advanced Micro Devices, Inc. All rights reserved.
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files(the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions :
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//------------------------------------------------------------------------------
// FidelityFX Super Resolution - Robust Contrast Adaptive Sharpening
#include "/Engine/Private/Common.ush"
#include "/Engine/Private/ScreenPass.ush"
// =====================================================================================
//
// SHADER RESOURCES
//
// =====================================================================================
// RCAS params
uint4 Const0;
float2 VPColor_ExtentInverse;
// =====================================================================================
//
// FIDELITYFX SETUP
//
// =====================================================================================
#define A_HLSL 1
#define A_GPU 1
#if USE_RCAS_DENOISE
#define FSR_RCAS_DENOISE 1
#endif
#if USE_PASSTHROUGH_ALPHA
#define FSR_RCAS_PASSTHROUGH_ALPHA 1
#endif
#if ENABLE_FP16
#define FSR_RCAS_H 1
#define FSR_RCAS_HX2 1
#define A_HALF 1
#include "ffx_a.ush"
Texture2D<AH4> InputTexture;
RWTexture2D<AH4> OutputTexture;
AH4 FsrRcasLoadH(min16int2 p) {return InputTexture.Load(int3(p, 0));}
AH4 FsrRcasLoadHx2(min16int2 p){return InputTexture.Load(int3(p, 0));}
#else
#define FSR_RCAS_F 1
#include "ffx_a.ush"
Texture2D InputTexture;
RWTexture2D<float4> OutputTexture;
AF4 FsrRcasLoadF(int2 p){return InputTexture.Load(int3(p, 0));}
#endif
// should be included after ffx_a.ush, defines FSR_OUTPUTDEVICE
#include "PostProcessMobileFFX_Common.ush"
// Note: Input is expected to be in Gamma2-encoding.
// LDR: This is already the case.
// HDR: A ColorConversionCS runs before FSR to provide Gamma2 input to FSR passes
#if ENABLE_FP16
void FsrRcasInputH (inout AH1 r,inout AH1 g,inout AH1 b){} // RCAS is done in perceptual space (sRGB~Gamma2)
void FsrRcasInputHx2(inout AH2 r,inout AH2 g,inout AH2 b){} // Hence, no need to modify r/g/b
#else
void FsrRcasInputF(inout AF1 r, inout AF1 g, inout AF1 b){}
#endif
#include "ffx_fsr1.ush"
// =====================================================================================
//
// RCAS IMPLEMENTATION
//
// =====================================================================================
#if ENABLE_FP16 // half precision floating point
AH4 RCASPassCommon(AU2 gxy)
#else
AF4 RCASPassCommon(AU2 gxy)
#endif
{
//
// RCAS()
//
#if ENABLE_FP16 // half precision floating point
#if USE_PASSTHROUGH_ALPHA
AH4 Gamma2Color = AH4(0, 0, 0, 0);
FsrRcasH(Gamma2Color.r, Gamma2Color.g, Gamma2Color.b, Gamma2Color.a, gxy, Const0);
#else
AH4 Gamma2Color = AH4(0, 0, 0, 1);
FsrRcasH(Gamma2Color.r, Gamma2Color.g, Gamma2Color.b, gxy, Const0);
#endif // USE_PASSTHROUGH_ALPHA
#else // FP32 - full precision floating point
#if USE_PASSTHROUGH_ALPHA
AF4 Gamma2Color = AF4(0, 0, 0, 0);
FsrRcasF(Gamma2Color.r, Gamma2Color.g, Gamma2Color.b, Gamma2Color.a, gxy, Const0);
#else
AF4 Gamma2Color = AF4(0, 0, 0, 1);
FsrRcasF(Gamma2Color.r, Gamma2Color.g, Gamma2Color.b, gxy, Const0);
#endif // USE_PASSTHROUGH_ALPHA
#endif // ENABLE_FP16
return Gamma2Color;
}
// =====================================================================================
//
// ENTRY POINTS
//
// =====================================================================================
#if COMPUTE_SHADER
[numthreads(THREADGROUP_SIZEX, THREADGROUP_SIZEY, THREADGROUP_SIZEZ)]
void MainCS(uint3 LocalThreadId : SV_GroupThreadID, uint3 WorkGroupId : SV_GroupID, uint3 Dtid : SV_DispatchThreadID)
{
// Do remapping of local xy in workgroup for a more PS-like swizzle pattern.
AU2 gxy = ARmp8x8(LocalThreadId.x) + AU2(WorkGroupId.x << 4u, WorkGroupId.y << 4u);
#if ENABLE_FP16
OutputTexture[gxy] = RCASPassCommon(gxy);
gxy.x += 8u;
OutputTexture[gxy] = RCASPassCommon(gxy);
gxy.y += 8u;
OutputTexture[gxy] = RCASPassCommon(gxy);
gxy.x -= 8u;
OutputTexture[gxy] = RCASPassCommon(gxy);
#else
OutputTexture[gxy] = RCASPassCommon(gxy);
gxy.x += 8u;
OutputTexture[gxy] = RCASPassCommon(gxy);
gxy.y += 8u;
OutputTexture[gxy] = RCASPassCommon(gxy);
gxy.x -= 8u;
OutputTexture[gxy] = RCASPassCommon(gxy);
#endif
}
#endif // COMPUTE_SHADER
#if ENABLE_FP16
void MainPS(noperspective float4 UVAndScreenPos : TEXCOORD0, float4 SvPosition : SV_POSITION, out AH4 OutColor : SV_Target0)
{
AU2 gxy = AU2(SvPosition.xy);
OutColor = RCASPassCommon(gxy);
}
#else
void MainPS(noperspective float4 UVAndScreenPos : TEXCOORD0, float4 SvPosition : SV_POSITION, out float4 OutColor : SV_Target0)
{
AU2 gxy = AU2(SvPosition.xy);
OutColor = RCASPassCommon(gxy);
}
#endif
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,35 @@
// Copyright Epic Games, Inc. All Rights Reserved.
using System.IO;
using UnrealBuildTool;
public class MobileFSR : ModuleRules
{
public MobileFSR(ReadOnlyTargetRules Target) : base(Target)
{
PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs;
PrivateIncludePaths.AddRange(
new string[] {
Path.Combine(GetModuleDirectory("Renderer"), "Private"),
Path.Combine(GetModuleDirectory("Renderer"), "Internal"),
PluginDirectory + "/Shaders/Private",
}
);
PrivateDependencyModuleNames.AddRange(
new string[]
{
"Core",
"Engine",
"Projects",
"RenderCore",
"Renderer",
"RHI",
}
);
PrecompileForTargets = PrecompileTargetsType.Any;
}
}
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<!--MobileFSR plugin additions-->
<root xmlns:android="http://schemas.android.com/apk/res/android">
<!-- init section is always evaluated once per architecture -->
<init>
<log text="MobileFSR init"/>
<setBool result="bSupported" value="false"/>
<isArch arch="armeabi-v7a">
<setBool result="bSupported" value="true"/>
</isArch>
<isArch arch="arm64-v8a">
<setBool result="bSupported" value="true"/>
</isArch>
</init>
</root>
@@ -0,0 +1,42 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "MobileFSRModule.h"
#include "ShaderCore.h"
#include "Interfaces/IPluginManager.h"
#include "Misc/Paths.h"
#include "MobileFSRViewExtension.h"
#include "Modules/ModuleManager.h"
#define LOCTEXT_NAMESPACE "MobileFSR"
DEFINE_LOG_CATEGORY(LogMobileFSR);
void FMobileFSRModule::StartupModule()
{
FString PluginShaderDir = FPaths::Combine(IPluginManager::Get().FindPlugin(TEXT("MobileFSR"))->GetBaseDir(), TEXT("Shaders"));
AddShaderSourceDirectoryMapping(TEXT("/Plugin/MobileFSR"), PluginShaderDir);
FCoreDelegates::OnPostEngineInit.AddRaw(this, &FMobileFSRModule::OnPostEngineInit);
UE_LOG(LogMobileFSR, Log, TEXT("Mobile FSR Started"));
}
void FMobileFSRModule::OnPostEngineInit()
{
ViewExtension = FSceneViewExtensions::NewExtension<FMobileFSRViewExtension>();
UE_LOG(LogMobileFSR, Log, TEXT("Mobile FSR OnPostEngineInit"));
}
void FMobileFSRModule::ShutdownModule()
{
ViewExtension = nullptr;
FCoreDelegates::OnPostEngineInit.RemoveAll(this);
UE_LOG(LogMobileFSR, Log, TEXT("Mobile FSR Shutdown"));
}
#undef LOCTEXT_NAMESPACE
IMPLEMENT_MODULE(FMobileFSRModule, MobileFSR)
@@ -0,0 +1,462 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "MobileFSRUpscaler.h"
#include "DataDrivenShaderPlatformInfo.h"
#include "ScenePrivate.h"
namespace MobileFSRPrivate
{
bool IsPostProcessingWithAlphaChannelSupported()
{
static IConsoleVariable* CVarPropagateAlpha = IConsoleManager::Get().FindConsoleVariable(TEXT("r.PostProcessing.PropagateAlpha"));
return CVarPropagateAlpha ? CVarPropagateAlpha->GetBool() : false;
}
}
#define A_CPU 1
// ffx_a.ush will override these definitions
#ifdef A_STATIC
#undef A_STATIC
#endif
#ifdef A_RESTRICT
#undef A_RESTRICT
#endif
#include "ffx_a.ush"
#include "ffx_fsr1.ush"
//////////////////////////////////////////////////////////////////////////
//! FSR
//////////////////////////////////////////////////////////////////////////
// permutation domains
class FMobileFSR_UseFP16Dim : SHADER_PERMUTATION_BOOL("ENABLE_FP16");
class FMobileFSR_UsePassthroughAlpha : SHADER_PERMUTATION_BOOL("USE_PASSTHROUGH_ALPHA");
class FMobileRCAS_DenoiseDim : SHADER_PERMUTATION_BOOL("USE_RCAS_DENOISE");
BEGIN_SHADER_PARAMETER_STRUCT(FMobileFSRPassParameters, )
SHADER_PARAMETER(FUintVector4, Const0)
SHADER_PARAMETER(FUintVector4, Const1)
SHADER_PARAMETER(FUintVector4, Const2)
SHADER_PARAMETER(FUintVector4, Const3)
SHADER_PARAMETER(FVector2f, VPColor_ExtentInverse)
SHADER_PARAMETER(FVector2f, VPColor_ViewportMin)
SHADER_PARAMETER_SAMPLER(SamplerState, samLinearClamp)
SHADER_PARAMETER_RDG_TEXTURE(Texture2D, InputTexture)
END_SHADER_PARAMETER_STRUCT()
BEGIN_SHADER_PARAMETER_STRUCT(FMobileRCASPassParameters, )
SHADER_PARAMETER_RDG_TEXTURE(Texture2D, InputTexture)
SHADER_PARAMETER(FUintVector4, Const0)
SHADER_PARAMETER(FVector2f, VPColor_ExtentInverse)
END_SHADER_PARAMETER_STRUCT()
static float GMobileFSRCASSharpness = 0.2; // 0.2 stops = 1 / (2^N) ~= 0.87 in linear [0-1] sharpening of CAS
static FAutoConsoleVariableRef CVarMobileFSRCASSharpness(
TEXT("r.Mobile.FSR.RCAS.Sharpness"),
GMobileFSRCASSharpness,
TEXT("FSR RCAS Sharpness in stops (0: sharpest, 1: 1/2 as sharp, 2: 1/4 as sharp, 3: 1/8 as sharp, etc.). A value of 0.2 would correspond to a ~0.87 sharpness in [0-1] linear scale"),
ECVF_Default);
static int32 GMobileFSRCASDenoise = 0;
static FAutoConsoleVariableRef CVarMobileFSRCASDenoise(
TEXT("r.Mobile.FSR.RCAS.Denoise"),
GMobileFSRCASDenoise,
TEXT("FSR RCAS Denoise support for grainy input such as dithered images or input with custom film grain effects applied prior to FSR. 1:On, 0:Off"),
ECVF_Default);
static int32 GMobileFSRDisableCompute = 0;
static FAutoConsoleVariableRef CVarMobileFSRDisableCompute(
TEXT("r.Mobile.FSR.DisableCompute"),
GMobileFSRDisableCompute,
TEXT("FSR Disable compute shaders for FSR (Recommended for Adreno). 1: Compute Disabled, 0: Compute Enabled (default)"),
ECVF_Default);
///
/// FSR COMPUTE SHADER
///
class FMobileFSRCS : public FGlobalShader
{
public:
DECLARE_GLOBAL_SHADER(FMobileFSRCS);
SHADER_USE_PARAMETER_STRUCT(FMobileFSRCS, FGlobalShader);
using FPermutationDomain = TShaderPermutationDomain<FMobileFSR_UseFP16Dim, FMobileFSR_UsePassthroughAlpha>;
BEGIN_SHADER_PARAMETER_STRUCT(FParameters, )
SHADER_PARAMETER_STRUCT_INCLUDE(FMobileFSRPassParameters, FSR)
SHADER_PARAMETER_RDG_TEXTURE_UAV(RWTexture2D, OutputTexture)
END_SHADER_PARAMETER_STRUCT()
static bool ShouldCompilePermutation(const FGlobalShaderPermutationParameters& Parameters)
{
FPermutationDomain PermutationVector(Parameters.PermutationId);
if (PermutationVector.Get<FMobileFSR_UseFP16Dim>())
{
if (IsSimulatedPlatform(Parameters.Platform) || !IsDxcEnabledForPlatform(Parameters.Platform))
{
return false;
}
}
return IsMobilePlatform(Parameters.Platform);
}
static void ModifyCompilationEnvironment(const FGlobalShaderPermutationParameters& Parameters, FShaderCompilerEnvironment& OutEnvironment)
{
FPermutationDomain PermutationVector(Parameters.PermutationId);
OutEnvironment.SetDefine(TEXT("COMPUTE_SHADER"), 1);
OutEnvironment.SetDefine(TEXT("THREADGROUP_SIZEX"), 64);
OutEnvironment.SetDefine(TEXT("THREADGROUP_SIZEY"), 1);
OutEnvironment.SetDefine(TEXT("THREADGROUP_SIZEZ"), 1);
}
};
IMPLEMENT_GLOBAL_SHADER(FMobileFSRCS, "/Plugin/MobileFSR/Private/PostProcessMobileFFX_FSR.usf", "MainCS", SF_Compute);
class FMobileFSRPS : public FGlobalShader
{
public:
DECLARE_GLOBAL_SHADER(FMobileFSRPS);
SHADER_USE_PARAMETER_STRUCT(FMobileFSRPS, FGlobalShader);
using FPermutationDomain = TShaderPermutationDomain<FMobileFSR_UseFP16Dim, FMobileFSR_UsePassthroughAlpha>;
BEGIN_SHADER_PARAMETER_STRUCT(FParameters, )
SHADER_PARAMETER_STRUCT_INCLUDE(FMobileFSRPassParameters, FSR)
RENDER_TARGET_BINDING_SLOTS()
END_SHADER_PARAMETER_STRUCT()
static bool ShouldCompilePermutation(const FGlobalShaderPermutationParameters& Parameters)
{
FPermutationDomain PermutationVector(Parameters.PermutationId);
if (PermutationVector.Get<FMobileFSR_UseFP16Dim>())
{
if (IsSimulatedPlatform(Parameters.Platform) || !IsDxcEnabledForPlatform(Parameters.Platform))
{
return false;
}
}
return IsMobilePlatform(Parameters.Platform);
}
static void ModifyCompilationEnvironment(const FGlobalShaderPermutationParameters& Parameters, FShaderCompilerEnvironment& OutEnvironment)
{
}
};
IMPLEMENT_GLOBAL_SHADER(FMobileFSRPS, "/Plugin/MobileFSR/Private/PostProcessMobileFFX_FSR.usf", "MainPS", SF_Pixel);
FScreenPassTexture AddEASUPass(FRDGBuilder& GraphBuilder, const FViewInfo& View, const ISpatialUpscaler::FInputs& PassInputs)
{
using namespace MobileFSRPrivate;
FScreenPassTexture OutputTexture;
bool bUseFP16 = !IsSimulatedPlatform(View.GetShaderPlatform()) && IsDxcEnabledForPlatform(View.GetShaderPlatform());
FRDGTextureDesc FSROutputTextureDesc = PassInputs.SceneColor.Texture->Desc;
FSROutputTextureDesc.Reset();
FSROutputTextureDesc.Extent = View.UnscaledViewRect.Max;
FSROutputTextureDesc.ClearValue = FClearValueBinding::Black;
FSROutputTextureDesc.Flags = TexCreate_ShaderResource | TexCreate_UAV | TexCreate_RenderTargetable;
if (PassInputs.OverrideOutput.IsValid())
{
OutputTexture = PassInputs.OverrideOutput;
}
else
{
OutputTexture = FScreenPassTexture(PassInputs.SceneColor);
OutputTexture.Texture = GraphBuilder.CreateTexture(FSROutputTextureDesc, TEXT("FSR-Output"), ERDGTextureFlags::MultiFrame);
}
OutputTexture.ViewRect = View.UnscaledViewRect;
FScreenPassTextureViewport OutputViewport = FScreenPassTextureViewport(OutputTexture);
FScreenPassTextureViewport InputViewport = FScreenPassTextureViewport(PassInputs.SceneColor);
FScreenPassTextureViewportParameters PassOutputViewportParams = GetScreenPassTextureViewportParameters(OutputViewport);
AU1 const0[4];
AU1 const1[4];
AU1 const2[4];
AU1 const3[4]; // Configure FSR
FsrEasuConOffset(const0, const1, const2, const3,
static_cast<AF1>(InputViewport.Rect.Width())
, static_cast<AF1>(InputViewport.Rect.Height()) // current frame render resolution
, static_cast<AF1>(PassInputs.SceneColor.Texture->Desc.Extent.X)
, static_cast<AF1>(PassInputs.SceneColor.Texture->Desc.Extent.Y) // input container resolution (for DRS)
, static_cast<AF1>(OutputViewport.Rect.Width())
, static_cast<AF1>(OutputViewport.Rect.Height()) // upscaled-to resolution
, static_cast<AF1>(InputViewport.Rect.Min.X)
, static_cast<AF1>(InputViewport.Rect.Min.Y)
);
const bool bOutputSupportsUAV = (OutputTexture.Texture->Desc.Flags & TexCreate_UAV) == TexCreate_UAV;
if (!bOutputSupportsUAV || GMobileFSRDisableCompute)
{
FMobileFSRPS::FParameters* PassParameters = GraphBuilder.AllocParameters<FMobileFSRPS::FParameters>();
for (int i = 0; i < 4; i++)
{
PassParameters->FSR.Const0[i] = const0[i];
PassParameters->FSR.Const1[i] = const1[i];
PassParameters->FSR.Const2[i] = const2[i];
PassParameters->FSR.Const3[i] = const3[i];
}
PassParameters->FSR.InputTexture = PassInputs.SceneColor.Texture;
PassParameters->FSR.samLinearClamp = TStaticSamplerState<SF_Bilinear, AM_Clamp, AM_Clamp, AM_Clamp>::GetRHI();
PassParameters->FSR.VPColor_ExtentInverse = PassOutputViewportParams.ExtentInverse;
PassParameters->FSR.VPColor_ViewportMin = PassOutputViewportParams.ViewportMin;
PassParameters->RenderTargets[0] = FRenderTargetBinding(OutputTexture.Texture, ERenderTargetLoadAction::EClear);
FMobileFSRPS::FPermutationDomain PSPermutationVector;
PSPermutationVector.Set<FMobileFSR_UseFP16Dim>(bUseFP16);
PSPermutationVector.Set<FMobileFSR_UsePassthroughAlpha>(IsPostProcessingWithAlphaChannelSupported());
TShaderMapRef<FMobileFSRPS> PixelShader(View.ShaderMap, PSPermutationVector);
AddDrawScreenPass(GraphBuilder,
RDG_EVENT_NAME("FSR/Upscale %dx%d -> %dx%d (PS)"
, InputViewport.Rect.Width(), InputViewport.Rect.Height()
, OutputViewport.Rect.Width(), OutputViewport.Rect.Height()),
View, OutputViewport, InputViewport,
PixelShader, PassParameters,
EScreenPassDrawFlags::None
);
}
// CS
else
{
FMobileFSRCS::FParameters* PassParameters = GraphBuilder.AllocParameters<FMobileFSRCS::FParameters>();
for (int i = 0; i < 4; i++)
{
PassParameters->FSR.Const0[i] = const0[i];
PassParameters->FSR.Const1[i] = const1[i];
PassParameters->FSR.Const2[i] = const2[i];
PassParameters->FSR.Const3[i] = const3[i];
}
PassParameters->FSR.InputTexture = PassInputs.SceneColor.Texture;
PassParameters->FSR.samLinearClamp = TStaticSamplerState<SF_Bilinear, AM_Clamp, AM_Clamp, AM_Clamp>::GetRHI();
PassParameters->FSR.VPColor_ExtentInverse = PassOutputViewportParams.ExtentInverse;
PassParameters->FSR.VPColor_ViewportMin = PassOutputViewportParams.ViewportMin;
PassParameters->OutputTexture = GraphBuilder.CreateUAV(OutputTexture.Texture);
FMobileFSRCS::FPermutationDomain CSPermutationVector;
CSPermutationVector.Set<FMobileFSR_UseFP16Dim>(bUseFP16);
CSPermutationVector.Set<FMobileFSR_UsePassthroughAlpha>(IsPostProcessingWithAlphaChannelSupported());
TShaderMapRef<FMobileFSRCS> ComputeShaderFSR(View.ShaderMap, CSPermutationVector);
FComputeShaderUtils::AddPass(
GraphBuilder,
RDG_EVENT_NAME("FSR/Upscale %dx%d -> %dx%d (CS)"
, InputViewport.Rect.Width(), InputViewport.Rect.Height()
, OutputViewport.Rect.Width(), OutputViewport.Rect.Height()),
ComputeShaderFSR,
PassParameters,
FComputeShaderUtils::GetGroupCount(OutputViewport.Rect.Size(), 16));
}
return MoveTemp(OutputTexture);
}
///
/// RCAS COMPUTE SHADER
///
class FMobileRCASCS : public FGlobalShader
{
public:
DECLARE_GLOBAL_SHADER(FMobileRCASCS);
SHADER_USE_PARAMETER_STRUCT(FMobileRCASCS, FGlobalShader);
using FPermutationDomain = TShaderPermutationDomain<FMobileFSR_UseFP16Dim, FMobileFSR_UsePassthroughAlpha, FMobileRCAS_DenoiseDim>;
BEGIN_SHADER_PARAMETER_STRUCT(FParameters, )
SHADER_PARAMETER_STRUCT_INCLUDE(FMobileRCASPassParameters, RCAS)
SHADER_PARAMETER_RDG_TEXTURE_UAV(RWTexture2D, OutputTexture)
END_SHADER_PARAMETER_STRUCT()
static bool ShouldCompilePermutation(const FGlobalShaderPermutationParameters& Parameters)
{
FPermutationDomain PermutationVector(Parameters.PermutationId);
if (PermutationVector.Get<FMobileFSR_UseFP16Dim>())
{
if (IsSimulatedPlatform(Parameters.Platform) || !IsDxcEnabledForPlatform(Parameters.Platform))
{
return false;
}
}
return IsMobilePlatform(Parameters.Platform);
}
static void ModifyCompilationEnvironment(const FGlobalShaderPermutationParameters& Parameters, FShaderCompilerEnvironment& OutEnvironment)
{
FPermutationDomain PermutationVector(Parameters.PermutationId);
OutEnvironment.SetDefine(TEXT("COMPUTE_SHADER"), 1);
OutEnvironment.SetDefine(TEXT("THREADGROUP_SIZEX"), 64);
OutEnvironment.SetDefine(TEXT("THREADGROUP_SIZEY"), 1);
OutEnvironment.SetDefine(TEXT("THREADGROUP_SIZEZ"), 1);
}
};
IMPLEMENT_GLOBAL_SHADER(FMobileRCASCS, "/Plugin/MobileFSR/Private/PostProcessMobileFFX_RCAS.usf", "MainCS", SF_Compute);
///
/// RCAS PIXEL SHADER
///
class FMobileRCASPS : public FGlobalShader
{
public:
DECLARE_GLOBAL_SHADER(FMobileRCASPS);
SHADER_USE_PARAMETER_STRUCT(FMobileRCASPS, FGlobalShader);
using FPermutationDomain = TShaderPermutationDomain<FMobileFSR_UseFP16Dim, FMobileFSR_UsePassthroughAlpha, FMobileRCAS_DenoiseDim>;
BEGIN_SHADER_PARAMETER_STRUCT(FParameters, )
SHADER_PARAMETER_STRUCT_INCLUDE(FMobileRCASPassParameters, RCAS)
RENDER_TARGET_BINDING_SLOTS()
END_SHADER_PARAMETER_STRUCT()
static bool ShouldCompilePermutation(const FGlobalShaderPermutationParameters& Parameters)
{
FPermutationDomain PermutationVector(Parameters.PermutationId);
if (PermutationVector.Get<FMobileFSR_UseFP16Dim>())
{
if (IsSimulatedPlatform(Parameters.Platform) || !IsDxcEnabledForPlatform(Parameters.Platform))
{
return false;
}
}
return IsMobilePlatform(Parameters.Platform);
}
static void ModifyCompilationEnvironment(const FGlobalShaderPermutationParameters& Parameters, FShaderCompilerEnvironment& OutEnvironment)
{
}
};
IMPLEMENT_GLOBAL_SHADER(FMobileRCASPS, "/Plugin/MobileFSR/Private/PostProcessMobileFFX_RCAS.usf", "MainPS", SF_Pixel);
FScreenPassTexture AddCASPass(FRDGBuilder& GraphBuilder, const FViewInfo& View, const ISpatialUpscaler::FInputs& PassInputs)
{
using namespace MobileFSRPrivate;
FScreenPassTexture OutputTexture;
bool bUseFP16 = !IsSimulatedPlatform(View.GetShaderPlatform()) && IsDxcEnabledForPlatform(View.GetShaderPlatform());
if (PassInputs.OverrideOutput.IsValid())
{
OutputTexture = PassInputs.OverrideOutput;
}
else
{
FRDGTextureDesc RCASOutputTextureDesc = PassInputs.SceneColor.Texture->Desc;
OutputTexture = FScreenPassTexture(PassInputs.SceneColor);
OutputTexture.Texture = GraphBuilder.CreateTexture(RCASOutputTextureDesc, TEXT("RCAS-Output"), ERDGTextureFlags::MultiFrame);
}
FScreenPassTextureViewport OutputViewport = FScreenPassTextureViewport(OutputTexture);
FScreenPassTextureViewportParameters PassOutputViewportParams = GetScreenPassTextureViewportParameters(OutputViewport);
AU1 const0[4]; // Configure FSR
FsrRcasCon(const0, GMobileFSRCASSharpness);
const bool bOutputSupportsUAV = (OutputTexture.Texture->Desc.Flags & TexCreate_UAV) == TexCreate_UAV;
if (!bOutputSupportsUAV || GMobileFSRDisableCompute)
{
FMobileRCASPS::FParameters* PassParameters = GraphBuilder.AllocParameters<FMobileRCASPS::FParameters>();
// set pass inputs
for (int i = 0; i < 4; i++) { PassParameters->RCAS.Const0[i] = const0[i]; }
PassParameters->RCAS.InputTexture = PassInputs.SceneColor.Texture;
PassParameters->RCAS.VPColor_ExtentInverse = PassOutputViewportParams.ExtentInverse;
PassParameters->RenderTargets[0] = FRenderTargetBinding(OutputTexture.Texture, ERenderTargetLoadAction::EClear);
// grab shaders
FMobileRCASPS::FPermutationDomain PSPermutationVector;
PSPermutationVector.Set<FMobileFSR_UseFP16Dim>(bUseFP16);
PSPermutationVector.Set<FMobileFSR_UsePassthroughAlpha>(IsPostProcessingWithAlphaChannelSupported());
PSPermutationVector.Set<FMobileRCAS_DenoiseDim>(GMobileFSRCASDenoise > 0);
TShaderMapRef<FMobileRCASPS> PixelShader(View.ShaderMap, PSPermutationVector);
AddDrawScreenPass(GraphBuilder,
RDG_EVENT_NAME("FSR/RCAS Sharpness=%.2f(PS)"
, GMobileFSRCASSharpness),
View, OutputViewport, OutputViewport,
PixelShader, PassParameters,
EScreenPassDrawFlags::None
);
}
// CS
else
{
FMobileRCASCS::FParameters* PassParameters = GraphBuilder.AllocParameters<FMobileRCASCS::FParameters>();
for (int i = 0; i < 4; i++)
{
PassParameters->RCAS.Const0[i] = const0[i];
}
PassParameters->RCAS.InputTexture = PassInputs.SceneColor.Texture;
PassParameters->RCAS.VPColor_ExtentInverse = PassOutputViewportParams.ExtentInverse;
PassParameters->OutputTexture = GraphBuilder.CreateUAV(OutputTexture.Texture);
FMobileRCASCS::FPermutationDomain CSPermutationVector;
CSPermutationVector.Set<FMobileFSR_UseFP16Dim>(bUseFP16);
CSPermutationVector.Set<FMobileFSR_UsePassthroughAlpha>(IsPostProcessingWithAlphaChannelSupported());
CSPermutationVector.Set<FMobileRCAS_DenoiseDim>(GMobileFSRCASDenoise > 0);
TShaderMapRef<FMobileRCASCS> ComputeShaderRCASPass(View.ShaderMap, CSPermutationVector);
FComputeShaderUtils::AddPass(
GraphBuilder,
RDG_EVENT_NAME("FSR/RCAS Sharpness=%.2f (CS)", GMobileFSRCASSharpness),
ComputeShaderRCASPass,
PassParameters,
FComputeShaderUtils::GetGroupCount(OutputViewport.Rect.Size(), 16));
}
return MoveTemp(OutputTexture);
}
DECLARE_GPU_STAT(MobileFSRUpscaler);
FMobileFSRUpscaler::FMobileFSRUpscaler(bool bInIsEASUPass) : bIsEASUPass(bInIsEASUPass)
{
}
ISpatialUpscaler* FMobileFSRUpscaler::Fork_GameThread(const class FSceneViewFamily& ViewFamily) const
{
return new FMobileFSRUpscaler(bIsEASUPass);
}
FScreenPassTexture FMobileFSRUpscaler::AddPasses(FRDGBuilder& GraphBuilder, const FViewInfo& View, const FInputs& PassInputs) const
{
RDG_EVENT_SCOPE_STAT(GraphBuilder, MobileFSRUpscaler, "MobileFSRUpscaler");
RDG_GPU_STAT_SCOPE(GraphBuilder, MobileFSRUpscaler);
FScreenPassTexture SceneColor;
if (bIsEASUPass)
{
SceneColor = AddEASUPass(GraphBuilder, View, PassInputs);
}
else
{
SceneColor = AddCASPass(GraphBuilder, View, PassInputs);
}
return MoveTemp(SceneColor);
}
@@ -0,0 +1,20 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "PostProcess/PostProcessUpscale.h"
class FMobileFSRUpscaler final : public ISpatialUpscaler
{
public:
FMobileFSRUpscaler(bool bInIsEASUPass);
// ISpatialUpscaler interface
const TCHAR* GetDebugName() const override { return TEXT("FMobileFSRUpscaler"); }
ISpatialUpscaler* Fork_GameThread(const class FSceneViewFamily& ViewFamily) const override;
FScreenPassTexture AddPasses(FRDGBuilder& GraphBuilder, const FViewInfo& View, const FInputs& PassInputs) const override;
private:
bool bIsEASUPass = false;
};
@@ -0,0 +1,37 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "MobileFSRViewExtension.h"
#include "MobileFSRUpscaler.h"
static TAutoConsoleVariable<int32> CVarEnableFSR(
TEXT("r.Mobile.FSR.Enabled"),
1,
TEXT("Enable Mobile FSR for Primary Upscale"),
ECVF_RenderThreadSafe);
TAutoConsoleVariable<int32> CVarMobileFSRCASEnabled(
TEXT("r.Mobile.FSR.RCAS.Enabled"),
1,
TEXT("FidelityFX FSR/RCAS : Robust Contrast Adaptive Sharpening Filter"),
ECVF_RenderThreadSafe);
TAutoConsoleVariable<int32>CVarMobileFSRUpsamplingEnabled(
TEXT("r.Mobile.FSR.Upsampling.Enabled"),
1,
TEXT("FidelityFX FSR/RCAS : Robust Contrast Adaptive Sharpening Filter"),
ECVF_Default);
void FMobileFSRViewExtension::BeginRenderViewFamily(FSceneViewFamily& InViewFamily)
{
if (InViewFamily.GetFeatureLevel() == ERHIFeatureLevel::ES3_1 && CVarEnableFSR.GetValueOnAnyThread() > 0)
{
if (CVarMobileFSRUpsamplingEnabled.GetValueOnAnyThread())
{
InViewFamily.SetPrimarySpatialUpscalerInterface(new FMobileFSRUpscaler(true));
}
if (CVarMobileFSRCASEnabled.GetValueOnAnyThread())
{
InViewFamily.SetSecondarySpatialUpscalerInterface(new FMobileFSRUpscaler(false));
}
}
}
@@ -0,0 +1,20 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "Modules/ModuleManager.h"
#include "Logging/LogMacros.h"
DECLARE_LOG_CATEGORY_EXTERN(LogMobileFSR, Verbose, All);
class FMobileFSRViewExtension;
class FMobileFSRModule final : public IModuleInterface
{
public:
void StartupModule() override;
void ShutdownModule() override;
void OnPostEngineInit();
TSharedPtr<FMobileFSRViewExtension, ESPMode::ThreadSafe> ViewExtension;
};
@@ -0,0 +1,13 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "SceneViewExtension.h"
class FMobileFSRViewExtension final : public FSceneViewExtensionBase
{
public:
FMobileFSRViewExtension(const FAutoRegister& AutoRegister) : FSceneViewExtensionBase(AutoRegister) {}
MOBILEFSR_API void BeginRenderViewFamily(FSceneViewFamily& InViewFamily) override;
};
@@ -0,0 +1,24 @@
{
"FileVersion": 3,
"Version": 1,
"VersionName": "1.0.0",
"FriendlyName": "Qwen TTS Streaming",
"Description": "Streams Qwen TTS Gateway audio_b64 chunks into a procedural audio component.",
"Category": "Audio",
"CreatedBy": "FunnyGirl",
"CanContainContent": false,
"IsBetaVersion": true,
"EnabledByDefault": true,
"Modules": [
{
"Name": "QwenTTSStreaming",
"Type": "Runtime",
"LoadingPhase": "Default"
},
{
"Name": "QwenTTSStreamingEditor",
"Type": "Editor",
"LoadingPhase": "Default"
}
]
}
+54
View File
@@ -0,0 +1,54 @@
# Qwen TTS Streaming
Runtime plugin for streaming Qwen TTS Gateway audio into Unreal Engine.
## Assets
Create two data assets in the editor:
- `Qwen TTS Streaming Settings Data Asset`
- `Qwen TTS Preset Data Asset`
They are registered in the Content Browser create menu under the Audio asset
category.
Default gateway URL:
```text
wss://services.ai-games-studio.ru/qwen-tts/generate/stream/ws
```
Default Kira preset id:
```text
23c7eff6-5a38-451c-9b31-2d312a4e8b3d
```
## Component
Add `Qwen TTS Streaming Component` to an actor. It inherits from `AudioComponent`.
Set:
- `Settings`
- `Preset`
Then call:
```text
Stream Text
```
The component opens a WebSocket, sends the request, receives JSON messages with
`audio_b64`, decodes each WAV chunk into PCM, and queues it into
`USoundWaveProcedural`.
Useful events:
- `On Stream Started`
- `On Audio Chunk`
- `On Stream Finished`
- `On Stream Error`
Each `On Audio Chunk` event includes the original `audio_b64`, decoded PCM byte
count, sample rate, channel count, and chunk index.
@@ -0,0 +1,8 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "QwenTTSPresetDataAsset.h"
UQwenTTSPresetDataAsset::UQwenTTSPresetDataAsset()
{
PresetId = TEXT("23c7eff6-5a38-451c-9b31-2d312a4e8b3d");
}
@@ -0,0 +1,454 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "QwenTTSStreamingComponent.h"
#include "Async/Async.h"
#include "IWebSocket.h"
#include "Json.h"
#include "Misc/Base64.h"
#include "QwenTTSPresetDataAsset.h"
#include "QwenTTSStreamingSettingsDataAsset.h"
#include "QwenTTSStreamingLog.h"
#include "Sound/SoundWaveProcedural.h"
#include "WebSocketsModule.h"
namespace
{
struct FDecodedWavChunk
{
TArray<uint8> PCM;
int32 SampleRate = 0;
int32 NumChannels = 0;
int32 BitsPerSample = 0;
};
uint16 ReadLE16(const uint8* Data)
{
return static_cast<uint16>(Data[0]) | (static_cast<uint16>(Data[1]) << 8);
}
uint32 ReadLE32(const uint8* Data)
{
return static_cast<uint32>(Data[0]) | (static_cast<uint32>(Data[1]) << 8) | (static_cast<uint32>(Data[2]) << 16) | (static_cast<uint32>(Data[3]) << 24);
}
bool HasTag(const TArray<uint8>& Bytes, int32 Offset, const ANSICHAR* Tag)
{
return Bytes.IsValidIndex(Offset + 3)
&& Bytes[Offset] == static_cast<uint8>(Tag[0])
&& Bytes[Offset + 1] == static_cast<uint8>(Tag[1])
&& Bytes[Offset + 2] == static_cast<uint8>(Tag[2])
&& Bytes[Offset + 3] == static_cast<uint8>(Tag[3]);
}
bool DecodeWavPCM16(const TArray<uint8>& WavBytes, FDecodedWavChunk& OutChunk, FString& OutError)
{
if (WavBytes.Num() < 44 || !HasTag(WavBytes, 0, "RIFF") || !HasTag(WavBytes, 8, "WAVE"))
{
OutError = TEXT("audio_b64 is not a RIFF/WAVE chunk");
return false;
}
bool bFoundFmt = false;
bool bFoundData = false;
uint16 AudioFormat = 0;
int32 Offset = 12;
int32 DataOffset = 0;
int32 DataSize = 0;
while (Offset + 8 <= WavBytes.Num())
{
const uint32 ChunkSize = ReadLE32(&WavBytes[Offset + 4]);
const int32 ChunkDataOffset = Offset + 8;
const int32 NextOffset = ChunkDataOffset + static_cast<int32>(ChunkSize) + (ChunkSize % 2);
if (ChunkDataOffset + static_cast<int32>(ChunkSize) > WavBytes.Num())
{
break;
}
if (HasTag(WavBytes, Offset, "fmt "))
{
if (ChunkSize < 16)
{
OutError = TEXT("Invalid WAV fmt chunk");
return false;
}
AudioFormat = ReadLE16(&WavBytes[ChunkDataOffset]);
OutChunk.NumChannels = static_cast<int32>(ReadLE16(&WavBytes[ChunkDataOffset + 2]));
OutChunk.SampleRate = static_cast<int32>(ReadLE32(&WavBytes[ChunkDataOffset + 4]));
OutChunk.BitsPerSample = static_cast<int32>(ReadLE16(&WavBytes[ChunkDataOffset + 14]));
bFoundFmt = true;
}
else if (HasTag(WavBytes, Offset, "data"))
{
DataOffset = ChunkDataOffset;
DataSize = static_cast<int32>(ChunkSize);
bFoundData = true;
}
Offset = NextOffset;
}
if (!bFoundFmt || !bFoundData)
{
OutError = TEXT("WAV chunk has no fmt/data section");
return false;
}
if (AudioFormat != 1 || OutChunk.BitsPerSample != 16)
{
OutError = FString::Printf(TEXT("Unsupported WAV format: format=%d bits=%d"), AudioFormat, OutChunk.BitsPerSample);
return false;
}
OutChunk.PCM.SetNumUninitialized(DataSize);
FMemory::Memcpy(OutChunk.PCM.GetData(), &WavBytes[DataOffset], DataSize);
return true;
}
void AddStringField(TSharedRef<FJsonObject> JsonObject, const FString& Key, const FString& Value)
{
if (!Value.IsEmpty())
{
JsonObject->SetStringField(Key, Value);
}
}
}
UQwenTTSStreamingComponent::UQwenTTSStreamingComponent()
{
bAutoActivate = false;
bAutoDestroy = false;
PrimaryComponentTick.bCanEverTick = false;
}
bool UQwenTTSStreamingComponent::StreamText(const FString& Text)
{
if (Text.TrimStartAndEnd().IsEmpty())
{
BroadcastError(TEXT("Text is empty"));
return false;
}
if (!Settings)
{
BroadcastError(TEXT("Qwen TTS settings data asset is not set"));
return false;
}
if (!Preset)
{
BroadcastError(TEXT("Qwen TTS preset data asset is not set"));
return false;
}
if (Settings->GatewayWebSocketUrl.IsEmpty())
{
BroadcastError(TEXT("GatewayWebSocketUrl is empty"));
return false;
}
if (Settings->bStopAudioBeforeRequest)
{
UE_LOG(LogQwenTTSStreaming, Verbose, TEXT("Stopping current audio before a new Qwen TTS request."));
Stop();
}
StopStreaming();
PendingText = Text;
UE_LOG(
LogQwenTTSStreaming,
Log,
TEXT("Starting Qwen TTS stream. Url=%s PresetId=%s Language=%s TextChars=%d"),
*Settings->GatewayWebSocketUrl,
Preset ? *Preset->PresetId : TEXT("<none>"),
Preset ? *Preset->Language : TEXT("<none>"),
Text.Len());
Connect();
return true;
}
void UQwenTTSStreamingComponent::StopStreaming()
{
if (bStreaming || Socket.IsValid() || ProceduralSoundWave)
{
UE_LOG(LogQwenTTSStreaming, Verbose, TEXT("Stopping Qwen TTS stream."));
}
bStreaming = false;
bPlaybackStarted = false;
PendingText.Reset();
if (Socket.IsValid())
{
Socket->Close();
Socket.Reset();
}
if (ProceduralSoundWave)
{
ProceduralSoundWave->ResetAudio();
}
}
bool UQwenTTSStreamingComponent::IsStreaming() const
{
return bStreaming && Socket.IsValid();
}
bool UQwenTTSStreamingComponent::IsGeneratingVoice() const
{
return bStreaming;
}
bool UQwenTTSStreamingComponent::IsPlayingVoiceAudio() const
{
return IsPlaying();
}
void UQwenTTSStreamingComponent::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
StopStreaming();
Super::EndPlay(EndPlayReason);
}
void UQwenTTSStreamingComponent::Connect()
{
FWebSocketsModule& WebSocketsModule = FModuleManager::LoadModuleChecked<FWebSocketsModule>(TEXT("WebSockets"));
Socket = WebSocketsModule.CreateWebSocket(Settings->GatewayWebSocketUrl);
UE_LOG(LogQwenTTSStreaming, Log, TEXT("Connecting Qwen TTS WebSocket: %s"), *Settings->GatewayWebSocketUrl);
Socket->OnConnected().AddUObject(this, &UQwenTTSStreamingComponent::HandleConnected);
Socket->OnConnectionError().AddUObject(this, &UQwenTTSStreamingComponent::HandleConnectionError);
Socket->OnClosed().AddUObject(this, &UQwenTTSStreamingComponent::HandleClosed);
Socket->OnMessage().AddUObject(this, &UQwenTTSStreamingComponent::HandleMessage);
bStreaming = true;
Socket->Connect();
}
void UQwenTTSStreamingComponent::HandleConnected()
{
if (!Socket.IsValid())
{
return;
}
const FString RequestJson = BuildRequestJson();
UE_LOG(LogQwenTTSStreaming, Log, TEXT("Qwen TTS WebSocket connected. Sending request bytes=%d"), RequestJson.Len());
Socket->Send(RequestJson);
OnStreamStarted.Broadcast();
}
void UQwenTTSStreamingComponent::HandleConnectionError(const FString& Error)
{
UE_LOG(LogQwenTTSStreaming, Error, TEXT("Qwen TTS WebSocket connection error: %s"), *Error);
bStreaming = false;
BroadcastError(Error);
}
void UQwenTTSStreamingComponent::HandleClosed(int32 StatusCode, const FString& Reason, bool bWasClean)
{
if (bWasClean)
{
UE_LOG(LogQwenTTSStreaming, Log, TEXT("Qwen TTS WebSocket closed. StatusCode=%d Clean=true Reason=%s"), StatusCode, *Reason);
}
else
{
UE_LOG(LogQwenTTSStreaming, Warning, TEXT("Qwen TTS WebSocket closed. StatusCode=%d Clean=false Reason=%s"), StatusCode, *Reason);
}
bStreaming = false;
if (!bWasClean && StatusCode != 1000)
{
BroadcastError(FString::Printf(TEXT("Qwen TTS socket closed: %d %s"), StatusCode, *Reason));
}
}
void UQwenTTSStreamingComponent::HandleMessage(const FString& Message)
{
TSharedPtr<FJsonObject> JsonObject;
const TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(Message);
if (!FJsonSerializer::Deserialize(Reader, JsonObject) || !JsonObject.IsValid())
{
UE_LOG(LogQwenTTSStreaming, Warning, TEXT("Bad JSON message from Qwen TTS gateway. Bytes=%d"), Message.Len());
BroadcastError(TEXT("Bad JSON message from Qwen TTS gateway"));
return;
}
const FString Type = JsonObject->GetStringField(TEXT("type"));
UE_LOG(LogQwenTTSStreaming, VeryVerbose, TEXT("Qwen TTS message received. Type=%s Bytes=%d"), *Type, Message.Len());
if (Type == TEXT("chunk"))
{
FString AudioBase64;
if (JsonObject->TryGetStringField(TEXT("audio_b64"), AudioBase64))
{
const int32 Index = static_cast<int32>(JsonObject->GetIntegerField(TEXT("index")));
HandleAudioBase64(Index, AudioBase64);
}
else
{
UE_LOG(LogQwenTTSStreaming, Warning, TEXT("Qwen TTS chunk message has no audio_b64 field."));
}
return;
}
if (Type == TEXT("done"))
{
const int32 ChunkCount = JsonObject->HasTypedField<EJson::Number>(TEXT("chunks"))
? static_cast<int32>(JsonObject->GetIntegerField(TEXT("chunks")))
: -1;
UE_LOG(LogQwenTTSStreaming, Log, TEXT("Qwen TTS stream finished. Chunks=%d"), ChunkCount);
bStreaming = false;
if (Socket.IsValid())
{
Socket->Close();
Socket.Reset();
}
OnStreamFinished.Broadcast();
return;
}
if (Type == TEXT("error"))
{
FString ErrorMessage;
if (!JsonObject->TryGetStringField(TEXT("message"), ErrorMessage))
{
JsonObject->TryGetStringField(TEXT("body"), ErrorMessage);
}
UE_LOG(LogQwenTTSStreaming, Error, TEXT("Qwen TTS gateway error message: %s"), *(ErrorMessage.IsEmpty() ? Message : ErrorMessage));
BroadcastError(ErrorMessage.IsEmpty() ? Message : ErrorMessage);
}
}
void UQwenTTSStreamingComponent::HandleAudioBase64(int32 Index, const FString& AudioBase64)
{
TArray<uint8> WavBytes;
if (!FBase64::Decode(AudioBase64, WavBytes))
{
UE_LOG(LogQwenTTSStreaming, Error, TEXT("Failed to decode Qwen TTS audio chunk. Index=%d Base64Chars=%d"), Index, AudioBase64.Len());
BroadcastError(TEXT("Failed to decode audio_b64"));
return;
}
FDecodedWavChunk DecodedChunk;
FString Error;
if (!DecodeWavPCM16(WavBytes, DecodedChunk, Error))
{
UE_LOG(LogQwenTTSStreaming, Error, TEXT("Failed to decode Qwen TTS WAV chunk. Index=%d Error=%s"), Index, *Error);
BroadcastError(Error);
return;
}
EnsureProceduralWave(DecodedChunk.SampleRate, DecodedChunk.NumChannels);
if (!ProceduralSoundWave)
{
UE_LOG(LogQwenTTSStreaming, Error, TEXT("Qwen TTS procedural sound wave is missing after decode. Index=%d"), Index);
return;
}
ProceduralSoundWave->QueueAudio(DecodedChunk.PCM.GetData(), DecodedChunk.PCM.Num());
UE_LOG(
LogQwenTTSStreaming,
Verbose,
TEXT("Queued Qwen TTS audio chunk. Index=%d SampleRate=%d Channels=%d PCMBytes=%d"),
Index,
DecodedChunk.SampleRate,
DecodedChunk.NumChannels,
DecodedChunk.PCM.Num());
if (Settings && Settings->bAutoPlayOnFirstChunk && !bPlaybackStarted)
{
SetSound(ProceduralSoundWave);
Play();
bPlaybackStarted = true;
UE_LOG(LogQwenTTSStreaming, Log, TEXT("Qwen TTS playback started on first chunk. Index=%d"), Index);
}
FQwenTTSStreamChunk Chunk;
Chunk.Index = Index;
Chunk.AudioBase64 = AudioBase64;
Chunk.SampleRate = DecodedChunk.SampleRate;
Chunk.NumChannels = DecodedChunk.NumChannels;
Chunk.PCMBytes = DecodedChunk.PCM.Num();
OnAudioChunk.Broadcast(Chunk);
}
void UQwenTTSStreamingComponent::EnsureProceduralWave(int32 SampleRate, int32 NumChannels)
{
if (ProceduralSoundWave && CurrentSampleRate == SampleRate && CurrentNumChannels == NumChannels)
{
return;
}
ProceduralSoundWave = NewObject<USoundWaveProcedural>(this);
ProceduralSoundWave->SetSampleRate(SampleRate);
ProceduralSoundWave->NumChannels = NumChannels;
ProceduralSoundWave->Duration = Settings ? Settings->ProceduralDurationSeconds : INDEFINITELY_LOOPING_DURATION;
ProceduralSoundWave->bLooping = false;
ProceduralSoundWave->SampleByteSize = sizeof(int16);
ProceduralSoundWave->SoundGroup = SOUNDGROUP_Voice;
CurrentSampleRate = SampleRate;
CurrentNumChannels = NumChannels;
SetSound(ProceduralSoundWave);
UE_LOG(
LogQwenTTSStreaming,
Log,
TEXT("Created Qwen TTS procedural sound wave. SampleRate=%d Channels=%d Duration=%.2f"),
SampleRate,
NumChannels,
ProceduralSoundWave->Duration);
}
FString UQwenTTSStreamingComponent::BuildRequestJson() const
{
TSharedRef<FJsonObject> JsonObject = MakeShared<FJsonObject>();
JsonObject->SetStringField(TEXT("text"), PendingText);
if (Preset)
{
AddStringField(JsonObject, TEXT("preset_id"), Preset->PresetId);
AddStringField(JsonObject, TEXT("language"), Preset->Language);
AddStringField(JsonObject, TEXT("mode"), Preset->Mode);
JsonObject->SetNumberField(TEXT("chunk_size"), Preset->ChunkSize);
JsonObject->SetNumberField(TEXT("first_chunk_size"), Preset->FirstChunkSize);
JsonObject->SetNumberField(TEXT("crossfade_samples"), Preset->CrossfadeSamples);
JsonObject->SetNumberField(TEXT("temperature"), Preset->Temperature);
JsonObject->SetNumberField(TEXT("top_k"), Preset->TopK);
JsonObject->SetNumberField(TEXT("repetition_penalty"), Preset->RepetitionPenalty);
JsonObject->SetNumberField(TEXT("repetition_penalty_window"), Preset->RepetitionPenaltyWindow);
for (const TPair<FString, FString>& Pair : Preset->ExtraFields)
{
AddStringField(JsonObject, Pair.Key, Pair.Value);
}
}
if (Settings)
{
for (const TPair<FString, FString>& Pair : Settings->ExtraFields)
{
AddStringField(JsonObject, Pair.Key, Pair.Value);
}
}
FString Output;
const TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&Output);
FJsonSerializer::Serialize(JsonObject, Writer);
return Output;
}
void UQwenTTSStreamingComponent::BroadcastError(const FString& Error)
{
UE_LOG(LogQwenTTSStreaming, Error, TEXT("Qwen TTS stream error: %s"), *Error);
OnStreamError.Broadcast(Error);
}
@@ -0,0 +1,9 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "QwenTTSStreamingLog.h"
#include "Modules/ModuleManager.h"
DEFINE_LOG_CATEGORY(LogQwenTTSStreaming);
IMPLEMENT_MODULE(FDefaultModuleImpl, QwenTTSStreaming)
@@ -0,0 +1,33 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "QwenTTSStreamingSettingsDataAsset.h"
UQwenTTSStreamingSettingsDataAsset::UQwenTTSStreamingSettingsDataAsset()
{
GatewayWebSocketUrl = TEXT("wss://services.ai-games-studio.ru/qwen-tts/generate/stream/ws");
GatewayStatusUrl = TEXT("https://services.ai-games-studio.ru/qwen-tts/status");
}
FString UQwenTTSStreamingSettingsDataAsset::GetResolvedGatewayStatusUrl() const
{
if (!GatewayStatusUrl.IsEmpty())
{
return GatewayStatusUrl;
}
FString ResolvedUrl = GatewayWebSocketUrl;
if (ResolvedUrl.StartsWith(TEXT("wss://")))
{
ResolvedUrl.RemoveFromStart(TEXT("wss://"));
ResolvedUrl.InsertAt(0, TEXT("https://"));
}
else if (ResolvedUrl.StartsWith(TEXT("ws://")))
{
ResolvedUrl.RemoveFromStart(TEXT("ws://"));
ResolvedUrl.InsertAt(0, TEXT("http://"));
}
ResolvedUrl.RemoveFromEnd(TEXT("/generate/stream/ws"));
ResolvedUrl.RemoveFromEnd(TEXT("/"));
return ResolvedUrl / TEXT("status");
}
@@ -0,0 +1,54 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "Engine/DataAsset.h"
#include "QwenTTSPresetDataAsset.generated.h"
class UQwenTTSStreamingSettingsDataAsset;
UCLASS(BlueprintType)
class QWENTTSSTREAMING_API UQwenTTSPresetDataAsset : public UPrimaryDataAsset
{
GENERATED_BODY()
public:
UQwenTTSPresetDataAsset();
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Qwen TTS|Preset")
TObjectPtr<UQwenTTSStreamingSettingsDataAsset> Settings;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Qwen TTS|Preset")
FString PresetId;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Qwen TTS|Preset")
FString Language = TEXT("Russian");
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Qwen TTS|Preset")
FString Mode = TEXT("voice_clone");
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Qwen TTS|Generation", meta = (ClampMin = "1"))
int32 ChunkSize = 25;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Qwen TTS|Generation", meta = (ClampMin = "1"))
int32 FirstChunkSize = 8;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Qwen TTS|Generation", meta = (ClampMin = "0"))
int32 CrossfadeSamples = 256;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Qwen TTS|Generation", meta = (ClampMin = "0.0", ClampMax = "2.0"))
float Temperature = 0.7f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Qwen TTS|Generation", meta = (ClampMin = "1"))
int32 TopK = 20;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Qwen TTS|Generation", meta = (ClampMin = "0.0"))
float RepetitionPenalty = 1.1f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Qwen TTS|Generation", meta = (ClampMin = "1"))
int32 RepetitionPenaltyWindow = 64;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Qwen TTS|Payload")
TMap<FString, FString> ExtraFields;
};
@@ -0,0 +1,82 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "Components/AudioComponent.h"
#include "QwenTTSStreamingTypes.h"
#include "QwenTTSStreamingComponent.generated.h"
class IWebSocket;
class UQwenTTSPresetDataAsset;
class UQwenTTSStreamingSettingsDataAsset;
class USoundWaveProcedural;
UCLASS(ClassGroup = (Audio, QwenTTS), meta = (BlueprintSpawnableComponent))
class QWENTTSSTREAMING_API UQwenTTSStreamingComponent : public UAudioComponent
{
GENERATED_BODY()
public:
UQwenTTSStreamingComponent();
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Qwen TTS")
TObjectPtr<UQwenTTSStreamingSettingsDataAsset> Settings;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Qwen TTS")
TObjectPtr<UQwenTTSPresetDataAsset> Preset;
UPROPERTY(BlueprintAssignable, Category = "Qwen TTS|Events")
FQwenTTSStreamSimpleDelegate OnStreamStarted;
UPROPERTY(BlueprintAssignable, Category = "Qwen TTS|Events")
FQwenTTSStreamChunkDelegate OnAudioChunk;
UPROPERTY(BlueprintAssignable, Category = "Qwen TTS|Events")
FQwenTTSStreamSimpleDelegate OnStreamFinished;
UPROPERTY(BlueprintAssignable, Category = "Qwen TTS|Events")
FQwenTTSStreamMessageDelegate OnStreamError;
UFUNCTION(BlueprintCallable, Category = "Qwen TTS")
bool StreamText(const FString& Text);
UFUNCTION(BlueprintCallable, Category = "Qwen TTS")
void StopStreaming();
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Qwen TTS")
bool IsStreaming() const;
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Qwen TTS")
bool IsGeneratingVoice() const;
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Qwen TTS")
bool IsPlayingVoiceAudio() const;
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Qwen TTS")
USoundWaveProcedural* GetProceduralSoundWave() const { return ProceduralSoundWave; }
virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override;
private:
TSharedPtr<IWebSocket> Socket;
UPROPERTY(Transient)
TObjectPtr<USoundWaveProcedural> ProceduralSoundWave;
FString PendingText;
bool bStreaming = false;
bool bPlaybackStarted = false;
int32 CurrentSampleRate = 0;
int32 CurrentNumChannels = 0;
void Connect();
void HandleConnected();
void HandleConnectionError(const FString& Error);
void HandleClosed(int32 StatusCode, const FString& Reason, bool bWasClean);
void HandleMessage(const FString& Message);
void HandleAudioBase64(int32 Index, const FString& AudioBase64);
void EnsureProceduralWave(int32 SampleRate, int32 NumChannels);
FString BuildRequestJson() const;
void BroadcastError(const FString& Error);
};
@@ -0,0 +1,7 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
QWENTTSSTREAMING_API DECLARE_LOG_CATEGORY_EXTERN(LogQwenTTSStreaming, Log, All);
@@ -0,0 +1,40 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "Engine/DataAsset.h"
#include "QwenTTSStreamingSettingsDataAsset.generated.h"
UCLASS(BlueprintType)
class QWENTTSSTREAMING_API UQwenTTSStreamingSettingsDataAsset : public UPrimaryDataAsset
{
GENERATED_BODY()
public:
UQwenTTSStreamingSettingsDataAsset();
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Qwen TTS|Gateway")
FString GatewayWebSocketUrl;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Qwen TTS|Gateway")
FString GatewayStatusUrl;
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Qwen TTS|Gateway")
FString GetResolvedGatewayStatusUrl() const;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Qwen TTS|Gateway", meta = (ClampMin = "1.0"))
float ConnectionTimeoutSeconds = 15.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Qwen TTS|Audio")
bool bAutoPlayOnFirstChunk = true;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Qwen TTS|Audio")
bool bStopAudioBeforeRequest = true;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Qwen TTS|Audio", meta = (ClampMin = "0.01"))
float ProceduralDurationSeconds = 300.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Qwen TTS|Payload")
TMap<FString, FString> ExtraFields;
};
@@ -0,0 +1,31 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "QwenTTSStreamingTypes.generated.h"
USTRUCT(BlueprintType)
struct QWENTTSSTREAMING_API FQwenTTSStreamChunk
{
GENERATED_BODY()
UPROPERTY(BlueprintReadOnly, Category = "Qwen TTS")
int32 Index = 0;
UPROPERTY(BlueprintReadOnly, Category = "Qwen TTS")
FString AudioBase64;
UPROPERTY(BlueprintReadOnly, Category = "Qwen TTS")
int32 SampleRate = 0;
UPROPERTY(BlueprintReadOnly, Category = "Qwen TTS")
int32 NumChannels = 0;
UPROPERTY(BlueprintReadOnly, Category = "Qwen TTS")
int32 PCMBytes = 0;
};
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FQwenTTSStreamChunkDelegate, const FQwenTTSStreamChunk&, Chunk);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FQwenTTSStreamMessageDelegate, const FString&, Message);
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FQwenTTSStreamSimpleDelegate);
@@ -0,0 +1,24 @@
using UnrealBuildTool;
public class QwenTTSStreaming : ModuleRules
{
public QwenTTSStreaming(ReadOnlyTargetRules Target) : base(Target)
{
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
PublicDependencyModuleNames.AddRange(new string[]
{
"Core",
"CoreUObject",
"Engine"
});
PrivateDependencyModuleNames.AddRange(new string[]
{
"AudioMixer",
"Json",
"JsonUtilities",
"WebSockets"
});
}
}
@@ -0,0 +1,30 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "QwenTTSDataAssetFactories.h"
#include "QwenTTSPresetDataAsset.h"
#include "QwenTTSStreamingSettingsDataAsset.h"
UQwenTTSStreamingSettingsDataAssetFactory::UQwenTTSStreamingSettingsDataAssetFactory()
{
SupportedClass = UQwenTTSStreamingSettingsDataAsset::StaticClass();
bCreateNew = true;
bEditAfterNew = true;
}
UObject* UQwenTTSStreamingSettingsDataAssetFactory::FactoryCreateNew(UClass* Class, UObject* InParent, FName Name, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn)
{
return NewObject<UQwenTTSStreamingSettingsDataAsset>(InParent, Class, Name, Flags | RF_Transactional);
}
UQwenTTSPresetDataAssetFactory::UQwenTTSPresetDataAssetFactory()
{
SupportedClass = UQwenTTSPresetDataAsset::StaticClass();
bCreateNew = true;
bEditAfterNew = true;
}
UObject* UQwenTTSPresetDataAssetFactory::FactoryCreateNew(UClass* Class, UObject* InParent, FName Name, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn)
{
return NewObject<UQwenTTSPresetDataAsset>(InParent, Class, Name, Flags | RF_Transactional);
}
@@ -0,0 +1,286 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "QwenTTSPresetDetailsCustomization.h"
#include "DetailCategoryBuilder.h"
#include "DetailLayoutBuilder.h"
#include "DetailWidgetRow.h"
#include "HttpModule.h"
#include "Interfaces/IHttpResponse.h"
#include "Json.h"
#include "PropertyHandle.h"
#include "QwenTTSPresetDataAsset.h"
#include "QwenTTSStreamingLog.h"
#include "QwenTTSStreamingSettingsDataAsset.h"
#include "Widgets/Input/SButton.h"
#include "Widgets/Input/SComboBox.h"
#include "Widgets/Text/STextBlock.h"
#define LOCTEXT_NAMESPACE "FQwenTTSPresetDetailsCustomization"
FText FQwenTTSPresetChoice::GetDisplayText() const
{
if (!Label.IsEmpty())
{
return FText::FromString(FString::Printf(TEXT("%s (%s)"), *Label, *Id));
}
return FText::FromString(Id);
}
TSharedRef<IDetailCustomization> FQwenTTSPresetDetailsCustomization::MakeInstance()
{
return MakeShared<FQwenTTSPresetDetailsCustomization>();
}
void FQwenTTSPresetDetailsCustomization::CustomizeDetails(IDetailLayoutBuilder& DetailBuilder)
{
TArray<TWeakObjectPtr<UObject>> Objects;
DetailBuilder.GetObjectsBeingCustomized(Objects);
if (Objects.Num() > 0)
{
PresetAsset = Cast<UQwenTTSPresetDataAsset>(Objects[0].Get());
}
SettingsHandle = DetailBuilder.GetProperty(GET_MEMBER_NAME_CHECKED(UQwenTTSPresetDataAsset, Settings));
PresetIdHandle = DetailBuilder.GetProperty(GET_MEMBER_NAME_CHECKED(UQwenTTSPresetDataAsset, PresetId));
DetailBuilder.HideProperty(SettingsHandle);
DetailBuilder.HideProperty(PresetIdHandle);
IDetailCategoryBuilder& PresetCategory = DetailBuilder.EditCategory(TEXT("Qwen TTS|Preset"));
PresetCategory.AddProperty(SettingsHandle.ToSharedRef());
PresetCategory.AddCustomRow(LOCTEXT("PresetPickerFilter", "Preset Picker"))
.NameContent()
[
SNew(STextBlock)
.Text(LOCTEXT("PresetPickerLabel", "Preset Picker"))
.Font(IDetailLayoutBuilder::GetDetailFont())
]
.ValueContent()
.MinDesiredWidth(520.0f)
[
SNew(SVerticalBox)
+ SVerticalBox::Slot()
.AutoHeight()
[
SNew(SHorizontalBox)
+ SHorizontalBox::Slot()
.FillWidth(1.0f)
[
SAssignNew(PresetComboBox, SComboBox<TSharedPtr<FQwenTTSPresetChoice>>)
.OptionsSource(&PresetChoices)
.OnGenerateWidget(this, &FQwenTTSPresetDetailsCustomization::MakePresetComboWidget)
.OnSelectionChanged(this, &FQwenTTSPresetDetailsCustomization::HandleSelectionChanged)
[
SNew(STextBlock)
.Text(this, &FQwenTTSPresetDetailsCustomization::GetSelectedPresetText)
.Font(IDetailLayoutBuilder::GetDetailFont())
]
]
+ SHorizontalBox::Slot()
.AutoWidth()
.Padding(6.0f, 0.0f, 0.0f, 0.0f)
[
SNew(SButton)
.Text(LOCTEXT("RefreshPresets", "Refresh"))
.OnClicked(this, &FQwenTTSPresetDetailsCustomization::RefreshPresets)
]
]
+ SVerticalBox::Slot()
.AutoHeight()
.Padding(0.0f, 4.0f, 0.0f, 0.0f)
[
SNew(STextBlock)
.Text(this, &FQwenTTSPresetDetailsCustomization::GetStatusText)
.Font(IDetailLayoutBuilder::GetDetailFont())
.AutoWrapText(true)
]
];
PresetCategory.AddProperty(PresetIdHandle.ToSharedRef());
RefreshPresets();
}
FReply FQwenTTSPresetDetailsCustomization::RefreshPresets()
{
const FString StatusUrl = GetStatusUrl();
if (StatusUrl.IsEmpty())
{
StatusText = LOCTEXT("MissingStatusUrl", "Set Settings or Gateway Status Url before refreshing presets.");
return FReply::Handled();
}
StatusText = FText::Format(LOCTEXT("LoadingPresets", "Loading presets from {0}"), FText::FromString(StatusUrl));
UE_LOG(LogQwenTTSStreaming, Log, TEXT("Refreshing Qwen TTS presets from %s"), *StatusUrl);
const TSharedRef<IHttpRequest, ESPMode::ThreadSafe> Request = FHttpModule::Get().CreateRequest();
Request->SetURL(StatusUrl);
Request->SetVerb(TEXT("GET"));
Request->SetHeader(TEXT("Accept"), TEXT("application/json"));
Request->OnProcessRequestComplete().BindSP(StaticCastSharedRef<FQwenTTSPresetDetailsCustomization>(AsShared()), &FQwenTTSPresetDetailsCustomization::HandlePresetResponse);
Request->ProcessRequest();
return FReply::Handled();
}
void FQwenTTSPresetDetailsCustomization::HandlePresetResponse(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful)
{
if (!bWasSuccessful || !Response.IsValid())
{
StatusText = LOCTEXT("PresetRequestFailed", "Preset request failed.");
UE_LOG(LogQwenTTSStreaming, Warning, TEXT("Qwen TTS preset request failed."));
return;
}
if (Response->GetResponseCode() < 200 || Response->GetResponseCode() >= 300)
{
StatusText = FText::Format(LOCTEXT("PresetHttpFailed", "Preset request failed with HTTP {0}."), Response->GetResponseCode());
UE_LOG(LogQwenTTSStreaming, Warning, TEXT("Qwen TTS preset request failed. HTTP=%d Body=%s"), Response->GetResponseCode(), *Response->GetContentAsString());
return;
}
SetPresetChoicesFromStatusJson(Response->GetContentAsString());
}
void FQwenTTSPresetDetailsCustomization::SetPresetChoicesFromStatusJson(const FString& ResponseText)
{
TSharedPtr<FJsonObject> JsonObject;
const TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(ResponseText);
if (!FJsonSerializer::Deserialize(Reader, JsonObject) || !JsonObject.IsValid())
{
StatusText = LOCTEXT("BadPresetJson", "Preset response is not valid JSON.");
UE_LOG(LogQwenTTSStreaming, Warning, TEXT("Qwen TTS preset response is not valid JSON."));
return;
}
const TArray<TSharedPtr<FJsonValue>>* PresetRefs = nullptr;
if (!JsonObject->TryGetArrayField(TEXT("preset_refs"), PresetRefs))
{
StatusText = LOCTEXT("NoPresetRefs", "Preset response has no preset_refs array.");
UE_LOG(LogQwenTTSStreaming, Warning, TEXT("Qwen TTS preset response has no preset_refs array."));
return;
}
PresetChoices.Reset();
for (const TSharedPtr<FJsonValue>& Value : *PresetRefs)
{
const TSharedPtr<FJsonObject> PresetObject = Value.IsValid() ? Value->AsObject() : nullptr;
if (!PresetObject.IsValid())
{
continue;
}
FString Id;
if (!PresetObject->TryGetStringField(TEXT("id"), Id) || Id.IsEmpty())
{
continue;
}
TSharedRef<FQwenTTSPresetChoice> Choice = MakeShared<FQwenTTSPresetChoice>();
Choice->Id = Id;
PresetObject->TryGetStringField(TEXT("label"), Choice->Label);
PresetObject->TryGetStringField(TEXT("ref_text"), Choice->RefText);
PresetObject->TryGetStringField(TEXT("source"), Choice->Source);
PresetChoices.Add(Choice);
}
if (PresetComboBox.IsValid())
{
PresetComboBox->RefreshOptions();
}
StatusText = FText::Format(LOCTEXT("PresetLoadedCount", "Loaded {0} preset(s)."), PresetChoices.Num());
UE_LOG(LogQwenTTSStreaming, Log, TEXT("Loaded %d Qwen TTS preset(s)."), PresetChoices.Num());
}
FString FQwenTTSPresetDetailsCustomization::GetStatusUrl() const
{
UObject* SettingsObject = nullptr;
if (SettingsHandle.IsValid() && SettingsHandle->GetValue(SettingsObject) == FPropertyAccess::Success)
{
if (const UQwenTTSStreamingSettingsDataAsset* Settings = Cast<UQwenTTSStreamingSettingsDataAsset>(SettingsObject))
{
return Settings->GetResolvedGatewayStatusUrl();
}
}
if (const UQwenTTSPresetDataAsset* Preset = PresetAsset.Get())
{
if (Preset->Settings)
{
return Preset->Settings->GetResolvedGatewayStatusUrl();
}
}
return GetDefault<UQwenTTSStreamingSettingsDataAsset>()->GetResolvedGatewayStatusUrl();
}
TSharedPtr<FQwenTTSPresetChoice> FQwenTTSPresetDetailsCustomization::GetSelectedChoice() const
{
FString CurrentPresetId;
if (PresetIdHandle.IsValid())
{
PresetIdHandle->GetValue(CurrentPresetId);
}
for (const TSharedPtr<FQwenTTSPresetChoice>& Choice : PresetChoices)
{
if (Choice.IsValid() && Choice->Id == CurrentPresetId)
{
return Choice;
}
}
return nullptr;
}
FText FQwenTTSPresetDetailsCustomization::GetSelectedPresetText() const
{
if (const TSharedPtr<FQwenTTSPresetChoice> Choice = GetSelectedChoice())
{
return Choice->GetDisplayText();
}
FString CurrentPresetId;
if (PresetIdHandle.IsValid() && PresetIdHandle->GetValue(CurrentPresetId) == FPropertyAccess::Success && !CurrentPresetId.IsEmpty())
{
return FText::FromString(CurrentPresetId);
}
return LOCTEXT("NoPresetSelected", "Select preset");
}
FText FQwenTTSPresetDetailsCustomization::GetStatusText() const
{
if (const TSharedPtr<FQwenTTSPresetChoice> Choice = GetSelectedChoice())
{
if (!Choice->RefText.IsEmpty())
{
return FText::FromString(FString::Printf(TEXT("%s: %s"), *Choice->Source, *Choice->RefText));
}
}
return StatusText;
}
void FQwenTTSPresetDetailsCustomization::HandleSelectionChanged(TSharedPtr<FQwenTTSPresetChoice> Choice, ESelectInfo::Type SelectInfo)
{
if (!Choice.IsValid() || SelectInfo == ESelectInfo::Direct)
{
return;
}
if (PresetIdHandle.IsValid())
{
PresetIdHandle->SetValue(Choice->Id);
StatusText = FText::Format(LOCTEXT("PresetSelected", "Selected {0}."), Choice->GetDisplayText());
UE_LOG(LogQwenTTSStreaming, Log, TEXT("Selected Qwen TTS preset. Label=%s Id=%s"), *Choice->Label, *Choice->Id);
}
}
TSharedRef<SWidget> FQwenTTSPresetDetailsCustomization::MakePresetComboWidget(TSharedPtr<FQwenTTSPresetChoice> Choice) const
{
return SNew(STextBlock)
.Text(Choice.IsValid() ? Choice->GetDisplayText() : LOCTEXT("InvalidPresetChoice", "Invalid preset"))
.ToolTipText(Choice.IsValid() ? FText::FromString(Choice->RefText) : FText::GetEmpty());
}
#undef LOCTEXT_NAMESPACE
@@ -0,0 +1,47 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "IDetailCustomization.h"
#include "Interfaces/IHttpRequest.h"
#include "Widgets/Input/SComboBox.h"
class IPropertyHandle;
class UQwenTTSPresetDataAsset;
struct FQwenTTSPresetChoice
{
FString Id;
FString Label;
FString RefText;
FString Source;
FText GetDisplayText() const;
};
class FQwenTTSPresetDetailsCustomization : public IDetailCustomization
{
public:
static TSharedRef<IDetailCustomization> MakeInstance();
virtual void CustomizeDetails(IDetailLayoutBuilder& DetailBuilder) override;
private:
TWeakObjectPtr<UQwenTTSPresetDataAsset> PresetAsset;
TSharedPtr<IPropertyHandle> PresetIdHandle;
TSharedPtr<IPropertyHandle> SettingsHandle;
TArray<TSharedPtr<FQwenTTSPresetChoice>> PresetChoices;
TSharedPtr<SComboBox<TSharedPtr<FQwenTTSPresetChoice>>> PresetComboBox;
FText StatusText = FText::FromString(TEXT("Not loaded"));
FReply RefreshPresets();
void HandlePresetResponse(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful);
void SetPresetChoicesFromStatusJson(const FString& ResponseText);
FString GetStatusUrl() const;
TSharedPtr<FQwenTTSPresetChoice> GetSelectedChoice() const;
FText GetSelectedPresetText() const;
FText GetStatusText() const;
void HandleSelectionChanged(TSharedPtr<FQwenTTSPresetChoice> Choice, ESelectInfo::Type SelectInfo);
TSharedRef<SWidget> MakePresetComboWidget(TSharedPtr<FQwenTTSPresetChoice> Choice) const;
};
@@ -0,0 +1,114 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "AssetTypeActions_Base.h"
#include "AssetToolsModule.h"
#include "IAssetTools.h"
#include "Modules/ModuleManager.h"
#include "PropertyEditorModule.h"
#include "QwenTTSPresetDataAsset.h"
#include "QwenTTSPresetDetailsCustomization.h"
#include "QwenTTSStreamingSettingsDataAsset.h"
#define LOCTEXT_NAMESPACE "FQwenTTSStreamingEditorModule"
namespace
{
class FQwenTTSSettingsAssetTypeActions : public FAssetTypeActions_Base
{
public:
explicit FQwenTTSSettingsAssetTypeActions(EAssetTypeCategories::Type InCategory)
: Category(InCategory)
{
}
virtual FText GetName() const override { return LOCTEXT("QwenTTSSettingsName", "Qwen TTS Streaming Settings"); }
virtual FColor GetTypeColor() const override { return FColor(86, 182, 194); }
virtual UClass* GetSupportedClass() const override { return UQwenTTSStreamingSettingsDataAsset::StaticClass(); }
virtual uint32 GetCategories() override { return Category; }
virtual FText GetAssetDescription(const FAssetData& AssetData) const override
{
return LOCTEXT("QwenTTSSettingsDescription", "Gateway and playback settings for Qwen TTS streaming.");
}
private:
EAssetTypeCategories::Type Category;
};
class FQwenTTSPresetAssetTypeActions : public FAssetTypeActions_Base
{
public:
explicit FQwenTTSPresetAssetTypeActions(EAssetTypeCategories::Type InCategory)
: Category(InCategory)
{
}
virtual FText GetName() const override { return LOCTEXT("QwenTTSPresetName", "Qwen TTS Preset"); }
virtual FColor GetTypeColor() const override { return FColor(120, 176, 96); }
virtual UClass* GetSupportedClass() const override { return UQwenTTSPresetDataAsset::StaticClass(); }
virtual uint32 GetCategories() override { return Category; }
virtual FText GetAssetDescription(const FAssetData& AssetData) const override
{
return LOCTEXT("QwenTTSPresetDescription", "Voice preset and generation parameters for Qwen TTS streaming.");
}
private:
EAssetTypeCategories::Type Category;
};
}
class FQwenTTSStreamingEditorModule : public IModuleInterface
{
public:
virtual void StartupModule() override
{
IAssetTools& AssetTools = FModuleManager::LoadModuleChecked<FAssetToolsModule>("AssetTools").Get();
AudioCategory = AssetTools.FindAdvancedAssetCategory(FName("Audio"));
if (AudioCategory == EAssetTypeCategories::None)
{
AudioCategory = AssetTools.RegisterAdvancedAssetCategory(FName("Audio"), LOCTEXT("AudioCategory", "Audio"));
}
RegisterAssetTypeAction(AssetTools, MakeShared<FQwenTTSSettingsAssetTypeActions>(AudioCategory));
RegisterAssetTypeAction(AssetTools, MakeShared<FQwenTTSPresetAssetTypeActions>(AudioCategory));
FPropertyEditorModule& PropertyEditor = FModuleManager::LoadModuleChecked<FPropertyEditorModule>("PropertyEditor");
PropertyEditor.RegisterCustomClassLayout(
UQwenTTSPresetDataAsset::StaticClass()->GetFName(),
FOnGetDetailCustomizationInstance::CreateStatic(&FQwenTTSPresetDetailsCustomization::MakeInstance));
PropertyEditor.NotifyCustomizationModuleChanged();
}
virtual void ShutdownModule() override
{
if (FModuleManager::Get().IsModuleLoaded("AssetTools"))
{
IAssetTools& AssetTools = FModuleManager::GetModuleChecked<FAssetToolsModule>("AssetTools").Get();
for (const TSharedRef<IAssetTypeActions>& Action : RegisteredAssetTypeActions)
{
AssetTools.UnregisterAssetTypeActions(Action);
}
}
RegisteredAssetTypeActions.Empty();
if (FModuleManager::Get().IsModuleLoaded("PropertyEditor"))
{
FPropertyEditorModule& PropertyEditor = FModuleManager::GetModuleChecked<FPropertyEditorModule>("PropertyEditor");
PropertyEditor.UnregisterCustomClassLayout(UQwenTTSPresetDataAsset::StaticClass()->GetFName());
PropertyEditor.NotifyCustomizationModuleChanged();
}
}
private:
void RegisterAssetTypeAction(IAssetTools& AssetTools, TSharedRef<IAssetTypeActions> Action)
{
AssetTools.RegisterAssetTypeActions(Action);
RegisteredAssetTypeActions.Add(Action);
}
EAssetTypeCategories::Type AudioCategory = EAssetTypeCategories::Sounds;
TArray<TSharedRef<IAssetTypeActions>> RegisteredAssetTypeActions;
};
IMPLEMENT_MODULE(FQwenTTSStreamingEditorModule, QwenTTSStreamingEditor)
#undef LOCTEXT_NAMESPACE
@@ -0,0 +1,31 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "Factories/Factory.h"
#include "QwenTTSDataAssetFactories.generated.h"
UCLASS()
class UQwenTTSStreamingSettingsDataAssetFactory : public UFactory
{
GENERATED_BODY()
public:
UQwenTTSStreamingSettingsDataAssetFactory();
virtual UObject* FactoryCreateNew(UClass* Class, UObject* InParent, FName Name, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) override;
virtual bool ShouldShowInNewMenu() const override { return true; }
};
UCLASS()
class UQwenTTSPresetDataAssetFactory : public UFactory
{
GENERATED_BODY()
public:
UQwenTTSPresetDataAssetFactory();
virtual UObject* FactoryCreateNew(UClass* Class, UObject* InParent, FName Name, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) override;
virtual bool ShouldShowInNewMenu() const override { return true; }
};
@@ -0,0 +1,29 @@
using UnrealBuildTool;
public class QwenTTSStreamingEditor : ModuleRules
{
public QwenTTSStreamingEditor(ReadOnlyTargetRules Target) : base(Target)
{
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
PublicDependencyModuleNames.AddRange(new string[]
{
"Core",
"CoreUObject",
"Engine",
"QwenTTSStreaming"
});
PrivateDependencyModuleNames.AddRange(new string[]
{
"AssetTools",
"HTTP",
"InputCore",
"Json",
"PropertyEditor",
"Slate",
"SlateCore",
"UnrealEd"
});
}
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More