2 Commits

Author SHA1 Message Date
9a8748687a Audio experiments 2026-05-05 12:18:42 +10:00
f836c53d10 Initial audio support 2026-05-04 14:32:29 +10:00
18 changed files with 1627 additions and 21 deletions

View File

@@ -18,6 +18,8 @@ if(NOT EXISTS "${GPUDIRECT_DIR}/lib/x64/dvp.lib")
endif() endif()
set(APP_SOURCES set(APP_SOURCES
"${APP_DIR}/AudioSupport.cpp"
"${APP_DIR}/AudioSupport.h"
"${APP_DIR}/ControlServer.cpp" "${APP_DIR}/ControlServer.cpp"
"${APP_DIR}/ControlServer.h" "${APP_DIR}/ControlServer.h"
"${APP_DIR}/DeckLinkAPI_i.c" "${APP_DIR}/DeckLinkAPI_i.c"
@@ -147,6 +149,21 @@ endif()
add_test(NAME OscServerTests COMMAND OscServerTests) add_test(NAME OscServerTests COMMAND OscServerTests)
add_executable(AudioSupportTests
"${APP_DIR}/AudioSupport.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/tests/AudioSupportTests.cpp"
)
target_include_directories(AudioSupportTests PRIVATE
"${APP_DIR}"
)
if(MSVC)
target_compile_options(AudioSupportTests PRIVATE /W3)
endif()
add_test(NAME AudioSupportTests COMMAND AudioSupportTests)
add_custom_command(TARGET LoopThroughWithOpenGLCompositing POST_BUILD add_custom_command(TARGET LoopThroughWithOpenGLCompositing POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${GPUDIRECT_DIR}/bin/x64/dvp.dll" "${GPUDIRECT_DIR}/bin/x64/dvp.dll"

View File

@@ -126,12 +126,18 @@ Current native test coverage includes:
"outputFrameRate": "59.94", "outputFrameRate": "59.94",
"autoReload": true, "autoReload": true,
"maxTemporalHistoryFrames": 12, "maxTemporalHistoryFrames": 12,
"audioEnabled": true,
"audioChannelCount": 2,
"audioSampleRate": 48000,
"audioDelayMode": "matchVideoPreroll",
"enableExternalKeying": true "enableExternalKeying": true
} }
``` ```
`inputVideoFormat`/`inputFrameRate` select the DeckLink capture mode. `outputVideoFormat`/`outputFrameRate` select the playout mode. The shader stack runs at input resolution and the final rendered frame is scaled once into the configured output mode. Common examples include `720p`/`50`, `720p`/`59.94`, `1080i`/`50`, `1080i`/`59.94`, `1080p`/`25`, `1080p`/`50`, `1080p`/`59.94`, and `2160p`/`59.94`, depending on card support. `inputVideoFormat`/`inputFrameRate` select the DeckLink capture mode. `outputVideoFormat`/`outputFrameRate` select the playout mode. The shader stack runs at input resolution and the final rendered frame is scaled once into the configured output mode. Common examples include `720p`/`50`, `720p`/`59.94`, `1080i`/`50`, `1080i`/`59.94`, `1080p`/`25`, `1080p`/`50`, `1080p`/`59.94`, and `2160p`/`59.94`, depending on card support.
`audioEnabled` enables embedded stereo 48 kHz PCM pass-through. Audio is delayed to match the scheduled video preroll and the synchronized level/spectrum data is exposed to shaders.
Legacy `videoFormat` and `frameRate` keys are still accepted and apply to both input and output unless the explicit input/output keys are present. Legacy `videoFormat` and `frameRate` keys are still accepted and apply to both input and output unless the explicit input/output keys are present.
The control UI is available at: The control UI is available at:

View File

@@ -125,6 +125,11 @@ struct ShaderContext
float bypass; float bypass;
int sourceHistoryLength; int sourceHistoryLength;
int temporalHistoryLength; int temporalHistoryLength;
float2 audioRms;
float2 audioPeak;
float audioMonoRms;
float audioMonoPeak;
float4 audioBands;
}; };
``` ```
@@ -140,6 +145,11 @@ Fields:
- `bypass`: `1.0` when the layer is bypassed, otherwise `0.0`. - `bypass`: `1.0` when the layer is bypassed, otherwise `0.0`.
- `sourceHistoryLength`: number of usable source-history frames currently available. - `sourceHistoryLength`: number of usable source-history frames currently available.
- `temporalHistoryLength`: number of usable temporal frames currently available for this layer. - `temporalHistoryLength`: number of usable temporal frames currently available for this layer.
- `audioRms`: left/right RMS level for the audio block synchronized with the rendered output frame.
- `audioPeak`: left/right peak level for the same synchronized audio block.
- `audioMonoRms`: mono RMS level derived from left/right.
- `audioMonoPeak`: mono peak level derived from left/right.
- `audioBands`: four smoothed, normalized low-to-high frequency bands.
## Helper Functions ## Helper Functions
@@ -149,6 +159,8 @@ The wrapper provides:
float4 sampleVideo(float2 uv); float4 sampleVideo(float2 uv);
float4 sampleSourceHistory(int framesAgo, float2 uv); float4 sampleSourceHistory(int framesAgo, float2 uv);
float4 sampleTemporalHistory(int framesAgo, float2 uv); float4 sampleTemporalHistory(int framesAgo, float2 uv);
float4 sampleAudioWaveform(float x);
float4 sampleAudioSpectrum(float x);
``` ```
`sampleVideo` samples the live decoded source video. `sampleVideo` samples the live decoded source video.
@@ -157,6 +169,10 @@ float4 sampleTemporalHistory(int framesAgo, float2 uv);
`sampleTemporalHistory` samples previous pre-layer input frames for temporal shaders that request `preLayerInput` history. `framesAgo` is clamped into the available range. If no temporal history is available, it falls back to `sampleVideo`. `sampleTemporalHistory` samples previous pre-layer input frames for temporal shaders that request `preLayerInput` history. `framesAgo` is clamped into the available range. If no temporal history is available, it falls back to `sampleVideo`.
`sampleAudioWaveform` samples the current synchronized audio waveform texture. `x` is normalized `0..1`; returned waveform channels are encoded from `-1..1` into `0..1`.
`sampleAudioSpectrum` samples the current synchronized audio spectrum texture. Values are normalized `0..1`.
Example: Example:
```slang ```slang

View File

@@ -0,0 +1,206 @@
#include "AudioSupport.h"
#include <algorithm>
#include <cmath>
#include <iterator>
#include <limits>
namespace
{
constexpr float kInt32ToFloat = 1.0f / 2147483648.0f;
constexpr std::size_t kAnalysisWindowSamples = 1024;
constexpr std::size_t kMaxBufferedAudioFrames = kAudioSampleRate * 10;
float Clamp01(float value)
{
return std::max(0.0f, std::min(1.0f, value));
}
float SampleToFloat(int32_t sample)
{
return std::max(-1.0f, std::min(1.0f, static_cast<float>(sample) * kInt32ToFloat));
}
float GoertzelMagnitude(const std::vector<float>& samples, float frequency)
{
if (samples.empty())
return 0.0f;
const double omega = 2.0 * 3.14159265358979323846 * static_cast<double>(frequency) / static_cast<double>(kAudioSampleRate);
const double coefficient = 2.0 * std::cos(omega);
double q0 = 0.0;
double q1 = 0.0;
double q2 = 0.0;
for (float sample : samples)
{
q0 = coefficient * q1 - q2 + static_cast<double>(sample);
q2 = q1;
q1 = q0;
}
const double power = q1 * q1 + q2 * q2 - coefficient * q1 * q2;
return static_cast<float>(std::sqrt(std::max(0.0, power)) / static_cast<double>(samples.size()));
}
}
uint64_t AudioSampleTimeForVideoFrame(uint64_t videoFrameIndex, uint64_t frameDuration, uint64_t frameTimescale, uint64_t audioSampleRate)
{
if (frameTimescale == 0)
return 0;
const uint64_t numerator = videoFrameIndex * frameDuration * audioSampleRate;
return (numerator + frameTimescale / 2) / frameTimescale;
}
unsigned AudioSamplesForVideoFrame(uint64_t videoFrameIndex, uint64_t frameDuration, uint64_t frameTimescale, uint64_t audioSampleRate)
{
const uint64_t start = AudioSampleTimeForVideoFrame(videoFrameIndex, frameDuration, frameTimescale, audioSampleRate);
const uint64_t end = AudioSampleTimeForVideoFrame(videoFrameIndex + 1, frameDuration, frameTimescale, audioSampleRate);
return static_cast<unsigned>(end > start ? end - start : 0);
}
void AudioDelayBuffer::Reset(unsigned delaySampleFrames)
{
std::lock_guard<std::mutex> lock(mMutex);
mSamples.clear();
mSamples.resize(static_cast<std::size_t>(delaySampleFrames) * kAudioChannelCount, 0);
mUnderrunCount = 0;
}
void AudioDelayBuffer::PushInterleaved(const int32_t* samples, std::size_t sampleFrameCount)
{
if (!samples || sampleFrameCount == 0)
return;
std::lock_guard<std::mutex> lock(mMutex);
const std::size_t sampleCount = sampleFrameCount * kAudioChannelCount;
for (std::size_t index = 0; index < sampleCount; ++index)
mSamples.push_back(samples[index]);
const std::size_t maxSamples = kMaxBufferedAudioFrames * kAudioChannelCount;
while (mSamples.size() > maxSamples)
mSamples.pop_front();
}
AudioFrameBlock AudioDelayBuffer::Pop(std::size_t sampleFrameCount, bool& underrun)
{
AudioFrameBlock block;
block.interleavedSamples.resize(sampleFrameCount * kAudioChannelCount, 0);
std::lock_guard<std::mutex> lock(mMutex);
const std::size_t requestedSamples = sampleFrameCount * kAudioChannelCount;
underrun = mSamples.size() < requestedSamples;
if (underrun)
++mUnderrunCount;
const std::size_t availableSamples = std::min(requestedSamples, mSamples.size());
for (std::size_t index = 0; index < availableSamples; ++index)
{
block.interleavedSamples[index] = mSamples.front();
mSamples.pop_front();
}
return block;
}
unsigned AudioDelayBuffer::BufferedSampleFrames() const
{
std::lock_guard<std::mutex> lock(mMutex);
return static_cast<unsigned>(mSamples.size() / kAudioChannelCount);
}
uint64_t AudioDelayBuffer::UnderrunCount() const
{
std::lock_guard<std::mutex> lock(mMutex);
return mUnderrunCount;
}
void AudioAnalyzer::Reset()
{
mMonoHistory.clear();
mSmoothedBands = { 0.0f, 0.0f, 0.0f, 0.0f };
mCurrent = AudioAnalysisSnapshot();
}
AudioAnalysisSnapshot AudioAnalyzer::Analyze(const AudioFrameBlock& block)
{
AudioAnalysisSnapshot next;
double sumSquares[2] = { 0.0, 0.0 };
float peak[2] = { 0.0f, 0.0f };
double monoSumSquares = 0.0;
float monoPeak = 0.0f;
const std::size_t frames = block.frameCount();
for (std::size_t frame = 0; frame < frames; ++frame)
{
const float left = SampleToFloat(block.interleavedSamples[frame * 2]);
const float right = SampleToFloat(block.interleavedSamples[frame * 2 + 1]);
const float mono = (left + right) * 0.5f;
sumSquares[0] += static_cast<double>(left) * left;
sumSquares[1] += static_cast<double>(right) * right;
peak[0] = std::max(peak[0], std::abs(left));
peak[1] = std::max(peak[1], std::abs(right));
monoSumSquares += static_cast<double>(mono) * mono;
monoPeak = std::max(monoPeak, std::abs(mono));
mMonoHistory.push_back(mono);
while (mMonoHistory.size() > kAnalysisWindowSamples)
mMonoHistory.pop_front();
}
if (frames > 0)
{
next.rms[0] = static_cast<float>(std::sqrt(sumSquares[0] / static_cast<double>(frames)));
next.rms[1] = static_cast<float>(std::sqrt(sumSquares[1] / static_cast<double>(frames)));
next.peak[0] = peak[0];
next.peak[1] = peak[1];
next.monoRms = static_cast<float>(std::sqrt(monoSumSquares / static_cast<double>(frames)));
next.monoPeak = monoPeak;
}
std::vector<float> window(mMonoHistory.begin(), mMonoHistory.end());
const float bandFrequencies[4] = { 90.0f, 300.0f, 1200.0f, 5000.0f };
for (std::size_t band = 0; band < next.bands.size(); ++band)
{
const float raw = Clamp01(GoertzelMagnitude(window, bandFrequencies[band]) * 8.0f);
const float smoothing = raw > mSmoothedBands[band] ? 0.45f : 0.12f;
mSmoothedBands[band] = mSmoothedBands[band] + (raw - mSmoothedBands[band]) * smoothing;
next.bands[band] = Clamp01(mSmoothedBands[band]);
}
for (unsigned x = 0; x < kAudioTextureWidth; ++x)
{
float mono = 0.0f;
if (!mMonoHistory.empty())
{
const std::size_t historyIndex = static_cast<std::size_t>(
(static_cast<uint64_t>(x) * static_cast<uint64_t>(mMonoHistory.size())) / kAudioTextureWidth);
auto it = mMonoHistory.begin();
std::advance(it, std::min(historyIndex, mMonoHistory.size() - 1));
mono = *it;
}
const std::size_t waveformOffset = x * 4;
next.texture[waveformOffset + 0] = mono * 0.5f + 0.5f;
next.texture[waveformOffset + 1] = next.texture[waveformOffset + 0];
next.texture[waveformOffset + 2] = next.monoRms;
next.texture[waveformOffset + 3] = 1.0f;
const float bandPosition = static_cast<float>(x) / static_cast<float>(kAudioTextureWidth - 1);
const float scaled = bandPosition * static_cast<float>(next.bands.size() - 1);
const unsigned bandA = static_cast<unsigned>(std::floor(scaled));
const unsigned bandB = std::min<unsigned>(bandA + 1, static_cast<unsigned>(next.bands.size() - 1));
const float t = scaled - static_cast<float>(bandA);
const float spectrum = next.bands[bandA] * (1.0f - t) + next.bands[bandB] * t;
const std::size_t spectrumOffset = (kAudioTextureWidth + x) * 4;
next.texture[spectrumOffset + 0] = spectrum;
next.texture[spectrumOffset + 1] = next.bands[0];
next.texture[spectrumOffset + 2] = next.bands[1];
next.texture[spectrumOffset + 3] = next.bands[2];
}
mCurrent = next;
return mCurrent;
}

View File

@@ -0,0 +1,71 @@
#pragma once
#include <array>
#include <cstdint>
#include <deque>
#include <mutex>
#include <vector>
constexpr unsigned kAudioSampleRate = 48000;
constexpr unsigned kAudioChannelCount = 2;
constexpr unsigned kAudioTextureWidth = 64;
constexpr unsigned kAudioTextureHeight = 2;
struct AudioFrameBlock
{
std::vector<int32_t> interleavedSamples;
std::size_t frameCount() const
{
return interleavedSamples.size() / kAudioChannelCount;
}
};
struct AudioAnalysisSnapshot
{
std::array<float, 2> rms = { 0.0f, 0.0f };
std::array<float, 2> peak = { 0.0f, 0.0f };
float monoRms = 0.0f;
float monoPeak = 0.0f;
std::array<float, 4> bands = { 0.0f, 0.0f, 0.0f, 0.0f };
std::array<float, kAudioTextureWidth * kAudioTextureHeight * 4> texture = {};
};
struct AudioStatusSnapshot
{
bool enabled = false;
unsigned bufferedSampleFrames = 0;
uint64_t underrunCount = 0;
AudioAnalysisSnapshot analysis;
};
class AudioDelayBuffer
{
public:
void Reset(unsigned delaySampleFrames);
void PushInterleaved(const int32_t* samples, std::size_t sampleFrameCount);
AudioFrameBlock Pop(std::size_t sampleFrameCount, bool& underrun);
unsigned BufferedSampleFrames() const;
uint64_t UnderrunCount() const;
private:
mutable std::mutex mMutex;
std::deque<int32_t> mSamples;
uint64_t mUnderrunCount = 0;
};
class AudioAnalyzer
{
public:
void Reset();
AudioAnalysisSnapshot Analyze(const AudioFrameBlock& block);
const AudioAnalysisSnapshot& Current() const { return mCurrent; }
private:
std::deque<float> mMonoHistory;
std::array<float, 4> mSmoothedBands = { 0.0f, 0.0f, 0.0f, 0.0f };
AudioAnalysisSnapshot mCurrent;
};
uint64_t AudioSampleTimeForVideoFrame(uint64_t videoFrameIndex, uint64_t frameDuration, uint64_t frameTimescale, uint64_t audioSampleRate = kAudioSampleRate);
unsigned AudioSamplesForVideoFrame(uint64_t videoFrameIndex, uint64_t frameDuration, uint64_t frameTimescale, uint64_t audioSampleRate = kAudioSampleRate);

View File

@@ -44,6 +44,7 @@
#include "OscServer.h" #include "OscServer.h"
#include <algorithm> #include <algorithm>
#include <chrono>
#include <cstdint> #include <cstdint>
#include <cstring> #include <cstring>
#include <cctype> #include <cctype>
@@ -51,6 +52,7 @@
#include <set> #include <set>
#include <sstream> #include <sstream>
#include <string> #include <string>
#include <thread>
#include <vector> #include <vector>
#include <initguid.h> #include <initguid.h>
@@ -60,9 +62,18 @@ DEFINE_GUID(IID_PinnedMemoryAllocator,
namespace namespace
{ {
constexpr GLuint kDecodedVideoTextureUnit = 1; constexpr GLuint kDecodedVideoTextureUnit = 1;
constexpr GLuint kSourceHistoryTextureUnitBase = 2; constexpr GLuint kAudioDataTextureUnit = 2;
constexpr GLuint kSourceHistoryTextureUnitBase = 3;
constexpr GLuint kPackedVideoTextureUnit = 2; constexpr GLuint kPackedVideoTextureUnit = 2;
constexpr GLuint kGlobalParamsBindingPoint = 0; constexpr GLuint kGlobalParamsBindingPoint = 0;
constexpr unsigned kVideoPrerollFrameCount = 5;
constexpr unsigned kAudioOutputWaterLevelSampleFrames = kAudioSampleRate / 2;
constexpr unsigned kAudioScheduleChunkSampleFrames = kAudioSampleRate / 100;
constexpr unsigned kDeckLinkOutputAudioChannelCount = 16;
#ifndef GL_RGBA32F
#define GL_RGBA32F 0x8814
#endif
const char* kVertexShaderSource = const char* kVertexShaderSource =
"#version 430 core\n" "#version 430 core\n"
"out vec2 vTexCoord;\n" "out vec2 vTexCoord;\n"
@@ -315,8 +326,10 @@ void AppendStd140Vec4(std::vector<unsigned char>& buffer, float x, float y, floa
OpenGLComposite::OpenGLComposite(HWND hWnd, HDC hDC, HGLRC hRC) : OpenGLComposite::OpenGLComposite(HWND hWnd, HDC hDC, HGLRC hRC) :
hGLWnd(hWnd), hGLDC(hDC), hGLRC(hRC), hGLWnd(hWnd), hGLDC(hDC), hGLRC(hRC),
mCaptureDelegate(NULL), mPlayoutDelegate(NULL), mCaptureDelegate(NULL), mPlayoutDelegate(NULL),
mDLInput(NULL), mDLOutput(NULL), mDLKeyer(NULL), mDLInput(NULL), mDLOutput(NULL), mDLInputConfiguration(NULL), mDLKeyer(NULL),
mPlayoutAllocator(NULL), mPlayoutAllocator(NULL),
mTotalPlayoutFrames(0),
mAudioOutputSampleTime(0),
mInputFrameWidth(0), mInputFrameHeight(0), mInputFrameWidth(0), mInputFrameHeight(0),
mOutputFrameWidth(0), mOutputFrameHeight(0), mOutputFrameWidth(0), mOutputFrameHeight(0),
mInputDisplayModeName("1080p59.94"), mInputDisplayModeName("1080p59.94"),
@@ -332,6 +345,7 @@ OpenGLComposite::OpenGLComposite(HWND hWnd, HDC hDC, HGLRC hRC) :
mLayerTempTexture(0), mLayerTempTexture(0),
mFBOTexture(0), mFBOTexture(0),
mOutputTexture(0), mOutputTexture(0),
mAudioDataTexture(0),
mUnpinnedTextureBuffer(0), mUnpinnedTextureBuffer(0),
mDecodeFrameBuf(0), mDecodeFrameBuf(0),
mLayerTempFrameBuf(0), mLayerTempFrameBuf(0),
@@ -347,6 +361,15 @@ OpenGLComposite::OpenGLComposite(HWND hWnd, HDC hDC, HGLRC hRC) :
mGlobalParamsUBOSize(0), mGlobalParamsUBOSize(0),
mViewWidth(0), mViewWidth(0),
mViewHeight(0), mViewHeight(0),
mAudioEnabled(false),
mAudioOutputEnabled(false),
mAudioScheduleEnabled(false),
mAudioPrerollEnabled(false),
mAudioScheduleSilence(false),
mAudioScheduleTone(false),
mAudioPrerolling(false),
mAudioSchedulerRunning(false),
mPlayoutCallbackActive(false),
mTemporalHistoryNeedsReset(true) mTemporalHistoryNeedsReset(true)
{ {
InitializeCriticalSection(&pMutex); InitializeCriticalSection(&pMutex);
@@ -357,11 +380,22 @@ OpenGLComposite::OpenGLComposite(HWND hWnd, HDC hDC, HGLRC hRC) :
OpenGLComposite::~OpenGLComposite() OpenGLComposite::~OpenGLComposite()
{ {
mAudioSchedulerRunning.store(false);
mAudioPacketQueued.notify_all();
if (mAudioSchedulerThread.joinable())
mAudioSchedulerThread.join();
// Cleanup for Capture // Cleanup for Capture
if (mDLInput != NULL) if (mDLInput != NULL)
{ {
mDLInput->SetCallback(NULL); mDLInput->SetCallback(NULL);
if (mDLInputConfiguration != NULL)
{
mDLInputConfiguration->Release();
mDLInputConfiguration = NULL;
}
mDLInput->Release(); mDLInput->Release();
mDLInput = NULL; mDLInput = NULL;
} }
@@ -394,6 +428,7 @@ OpenGLComposite::~OpenGLComposite()
} }
mDLOutput->SetScheduledFrameCompletionCallback(NULL); mDLOutput->SetScheduledFrameCompletionCallback(NULL);
mDLOutput->SetAudioCallback(NULL);
mDLOutput->Release(); mDLOutput->Release();
mDLOutput = NULL; mDLOutput = NULL;
@@ -435,6 +470,8 @@ OpenGLComposite::~OpenGLComposite()
glDeleteTextures(1, &mFBOTexture); glDeleteTextures(1, &mFBOTexture);
if (mOutputTexture != 0) if (mOutputTexture != 0)
glDeleteTextures(1, &mOutputTexture); glDeleteTextures(1, &mOutputTexture);
if (mAudioDataTexture != 0)
glDeleteTextures(1, &mAudioDataTexture);
if (mOutputFrameBuf != 0) if (mOutputFrameBuf != 0)
glDeleteFramebuffers(1, &mOutputFrameBuf); glDeleteFramebuffers(1, &mOutputFrameBuf);
if (mUnpinnedTextureBuffer != 0) if (mUnpinnedTextureBuffer != 0)
@@ -617,6 +654,11 @@ bool OpenGLComposite::InitDeckLink()
if (! CheckOpenGLExtensions()) if (! CheckOpenGLExtensions())
goto error; goto error;
if (mAudioOutputEnabled)
{
mFastTransferExtensionAvailable = false;
OutputDebugStringA("Audio output enabled; using DeckLink-owned output video frames for SDI stability.\n");
}
if (mInputFrameWidth != mOutputFrameWidth || mInputFrameHeight != mOutputFrameHeight) if (mInputFrameWidth != mOutputFrameWidth || mInputFrameHeight != mOutputFrameHeight)
{ {
mFastTransferExtensionAvailable = false; mFastTransferExtensionAvailable = false;
@@ -667,6 +709,31 @@ bool OpenGLComposite::InitDeckLink()
goto error; goto error;
} }
mAudioEnabled = mRuntimeHost ? mRuntimeHost->AudioEnabled() : true;
mAudioOutputEnabled = mAudioEnabled && (mRuntimeHost ? mRuntimeHost->AudioOutputEnabled() : true);
mAudioScheduleEnabled = mAudioOutputEnabled && (mRuntimeHost ? mRuntimeHost->AudioScheduleEnabled() : true);
mAudioPrerollEnabled = mAudioScheduleEnabled && (mRuntimeHost ? mRuntimeHost->AudioPrerollEnabled() : true);
mAudioScheduleSilence = mAudioScheduleEnabled && (mRuntimeHost ? mRuntimeHost->AudioScheduleSilence() : false);
mAudioScheduleTone = mAudioScheduleEnabled && (mRuntimeHost ? mRuntimeHost->AudioScheduleTone() : false);
if (mAudioEnabled)
{
if (mDLInput->QueryInterface(IID_IDeckLinkConfiguration, (void**)&mDLInputConfiguration) == S_OK && mDLInputConfiguration != NULL)
{
if (mDLInputConfiguration->SetInt(bmdDeckLinkConfigAudioInputConnection, bmdAudioConnectionEmbedded) != S_OK)
OutputDebugStringA("Could not force DeckLink audio input connection to embedded; using current device setting.\n");
}
else
{
OutputDebugStringA("Could not query DeckLink input configuration; using current audio input connection.\n");
}
}
if (mAudioEnabled && mDLInput->EnableAudioInput(bmdAudioSampleRate48kHz, bmdAudioSampleType32bitInteger, kAudioChannelCount) != S_OK)
{
OutputDebugStringA("Could not enable DeckLink audio input; continuing without audio.\n");
mAudioEnabled = false;
}
mCaptureDelegate = new CaptureDelegate(this); mCaptureDelegate = new CaptureDelegate(this);
if (mDLInput->SetCallback(mCaptureDelegate) != S_OK) if (mDLInput->SetCallback(mCaptureDelegate) != S_OK)
goto error; goto error;
@@ -680,6 +747,12 @@ bool OpenGLComposite::InitDeckLink()
if (mDLOutput->EnableVideoOutput(outputDisplayMode, bmdVideoOutputFlagDefault) != S_OK) if (mDLOutput->EnableVideoOutput(outputDisplayMode, bmdVideoOutputFlagDefault) != S_OK)
goto error; goto error;
if (mAudioOutputEnabled && mDLOutput->EnableAudioOutput(bmdAudioSampleRate48kHz, bmdAudioSampleType32bitInteger, kDeckLinkOutputAudioChannelCount, bmdAudioOutputStreamContinuous) != S_OK)
{
OutputDebugStringA("Could not enable DeckLink audio output; continuing without audio.\n");
mAudioOutputEnabled = false;
}
if (mDLOutput->QueryInterface(IID_IDeckLinkKeyer, (void**)&mDLKeyer) == S_OK && mDLKeyer != NULL) if (mDLOutput->QueryInterface(IID_IDeckLinkKeyer, (void**)&mDLKeyer) == S_OK && mDLKeyer != NULL)
mDeckLinkKeyerInterfaceAvailable = true; mDeckLinkKeyerInterfaceAvailable = true;
@@ -730,12 +803,23 @@ bool OpenGLComposite::InitDeckLink()
// If you want RGB 4:4:4 format to be played out "over the wire" in SDI, turn on the "Use 4:4:4 SDI" in the control // If you want RGB 4:4:4 format to be played out "over the wire" in SDI, turn on the "Use 4:4:4 SDI" in the control
// panel or turn on the bmdDeckLinkConfig444SDIVideoOutput flag using the IDeckLinkConfiguration interface. // panel or turn on the bmdDeckLinkConfig444SDIVideoOutput flag using the IDeckLinkConfiguration interface.
IDeckLinkMutableVideoFrame* outputFrame; IDeckLinkMutableVideoFrame* outputFrame;
IDeckLinkVideoBuffer* outputFrameBuffer = NULL; if (mAudioOutputEnabled)
{
if (mDLOutput->CreateVideoFrame(mOutputFrameWidth, mOutputFrameHeight, outputFrameRowBytes, bmdFormat8BitBGRA, bmdFrameFlagFlipVertical, &outputFrame) != S_OK)
goto error;
}
else
{
IDeckLinkVideoBuffer* outputFrameBuffer = NULL;
if (mPlayoutAllocator->AllocateVideoBuffer(&outputFrameBuffer) != S_OK) if (mPlayoutAllocator->AllocateVideoBuffer(&outputFrameBuffer) != S_OK)
goto error; goto error;
if (mDLOutput->CreateVideoFrameWithBuffer(mOutputFrameWidth, mOutputFrameHeight, outputFrameRowBytes, bmdFormat8BitBGRA, bmdFrameFlagFlipVertical, outputFrameBuffer, &outputFrame) != S_OK) if (mDLOutput->CreateVideoFrameWithBuffer(mOutputFrameWidth, mOutputFrameHeight, outputFrameRowBytes, bmdFormat8BitBGRA, bmdFrameFlagFlipVertical, outputFrameBuffer, &outputFrame) != S_OK)
goto error;
}
if (outputFrame == NULL)
goto error; goto error;
mDLOutputVideoFrameQueue.push_back(outputFrame); mDLOutputVideoFrameQueue.push_back(outputFrame);
@@ -748,6 +832,13 @@ bool OpenGLComposite::InitDeckLink()
if (mDLOutput->SetScheduledFrameCompletionCallback(mPlayoutDelegate) != S_OK) if (mDLOutput->SetScheduledFrameCompletionCallback(mPlayoutDelegate) != S_OK)
goto error; goto error;
if (mAudioOutputEnabled && mDLOutput->SetAudioCallback(mPlayoutDelegate) != S_OK)
{
OutputDebugStringA("Could not set DeckLink audio output callback; continuing without audio.\n");
mDLOutput->DisableAudioOutput();
mAudioOutputEnabled = false;
}
bSuccess = true; bSuccess = true;
error: error:
@@ -770,6 +861,11 @@ error:
mDLOutput->Release(); mDLOutput->Release();
mDLOutput = NULL; mDLOutput = NULL;
} }
if (mDLInputConfiguration != NULL)
{
mDLInputConfiguration->Release();
mDLInputConfiguration = NULL;
}
} }
if (pDL != NULL) if (pDL != NULL)
@@ -1052,6 +1148,14 @@ bool OpenGLComposite::InitOpenGLState()
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, mOutputFrameWidth, mOutputFrameHeight, 0, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, NULL); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, mOutputFrameWidth, mOutputFrameHeight, 0, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, NULL);
glGenTextures(1, &mAudioDataTexture);
glBindTexture(GL_TEXTURE_2D, mAudioDataTexture);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA32F, kAudioTextureWidth, kAudioTextureHeight, 0, GL_RGBA, GL_FLOAT, mAudioAnalysis.texture.data());
glBindFramebuffer(GL_FRAMEBUFFER, mOutputFrameBuf); glBindFramebuffer(GL_FRAMEBUFFER, mOutputFrameBuf);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, mOutputTexture, 0); glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, mOutputTexture, 0);
glStatus = glCheckFramebufferStatus(GL_FRAMEBUFFER); glStatus = glCheckFramebufferStatus(GL_FRAMEBUFFER);
@@ -1135,16 +1239,217 @@ void OpenGLComposite::VideoFrameArrived(IDeckLinkVideoInputFrame* inputFrame, bo
inputFrameBuffer->Release(); inputFrameBuffer->Release();
} }
void OpenGLComposite::AudioPacketArrived(IDeckLinkAudioInputPacket* audioPacket)
{
if (!mAudioEnabled || !audioPacket)
return;
void* audioBytes = nullptr;
if (audioPacket->GetBytes(&audioBytes) != S_OK || !audioBytes)
return;
const long sampleFrameCount = audioPacket->GetSampleFrameCount();
if (sampleFrameCount <= 0)
return;
TimestampedAudioPacket packet;
packet.block.interleavedSamples.assign(
static_cast<const int32_t*>(audioBytes),
static_cast<const int32_t*>(audioBytes) + (static_cast<std::size_t>(sampleFrameCount) * kAudioChannelCount));
if (!mAudioScheduleEnabled)
{
AudioAnalysisSnapshot audioAnalysis;
{
std::lock_guard<std::mutex> analyzerLock(mAudioAnalyzerMutex);
audioAnalysis = mAudioAnalyzer.Analyze(packet.block);
}
{
std::lock_guard<std::mutex> audioLock(mAudioStateMutex);
mAudioAnalysis = audioAnalysis;
}
updateAudioStatus();
return;
}
{
std::lock_guard<std::mutex> audioLock(mAudioStateMutex);
for (int32_t sample : packet.block.interleavedSamples)
mAudioSampleQueue.push_back(sample);
mQueuedAudioSampleFrames += static_cast<unsigned>(sampleFrameCount);
}
mAudioPacketQueued.notify_one();
ScheduleAudioToWaterLevel();
}
HRESULT OpenGLComposite::RenderAudioSamples(BOOL preroll)
{
return ScheduleAudioToWaterLevel();
}
HRESULT OpenGLComposite::ScheduleAudioToWaterLevel()
{
if (!mAudioScheduleEnabled || !mDLOutput)
return S_OK;
if (mPlayoutCallbackActive.load(std::memory_order_acquire))
return S_FALSE;
unsigned bufferedSampleFrames = 0;
if (mDLOutput->GetBufferedAudioSampleFrameCount(&bufferedSampleFrames) != S_OK)
{
OutputDebugStringA("Could not query DeckLink buffered audio sample count.\n");
return E_FAIL;
}
const unsigned audioWaterLevel = static_cast<unsigned>(AudioSampleTimeForVideoFrame(kVideoPrerollFrameCount, mFrameDuration, mFrameTimescale));
if (bufferedSampleFrames >= audioWaterLevel)
return S_OK;
TimestampedAudioPacket packet;
bool poppedCapturedAudio = false;
{
std::unique_lock<std::mutex> audioLock(mAudioStateMutex, std::try_to_lock);
if (!audioLock.owns_lock())
return S_FALSE;
const unsigned audioDeficitFrames = audioWaterLevel - bufferedSampleFrames;
const unsigned requestedFrames = audioDeficitFrames < kAudioScheduleChunkSampleFrames ? audioDeficitFrames : kAudioScheduleChunkSampleFrames;
if (requestedFrames == 0)
return S_OK;
if (mAudioScheduleTone)
{
const std::size_t requestedSamples = static_cast<std::size_t>(requestedFrames) * kAudioChannelCount;
packet.block.interleavedSamples.reserve(requestedSamples);
for (unsigned frame = 0; frame < requestedFrames; ++frame)
{
const double phase = (static_cast<double>(mAudioToneSampleIndex++) * 440.0 * 6.28318530717958647692) / static_cast<double>(kAudioSampleRate);
const int32_t sample = static_cast<int32_t>(std::sin(phase) * 0.125 * 2147483647.0);
for (unsigned channel = 0; channel < kAudioChannelCount; ++channel)
packet.block.interleavedSamples.push_back(sample);
}
}
else if (mAudioScheduleSilence)
{
packet.block.interleavedSamples.assign(static_cast<std::size_t>(requestedFrames) * kAudioChannelCount, 0);
}
else
{
const std::size_t requestedSamples = static_cast<std::size_t>(requestedFrames) * kAudioChannelCount;
packet.block.interleavedSamples.reserve(requestedSamples);
while (!mAudioSampleQueue.empty() && packet.block.interleavedSamples.size() < requestedSamples)
{
packet.block.interleavedSamples.push_back(mAudioSampleQueue.front());
mAudioSampleQueue.pop_front();
}
if (packet.block.interleavedSamples.size() < requestedSamples)
{
mAudioUnderrunCount++;
packet.block.interleavedSamples.resize(requestedSamples, 0);
}
const auto frameCount = static_cast<unsigned>(packet.block.frameCount());
mQueuedAudioSampleFrames = frameCount <= mQueuedAudioSampleFrames ? mQueuedAudioSampleFrames - frameCount : 0;
poppedCapturedAudio = true;
}
}
const unsigned sampleFrames = static_cast<unsigned>(packet.block.frameCount());
if (sampleFrames == 0)
return S_FALSE;
std::vector<int32_t> deckLinkAudioSamples(static_cast<std::size_t>(sampleFrames) * kDeckLinkOutputAudioChannelCount, 0);
for (unsigned frame = 0; frame < sampleFrames; ++frame)
{
const std::size_t source = static_cast<std::size_t>(frame) * kAudioChannelCount;
const std::size_t destination = static_cast<std::size_t>(frame) * kDeckLinkOutputAudioChannelCount;
deckLinkAudioSamples[destination] = packet.block.interleavedSamples[source];
deckLinkAudioSamples[destination + 1] = packet.block.interleavedSamples[source + 1];
}
if (mPlayoutCallbackActive.load(std::memory_order_acquire))
{
std::lock_guard<std::mutex> audioLock(mAudioStateMutex);
if (poppedCapturedAudio)
{
for (auto it = packet.block.interleavedSamples.rbegin(); it != packet.block.interleavedSamples.rend(); ++it)
mAudioSampleQueue.push_front(*it);
mQueuedAudioSampleFrames += sampleFrames;
}
return S_FALSE;
}
unsigned sampleFramesWritten = 0;
HRESULT scheduleResult = mDLOutput->ScheduleAudioSamples(
deckLinkAudioSamples.data(),
sampleFrames,
static_cast<BMDTimeValue>(mAudioOutputSampleTime),
kAudioSampleRate,
&sampleFramesWritten);
if (scheduleResult == S_OK)
{
if (sampleFramesWritten == 0 && sampleFrames > 0)
OutputDebugStringA("DeckLink accepted audio schedule call but wrote 0 sample frames.\n");
mAudioOutputSampleTime += sampleFramesWritten;
AudioFrameBlock analysisBlock = packet.block;
AudioAnalysisSnapshot audioAnalysis;
{
std::lock_guard<std::mutex> analyzerLock(mAudioAnalyzerMutex);
audioAnalysis = mAudioAnalyzer.Analyze(analysisBlock);
}
{
std::lock_guard<std::mutex> audioLock(mAudioStateMutex);
mAudioAnalysis = audioAnalysis;
packet.scheduledOutputSamples = std::move(deckLinkAudioSamples);
mScheduledAudioPacketRetainQueue.push_back(std::move(packet));
while (mScheduledAudioPacketRetainQueue.size() > 64)
mScheduledAudioPacketRetainQueue.pop_front();
}
updateAudioStatus();
}
else
{
OutputDebugStringA("DeckLink ScheduleAudioSamples failed while topping up audio output.\n");
}
return scheduleResult;
}
void OpenGLComposite::AudioSchedulingLoop()
{
while (mAudioSchedulerRunning.load())
{
ScheduleAudioToWaterLevel();
std::unique_lock<std::mutex> audioLock(mAudioStateMutex);
mAudioPacketQueued.wait_for(audioLock, std::chrono::milliseconds(20), [this]()
{
return !mAudioSchedulerRunning.load() || !mAudioPacketQueue.empty();
});
}
}
// Render the live video texture through the runtime shader into the off-screen framebuffer. // Render the live video texture through the runtime shader into the off-screen framebuffer.
// Read the result back from the frame buffer and schedule it for playout. // Read the result back from the frame buffer and schedule it for playout.
void OpenGLComposite::PlayoutFrameCompleted(IDeckLinkVideoFrame* completedFrame, BMDOutputFrameCompletionResult completionResult) void OpenGLComposite::PlayoutFrameCompleted(IDeckLinkVideoFrame* completedFrame, BMDOutputFrameCompletionResult completionResult)
{ {
mPlayoutCallbackActive.store(true, std::memory_order_release);
EnterCriticalSection(&pMutex); EnterCriticalSection(&pMutex);
auto leavePlayoutCallback = [this]()
{
mPlayoutCallbackActive.store(false, std::memory_order_release);
LeaveCriticalSection(&pMutex);
};
// Get the first frame from the queue if (!completedFrame)
IDeckLinkMutableVideoFrame* outputVideoFrame = mDLOutputVideoFrameQueue.front(); {
mDLOutputVideoFrameQueue.push_back(outputVideoFrame); leavePlayoutCallback();
mDLOutputVideoFrameQueue.pop_front(); return;
}
IDeckLinkVideoFrame* outputVideoFrame = completedFrame;
// make GL context current in this thread // make GL context current in this thread
wglMakeCurrent( hGLDC, hGLRC ); wglMakeCurrent( hGLDC, hGLRC );
@@ -1177,14 +1482,14 @@ void OpenGLComposite::PlayoutFrameCompleted(IDeckLinkVideoFrame* completedFrame,
IDeckLinkVideoBuffer* outputVideoFrameBuffer; IDeckLinkVideoBuffer* outputVideoFrameBuffer;
if (outputVideoFrame->QueryInterface(IID_IDeckLinkVideoBuffer, (void**)&outputVideoFrameBuffer) != S_OK) if (outputVideoFrame->QueryInterface(IID_IDeckLinkVideoBuffer, (void**)&outputVideoFrameBuffer) != S_OK)
{ {
LeaveCriticalSection(&pMutex); leavePlayoutCallback();
return; return;
} }
if (outputVideoFrameBuffer->StartAccess(bmdBufferAccessWrite) != S_OK) if (outputVideoFrameBuffer->StartAccess(bmdBufferAccessWrite) != S_OK)
{ {
outputVideoFrameBuffer->Release(); outputVideoFrameBuffer->Release();
LeaveCriticalSection(&pMutex); leavePlayoutCallback();
return; return;
} }
@@ -1225,15 +1530,30 @@ void OpenGLComposite::PlayoutFrameCompleted(IDeckLinkVideoFrame* completedFrame,
wglMakeCurrent( NULL, NULL ); wglMakeCurrent( NULL, NULL );
LeaveCriticalSection(&pMutex); leavePlayoutCallback();
} }
bool OpenGLComposite::Start() bool OpenGLComposite::Start()
{ {
mTotalPlayoutFrames = 0; mTotalPlayoutFrames = 0;
initializeAudioDelay();
if (mAudioPrerollEnabled)
{
mDLOutput->FlushBufferedAudioSamples();
if (mDLOutput->BeginAudioPreroll() != S_OK)
{
OutputDebugStringA("Could not begin DeckLink audio preroll; continuing without audio.\n");
mDLOutput->DisableAudioOutput();
mAudioOutputEnabled = false;
}
else
{
mAudioPrerolling = true;
}
}
// Preroll frames // Preroll frames
for (unsigned i = 0; i < 5; i++) for (unsigned i = 0; i < kVideoPrerollFrameCount; i++)
{ {
// Take each video frame from the front of the queue and move it to the back // Take each video frame from the front of the queue and move it to the back
IDeckLinkMutableVideoFrame* outputVideoFrame = mDLOutputVideoFrameQueue.front(); IDeckLinkMutableVideoFrame* outputVideoFrame = mDLOutputVideoFrameQueue.front();
@@ -1264,8 +1584,37 @@ bool OpenGLComposite::Start()
mTotalPlayoutFrames++; mTotalPlayoutFrames++;
} }
mDLInput->StartStreams(); if (mDLInput->StartStreams() != S_OK)
mDLOutput->StartScheduledPlayback(0, mFrameTimescale, 1.0); {
return false;
}
if (mAudioPrerolling)
{
const unsigned audioWaterLevel = static_cast<unsigned>(AudioSampleTimeForVideoFrame(kVideoPrerollFrameCount, mFrameDuration, mFrameTimescale));
const auto prerollDeadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(750);
while (mAudioScheduleEnabled && std::chrono::steady_clock::now() < prerollDeadline)
{
unsigned bufferedSampleFrames = 0;
if (mDLOutput->GetBufferedAudioSampleFrameCount(&bufferedSampleFrames) == S_OK && bufferedSampleFrames >= audioWaterLevel)
break;
std::this_thread::sleep_for(std::chrono::milliseconds(5));
}
if (mDLOutput->EndAudioPreroll() != S_OK)
{
OutputDebugStringA("Could not end DeckLink audio preroll; continuing without audio.\n");
mDLOutput->DisableAudioOutput();
mAudioOutputEnabled = false;
mAudioScheduleEnabled = false;
}
mAudioPrerolling = false;
}
if (mDLOutput->StartScheduledPlayback(0, mFrameTimescale, 1.0) != S_OK)
{
return false;
}
return true; return true;
} }
@@ -1295,11 +1644,23 @@ bool OpenGLComposite::Stop()
} }
} }
mAudioSchedulerRunning.store(false);
mAudioPacketQueued.notify_all();
if (mAudioSchedulerThread.joinable())
mAudioSchedulerThread.join();
mDLInput->StopStreams(); mDLInput->StopStreams();
mDLInput->DisableVideoInput(); mDLInput->DisableVideoInput();
if (mAudioEnabled)
mDLInput->DisableAudioInput();
mDLOutput->StopScheduledPlayback(0, NULL, 0); mDLOutput->StopScheduledPlayback(0, NULL, 0);
mDLOutput->SetAudioCallback(NULL);
mDLOutput->SetScheduledFrameCompletionCallback(NULL);
mDLOutput->DisableVideoOutput(); mDLOutput->DisableVideoOutput();
mAudioPrerolling = false;
if (mAudioOutputEnabled)
mDLOutput->DisableAudioOutput();
return true; return true;
} }
@@ -1411,6 +1772,9 @@ bool OpenGLComposite::compileSingleLayerProgram(const RuntimeRenderState& state,
const GLint videoInputLocation = glGetUniformLocation(newProgram.get(), "gVideoInput"); const GLint videoInputLocation = glGetUniformLocation(newProgram.get(), "gVideoInput");
if (videoInputLocation >= 0) if (videoInputLocation >= 0)
glUniform1i(videoInputLocation, static_cast<GLint>(kDecodedVideoTextureUnit)); glUniform1i(videoInputLocation, static_cast<GLint>(kDecodedVideoTextureUnit));
const GLint audioDataLocation = glGetUniformLocation(newProgram.get(), "gAudioData");
if (audioDataLocation >= 0)
glUniform1i(audioDataLocation, static_cast<GLint>(kAudioDataTextureUnit));
for (unsigned index = 0; index < historyCap; ++index) for (unsigned index = 0; index < historyCap; ++index)
{ {
const std::string sourceSamplerName = "gSourceHistory" + std::to_string(index); const std::string sourceSamplerName = "gSourceHistory" + std::to_string(index);
@@ -1973,6 +2337,8 @@ void OpenGLComposite::renderShaderProgram(GLuint sourceTexture, GLuint destinati
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glActiveTexture(GL_TEXTURE0 + kDecodedVideoTextureUnit); glActiveTexture(GL_TEXTURE0 + kDecodedVideoTextureUnit);
glBindTexture(GL_TEXTURE_2D, sourceTexture); glBindTexture(GL_TEXTURE_2D, sourceTexture);
glActiveTexture(GL_TEXTURE0 + kAudioDataTextureUnit);
glBindTexture(GL_TEXTURE_2D, mAudioDataTexture);
bindHistorySamplers(state, sourceTexture); bindHistorySamplers(state, sourceTexture);
bindLayerTextureAssets(layerProgram); bindLayerTextureAssets(layerProgram);
glBindVertexArray(mFullscreenVAO); glBindVertexArray(mFullscreenVAO);
@@ -1995,6 +2361,8 @@ void OpenGLComposite::renderShaderProgram(GLuint sourceTexture, GLuint destinati
glActiveTexture(GL_TEXTURE0 + shaderTextureBase + static_cast<GLuint>(index)); glActiveTexture(GL_TEXTURE0 + shaderTextureBase + static_cast<GLuint>(index));
glBindTexture(GL_TEXTURE_2D, 0); glBindTexture(GL_TEXTURE_2D, 0);
} }
glActiveTexture(GL_TEXTURE0 + kAudioDataTextureUnit);
glBindTexture(GL_TEXTURE_2D, 0);
glActiveTexture(GL_TEXTURE0 + kDecodedVideoTextureUnit); glActiveTexture(GL_TEXTURE0 + kDecodedVideoTextureUnit);
glBindTexture(GL_TEXTURE_2D, 0); glBindTexture(GL_TEXTURE_2D, 0);
glActiveTexture(GL_TEXTURE0); glActiveTexture(GL_TEXTURE0);
@@ -2066,6 +2434,58 @@ void OpenGLComposite::broadcastRuntimeState()
mControlServer->BroadcastState(); mControlServer->BroadcastState();
} }
BMDTimeValue OpenGLComposite::delayedAudioStreamTime() const
{
return static_cast<BMDTimeValue>(kVideoPrerollFrameCount) * mFrameDuration;
}
void OpenGLComposite::initializeAudioDelay()
{
{
std::lock_guard<std::mutex> analyzerLock(mAudioAnalyzerMutex);
mAudioAnalyzer.Reset();
}
{
std::lock_guard<std::mutex> audioLock(mAudioStateMutex);
mAudioAnalysis = AudioAnalysisSnapshot();
mAudioPacketQueue.clear();
mScheduledAudioPacketRetainQueue.clear();
mAudioSampleQueue.clear();
mQueuedAudioSampleFrames = 0;
mAudioUnderrunCount = 0;
mAudioOutputSampleTime = 0;
mAudioToneSampleIndex = 0;
mHasFirstAudioPacketTime = false;
mFirstAudioPacketTime = 0;
}
updateAudioStatus();
}
void OpenGLComposite::updateAudioDataTexture(const AudioAnalysisSnapshot& analysis)
{
if (mAudioDataTexture == 0)
return;
glActiveTexture(GL_TEXTURE0 + kAudioDataTextureUnit);
glBindTexture(GL_TEXTURE_2D, mAudioDataTexture);
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, kAudioTextureWidth, kAudioTextureHeight, GL_RGBA, GL_FLOAT, analysis.texture.data());
glBindTexture(GL_TEXTURE_2D, 0);
glActiveTexture(GL_TEXTURE0);
}
void OpenGLComposite::updateAudioStatus()
{
if (!mRuntimeHost)
return;
AudioStatusSnapshot status;
status.enabled = mAudioEnabled;
status.bufferedSampleFrames = mQueuedAudioSampleFrames;
status.underrunCount = mAudioUnderrunCount;
status.analysis = mAudioAnalysis;
mRuntimeHost->SetAudioStatus(status);
}
bool OpenGLComposite::updateGlobalParamsBuffer(const RuntimeRenderState& state, unsigned availableSourceHistoryLength, unsigned availableTemporalHistoryLength) bool OpenGLComposite::updateGlobalParamsBuffer(const RuntimeRenderState& state, unsigned availableSourceHistoryLength, unsigned availableTemporalHistoryLength)
{ {
std::vector<unsigned char> buffer; std::vector<unsigned char> buffer;
@@ -2085,6 +2505,15 @@ bool OpenGLComposite::updateGlobalParamsBuffer(const RuntimeRenderState& state,
: 0u; : 0u;
AppendStd140Int(buffer, static_cast<int>(effectiveSourceHistoryLength)); AppendStd140Int(buffer, static_cast<int>(effectiveSourceHistoryLength));
AppendStd140Int(buffer, static_cast<int>(effectiveTemporalHistoryLength)); AppendStd140Int(buffer, static_cast<int>(effectiveTemporalHistoryLength));
AppendStd140Vec2(buffer, state.audioAnalysis.rms[0], state.audioAnalysis.rms[1]);
AppendStd140Vec2(buffer, state.audioAnalysis.peak[0], state.audioAnalysis.peak[1]);
AppendStd140Float(buffer, state.audioAnalysis.monoRms);
AppendStd140Float(buffer, state.audioAnalysis.monoPeak);
AppendStd140Vec4(buffer,
state.audioAnalysis.bands[0],
state.audioAnalysis.bands[1],
state.audioAnalysis.bands[2],
state.audioAnalysis.bands[3]);
for (const ShaderParameterDefinition& definition : state.parameterDefinitions) for (const ShaderParameterDefinition& definition : state.parameterDefinitions)
{ {
@@ -2623,11 +3052,14 @@ ULONG CaptureDelegate::Release()
return newCount; return newCount;
} }
HRESULT CaptureDelegate::VideoInputFrameArrived(IDeckLinkVideoInputFrame* inputFrame, IDeckLinkAudioInputPacket* /*audioPacket*/) HRESULT CaptureDelegate::VideoInputFrameArrived(IDeckLinkVideoInputFrame* inputFrame, IDeckLinkAudioInputPacket* audioPacket)
{ {
if (audioPacket)
m_pOwner->AudioPacketArrived(audioPacket);
if (! inputFrame) if (! inputFrame)
{ {
// It's possible to receive a NULL inputFrame, but a valid audioPacket. Ignore audio-only frame. // It's possible to receive a NULL inputFrame, but a valid audioPacket.
return S_OK; return S_OK;
} }
@@ -2653,6 +3085,23 @@ PlayoutDelegate::PlayoutDelegate(OpenGLComposite* pOwner) :
HRESULT PlayoutDelegate::QueryInterface(REFIID iid, LPVOID *ppv) HRESULT PlayoutDelegate::QueryInterface(REFIID iid, LPVOID *ppv)
{ {
if (ppv == nullptr)
return E_POINTER;
if (iid == IID_IUnknown || iid == IID_IDeckLinkVideoOutputCallback)
{
*ppv = static_cast<IDeckLinkVideoOutputCallback*>(this);
AddRef();
return S_OK;
}
if (iid == IID_IDeckLinkAudioOutputCallback)
{
*ppv = static_cast<IDeckLinkAudioOutputCallback*>(this);
AddRef();
return S_OK;
}
*ppv = NULL; *ppv = NULL;
return E_NOINTERFACE; return E_NOINTERFACE;
} }
@@ -2694,3 +3143,8 @@ HRESULT PlayoutDelegate::ScheduledPlaybackHasStopped ()
{ {
return S_OK; return S_OK;
} }
HRESULT PlayoutDelegate::RenderAudioSamples (BOOL preroll)
{
return m_pOwner->RenderAudioSamples(preroll);
}

View File

@@ -52,13 +52,18 @@
#include <comutil.h> #include <comutil.h>
#include "DeckLinkAPI_h.h" #include "DeckLinkAPI_h.h"
#include "AudioSupport.h"
#include "VideoFrameTransfer.h" #include "VideoFrameTransfer.h"
#include "RuntimeHost.h" #include "RuntimeHost.h"
#include <atomic> #include <atomic>
#include <condition_variable>
#include <cstdint>
#include <functional> #include <functional>
#include <map> #include <map>
#include <memory> #include <memory>
#include <mutex>
#include <thread>
#include <vector> #include <vector>
#include <deque> #include <deque>
@@ -96,6 +101,10 @@ public:
void paintGL(); void paintGL();
void VideoFrameArrived(IDeckLinkVideoInputFrame* inputFrame, bool hasNoInputSource); void VideoFrameArrived(IDeckLinkVideoInputFrame* inputFrame, bool hasNoInputSource);
void AudioPacketArrived(IDeckLinkAudioInputPacket* audioPacket);
HRESULT RenderAudioSamples(BOOL preroll);
HRESULT ScheduleAudioToWaterLevel();
void AudioSchedulingLoop();
void PlayoutFrameCompleted(IDeckLinkVideoFrame* completedFrame, BMDOutputFrameCompletionResult result); void PlayoutFrameCompleted(IDeckLinkVideoFrame* completedFrame, BMDOutputFrameCompletionResult result);
private: private:
@@ -112,12 +121,14 @@ private:
// DeckLink // DeckLink
IDeckLinkInput* mDLInput; IDeckLinkInput* mDLInput;
IDeckLinkOutput* mDLOutput; IDeckLinkOutput* mDLOutput;
IDeckLinkConfiguration* mDLInputConfiguration;
IDeckLinkKeyer* mDLKeyer; IDeckLinkKeyer* mDLKeyer;
std::deque<IDeckLinkMutableVideoFrame*> mDLOutputVideoFrameQueue; std::deque<IDeckLinkMutableVideoFrame*> mDLOutputVideoFrameQueue;
PinnedMemoryAllocator* mPlayoutAllocator; PinnedMemoryAllocator* mPlayoutAllocator;
BMDTimeValue mFrameDuration; BMDTimeValue mFrameDuration;
BMDTimeScale mFrameTimescale; BMDTimeScale mFrameTimescale;
unsigned mTotalPlayoutFrames; unsigned mTotalPlayoutFrames;
uint64_t mAudioOutputSampleTime;
unsigned mInputFrameWidth; unsigned mInputFrameWidth;
unsigned mInputFrameHeight; unsigned mInputFrameHeight;
unsigned mOutputFrameWidth; unsigned mOutputFrameWidth;
@@ -139,6 +150,7 @@ private:
GLuint mLayerTempTexture; GLuint mLayerTempTexture;
GLuint mFBOTexture; GLuint mFBOTexture;
GLuint mOutputTexture; GLuint mOutputTexture;
GLuint mAudioDataTexture;
GLuint mUnpinnedTextureBuffer; GLuint mUnpinnedTextureBuffer;
GLuint mDecodeFrameBuf; GLuint mDecodeFrameBuf;
GLuint mLayerTempFrameBuf; GLuint mLayerTempFrameBuf;
@@ -157,6 +169,35 @@ private:
std::unique_ptr<RuntimeHost> mRuntimeHost; std::unique_ptr<RuntimeHost> mRuntimeHost;
std::unique_ptr<ControlServer> mControlServer; std::unique_ptr<ControlServer> mControlServer;
std::unique_ptr<OscServer> mOscServer; std::unique_ptr<OscServer> mOscServer;
bool mAudioEnabled;
bool mAudioOutputEnabled;
bool mAudioScheduleEnabled;
bool mAudioPrerollEnabled;
bool mAudioScheduleSilence;
bool mAudioScheduleTone;
bool mAudioPrerolling;
std::atomic<bool> mAudioSchedulerRunning;
std::atomic<bool> mPlayoutCallbackActive;
std::thread mAudioSchedulerThread;
std::mutex mAudioStateMutex;
std::mutex mAudioAnalyzerMutex;
AudioAnalyzer mAudioAnalyzer;
AudioAnalysisSnapshot mAudioAnalysis;
struct TimestampedAudioPacket
{
AudioFrameBlock block;
std::vector<int32_t> scheduledOutputSamples;
BMDTimeValue streamTime = 0;
};
std::deque<TimestampedAudioPacket> mAudioPacketQueue;
std::deque<TimestampedAudioPacket> mScheduledAudioPacketRetainQueue;
std::deque<int32_t> mAudioSampleQueue;
std::condition_variable mAudioPacketQueued;
unsigned mQueuedAudioSampleFrames = 0;
uint64_t mAudioUnderrunCount = 0;
uint64_t mAudioToneSampleIndex = 0;
bool mHasFirstAudioPacketTime = false;
BMDTimeValue mFirstAudioPacketTime = 0;
struct LayerProgram struct LayerProgram
{ {
@@ -209,6 +250,10 @@ private:
void renderEffect(); void renderEffect();
bool PollRuntimeChanges(); bool PollRuntimeChanges();
void broadcastRuntimeState(); void broadcastRuntimeState();
void initializeAudioDelay();
BMDTimeValue delayedAudioStreamTime() const;
void updateAudioDataTexture(const AudioAnalysisSnapshot& analysis);
void updateAudioStatus();
bool updateGlobalParamsBuffer(const RuntimeRenderState& state, unsigned availableSourceHistoryLength, unsigned availableTemporalHistoryLength); bool updateGlobalParamsBuffer(const RuntimeRenderState& state, unsigned availableSourceHistoryLength, unsigned availableTemporalHistoryLength);
bool validateTemporalTextureUnitBudget(const std::vector<RuntimeRenderState>& layerStates, std::string& error) const; bool validateTemporalTextureUnitBudget(const std::vector<RuntimeRenderState>& layerStates, std::string& error) const;
bool ensureTemporalHistoryResources(const std::vector<RuntimeRenderState>& layerStates, std::string& error); bool ensureTemporalHistoryResources(const std::vector<RuntimeRenderState>& layerStates, std::string& error);
@@ -341,7 +386,7 @@ public:
// Render Delegate Class // Render Delegate Class
//////////////////////////////////////////// ////////////////////////////////////////////
class PlayoutDelegate : public IDeckLinkVideoOutputCallback class PlayoutDelegate : public IDeckLinkVideoOutputCallback, public IDeckLinkAudioOutputCallback
{ {
OpenGLComposite* m_pOwner; OpenGLComposite* m_pOwner;
LONG mRefCount; LONG mRefCount;
@@ -356,6 +401,7 @@ public:
virtual HRESULT STDMETHODCALLTYPE ScheduledFrameCompleted (IDeckLinkVideoFrame* completedFrame, BMDOutputFrameCompletionResult result); virtual HRESULT STDMETHODCALLTYPE ScheduledFrameCompleted (IDeckLinkVideoFrame* completedFrame, BMDOutputFrameCompletionResult result);
virtual HRESULT STDMETHODCALLTYPE ScheduledPlaybackHasStopped (); virtual HRESULT STDMETHODCALLTYPE ScheduledPlaybackHasStopped ();
virtual HRESULT STDMETHODCALLTYPE RenderAudioSamples (BOOL preroll);
}; };
#endif // __OPENGL_COMPOSITE_H__ #endif // __OPENGL_COMPOSITE_H__

View File

@@ -1055,6 +1055,12 @@ void RuntimeHost::SetPerformanceStats(double frameBudgetMilliseconds, double ren
mSmoothedRenderMilliseconds = mSmoothedRenderMilliseconds * 0.9 + mRenderMilliseconds * 0.1; mSmoothedRenderMilliseconds = mSmoothedRenderMilliseconds * 0.9 + mRenderMilliseconds * 0.1;
} }
void RuntimeHost::SetAudioStatus(const AudioStatusSnapshot& status)
{
std::lock_guard<std::mutex> lock(mMutex);
mAudioStatus = status;
}
void RuntimeHost::AdvanceFrame() void RuntimeHost::AdvanceFrame()
{ {
std::lock_guard<std::mutex> lock(mMutex); std::lock_guard<std::mutex> lock(mMutex);
@@ -1121,6 +1127,7 @@ std::vector<RuntimeRenderState> RuntimeHost::GetLayerRenderStates(unsigned outpu
state.inputHeight = mSignalHeight; state.inputHeight = mSignalHeight;
state.outputWidth = outputWidth; state.outputWidth = outputWidth;
state.outputHeight = outputHeight; state.outputHeight = outputHeight;
state.audioAnalysis = mAudioStatus.analysis;
state.parameterDefinitions = shaderIt->second.parameters; state.parameterDefinitions = shaderIt->second.parameters;
state.textureAssets = shaderIt->second.textureAssets; state.textureAssets = shaderIt->second.textureAssets;
state.isTemporal = shaderIt->second.temporal.enabled; state.isTemporal = shaderIt->second.temporal.enabled;
@@ -1182,6 +1189,31 @@ bool RuntimeHost::LoadConfig(std::string& error)
} }
if (const JsonValue* enableExternalKeyingValue = configJson.find("enableExternalKeying")) if (const JsonValue* enableExternalKeyingValue = configJson.find("enableExternalKeying"))
mConfig.enableExternalKeying = enableExternalKeyingValue->asBoolean(mConfig.enableExternalKeying); mConfig.enableExternalKeying = enableExternalKeyingValue->asBoolean(mConfig.enableExternalKeying);
if (const JsonValue* audioEnabledValue = configJson.find("audioEnabled"))
mConfig.audioEnabled = audioEnabledValue->asBoolean(mConfig.audioEnabled);
if (const JsonValue* audioOutputEnabledValue = configJson.find("audioOutputEnabled"))
mConfig.audioOutputEnabled = audioOutputEnabledValue->asBoolean(mConfig.audioOutputEnabled);
if (const JsonValue* audioScheduleEnabledValue = configJson.find("audioScheduleEnabled"))
mConfig.audioScheduleEnabled = audioScheduleEnabledValue->asBoolean(mConfig.audioScheduleEnabled);
if (const JsonValue* audioPrerollEnabledValue = configJson.find("audioPrerollEnabled"))
mConfig.audioPrerollEnabled = audioPrerollEnabledValue->asBoolean(mConfig.audioPrerollEnabled);
if (const JsonValue* audioScheduleSilenceValue = configJson.find("audioScheduleSilence"))
mConfig.audioScheduleSilence = audioScheduleSilenceValue->asBoolean(mConfig.audioScheduleSilence);
if (const JsonValue* audioScheduleToneValue = configJson.find("audioScheduleTone"))
mConfig.audioScheduleTone = audioScheduleToneValue->asBoolean(mConfig.audioScheduleTone);
if (const JsonValue* audioChannelCountValue = configJson.find("audioChannelCount"))
mConfig.audioChannelCount = static_cast<unsigned>(audioChannelCountValue->asNumber(static_cast<double>(mConfig.audioChannelCount)));
if (const JsonValue* audioSampleRateValue = configJson.find("audioSampleRate"))
mConfig.audioSampleRate = static_cast<unsigned>(audioSampleRateValue->asNumber(static_cast<double>(mConfig.audioSampleRate)));
if (const JsonValue* audioDelayModeValue = configJson.find("audioDelayMode"))
{
if (audioDelayModeValue->isString() && !audioDelayModeValue->asString().empty())
mConfig.audioDelayMode = audioDelayModeValue->asString();
}
if (mConfig.audioChannelCount != kAudioChannelCount)
mConfig.audioChannelCount = kAudioChannelCount;
if (mConfig.audioSampleRate != kAudioSampleRate)
mConfig.audioSampleRate = kAudioSampleRate;
if (const JsonValue* videoFormatValue = configJson.find("videoFormat")) if (const JsonValue* videoFormatValue = configJson.find("videoFormat"))
{ {
if (videoFormatValue->isString() && !videoFormatValue->asString().empty()) if (videoFormatValue->isString() && !videoFormatValue->asString().empty())
@@ -1519,6 +1551,15 @@ JsonValue RuntimeHost::BuildStateValue() const
app.set("autoReload", JsonValue(mAutoReloadEnabled)); app.set("autoReload", JsonValue(mAutoReloadEnabled));
app.set("maxTemporalHistoryFrames", JsonValue(static_cast<double>(mConfig.maxTemporalHistoryFrames))); app.set("maxTemporalHistoryFrames", JsonValue(static_cast<double>(mConfig.maxTemporalHistoryFrames)));
app.set("enableExternalKeying", JsonValue(mConfig.enableExternalKeying)); app.set("enableExternalKeying", JsonValue(mConfig.enableExternalKeying));
app.set("audioEnabled", JsonValue(mConfig.audioEnabled));
app.set("audioOutputEnabled", JsonValue(mConfig.audioOutputEnabled));
app.set("audioScheduleEnabled", JsonValue(mConfig.audioScheduleEnabled));
app.set("audioPrerollEnabled", JsonValue(mConfig.audioPrerollEnabled));
app.set("audioScheduleSilence", JsonValue(mConfig.audioScheduleSilence));
app.set("audioScheduleTone", JsonValue(mConfig.audioScheduleTone));
app.set("audioChannelCount", JsonValue(static_cast<double>(mConfig.audioChannelCount)));
app.set("audioSampleRate", JsonValue(static_cast<double>(mConfig.audioSampleRate)));
app.set("audioDelayMode", JsonValue(mConfig.audioDelayMode));
app.set("inputVideoFormat", JsonValue(mConfig.inputVideoFormat)); app.set("inputVideoFormat", JsonValue(mConfig.inputVideoFormat));
app.set("inputFrameRate", JsonValue(mConfig.inputFrameRate)); app.set("inputFrameRate", JsonValue(mConfig.inputFrameRate));
app.set("outputVideoFormat", JsonValue(mConfig.outputVideoFormat)); app.set("outputVideoFormat", JsonValue(mConfig.outputVideoFormat));
@@ -1538,6 +1579,26 @@ JsonValue RuntimeHost::BuildStateValue() const
video.set("modeName", JsonValue(mSignalModeName)); video.set("modeName", JsonValue(mSignalModeName));
root.set("video", video); root.set("video", video);
JsonValue audio = JsonValue::MakeObject();
audio.set("enabled", JsonValue(mAudioStatus.enabled));
audio.set("bufferedSampleFrames", JsonValue(static_cast<double>(mAudioStatus.bufferedSampleFrames)));
audio.set("underrunCount", JsonValue(static_cast<double>(mAudioStatus.underrunCount)));
JsonValue rms = JsonValue::MakeArray();
rms.pushBack(JsonValue(static_cast<double>(mAudioStatus.analysis.rms[0])));
rms.pushBack(JsonValue(static_cast<double>(mAudioStatus.analysis.rms[1])));
audio.set("rms", rms);
JsonValue peak = JsonValue::MakeArray();
peak.pushBack(JsonValue(static_cast<double>(mAudioStatus.analysis.peak[0])));
peak.pushBack(JsonValue(static_cast<double>(mAudioStatus.analysis.peak[1])));
audio.set("peak", peak);
audio.set("monoRms", JsonValue(static_cast<double>(mAudioStatus.analysis.monoRms)));
audio.set("monoPeak", JsonValue(static_cast<double>(mAudioStatus.analysis.monoPeak)));
JsonValue bands = JsonValue::MakeArray();
for (float band : mAudioStatus.analysis.bands)
bands.pushBack(JsonValue(static_cast<double>(band)));
audio.set("bands", bands);
root.set("audio", audio);
JsonValue deckLink = JsonValue::MakeObject(); JsonValue deckLink = JsonValue::MakeObject();
deckLink.set("modelName", JsonValue(mDeckLinkOutputStatus.modelName)); deckLink.set("modelName", JsonValue(mDeckLinkOutputStatus.modelName));
deckLink.set("supportsInternalKeying", JsonValue(mDeckLinkOutputStatus.supportsInternalKeying)); deckLink.set("supportsInternalKeying", JsonValue(mDeckLinkOutputStatus.supportsInternalKeying));

View File

@@ -38,6 +38,7 @@ public:
void SetDeckLinkOutputStatus(const std::string& modelName, bool supportsInternalKeying, bool supportsExternalKeying, void SetDeckLinkOutputStatus(const std::string& modelName, bool supportsInternalKeying, bool supportsExternalKeying,
bool keyerInterfaceAvailable, bool externalKeyingRequested, bool externalKeyingActive, const std::string& statusMessage); bool keyerInterfaceAvailable, bool externalKeyingRequested, bool externalKeyingActive, const std::string& statusMessage);
void SetPerformanceStats(double frameBudgetMilliseconds, double renderMilliseconds); void SetPerformanceStats(double frameBudgetMilliseconds, double renderMilliseconds);
void SetAudioStatus(const AudioStatusSnapshot& status);
void AdvanceFrame(); void AdvanceFrame();
bool BuildLayerFragmentShaderSource(const std::string& layerId, std::string& fragmentShaderSource, std::string& error); bool BuildLayerFragmentShaderSource(const std::string& layerId, std::string& fragmentShaderSource, std::string& error);
@@ -52,6 +53,14 @@ public:
unsigned short GetOscPort() const { return mConfig.oscPort; } unsigned short GetOscPort() const { return mConfig.oscPort; }
unsigned GetMaxTemporalHistoryFrames() const { return mConfig.maxTemporalHistoryFrames; } unsigned GetMaxTemporalHistoryFrames() const { return mConfig.maxTemporalHistoryFrames; }
bool ExternalKeyingEnabled() const { return mConfig.enableExternalKeying; } bool ExternalKeyingEnabled() const { return mConfig.enableExternalKeying; }
bool AudioEnabled() const { return mConfig.audioEnabled; }
bool AudioOutputEnabled() const { return mConfig.audioOutputEnabled; }
bool AudioScheduleEnabled() const { return mConfig.audioScheduleEnabled; }
bool AudioPrerollEnabled() const { return mConfig.audioPrerollEnabled; }
bool AudioScheduleSilence() const { return mConfig.audioScheduleSilence; }
bool AudioScheduleTone() const { return mConfig.audioScheduleTone; }
unsigned AudioChannelCount() const { return mConfig.audioChannelCount; }
unsigned AudioSampleRate() const { return mConfig.audioSampleRate; }
const std::string& GetInputVideoFormat() const { return mConfig.inputVideoFormat; } const std::string& GetInputVideoFormat() const { return mConfig.inputVideoFormat; }
const std::string& GetInputFrameRate() const { return mConfig.inputFrameRate; } const std::string& GetInputFrameRate() const { return mConfig.inputFrameRate; }
const std::string& GetOutputVideoFormat() const { return mConfig.outputVideoFormat; } const std::string& GetOutputVideoFormat() const { return mConfig.outputVideoFormat; }
@@ -68,6 +77,15 @@ private:
bool autoReload = true; bool autoReload = true;
unsigned maxTemporalHistoryFrames = 4; unsigned maxTemporalHistoryFrames = 4;
bool enableExternalKeying = false; bool enableExternalKeying = false;
bool audioEnabled = true;
bool audioOutputEnabled = true;
bool audioScheduleEnabled = true;
bool audioPrerollEnabled = true;
bool audioScheduleSilence = false;
bool audioScheduleTone = false;
unsigned audioChannelCount = kAudioChannelCount;
unsigned audioSampleRate = kAudioSampleRate;
std::string audioDelayMode = "matchVideoPreroll";
std::string inputVideoFormat = "1080p"; std::string inputVideoFormat = "1080p";
std::string inputFrameRate = "59.94"; std::string inputFrameRate = "59.94";
std::string outputVideoFormat = "1080p"; std::string outputVideoFormat = "1080p";
@@ -148,6 +166,7 @@ private:
double mRenderMilliseconds; double mRenderMilliseconds;
double mSmoothedRenderMilliseconds; double mSmoothedRenderMilliseconds;
DeckLinkOutputStatus mDeckLinkOutputStatus; DeckLinkOutputStatus mDeckLinkOutputStatus;
AudioStatusSnapshot mAudioStatus;
unsigned short mServerPort; unsigned short mServerPort;
bool mAutoReloadEnabled; bool mAutoReloadEnabled;
std::chrono::steady_clock::time_point mStartTime; std::chrono::steady_clock::time_point mStartTime;

View File

@@ -5,6 +5,8 @@
#include <string> #include <string>
#include <vector> #include <vector>
#include "AudioSupport.h"
enum class ShaderParameterType enum class ShaderParameterType
{ {
Float, Float,
@@ -95,6 +97,7 @@ struct RuntimeRenderState
unsigned inputHeight = 0; unsigned inputHeight = 0;
unsigned outputWidth = 0; unsigned outputWidth = 0;
unsigned outputHeight = 0; unsigned outputHeight = 0;
AudioAnalysisSnapshot audioAnalysis;
bool isTemporal = false; bool isTemporal = false;
TemporalHistorySource temporalHistorySource = TemporalHistorySource::None; TemporalHistorySource temporalHistorySource = TemporalHistorySource::None;
unsigned requestedTemporalHistoryLength = 0; unsigned requestedTemporalHistoryLength = 0;

View File

@@ -8,5 +8,14 @@
"outputFrameRate": "59.94", "outputFrameRate": "59.94",
"autoReload": true, "autoReload": true,
"maxTemporalHistoryFrames": 12, "maxTemporalHistoryFrames": 12,
"audioEnabled": true,
"audioOutputEnabled": true,
"audioScheduleEnabled": true,
"audioPrerollEnabled": true,
"audioScheduleSilence": false,
"audioScheduleTone": false,
"audioChannelCount": 2,
"audioSampleRate": 48000,
"audioDelayMode": "matchVideoPreroll",
"enableExternalKeying": true "enableExternalKeying": true
} }

View File

@@ -0,0 +1,406 @@
# Audio / SDI Tearing Investigation
Date: 2026-05-05
## Problem
After adding DeckLink audio pass-through, the SDI output intermittently shows a torn/corrupted frame. The preview window does not show the artifact.
Observed artifact:
- Bottom portion of the SDI image can show an offset mix of current/previous frame.
- Looks like a frame-buffer or output-transfer issue rather than shader rendering.
- Occurs even with all shaders bypassed.
- Main branch is known good with no tearing.
Later tests also showed audio tearing/stutter when non-silent audio was scheduled.
## Known Good Baseline
- `main` branch has no SDI tearing.
- Current branch with `audioEnabled: false` ran for several minutes with no visible tearing.
This strongly suggests the issue is tied to DeckLink audio output/scheduling rather than the shader stack.
## SDK References Checked
### `InputLoopThrough`
Location:
`3rdParty/Blackmagic DeckLink SDK 16.0/Win/Samples/InputLoopThrough`
Findings:
- This is the SDK loop-through sample that keeps audio.
- It preserves DeckLink audio packet timestamps using `GetPacketTime(..., m_frameTimescale)`.
- It schedules audio packets with `ScheduleAudioSamples(..., packetTime, m_frameTimescale, ...)`.
- It uses 16-channel 32-bit embedded audio by default.
- It has separate scheduling threads for video/audio.
- It waits for both video and audio preroll before `StartScheduledPlayback`.
### `LoopThroughWithOpenGLCompositing`
Location:
`3rdParty/Blackmagic DeckLink SDK 16.0/Win/Samples/LoopThroughWithOpenGLCompositing`
Findings:
- This sample is the base for this app.
- It ignores `IDeckLinkAudioInputPacket`.
- It does not demonstrate audio pass-through.
### `SignalGenerator`
Location:
`3rdParty/Blackmagic DeckLink SDK 16.0/Win/Samples/SignalGenerator`
Findings:
- Uses `RenderAudioSamples()` callback to top up audio when DeckLink requests samples.
- Uses `GetBufferedAudioSampleFrameCount()` and a water level before scheduling more audio.
## Tests Tried And Results
### 1. Initial audio pass-through with FIFO and sample-time accumulator
Implementation:
- Copied incoming audio into a stereo FIFO.
- Scheduled audio with a generated `mNextAudioSampleFrame` clock in 48 kHz timescale.
- Matched delay to video preroll.
Result:
- Audio eventually worked.
- SDI video tearing appeared.
Conclusion:
- Basic audio output path triggered SDI instability.
### 2. Reworked audio toward SDK `InputLoopThrough` packet-timestamp model
Implementation:
- Preserved incoming packet time via `GetPacketTime(..., mFrameTimescale)`.
- Queued timestamped audio packets.
- Scheduled packets with `ScheduleAudioSamples(..., packet.streamTime, mFrameTimescale, ...)`.
Result:
- Tearing persisted.
Conclusion:
- Simply matching SDK timestamp domain did not fix the issue.
### 3. Restored video callback closer to `main`
Implementation:
- Removed extra `glFinish()` calls.
- Restored preview/readback ordering closer to `main`.
- Re-enabled fast transfer path after earlier tests disabled it.
- Removed audio texture upload from video playout callback.
- Removed audio analysis and audio locks from video playout callback.
- Removed DeckLink scheduling mutex around `ScheduleVideoFrame`.
Result:
- Tearing frequency seemed reduced at one point, but tearing persisted.
Conclusion:
- Extra work in the playout callback may have made timing worse, but was not the root cause.
### 4. Disabled audio completely
Config:
```json
"audioEnabled": false
```
Result:
- Ran for several minutes with no visible tearing.
Conclusion:
- The tearing is tied to audio being enabled.
### 5. Enabled audio input/analysis but disabled DeckLink audio output
Config:
```json
"audioEnabled": true,
"audioOutputEnabled": false
```
Result:
- No tearing appeared.
Conclusion:
- DeckLink audio input and CPU analysis are not the trigger.
- The problem is on the DeckLink audio output side.
### 6. Enabled DeckLink audio output but disabled scheduling
Config:
```json
"audioEnabled": true,
"audioOutputEnabled": true,
"audioScheduleEnabled": false
```
Result:
- No video tearing.
- Slight stutter appeared.
Conclusion:
- `EnableAudioOutput()` alone did not produce the tearing.
- Stutter was likely from enabling an audio output stream without feeding it samples.
### 7. Enabled audio scheduling but skipped audio preroll
Config:
```json
"audioEnabled": true,
"audioOutputEnabled": true,
"audioScheduleEnabled": true,
"audioPrerollEnabled": false
```
Result:
- Video tearing returned.
- Stutter also present.
Conclusion:
- `BeginAudioPreroll()` / `EndAudioPreroll()` are not required to trigger the tear.
- `ScheduleAudioSamples()` is strongly implicated.
### 8. Retained scheduled audio packet memory after `ScheduleAudioSamples`
Implementation:
- Kept scheduled packet buffers alive in a retain queue after scheduling.
- Avoided passing DeckLink pointers to vectors that immediately went out of scope.
Result:
- Video tearing and stutter persisted.
Conclusion:
- Buffer lifetime after `ScheduleAudioSamples()` was not the root cause.
### 9. Added audio water-level cap
Implementation:
- Restored SDK-style `GetBufferedAudioSampleFrameCount()` check.
- Only scheduled more audio if DeckLink buffer was below the target water level.
Result:
- Stutter was reduced.
- Video tearing persisted.
Conclusion:
- Overscheduling contributed to stutter/timing pressure.
- It did not explain the tearing.
### 10. Removed standalone audio scheduler thread
Implementation:
- Stopped starting the dedicated audio scheduler thread.
- Audio top-up occurred from input packet arrival and `RenderAudioSamples()` callback.
Result:
- No meaningful change.
Conclusion:
- The polling thread itself was not the cause.
### 11. Switched from timestamped audio output to continuous audio output
Implementation:
- Changed audio output to `bmdAudioOutputStreamContinuous`.
- Scheduled audio using a monotonic 48 kHz sample clock.
Result:
- Video tearing and stutter persisted.
Conclusion:
- The issue was not specific to timestamped output mode.
### 12. Rendered into the actual `completedFrame`
Implementation:
- Changed `PlayoutFrameCompleted()` to reuse the exact `completedFrame` passed by DeckLink rather than rotating an independent output-frame queue.
Result:
- No change.
Conclusion:
- The app was probably not overwriting a still-in-use frame from its output queue.
### 13. Scheduled generated silence instead of captured audio
Config:
```json
"audioScheduleSilence": true
```
Result:
- Occasional stutter.
- No video tearing.
Conclusion:
- Scheduling audio buffers itself can be stable if the audio data is zero.
- Non-zero audio data appears to be important.
### 14. Flattened captured audio into PCM FIFO and scheduled fixed chunks
Implementation:
- Captured packets were flattened into a PCM FIFO.
- DeckLink received fixed 10 ms chunks rather than original packet boundaries.
- Missing audio was padded with silence.
Result:
- Video tearing returned.
- Audio stutter/tearing returned.
Conclusion:
- Packet boundaries/timestamps were not the whole cause.
- Non-zero captured audio data still triggered instability.
### 15. Scheduled generated 440 Hz tone
Config:
```json
"audioScheduleTone": true
```
Result:
- Video tearing occurred.
- Tone/audio also tore.
Conclusion:
- The issue is not specific to captured input data.
- Non-zero scheduled audio, even generated tone, triggers the problem.
### 16. Changed DeckLink output to 16 embedded audio channels
Implementation:
- Enabled DeckLink audio output with 16 channels instead of 2.
- Mapped stereo to channels 1/2.
- Filled channels 3-16 with silence.
Result:
- Video tearing and audio tearing still occurred.
Conclusion:
- The issue is not simply caused by 2-channel embedded audio output.
### 17. Used DeckLink-owned output video frames with audio enabled
Implementation:
- When audio output is enabled:
- disabled fast transfer path
- created output frames with `CreateVideoFrame()`
- avoided `CreateVideoFrameWithBuffer()` and the custom pinned playout allocator
Result:
- Video tearing and audio tearing still occurred.
Conclusion:
- The custom pinned output video buffers are likely not the root cause.
## Current Strong Conclusions
- Shader stack is not the cause.
- Preview/render output is not showing the issue, so the artifact is SDI/output-side.
- DeckLink audio input is not the cause.
- DeckLink audio output enabled but unscheduled does not cause tearing.
- `ScheduleAudioSamples()` with zero/silent buffers does not cause tearing.
- `ScheduleAudioSamples()` with non-zero audio causes both video tearing and audio tearing.
- The problem persists across:
- timestamped audio output
- continuous audio output
- captured audio
- generated tone
- 2-channel output
- 16-channel embedded output
- app-owned/pinned output video buffers
- DeckLink-owned output video frames
## Current Hypothesis
The issue appears to be a DeckLink output interaction where non-zero embedded audio samples disturb SDI video/audio output in this apps scheduling model.
Since silence is stable but tone is not, the next likely areas to investigate are:
- Audio sample format/range/endian expectations.
- Whether DeckLink expects 32-bit audio samples to be in a different effective range than we are providing.
- Whether the scheduled audio buffer layout for the selected hardware/output mode differs from our assumptions.
- Whether the selected output mode/keyer/SDI configuration has constraints when non-zero embedded audio is present.
- Whether the SDK sample behaves correctly on the same hardware with a generated tone and same video mode.
## Suggested Next Tests
1. Schedule very low amplitude non-zero audio, e.g. constant `1`, then `256`, then a very quiet sine.
2. Try 16-bit audio output instead of 32-bit if supported.
3. Try `bmdAudioOutputStreamContinuousDontResample`.
4. Disable external keying and test with non-zero audio.
5. Build/run the SDK `SignalGenerator` or `InputLoopThrough` sample on the same DeckLink device, video mode, and SDI output path with non-zero embedded audio.
6. Add instrumentation for DeckLink status/errors around scheduled video/audio completion.
7. Confirm Desktop Video setup panel audio/SDI settings for the selected output.
## Current Config At Time Of Note
```json
"audioEnabled": true,
"audioOutputEnabled": true,
"audioScheduleEnabled": true,
"audioPrerollEnabled": true,
"audioScheduleSilence": false,
"audioScheduleTone": false
```

View File

@@ -16,6 +16,11 @@ struct ShaderContext
float bypass; float bypass;
int sourceHistoryLength; int sourceHistoryLength;
int temporalHistoryLength; int temporalHistoryLength;
float2 audioRms;
float2 audioPeak;
float audioMonoRms;
float audioMonoPeak;
float4 audioBands;
}; };
cbuffer GlobalParams cbuffer GlobalParams
@@ -28,15 +33,31 @@ cbuffer GlobalParams
float gBypass; float gBypass;
int gSourceHistoryLength; int gSourceHistoryLength;
int gTemporalHistoryLength; int gTemporalHistoryLength;
float2 gAudioRms;
float2 gAudioPeak;
float gAudioMonoRms;
float gAudioMonoPeak;
float4 gAudioBands;
{{PARAMETER_UNIFORMS}}}; {{PARAMETER_UNIFORMS}}};
Sampler2D<float4> gVideoInput; Sampler2D<float4> gVideoInput;
Sampler2D<float4> gAudioData;
{{SOURCE_HISTORY_SAMPLERS}}{{TEMPORAL_HISTORY_SAMPLERS}}{{TEXTURE_SAMPLERS}} {{SOURCE_HISTORY_SAMPLERS}}{{TEMPORAL_HISTORY_SAMPLERS}}{{TEXTURE_SAMPLERS}}
float4 sampleVideo(float2 tc) float4 sampleVideo(float2 tc)
{ {
return gVideoInput.Sample(tc); return gVideoInput.Sample(tc);
} }
float4 sampleAudioWaveform(float x)
{
return gAudioData.Sample(float2(saturate(x), 0.25));
}
float4 sampleAudioSpectrum(float x)
{
return gAudioData.Sample(float2(saturate(x), 0.75));
}
float4 sampleSourceHistory(int framesAgo, float2 tc) float4 sampleSourceHistory(int framesAgo, float2 tc)
{ {
if (gSourceHistoryLength <= 0) if (gSourceHistoryLength <= 0)
@@ -83,6 +104,11 @@ float4 fragmentMain(FragmentInput input) : SV_Target
context.bypass = gBypass; context.bypass = gBypass;
context.sourceHistoryLength = gSourceHistoryLength; context.sourceHistoryLength = gSourceHistoryLength;
context.temporalHistoryLength = gTemporalHistoryLength; context.temporalHistoryLength = gTemporalHistoryLength;
context.audioRms = gAudioRms;
context.audioPeak = gAudioPeak;
context.audioMonoRms = gAudioMonoRms;
context.audioMonoPeak = gAudioMonoPeak;
context.audioBands = gAudioBands;
float4 effectedColor = {{ENTRY_POINT_CALL}}; float4 effectedColor = {{ENTRY_POINT_CALL}};
float mixValue = clamp(gBypass > 0.5 ? 0.0 : gMixAmount, 0.0, 1.0); float mixValue = clamp(gBypass > 0.5 ? 0.0 : gMixAmount, 0.0, 1.0);
return lerp(context.sourceColor, effectedColor, mixValue); return lerp(context.sourceColor, effectedColor, mixValue);

View File

@@ -0,0 +1,76 @@
{
"id": "audio-vu-meter",
"name": "Audio VU Meter",
"description": "Draws stereo audio level meters from the runtime audio analysis data.",
"category": "Utility",
"entryPoint": "shadeVideo",
"parameters": [
{
"id": "meterPosition",
"label": "Position",
"type": "vec2",
"default": [0.08, 0.82],
"min": [0.0, 0.0],
"max": [1.0, 1.0],
"step": [0.01, 0.01]
},
{
"id": "meterScale",
"label": "Scale",
"type": "float",
"default": 0.35,
"min": 0.1,
"max": 1.0,
"step": 0.01
},
{
"id": "meterOpacity",
"label": "Opacity",
"type": "float",
"default": 0.9,
"min": 0.0,
"max": 1.0,
"step": 0.01
},
{
"id": "noiseGate",
"label": "Noise Gate",
"type": "float",
"default": 0.03,
"min": 0.0,
"max": 0.5,
"step": 0.01
},
{
"id": "meterColor",
"label": "Meter Color",
"type": "color",
"default": [0.2, 1.0, 0.55, 1.0]
},
{
"id": "peakColor",
"label": "Peak Color",
"type": "color",
"default": [1.0, 0.85, 0.2, 1.0]
},
{
"id": "backgroundOpacity",
"label": "Background",
"type": "float",
"default": 0.45,
"min": 0.0,
"max": 1.0,
"step": 0.01
},
{
"id": "orientation",
"label": "Orientation",
"type": "enum",
"default": "horizontal",
"options": [
{ "value": "horizontal", "label": "Horizontal" },
{ "value": "vertical", "label": "Vertical" }
]
}
]
}

View File

@@ -0,0 +1,59 @@
float rectMask(float2 uv, float2 minUv, float2 maxUv)
{
float2 insideMin = step(minUv, uv);
float2 insideMax = step(uv, maxUv);
return insideMin.x * insideMin.y * insideMax.x * insideMax.y;
}
float denoiseLevel(float value)
{
float gate = saturate(noiseGate);
float clean = saturate((value - gate) / max(1.0 - gate, 0.001));
return smoothstep(0.0, 1.0, clean);
}
float4 shadeVideo(ShaderContext context)
{
float4 color = context.sourceColor;
float2 size = orientation == 0 ? float2(meterScale, meterScale * 0.18) : float2(meterScale * 0.18, meterScale);
float2 minUv = clamp(meterPosition, 0.0, 1.0 - size);
float2 local = (context.uv - minUv) / max(size, float2(0.001));
float inside = rectMask(local, float2(0.0), float2(1.0));
if (inside <= 0.0)
return color;
float3 bg = lerp(color.rgb, float3(0.0), saturate(backgroundOpacity));
float leftLevel = denoiseLevel(context.audioRms.x * 2.4);
float rightLevel = denoiseLevel(context.audioRms.y * 2.4);
float leftPeak = denoiseLevel(context.audioPeak.x);
float rightPeak = denoiseLevel(context.audioPeak.y);
float bar = 0.0;
float peak = 0.0;
if (orientation == 0)
{
float leftRow = rectMask(local, float2(0.04, 0.58), float2(0.96, 0.86));
float rightRow = rectMask(local, float2(0.04, 0.14), float2(0.96, 0.42));
float leftFill = rectMask(local, float2(0.04, 0.58), float2(0.04 + 0.92 * leftLevel, 0.86));
float rightFill = rectMask(local, float2(0.04, 0.14), float2(0.04 + 0.92 * rightLevel, 0.42));
float leftPeakLine = rectMask(local, float2(0.04 + 0.92 * leftPeak - 0.006, 0.55), float2(0.04 + 0.92 * leftPeak + 0.006, 0.89));
float rightPeakLine = rectMask(local, float2(0.04 + 0.92 * rightPeak - 0.006, 0.11), float2(0.04 + 0.92 * rightPeak + 0.006, 0.45));
bar = max(leftFill, rightFill);
peak = max(leftPeakLine * leftRow, rightPeakLine * rightRow);
}
else
{
float leftColumn = rectMask(local, float2(0.14, 0.04), float2(0.42, 0.96));
float rightColumn = rectMask(local, float2(0.58, 0.04), float2(0.86, 0.96));
float leftFill = rectMask(local, float2(0.14, 0.04), float2(0.42, 0.04 + 0.92 * leftLevel));
float rightFill = rectMask(local, float2(0.58, 0.04), float2(0.86, 0.04 + 0.92 * rightLevel));
float leftPeakLine = rectMask(local, float2(0.11, 0.04 + 0.92 * leftPeak - 0.006), float2(0.45, 0.04 + 0.92 * leftPeak + 0.006));
float rightPeakLine = rectMask(local, float2(0.55, 0.04 + 0.92 * rightPeak - 0.006), float2(0.89, 0.04 + 0.92 * rightPeak + 0.006));
bar = max(leftFill * leftColumn, rightFill * rightColumn);
peak = max(leftPeakLine, rightPeakLine);
}
float3 metered = lerp(bg, meterColor.rgb, bar * saturate(meterOpacity) * meterColor.a);
metered = lerp(metered, peakColor.rgb, peak * saturate(meterOpacity) * peakColor.a);
return float4(metered, color.a);
}

115
tests/AudioSupportTests.cpp Normal file
View File

@@ -0,0 +1,115 @@
#include "AudioSupport.h"
#include <algorithm>
#include <cmath>
#include <cstdint>
#include <iostream>
#include <vector>
namespace
{
int gFailures = 0;
void Expect(bool condition, const char* message)
{
if (condition)
return;
std::cerr << "FAIL: " << message << "\n";
++gFailures;
}
int32_t ToSample(float value)
{
const double clamped = std::max(-1.0, std::min(1.0, static_cast<double>(value)));
return static_cast<int32_t>(clamped * 2147483647.0);
}
void TestFrameSampleCounts()
{
Expect(AudioSamplesForVideoFrame(0, 1, 50) == 960, "50 fps first frame has 960 audio samples");
Expect(AudioSamplesForVideoFrame(0, 1, 60) == 800, "60 fps first frame has 800 audio samples");
uint64_t total = 0;
for (uint64_t frame = 0; frame < 600; ++frame)
total += AudioSamplesForVideoFrame(frame, 1001, 60000);
Expect(total == AudioSampleTimeForVideoFrame(600, 1001, 60000), "59.94 fps sample counts do not drift");
}
void TestDelayBuffer()
{
AudioDelayBuffer buffer;
buffer.Reset(4);
std::vector<int32_t> input = {
11, 12,
21, 22,
31, 32,
41, 42
};
buffer.PushInterleaved(input.data(), 4);
bool underrun = false;
AudioFrameBlock first = buffer.Pop(4, underrun);
Expect(!underrun, "delay-buffer initial silence does not underrun");
Expect(first.frameCount() == 4, "delay-buffer returns requested frame count");
Expect(first.interleavedSamples[0] == 0 && first.interleavedSamples[7] == 0, "delay-buffer emits initial silence");
AudioFrameBlock second = buffer.Pop(4, underrun);
Expect(!underrun, "delay-buffer emits delayed input without underrun");
Expect(second.interleavedSamples == input, "delay-buffer preserves delayed interleaved samples");
AudioFrameBlock third = buffer.Pop(2, underrun);
Expect(underrun, "delay-buffer reports underrun");
Expect(third.interleavedSamples[0] == 0 && third.interleavedSamples[3] == 0, "delay-buffer underrun fills silence");
}
void TestAnalyzerSilence()
{
AudioAnalyzer analyzer;
AudioFrameBlock block;
block.interleavedSamples.resize(512 * kAudioChannelCount, 0);
AudioAnalysisSnapshot analysis = analyzer.Analyze(block);
Expect(analysis.rms[0] == 0.0f && analysis.rms[1] == 0.0f, "silence rms is zero");
Expect(analysis.peak[0] == 0.0f && analysis.peak[1] == 0.0f, "silence peak is zero");
Expect(analysis.bands[0] == 0.0f && analysis.bands[3] == 0.0f, "silence bands are zero");
}
void TestAnalyzerSineAndStereo()
{
AudioAnalyzer analyzer;
AudioFrameBlock block;
block.interleavedSamples.resize(1024 * kAudioChannelCount, 0);
for (std::size_t frame = 0; frame < 1024; ++frame)
{
const float phase = static_cast<float>(frame) * 2.0f * 3.14159265f * 300.0f / static_cast<float>(kAudioSampleRate);
block.interleavedSamples[frame * 2] = ToSample(std::sin(phase) * 0.8f);
block.interleavedSamples[frame * 2 + 1] = ToSample(0.25f);
}
AudioAnalysisSnapshot analysis = analyzer.Analyze(block);
Expect(analysis.peak[0] > 0.75f && analysis.peak[0] <= 0.81f, "left sine peak is detected");
Expect(analysis.rms[0] > 0.45f && analysis.rms[0] < 0.65f, "left sine rms is detected");
Expect(analysis.peak[1] > 0.24f && analysis.peak[1] < 0.26f, "right constant peak remains independent");
Expect(analysis.rms[1] > 0.24f && analysis.rms[1] < 0.26f, "right constant rms remains independent");
Expect(analysis.bands[1] >= analysis.bands[0], "300 Hz sine activates lower-mid band");
}
}
int main()
{
TestFrameSampleCounts();
TestDelayBuffer();
TestAnalyzerSilence();
TestAnalyzerSineAndStereo();
if (gFailures != 0)
{
std::cerr << gFailures << " AudioSupport test failure(s).\n";
return 1;
}
std::cout << "AudioSupport tests passed.\n";
return 0;
}

View File

@@ -19,6 +19,7 @@ function App() {
const performance = appState?.performance ?? {}; const performance = appState?.performance ?? {};
const runtime = appState?.runtime ?? {}; const runtime = appState?.runtime ?? {};
const video = appState?.video ?? {}; const video = appState?.video ?? {};
const audio = appState?.audio ?? {};
const app = appState?.app ?? {}; const app = appState?.app ?? {};
const stackPresets = appState?.stackPresets ?? []; const stackPresets = appState?.stackPresets ?? [];
@@ -67,7 +68,7 @@ function App() {
</header> </header>
<section className="dashboard-grid"> <section className="dashboard-grid">
<StatusPanels app={app} performance={performance} runtime={runtime} video={video} /> <StatusPanels app={app} audio={audio} performance={performance} runtime={runtime} video={video} />
<StackPresetToolbar <StackPresetToolbar
presetName={presetName} presetName={presetName}
selectedPresetName={selectedPresetName} selectedPresetName={selectedPresetName}

View File

@@ -4,7 +4,7 @@ function formatNumber(value, digits = 3) {
return Number(value ?? 0).toFixed(digits); return Number(value ?? 0).toFixed(digits);
} }
export function StatusPanels({ app, performance, runtime, video }) { export function StatusPanels({ app, audio, performance, runtime, video }) {
return ( return (
<> <>
<div className="panel panel--runtime"> <div className="panel panel--runtime">
@@ -36,6 +36,21 @@ export function StatusPanels({ app, performance, runtime, video }) {
/> />
</div> </div>
<div className="panel panel--audio">
<h2>Audio</h2>
<KvList
values={[
["Enabled", audio.enabled ? "On" : "Off"],
["Sample Rate", `${app.audioSampleRate || 0} Hz`],
["Channels", `${app.audioChannelCount || 0}`],
["Buffered", `${audio.bufferedSampleFrames || 0} samples`],
["Underruns", `${audio.underrunCount || 0}`],
["RMS L/R", `${formatNumber(audio.rms?.[0], 3)} / ${formatNumber(audio.rms?.[1], 3)}`],
["Peak L/R", `${formatNumber(audio.peak?.[0], 3)} / ${formatNumber(audio.peak?.[1], 3)}`],
]}
/>
</div>
<div className="panel panel--compiler"> <div className="panel panel--compiler">
<h2>Compiler</h2> <h2>Compiler</h2>
<pre>{runtime.compileMessage || "No compiler output."}</pre> <pre>{runtime.compileMessage || "No compiler output."}</pre>