400 lines
13 KiB
C++
400 lines
13 KiB
C++
// Copyright Epic Games, Inc. All Rights Reserved.
|
|
|
|
#include "Audio/SimpleWaveWriter.h"
|
|
#include "NumberedFileCache.h"
|
|
#include "HAL/FileManager.h"
|
|
#include "MetasoundBuildError.h"
|
|
#include "MetasoundEnumRegistrationMacro.h"
|
|
#include "MetasoundExecutableOperator.h"
|
|
#include "MetasoundNodeRegistrationMacro.h"
|
|
#include "MetasoundParamHelper.h"
|
|
#include "MetasoundPrimitives.h"
|
|
#include "MetasoundStandardNodesCategories.h"
|
|
#include "MetasoundStandardNodesNames.h"
|
|
#include "MetasoundVertex.h"
|
|
#include "Misc/Paths.h"
|
|
#include "Misc/ScopeLock.h"
|
|
|
|
#define LOCTEXT_NAMESPACE "MetasoundStandardNodes_WaveWriterNode"
|
|
|
|
|
|
namespace Metasound
|
|
{
|
|
class FNode;
|
|
class FBuildErrorBase;
|
|
class IOperator;
|
|
|
|
namespace WaveWriterVertexNames
|
|
{
|
|
METASOUND_PARAM(InEnabledPin, "Enabled", "If this wave writer is enabled or not.");
|
|
METASOUND_PARAM(InFilenamePrefixPin, "Filename Prefix", "Filename Prefix of file you are writing.");
|
|
}
|
|
|
|
class FFileWriteError : public FBuildErrorBase
|
|
{
|
|
public:
|
|
static const FName ErrorType;
|
|
|
|
virtual ~FFileWriteError() = default;
|
|
|
|
FFileWriteError(const FNode& InNode, const FString& InFilename)
|
|
#if WITH_EDITOR
|
|
: FBuildErrorBase(ErrorType, METASOUND_LOCTEXT_FORMAT("MetasoundFileWriterErrorDescription", "File Writer Error while trying to write '{0}'", FText::FromString(InFilename)))
|
|
#else
|
|
: FBuildErrorBase(ErrorType, FText::GetEmpty())
|
|
#endif // WITH_EDITOR
|
|
{
|
|
AddNode(InNode);
|
|
}
|
|
};
|
|
const FName FFileWriteError::ErrorType = FName(TEXT("MetasoundFileWriterError"));
|
|
|
|
namespace WaveWriterOperatorPrivate
|
|
{
|
|
// Need to keep this outside the template so there's only 1
|
|
TSharedPtr<FNumberedFileCache> GetNameCache()
|
|
{
|
|
static const TCHAR* WaveExt = TEXT(".wav");
|
|
|
|
// Build cache of numbered files (do this once only).
|
|
static TSharedPtr<FNumberedFileCache> NumberedFileCacheSP = MakeShared<FNumberedFileCache>(
|
|
*FPaths::AudioCaptureDir(), WaveExt, IFileManager::Get());
|
|
|
|
return NumberedFileCacheSP;
|
|
}
|
|
|
|
static const FString GetDefaultFileName()
|
|
{
|
|
static const FString DefaultFileName = TEXT("Output");
|
|
return DefaultFileName;
|
|
}
|
|
}
|
|
|
|
template<int32 NumInputChannels>
|
|
class TWaveWriterOperator : public TExecutableOperator<TWaveWriterOperator<NumInputChannels>>
|
|
{
|
|
public:
|
|
// Theoretical limit of .WAV files.
|
|
static_assert(NumInputChannels > 0 && NumInputChannels <= 65535, "Num Channels > 0 and <= 65535");
|
|
|
|
TWaveWriterOperator(const FOperatorSettings& InSettings, TArray<FAudioBufferReadRef>&& InAudioBuffers, FBoolReadRef&& InEnabled, const TSharedPtr<FNumberedFileCache, ESPMode::ThreadSafe>& InNumberedFileCache, FStringReadRef&& InFilenamePrefix)
|
|
: AudioInputs{ MoveTemp(InAudioBuffers) }
|
|
, Enabled{ MoveTemp(InEnabled) }
|
|
, NumberedFileCacheSP{ InNumberedFileCache }
|
|
, FileNamePrefix{ MoveTemp(InFilenamePrefix) }
|
|
, SampleRate{ InSettings.GetSampleRate() }
|
|
{
|
|
check(AudioInputs.Num() == NumInputChannels);
|
|
|
|
// Make an interleave buffer if we need one.
|
|
if (NumInputChannels > 1)
|
|
{
|
|
InterleaveBuffer.SetNum(InSettings.GetNumFramesPerBlock() * NumInputChannels);
|
|
AudioInputBufferPtrs.SetNum(NumInputChannels);
|
|
for (int32 i = 0; i < NumInputChannels; ++i)
|
|
{
|
|
AudioInputBufferPtrs[i] = AudioInputs[i]->GetData();
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
virtual void BindInputs(FInputVertexInterfaceData& InOutVertexData) override
|
|
{
|
|
for (int32 i = 0; i < NumInputChannels; ++i)
|
|
{
|
|
InOutVertexData.BindReadVertex(GetAudioInputName(i), AudioInputs[i]);
|
|
}
|
|
|
|
using namespace WaveWriterVertexNames;
|
|
InOutVertexData.BindReadVertex(METASOUND_GET_PARAM_NAME(InEnabledPin), Enabled);
|
|
InOutVertexData.BindReadVertex(METASOUND_GET_PARAM_NAME(InFilenamePrefixPin), FileNamePrefix);
|
|
}
|
|
|
|
virtual void BindOutputs(FOutputVertexInterfaceData&) override
|
|
{
|
|
}
|
|
|
|
static const FVertexInterface& DeclareVertexInterface()
|
|
{
|
|
auto CreateDefaultInterface = []()-> FVertexInterface
|
|
{
|
|
using namespace WaveWriterOperatorPrivate;
|
|
using namespace WaveWriterVertexNames;
|
|
|
|
// inputs
|
|
FInputVertexInterface InputInterface(
|
|
TInputDataVertex<FString>(METASOUND_GET_PARAM_NAME_AND_METADATA(InFilenamePrefixPin), GetDefaultFileName()),
|
|
TInputDataVertex<bool>(METASOUND_GET_PARAM_NAME_AND_METADATA(InEnabledPin), true)
|
|
);
|
|
|
|
|
|
// For backwards compatibility with previous (mono) node, in the case of 1 channels, just provide the old interface.
|
|
for (int32 InputIndex = 0; InputIndex < NumInputChannels; ++InputIndex)
|
|
{
|
|
#if WITH_EDITOR
|
|
const FDataVertexMetadata AudioInputMetadata
|
|
{
|
|
GetAudioInputDescription(InputIndex) // description
|
|
, GetAudioInputDisplayName(InputIndex) // display name
|
|
};
|
|
#else
|
|
const FDataVertexMetadata AudioInputMetadata;
|
|
#endif //WITH_EDITOR
|
|
InputInterface.Add(TInputDataVertex<FAudioBuffer>(GetAudioInputName(InputIndex), AudioInputMetadata));
|
|
}
|
|
FOutputVertexInterface OutputInterface;
|
|
|
|
return FVertexInterface(InputInterface, OutputInterface);
|
|
};
|
|
|
|
static const FVertexInterface DefaultInterface = CreateDefaultInterface();
|
|
return DefaultInterface;
|
|
}
|
|
|
|
static const FNodeClassMetadata& GetNodeInfo()
|
|
{
|
|
// used if NumChannels == 1
|
|
auto CreateNodeClassMetadataMono = []() -> FNodeClassMetadata
|
|
{
|
|
// For backwards compatibility with previous (mono) WaveWriters keep the node name the same.
|
|
FName OperatorName = TEXT("WaveWriter");
|
|
FText NodeDisplayName = METASOUND_LOCTEXT("Metasound_WaveWriterNodeMonoDisplayName", "Wave Writer (1-channel, Mono)");
|
|
const FText NodeDescription = METASOUND_LOCTEXT("Metasound_WaveWriterNodeMonoDescription", "Write a mono audio signal to disk");
|
|
FVertexInterface NodeInterface = DeclareVertexInterface();
|
|
|
|
return CreateNodeClassMetadata(OperatorName, NodeDisplayName, NodeDescription, NodeInterface);
|
|
};
|
|
|
|
// used if NumChannels == 2
|
|
auto CreateNodeClassMetadataStereo = []() -> FNodeClassMetadata
|
|
{
|
|
FName OperatorName = TEXT("Wave Writer (Stereo)");
|
|
FText NodeDisplayName = METASOUND_LOCTEXT("Metasound_WaveWriterNodeStereoDisplayName", "Wave Writer (2-channel, Stereo)");
|
|
const FText NodeDescription = METASOUND_LOCTEXT("Metasound_WaveWriterNodeStereoDescription", "Write a stereo audio signal to disk");
|
|
FVertexInterface NodeInterface = DeclareVertexInterface();
|
|
|
|
return CreateNodeClassMetadata(OperatorName, NodeDisplayName, NodeDescription, NodeInterface);
|
|
};
|
|
|
|
// used if NumChannels > 2
|
|
auto CreateNodeClassMetadataMultiChan = []() -> FNodeClassMetadata
|
|
{
|
|
FName OperatorName = *FString::Printf(TEXT("Wave Writer (%d-Channel)"), NumInputChannels);
|
|
FText NodeDisplayName = METASOUND_LOCTEXT_FORMAT("Metasound_WaveWriterNodeMultiChannelDisplayName", "Wave Writer ({0}-channel)", NumInputChannels);
|
|
const FText NodeDescription = METASOUND_LOCTEXT("Metasound_WaveWriterNodeMultiDescription", "Write a multi-channel audio signal to disk");
|
|
FVertexInterface NodeInterface = DeclareVertexInterface();
|
|
|
|
return CreateNodeClassMetadata(OperatorName, NodeDisplayName, NodeDescription, NodeInterface);
|
|
};
|
|
|
|
static const FNodeClassMetadata Metadata = (NumInputChannels == 1) ? CreateNodeClassMetadataMono()
|
|
: (NumInputChannels == 2) ? CreateNodeClassMetadataStereo() : CreateNodeClassMetadataMultiChan();
|
|
return Metadata;
|
|
}
|
|
|
|
static TUniquePtr<IOperator> CreateOperator(const FBuildOperatorParams& InParams, FBuildResults& OutResults);
|
|
|
|
void Execute()
|
|
{
|
|
// Enabled and wasn't before? Enable.
|
|
if (!bIsEnabled && *Enabled)
|
|
{
|
|
Enable();
|
|
}
|
|
// Disabled but currently Enabled? Disable.
|
|
else if (bIsEnabled && !*Enabled)
|
|
{
|
|
Disable();
|
|
}
|
|
|
|
// If we have a valid writer and enabled.
|
|
if (Writer && *Enabled)
|
|
{
|
|
// Need to Interleave?
|
|
if (NumInputChannels > 1)
|
|
{
|
|
InterleaveChannels(AudioInputBufferPtrs.GetData(), NumInputChannels, AudioInputs[0]->Num(), InterleaveBuffer.GetData());
|
|
Writer->Write(MakeArrayView(InterleaveBuffer.GetData(), InterleaveBuffer.Num()));
|
|
}
|
|
else if (NumInputChannels == 1)
|
|
{
|
|
Writer->Write(MakeArrayView(AudioInputs[0]->GetData(), AudioInputs[0]->Num()));
|
|
}
|
|
}
|
|
}
|
|
|
|
void Reset(const IOperator::FResetParams& InParams)
|
|
{
|
|
if (bIsEnabled)
|
|
{
|
|
Disable();
|
|
}
|
|
}
|
|
|
|
protected:
|
|
static const FVertexName GetAudioInputName(int32 InInputIndex)
|
|
{
|
|
if (NumInputChannels == 1)
|
|
{
|
|
// To maintain backwards compatibility with previous implementation keep the pin name the same.
|
|
static const FName AudioInputPinName = TEXT("In");
|
|
return AudioInputPinName;
|
|
}
|
|
else if (NumInputChannels == 2)
|
|
{
|
|
return *FString::Printf(TEXT("In %d %s"), InInputIndex, (InInputIndex == 0) ? TEXT("L") : TEXT("R"));
|
|
}
|
|
|
|
return *FString::Printf(TEXT("In %d"), InInputIndex);
|
|
}
|
|
|
|
#if WITH_EDITOR
|
|
static const FText GetAudioInputDisplayName(int32 InInputIndex)
|
|
{
|
|
if (NumInputChannels == 1)
|
|
{
|
|
// To maintain backwards compatibility with previous implementation keep the pin name the same.
|
|
static const FText AudioInputPinName = METASOUND_LOCTEXT("AudioInputPinNameIn", "In");
|
|
return AudioInputPinName;
|
|
}
|
|
else if (NumInputChannels == 2)
|
|
{
|
|
if (InInputIndex == 0)
|
|
{
|
|
return METASOUND_LOCTEXT_FORMAT("AudioInputIn2ChannelNameL", "In {0} L", InInputIndex);
|
|
}
|
|
else
|
|
{
|
|
return METASOUND_LOCTEXT_FORMAT("AudioInputIn2ChannelNameR", "In {0} R", InInputIndex);
|
|
}
|
|
}
|
|
return METASOUND_LOCTEXT_FORMAT("AudioInputInChannelName", "In {0}", InInputIndex);
|
|
}
|
|
|
|
static const FText GetAudioInputDescription(int32 InputIndex)
|
|
{
|
|
return METASOUND_LOCTEXT_FORMAT("WaveWriterAudioInputDescription", "Audio Input #: {0}", InputIndex);
|
|
}
|
|
#endif // WITH_EDITOR
|
|
|
|
static FNodeClassMetadata CreateNodeClassMetadata(const FName& InOperatorName, const FText& InDisplayName, const FText& InDescription, const FVertexInterface& InDefaultInterface)
|
|
{
|
|
FNodeClassMetadata Metadata
|
|
{
|
|
FNodeClassName { StandardNodes::Namespace, InOperatorName, StandardNodes::AudioVariant },
|
|
1, // Major Version
|
|
1, // Minor Version
|
|
InDisplayName,
|
|
InDescription,
|
|
PluginAuthor,
|
|
PluginNodeMissingPrompt,
|
|
InDefaultInterface,
|
|
{ NodeCategories::Io },
|
|
{ METASOUND_LOCTEXT("Metasound_AudioMixerKeyword", "Writer") },
|
|
FNodeDisplayStyle{}
|
|
};
|
|
return Metadata;
|
|
}
|
|
|
|
// TODO. Move to DSP lib.
|
|
static void InterleaveChannels(const float* RESTRICT InMonoChannelsToInterleave[], const int32 InNumChannelsToInterleave, const int32 NumSamplesPerChannel, float* RESTRICT OutInterleavedBuffer)
|
|
{
|
|
for (int32 Sample = 0; Sample < NumSamplesPerChannel; ++Sample)
|
|
{
|
|
for (int32 Channel = 0; Channel < InNumChannelsToInterleave; ++Channel)
|
|
{
|
|
*OutInterleavedBuffer++ = InMonoChannelsToInterleave[Channel][Sample];
|
|
}
|
|
}
|
|
}
|
|
|
|
void Enable()
|
|
{
|
|
if (ensure(!bIsEnabled))
|
|
{
|
|
bIsEnabled = true;
|
|
FString Filename = NumberedFileCacheSP->GenerateNextNumberedFilename(*FileNamePrefix);
|
|
TUniquePtr<FArchive> Stream{ IFileManager::Get().CreateFileWriter(*Filename, IO_WRITE) };
|
|
if (Stream.IsValid())
|
|
{
|
|
Writer = MakeUnique<Audio::FSimpleWaveWriter>(MoveTemp(Stream), SampleRate, NumInputChannels, true);
|
|
}
|
|
}
|
|
}
|
|
void Disable()
|
|
{
|
|
if (ensure(bIsEnabled))
|
|
{
|
|
bIsEnabled = false;
|
|
Writer.Reset();
|
|
}
|
|
}
|
|
|
|
TArray<FAudioBufferReadRef> AudioInputs;
|
|
TArray<const float*> AudioInputBufferPtrs;
|
|
TArray<float> InterleaveBuffer;
|
|
FBoolReadRef Enabled;
|
|
TUniquePtr<Audio::FSimpleWaveWriter> Writer;
|
|
TSharedPtr<FNumberedFileCache, ESPMode::ThreadSafe> NumberedFileCacheSP;
|
|
FStringReadRef FileNamePrefix;
|
|
float SampleRate = 0.f;
|
|
bool bIsEnabled = false;
|
|
};
|
|
|
|
template<int32 NumInputChannels>
|
|
TUniquePtr<Metasound::IOperator> TWaveWriterOperator<NumInputChannels>::CreateOperator(const FBuildOperatorParams& InParams, FBuildResults& OutResults)
|
|
{
|
|
using namespace WaveWriterOperatorPrivate;
|
|
using namespace WaveWriterVertexNames;
|
|
|
|
const FOperatorSettings& Settings = InParams.OperatorSettings;
|
|
const FInputVertexInterfaceData& InputData = InParams.InputData;
|
|
|
|
int32 NumConnectedAudioPins = 0;
|
|
TArray<FAudioBufferReadRef> InputBuffers;
|
|
for (int32 i = 0; i < NumInputChannels; ++i)
|
|
{
|
|
const FVertexName PinName = GetAudioInputName(i);
|
|
if (InputData.IsVertexBound(PinName))
|
|
{
|
|
NumConnectedAudioPins++;
|
|
}
|
|
InputBuffers.Add(InputData.GetOrCreateDefaultDataReadReference<FAudioBuffer>(PinName, InParams.OperatorSettings));
|
|
}
|
|
|
|
// Only create a real operator if there's some connected pins.
|
|
if (NumConnectedAudioPins > 0)
|
|
{
|
|
return MakeUnique<TWaveWriterOperator>(
|
|
Settings,
|
|
MoveTemp(InputBuffers),
|
|
InputData.GetOrCreateDefaultDataReadReference<bool>(METASOUND_GET_PARAM_NAME(InEnabledPin), Settings),
|
|
GetNameCache(),
|
|
InputData.GetOrCreateDefaultDataReadReference<FString>(METASOUND_GET_PARAM_NAME(InFilenamePrefixPin), Settings)
|
|
);
|
|
}
|
|
|
|
// Create a no-op operator.
|
|
return MakeUnique<FNoOpOperator>();
|
|
}
|
|
|
|
template<int32 NumInputChannels>
|
|
using TWaveWriterNode = TNodeFacade<TWaveWriterOperator<NumInputChannels>>;
|
|
|
|
#define REGISTER_WAVEWRITER_NODE(A) \
|
|
using FWaveWriterNode_##A = TWaveWriterNode<A>; \
|
|
METASOUND_REGISTER_NODE(FWaveWriterNode_##A)
|
|
|
|
REGISTER_WAVEWRITER_NODE(1);
|
|
REGISTER_WAVEWRITER_NODE(2);
|
|
REGISTER_WAVEWRITER_NODE(3);
|
|
REGISTER_WAVEWRITER_NODE(4);
|
|
REGISTER_WAVEWRITER_NODE(5);
|
|
REGISTER_WAVEWRITER_NODE(6);
|
|
REGISTER_WAVEWRITER_NODE(7);
|
|
REGISTER_WAVEWRITER_NODE(8);
|
|
}
|
|
|
|
#undef LOCTEXT_NAMESPACE
|