mirror of
https://github.com/webadderallorg/Recordly.git
synced 2026-09-24 23:05:49 +00:00
fix(recording): soften native mic gap boundaries
This commit is contained in:
@@ -1,35 +1,35 @@
|
||||
{
|
||||
"version": 1,
|
||||
"platform": "win32",
|
||||
"arch": "x64",
|
||||
"helpers": {
|
||||
"wgc-capture": {
|
||||
"binaryName": "wgc-capture.exe",
|
||||
"binarySha256": "31beb9528e79262da6aa31fef09b6125153a838f957060ccc1ec5d4da844516c",
|
||||
"sourceDir": "electron/native/wgc-capture",
|
||||
"sourceFingerprint": "4bb09b770a21eb32daf6325d46d73d0522f20a4cbce9dda8b3df2277f8cfd0be",
|
||||
"updatedAt": "2026-05-05T18:41:50.078Z"
|
||||
},
|
||||
"cursor-monitor": {
|
||||
"binaryName": "cursor-monitor.exe",
|
||||
"binarySha256": "6ae6d91103b6e891a851e8ea5791e1c1f9aaab700134c18bc4c46cfffd7fdd12",
|
||||
"sourceDir": "electron/native/cursor-monitor",
|
||||
"sourceFingerprint": "6ad1b8b50bb336f2a48937b06f5ec56d90b6ab4a3e56a4bca278cf67a5d3e52e",
|
||||
"updatedAt": "2026-05-05T18:42:26.007Z"
|
||||
},
|
||||
"recordly-gpu-export": {
|
||||
"binaryName": "recordly-gpu-export.exe",
|
||||
"binarySha256": "e79cb823dd2cfade857db67696ae6456ae0ba3ae9381b6792c948ac648521bd4",
|
||||
"sourceDir": "electron/native/gpu-export-probe",
|
||||
"sourceFingerprint": "45aaa4c0cbeb45ecaa9ff0a87b870c33bd0d5b09fb5b3acef45b246970a12e7f",
|
||||
"updatedAt": "2026-05-05T18:41:54.966Z"
|
||||
},
|
||||
"recordly-nvidia-cuda-compositor": {
|
||||
"binaryName": "recordly-nvidia-cuda-compositor.exe",
|
||||
"binarySha256": "2913d78e2c1114d32c92fb4238749f7ff46131ec92cf3ad0b55af910c7cdbb07",
|
||||
"sourceDir": "electron/native/nvidia-cuda-compositor",
|
||||
"sourceFingerprint": "a60538981da7dd3d9645f59926d42f4f2f0f1f0663d3905e0416fdff8becb314",
|
||||
"updatedAt": "2026-05-05T18:42:20.123Z"
|
||||
}
|
||||
}
|
||||
"version": 1,
|
||||
"platform": "win32",
|
||||
"arch": "x64",
|
||||
"helpers": {
|
||||
"wgc-capture": {
|
||||
"binaryName": "wgc-capture.exe",
|
||||
"binarySha256": "b016da7435ba68b9c235e85818b5f4e8c1647f2386b4f2205e9a690cd057f908",
|
||||
"sourceDir": "electron/native/wgc-capture",
|
||||
"sourceFingerprint": "7ef55b0d4bb9674de2b263e9dad2de2a593d0cb6aa09aea9ab3dc1832010a9d0",
|
||||
"updatedAt": "2026-05-06T11:48:48.808Z"
|
||||
},
|
||||
"cursor-monitor": {
|
||||
"binaryName": "cursor-monitor.exe",
|
||||
"binarySha256": "6ae6d91103b6e891a851e8ea5791e1c1f9aaab700134c18bc4c46cfffd7fdd12",
|
||||
"sourceDir": "electron/native/cursor-monitor",
|
||||
"sourceFingerprint": "6ad1b8b50bb336f2a48937b06f5ec56d90b6ab4a3e56a4bca278cf67a5d3e52e",
|
||||
"updatedAt": "2026-05-05T18:42:26.007Z"
|
||||
},
|
||||
"recordly-gpu-export": {
|
||||
"binaryName": "recordly-gpu-export.exe",
|
||||
"binarySha256": "e79cb823dd2cfade857db67696ae6456ae0ba3ae9381b6792c948ac648521bd4",
|
||||
"sourceDir": "electron/native/gpu-export-probe",
|
||||
"sourceFingerprint": "45aaa4c0cbeb45ecaa9ff0a87b870c33bd0d5b09fb5b3acef45b246970a12e7f",
|
||||
"updatedAt": "2026-05-05T18:41:54.966Z"
|
||||
},
|
||||
"recordly-nvidia-cuda-compositor": {
|
||||
"binaryName": "recordly-nvidia-cuda-compositor.exe",
|
||||
"binarySha256": "2913d78e2c1114d32c92fb4238749f7ff46131ec92cf3ad0b55af910c7cdbb07",
|
||||
"sourceDir": "electron/native/nvidia-cuda-compositor",
|
||||
"sourceFingerprint": "a60538981da7dd3d9645f59926d42f4f2f0f1f0663d3905e0416fdff8becb314",
|
||||
"updatedAt": "2026-05-05T18:42:20.123Z"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
@@ -225,6 +225,10 @@ static void writeCompanionAudioTimingMetadata(
|
||||
metadataFile << "{\"startDelayMs\":" << startDelayMs;
|
||||
metadataFile << ",\"capturedDurationMs\":" << capture.capturedDurationMs();
|
||||
metadataFile << ",\"dataBytes\":" << capture.totalDataBytes();
|
||||
metadataFile << ",\"sampleRate\":" << capture.sampleRate();
|
||||
metadataFile << ",\"channels\":" << capture.channelCount();
|
||||
metadataFile << ",\"gapFillCount\":" << capture.gapFillCount();
|
||||
metadataFile << ",\"insertedSilenceFrames\":" << capture.insertedSilenceFrames();
|
||||
const uint32_t discontinuityCount = capture.dataDiscontinuityCount();
|
||||
if (discontinuityCount > 0) {
|
||||
metadataFile << ",\"dataDiscontinuityCount\":" << discontinuityCount;
|
||||
@@ -438,18 +442,32 @@ int main(int argc, char* argv[]) {
|
||||
|
||||
std::cout << "Recording stopped. Output path: " << config.outputPath << std::endl;
|
||||
if (audioActive) {
|
||||
if (loopback.dataDiscontinuityCount() > 0 || loopback.timestampErrorCount() > 0) {
|
||||
if (
|
||||
loopback.dataDiscontinuityCount() > 0 ||
|
||||
loopback.timestampErrorCount() > 0 ||
|
||||
loopback.gapFillCount() > 0
|
||||
) {
|
||||
std::cerr << "WARNING: System audio timing metadata includes discontinuities="
|
||||
<< loopback.dataDiscontinuityCount()
|
||||
<< " timestampErrors=" << loopback.timestampErrorCount() << std::endl;
|
||||
<< " timestampErrors=" << loopback.timestampErrorCount()
|
||||
<< " gapFills=" << loopback.gapFillCount()
|
||||
<< " insertedSilenceFrames=" << loopback.insertedSilenceFrames()
|
||||
<< std::endl;
|
||||
}
|
||||
std::cout << "Audio path: " << config.audioOutputPath << std::endl;
|
||||
}
|
||||
if (micActive) {
|
||||
if (micCapture.dataDiscontinuityCount() > 0 || micCapture.timestampErrorCount() > 0) {
|
||||
if (
|
||||
micCapture.dataDiscontinuityCount() > 0 ||
|
||||
micCapture.timestampErrorCount() > 0 ||
|
||||
micCapture.gapFillCount() > 0
|
||||
) {
|
||||
std::cerr << "WARNING: Microphone timing metadata includes discontinuities="
|
||||
<< micCapture.dataDiscontinuityCount()
|
||||
<< " timestampErrors=" << micCapture.timestampErrorCount() << std::endl;
|
||||
<< " timestampErrors=" << micCapture.timestampErrorCount()
|
||||
<< " gapFills=" << micCapture.gapFillCount()
|
||||
<< " insertedSilenceFrames=" << micCapture.insertedSilenceFrames()
|
||||
<< std::endl;
|
||||
}
|
||||
std::cout << "Mic path: " << config.micOutputPath << std::endl;
|
||||
}
|
||||
|
||||
@@ -3,12 +3,16 @@
|
||||
#include <iostream>
|
||||
#include <cstring>
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
#pragma comment(lib, "ole32.lib")
|
||||
|
||||
namespace {
|
||||
constexpr int64_t kHundredNanosecondsPerSecond = 10000000;
|
||||
constexpr uint64_t kSilenceWriteChunkFrames = 4096;
|
||||
constexpr uint32_t kMinimumGapFillThresholdMs = 50;
|
||||
constexpr uint32_t kDiscontinuityGapFillThresholdMs = 10;
|
||||
constexpr uint32_t kBoundaryFadeInMs = 5;
|
||||
|
||||
int64_t queryPerformanceCounterHns() {
|
||||
LARGE_INTEGER counter;
|
||||
@@ -187,6 +191,9 @@ bool WasapiCapture::start() {
|
||||
accumulatedPausedQpcHns_ = 0;
|
||||
dataDiscontinuityCount_ = 0;
|
||||
timestampErrorCount_ = 0;
|
||||
gapFillCount_ = 0;
|
||||
insertedSilenceFrames_ = 0;
|
||||
fadeInFramesRemaining_ = 0;
|
||||
writeWavHeader(outputFile_, 0);
|
||||
|
||||
HRESULT hr = audioClient_->Start();
|
||||
@@ -220,6 +227,7 @@ bool WasapiCapture::resume() {
|
||||
}
|
||||
pauseStartQpcHns_ = 0;
|
||||
paused_ = false;
|
||||
fadeInFramesRemaining_ = boundaryFadeInFrameCount();
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -242,6 +250,7 @@ void WasapiCapture::stop() {
|
||||
paused_ = false;
|
||||
pauseStartQpcHns_ = 0;
|
||||
accumulatedPausedQpcHns_ = 0;
|
||||
fadeInFramesRemaining_ = 0;
|
||||
}
|
||||
|
||||
static int16_t floatToInt16(float v) {
|
||||
@@ -282,6 +291,34 @@ void WasapiCapture::writePcmFrames(const int16_t* samples, UINT32 frameCount, WO
|
||||
}
|
||||
|
||||
const DWORD bytesToWrite = frameCount * channels * sizeof(int16_t);
|
||||
const uint32_t fadeFrames = fadeInFramesRemaining_.load();
|
||||
if (fadeFrames > 0) {
|
||||
const uint32_t totalFadeFrames = boundaryFadeInFrameCount();
|
||||
const uint32_t remainingFadeFrames = (std::min)(fadeFrames, totalFadeFrames);
|
||||
const uint32_t framesToFade =
|
||||
(std::min)(static_cast<uint32_t>(frameCount), remainingFadeFrames);
|
||||
const uint32_t completedFadeFrames = totalFadeFrames - remainingFadeFrames;
|
||||
std::vector<int16_t> faded(samples, samples + frameCount * channels);
|
||||
for (uint32_t frame = 0; frame < framesToFade; frame++) {
|
||||
const double scale =
|
||||
static_cast<double>(completedFadeFrames + frame + 1) /
|
||||
static_cast<double>(totalFadeFrames);
|
||||
for (WORD channel = 0; channel < channels; channel++) {
|
||||
const size_t index = static_cast<size_t>(frame) * channels + channel;
|
||||
const long scaled = std::lround(static_cast<double>(faded[index]) * scale);
|
||||
faded[index] = static_cast<int16_t>(
|
||||
std::clamp<long>(scaled, -32768, 32767));
|
||||
}
|
||||
}
|
||||
fadeInFramesRemaining_ = remainingFadeFrames - framesToFade;
|
||||
|
||||
DWORD written = 0;
|
||||
WriteFile(outputFile_, faded.data(), bytesToWrite, &written, nullptr);
|
||||
totalDataBytes_.fetch_add(written);
|
||||
framesWritten_.fetch_add(written / (channels * sizeof(int16_t)));
|
||||
return;
|
||||
}
|
||||
|
||||
DWORD written = 0;
|
||||
WriteFile(outputFile_, samples, bytesToWrite, &written, nullptr);
|
||||
totalDataBytes_.fetch_add(written);
|
||||
@@ -293,12 +330,15 @@ void WasapiCapture::writeSilenceFrames(uint64_t frameCount, WORD channels) {
|
||||
return;
|
||||
}
|
||||
|
||||
gapFillCount_.fetch_add(1);
|
||||
insertedSilenceFrames_.fetch_add(frameCount);
|
||||
std::vector<int16_t> silence(static_cast<size_t>(kSilenceWriteChunkFrames * channels), 0);
|
||||
while (frameCount > 0) {
|
||||
const uint64_t chunkFrames = std::min<uint64_t>(frameCount, kSilenceWriteChunkFrames);
|
||||
const uint64_t chunkFrames = (std::min)(frameCount, kSilenceWriteChunkFrames);
|
||||
writePcmFrames(silence.data(), static_cast<UINT32>(chunkFrames), channels);
|
||||
frameCount -= chunkFrames;
|
||||
}
|
||||
fadeInFramesRemaining_ = boundaryFadeInFrameCount();
|
||||
}
|
||||
|
||||
uint64_t WasapiCapture::capturedDurationMs() const {
|
||||
@@ -309,6 +349,23 @@ uint64_t WasapiCapture::capturedDurationMs() const {
|
||||
return (framesWritten_.load() * 1000) / mixFormat_->nSamplesPerSec;
|
||||
}
|
||||
|
||||
uint32_t WasapiCapture::sampleRate() const {
|
||||
return mixFormat_ ? mixFormat_->nSamplesPerSec : 0;
|
||||
}
|
||||
|
||||
uint16_t WasapiCapture::channelCount() const {
|
||||
return mixFormat_ ? mixFormat_->nChannels : 0;
|
||||
}
|
||||
|
||||
uint32_t WasapiCapture::boundaryFadeInFrameCount() const {
|
||||
const uint32_t rate = sampleRate();
|
||||
if (rate == 0) {
|
||||
return 1;
|
||||
}
|
||||
const uint32_t fadeFrames = (rate * kBoundaryFadeInMs) / 1000;
|
||||
return fadeFrames > 0 ? fadeFrames : 1;
|
||||
}
|
||||
|
||||
void WasapiCapture::captureThread() {
|
||||
// COM must be initialized on every thread that uses COM objects.
|
||||
// The main thread calls winrt::init_apartment(MTA) but that only covers
|
||||
@@ -362,7 +419,9 @@ void WasapiCapture::captureThread() {
|
||||
break;
|
||||
}
|
||||
|
||||
if ((flags & AUDCLNT_BUFFERFLAGS_DATA_DISCONTINUITY) != 0) {
|
||||
const bool hasDataDiscontinuity =
|
||||
(flags & AUDCLNT_BUFFERFLAGS_DATA_DISCONTINUITY) != 0;
|
||||
if (hasDataDiscontinuity) {
|
||||
dataDiscontinuityCount_.fetch_add(1);
|
||||
}
|
||||
if ((flags & AUDCLNT_BUFFERFLAGS_TIMESTAMP_ERROR) != 0) {
|
||||
@@ -385,15 +444,22 @@ void WasapiCapture::captureThread() {
|
||||
) {
|
||||
const int64_t elapsedHns =
|
||||
static_cast<int64_t>(qpcPosition) - firstPacketQpcHns;
|
||||
const int64_t adjustedElapsedHns = std::max<int64_t>(
|
||||
0,
|
||||
elapsedHns - accumulatedPausedQpcHns_.load());
|
||||
const int64_t adjustedElapsedHns =
|
||||
elapsedHns > accumulatedPausedQpcHns_.load()
|
||||
? elapsedHns - accumulatedPausedQpcHns_.load()
|
||||
: 0;
|
||||
const uint64_t expectedStartFrame =
|
||||
(static_cast<uint64_t>(adjustedElapsedHns) * mixFormat_->nSamplesPerSec +
|
||||
kHundredNanosecondsPerSecond / 2) /
|
||||
kHundredNanosecondsPerSecond;
|
||||
const uint64_t writtenFrames = framesWritten_.load();
|
||||
const uint64_t gapThresholdFrames = mixFormat_->nSamplesPerSec / 100;
|
||||
const uint32_t gapThresholdMs = hasDataDiscontinuity
|
||||
? kDiscontinuityGapFillThresholdMs
|
||||
: kMinimumGapFillThresholdMs;
|
||||
const uint32_t gapThresholdFrameCount =
|
||||
(mixFormat_->nSamplesPerSec * gapThresholdMs) / 1000;
|
||||
const uint64_t gapThresholdFrames =
|
||||
gapThresholdFrameCount > 0 ? gapThresholdFrameCount : 1;
|
||||
|
||||
if (expectedStartFrame > writtenFrames + gapThresholdFrames) {
|
||||
writeSilenceFrames(expectedStartFrame - writtenFrames, channels);
|
||||
|
||||
@@ -22,8 +22,12 @@ public:
|
||||
int64_t firstPacketQpcHns() const { return firstPacketQpcHns_.load(); }
|
||||
uint64_t capturedDurationMs() const;
|
||||
uint64_t totalDataBytes() const { return totalDataBytes_.load(); }
|
||||
uint32_t sampleRate() const;
|
||||
uint16_t channelCount() const;
|
||||
uint32_t dataDiscontinuityCount() const { return dataDiscontinuityCount_.load(); }
|
||||
uint32_t timestampErrorCount() const { return timestampErrorCount_.load(); }
|
||||
uint32_t gapFillCount() const { return gapFillCount_.load(); }
|
||||
uint64_t insertedSilenceFrames() const { return insertedSilenceFrames_.load(); }
|
||||
|
||||
private:
|
||||
bool initializeCommon();
|
||||
@@ -31,6 +35,7 @@ private:
|
||||
bool writeWavHeader(HANDLE file, DWORD dataSize);
|
||||
void writePcmFrames(const int16_t* samples, UINT32 frameCount, WORD channels);
|
||||
void writeSilenceFrames(uint64_t frameCount, WORD channels);
|
||||
uint32_t boundaryFadeInFrameCount() const;
|
||||
IMMDevice* findCaptureDeviceByName(const std::wstring& name);
|
||||
|
||||
std::string outputPath_;
|
||||
@@ -54,4 +59,7 @@ private:
|
||||
std::atomic<int64_t> accumulatedPausedQpcHns_{0};
|
||||
std::atomic<uint32_t> dataDiscontinuityCount_{0};
|
||||
std::atomic<uint32_t> timestampErrorCount_{0};
|
||||
std::atomic<uint32_t> gapFillCount_{0};
|
||||
std::atomic<uint64_t> insertedSilenceFrames_{0};
|
||||
std::atomic<uint32_t> fadeInFramesRemaining_{0};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user