mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-04-03 09:46:54 +02:00
Adds a video trim dialog to ZoomIt (#45334)
## Summary of the Pull Request Adds a video trim dialog to ZoomIt ## PR Checklist Closes 45333 ## Validation Steps Performed Manual validation --------- Co-authored-by: Mark Russinovich <markruss@ntdev.microsoft.com> Co-authored-by: foxmsft <foxmsft@hotmail.com>
This commit is contained in:
63
.github/actions/spell-check/allow/zoomit.txt
vendored
Normal file
63
.github/actions/spell-check/allow/zoomit.txt
vendored
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
acq
|
||||||
|
APPLYTOSUBMENUS
|
||||||
|
AUDCLNT
|
||||||
|
bitmaps
|
||||||
|
BUFFERFLAGS
|
||||||
|
centiseconds
|
||||||
|
Ctl
|
||||||
|
CTLCOLOR
|
||||||
|
CTLCOLORBTN
|
||||||
|
CTLCOLORDLG
|
||||||
|
CTLCOLOREDIT
|
||||||
|
CTLCOLORLISTBOX
|
||||||
|
CTrim
|
||||||
|
DFCS
|
||||||
|
dlg
|
||||||
|
dlu
|
||||||
|
DONTCARE
|
||||||
|
DRAWITEM
|
||||||
|
DRAWITEMSTRUCT
|
||||||
|
DWLP
|
||||||
|
EDITCONTROL
|
||||||
|
ENABLEHOOK
|
||||||
|
FDE
|
||||||
|
GETCHANNELRECT
|
||||||
|
GETCHECK
|
||||||
|
GETTHUMBRECT
|
||||||
|
GIFs
|
||||||
|
HTBOTTOMRIGHT
|
||||||
|
HTHEME
|
||||||
|
KSDATAFORMAT
|
||||||
|
LEFTNOWORDWRAP
|
||||||
|
letterbox
|
||||||
|
lld
|
||||||
|
logfont
|
||||||
|
lround
|
||||||
|
MENUINFO
|
||||||
|
mic
|
||||||
|
MMRESULT
|
||||||
|
OWNERDRAW
|
||||||
|
PBGRA
|
||||||
|
pfdc
|
||||||
|
playhead
|
||||||
|
pwfx
|
||||||
|
quantums
|
||||||
|
REFKNOWNFOLDERID
|
||||||
|
reposted
|
||||||
|
SCROLLSIZEGRIP
|
||||||
|
SETDEFID
|
||||||
|
SETRECT
|
||||||
|
SHAREMODE
|
||||||
|
SHAREVIOLATION
|
||||||
|
STREAMFLAGS
|
||||||
|
submix
|
||||||
|
tci
|
||||||
|
TEXTMETRIC
|
||||||
|
tme
|
||||||
|
TRACKMOUSEEVENT
|
||||||
|
Unadvise
|
||||||
|
WASAPI
|
||||||
|
WAVEFORMATEX
|
||||||
|
WAVEFORMATEXTENSIBLE
|
||||||
|
wil
|
||||||
|
WMU
|
||||||
@@ -1,9 +1,24 @@
|
|||||||
#include "pch.h"
|
#include "pch.h"
|
||||||
#include "AudioSampleGenerator.h"
|
#include "AudioSampleGenerator.h"
|
||||||
#include "CaptureFrameWait.h"
|
#include "CaptureFrameWait.h"
|
||||||
|
#include "LoopbackCapture.h"
|
||||||
|
#include <wrl/client.h>
|
||||||
|
|
||||||
extern TCHAR g_MicrophoneDeviceId[];
|
extern TCHAR g_MicrophoneDeviceId[];
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
// Declare the IMemoryBufferByteAccess interface for accessing raw buffer data
|
||||||
|
MIDL_INTERFACE("5b0d3235-4dba-4d44-8657-1f1d0f83e9a3")
|
||||||
|
IMemoryBufferByteAccess : public IUnknown
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
virtual HRESULT STDMETHODCALLTYPE GetBuffer(
|
||||||
|
BYTE** value,
|
||||||
|
UINT32* capacity) = 0;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
namespace winrt
|
namespace winrt
|
||||||
{
|
{
|
||||||
using namespace Windows::Foundation;
|
using namespace Windows::Foundation;
|
||||||
@@ -19,17 +34,23 @@ namespace winrt
|
|||||||
using namespace Windows::Devices::Enumeration;
|
using namespace Windows::Devices::Enumeration;
|
||||||
}
|
}
|
||||||
|
|
||||||
AudioSampleGenerator::AudioSampleGenerator()
|
AudioSampleGenerator::AudioSampleGenerator(bool captureMicrophone, bool captureSystemAudio)
|
||||||
|
: m_captureMicrophone(captureMicrophone)
|
||||||
|
, m_captureSystemAudio(captureSystemAudio)
|
||||||
{
|
{
|
||||||
|
OutputDebugStringA(("AudioSampleGenerator created, captureMicrophone=" +
|
||||||
|
std::string(captureMicrophone ? "true" : "false") +
|
||||||
|
", captureSystemAudio=" + std::string(captureSystemAudio ? "true" : "false") + "\n").c_str());
|
||||||
m_audioEvent.create(wil::EventOptions::ManualReset);
|
m_audioEvent.create(wil::EventOptions::ManualReset);
|
||||||
m_endEvent.create(wil::EventOptions::ManualReset);
|
m_endEvent.create(wil::EventOptions::ManualReset);
|
||||||
|
m_startEvent.create(wil::EventOptions::ManualReset);
|
||||||
m_asyncInitialized.create(wil::EventOptions::ManualReset);
|
m_asyncInitialized.create(wil::EventOptions::ManualReset);
|
||||||
}
|
}
|
||||||
|
|
||||||
AudioSampleGenerator::~AudioSampleGenerator()
|
AudioSampleGenerator::~AudioSampleGenerator()
|
||||||
{
|
{
|
||||||
Stop();
|
Stop();
|
||||||
if (m_started.load())
|
if (m_audioGraph)
|
||||||
{
|
{
|
||||||
m_audioGraph.Close();
|
m_audioGraph.Close();
|
||||||
}
|
}
|
||||||
@@ -40,6 +61,10 @@ winrt::IAsyncAction AudioSampleGenerator::InitializeAsync()
|
|||||||
auto expected = false;
|
auto expected = false;
|
||||||
if (m_initialized.compare_exchange_strong(expected, true))
|
if (m_initialized.compare_exchange_strong(expected, true))
|
||||||
{
|
{
|
||||||
|
// Reset state in case this instance is reused.
|
||||||
|
m_endEvent.ResetEvent();
|
||||||
|
m_startEvent.ResetEvent();
|
||||||
|
|
||||||
// Initialize the audio graph
|
// Initialize the audio graph
|
||||||
auto audioGraphSettings = winrt::AudioGraphSettings(winrt::AudioRenderCategory::Media);
|
auto audioGraphSettings = winrt::AudioGraphSettings(winrt::AudioRenderCategory::Media);
|
||||||
auto audioGraphResult = co_await winrt::AudioGraph::CreateAsync(audioGraphSettings);
|
auto audioGraphResult = co_await winrt::AudioGraph::CreateAsync(audioGraphSettings);
|
||||||
@@ -49,28 +74,88 @@ winrt::IAsyncAction AudioSampleGenerator::InitializeAsync()
|
|||||||
}
|
}
|
||||||
m_audioGraph = audioGraphResult.Graph();
|
m_audioGraph = audioGraphResult.Graph();
|
||||||
|
|
||||||
// Initialize the selected microphone
|
// Get AudioGraph encoding properties for resampling
|
||||||
auto defaultMicrophoneId = winrt::MediaDevice::GetDefaultAudioCaptureId(winrt::AudioDeviceRole::Default);
|
auto graphProps = m_audioGraph.EncodingProperties();
|
||||||
auto microphoneId = (g_MicrophoneDeviceId[0] == 0) ? defaultMicrophoneId : winrt::to_hstring(g_MicrophoneDeviceId);
|
m_graphSampleRate = graphProps.SampleRate();
|
||||||
auto microphone = co_await winrt::DeviceInformation::CreateFromIdAsync(microphoneId);
|
m_graphChannels = graphProps.ChannelCount();
|
||||||
|
|
||||||
// Initialize audio input and output nodes
|
OutputDebugStringA(("AudioGraph initialized: " + std::to_string(m_graphSampleRate) +
|
||||||
auto inputNodeResult = co_await m_audioGraph.CreateDeviceInputNodeAsync(winrt::MediaCategory::Media, m_audioGraph.EncodingProperties(), microphone);
|
" Hz, " + std::to_string(m_graphChannels) + " ch\n").c_str());
|
||||||
if (inputNodeResult.Status() != winrt::AudioDeviceNodeCreationStatus::Success && microphoneId != defaultMicrophoneId)
|
|
||||||
{
|
// Create submix node to mix microphone and loopback audio
|
||||||
// If the selected microphone failed, try again with the default
|
m_submixNode = m_audioGraph.CreateSubmixNode();
|
||||||
microphone = co_await winrt::DeviceInformation::CreateFromIdAsync(defaultMicrophoneId);
|
|
||||||
inputNodeResult = co_await m_audioGraph.CreateDeviceInputNodeAsync(winrt::MediaCategory::Media, m_audioGraph.EncodingProperties(), microphone);
|
|
||||||
}
|
|
||||||
if (inputNodeResult.Status() != winrt::AudioDeviceNodeCreationStatus::Success)
|
|
||||||
{
|
|
||||||
throw winrt::hresult_error(E_FAIL, L"Failed to initialize input audio node!");
|
|
||||||
}
|
|
||||||
m_audioInputNode = inputNodeResult.DeviceInputNode();
|
|
||||||
m_audioOutputNode = m_audioGraph.CreateFrameOutputNode();
|
m_audioOutputNode = m_audioGraph.CreateFrameOutputNode();
|
||||||
|
m_submixNode.AddOutgoingConnection(m_audioOutputNode);
|
||||||
|
|
||||||
|
// Initialize WASAPI loopback capture for system audio (if enabled)
|
||||||
|
if (m_captureSystemAudio)
|
||||||
|
{
|
||||||
|
m_loopbackCapture = std::make_unique<LoopbackCapture>();
|
||||||
|
}
|
||||||
|
if (m_loopbackCapture && SUCCEEDED(m_loopbackCapture->Initialize()))
|
||||||
|
{
|
||||||
|
auto loopbackFormat = m_loopbackCapture->GetFormat();
|
||||||
|
if (loopbackFormat)
|
||||||
|
{
|
||||||
|
m_loopbackChannels = loopbackFormat->nChannels;
|
||||||
|
m_loopbackSampleRate = loopbackFormat->nSamplesPerSec;
|
||||||
|
m_resampleRatio = static_cast<double>(m_loopbackSampleRate) / static_cast<double>(m_graphSampleRate);
|
||||||
|
|
||||||
|
OutputDebugStringA(("Loopback initialized: " + std::to_string(m_loopbackSampleRate) +
|
||||||
|
" Hz, " + std::to_string(m_loopbackChannels) + " ch, resample ratio=" +
|
||||||
|
std::to_string(m_resampleRatio) + "\n").c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (m_captureSystemAudio)
|
||||||
|
{
|
||||||
|
OutputDebugStringA("WARNING: Failed to initialize loopback capture\n");
|
||||||
|
m_loopbackCapture.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always initialize a microphone input node to keep the AudioGraph running at real-time pace.
|
||||||
|
// When mic capture is disabled, we mute it so only loopback audio is captured.
|
||||||
|
{
|
||||||
|
auto defaultMicrophoneId = winrt::MediaDevice::GetDefaultAudioCaptureId(winrt::AudioDeviceRole::Default);
|
||||||
|
auto microphoneId = (m_captureMicrophone && g_MicrophoneDeviceId[0] != 0)
|
||||||
|
? winrt::to_hstring(g_MicrophoneDeviceId)
|
||||||
|
: defaultMicrophoneId;
|
||||||
|
if (!microphoneId.empty())
|
||||||
|
{
|
||||||
|
auto microphone = co_await winrt::DeviceInformation::CreateFromIdAsync(microphoneId);
|
||||||
|
|
||||||
|
// Initialize audio input node
|
||||||
|
auto inputNodeResult = co_await m_audioGraph.CreateDeviceInputNodeAsync(winrt::MediaCategory::Media, m_audioGraph.EncodingProperties(), microphone);
|
||||||
|
if (inputNodeResult.Status() != winrt::AudioDeviceNodeCreationStatus::Success && microphoneId != defaultMicrophoneId)
|
||||||
|
{
|
||||||
|
// If the selected microphone failed, try again with the default
|
||||||
|
microphone = co_await winrt::DeviceInformation::CreateFromIdAsync(defaultMicrophoneId);
|
||||||
|
inputNodeResult = co_await m_audioGraph.CreateDeviceInputNodeAsync(winrt::MediaCategory::Media, m_audioGraph.EncodingProperties(), microphone);
|
||||||
|
}
|
||||||
|
if (inputNodeResult.Status() == winrt::AudioDeviceNodeCreationStatus::Success)
|
||||||
|
{
|
||||||
|
m_audioInputNode = inputNodeResult.DeviceInputNode();
|
||||||
|
m_audioInputNode.AddOutgoingConnection(m_submixNode);
|
||||||
|
|
||||||
|
// If mic capture is disabled, mute the input so only loopback is captured
|
||||||
|
if (!m_captureMicrophone)
|
||||||
|
{
|
||||||
|
m_audioInputNode.OutgoingGain(0.0);
|
||||||
|
OutputDebugStringA("Mic input created but muted (loopback-only mode)\n");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
OutputDebugStringA("Mic input created and active\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Loopback capture is only required when system audio capture is enabled
|
||||||
|
if (m_captureSystemAudio && !m_loopbackCapture)
|
||||||
|
{
|
||||||
|
throw winrt::hresult_error(E_FAIL, L"Failed to initialize loopback audio capture!");
|
||||||
|
}
|
||||||
|
|
||||||
// Hookup audio nodes
|
|
||||||
m_audioInputNode.AddOutgoingConnection(m_audioOutputNode);
|
|
||||||
m_audioGraph.QuantumStarted({ this, &AudioSampleGenerator::OnAudioQuantumStarted });
|
m_audioGraph.QuantumStarted({ this, &AudioSampleGenerator::OnAudioQuantumStarted });
|
||||||
|
|
||||||
m_asyncInitialized.SetEvent();
|
m_asyncInitialized.SetEvent();
|
||||||
@@ -86,7 +171,37 @@ winrt::AudioEncodingProperties AudioSampleGenerator::GetEncodingProperties()
|
|||||||
std::optional<winrt::MediaStreamSample> AudioSampleGenerator::TryGetNextSample()
|
std::optional<winrt::MediaStreamSample> AudioSampleGenerator::TryGetNextSample()
|
||||||
{
|
{
|
||||||
CheckInitialized();
|
CheckInitialized();
|
||||||
CheckStarted();
|
|
||||||
|
// The MediaStreamSource can request audio samples before we've started the audio graph.
|
||||||
|
// Instead of throwing (which crashes the app), wait until either Start() is called
|
||||||
|
// or Stop() signals end-of-stream.
|
||||||
|
if (!m_started.load())
|
||||||
|
{
|
||||||
|
std::vector<HANDLE> events = { m_endEvent.get(), m_startEvent.get() };
|
||||||
|
auto waitResult = WaitForMultipleObjectsEx(static_cast<DWORD>(events.size()), events.data(), false, INFINITE, false);
|
||||||
|
auto eventIndex = -1;
|
||||||
|
switch (waitResult)
|
||||||
|
{
|
||||||
|
case WAIT_OBJECT_0:
|
||||||
|
case WAIT_OBJECT_0 + 1:
|
||||||
|
eventIndex = waitResult - WAIT_OBJECT_0;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
WINRT_VERIFY(eventIndex >= 0);
|
||||||
|
|
||||||
|
if (events[eventIndex] == m_endEvent.get())
|
||||||
|
{
|
||||||
|
// End event signaled, but check if there are any remaining samples in the queue
|
||||||
|
auto lock = m_lock.lock_exclusive();
|
||||||
|
if (!m_samples.empty())
|
||||||
|
{
|
||||||
|
std::optional result(m_samples.front());
|
||||||
|
m_samples.pop_front();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
auto lock = m_lock.lock_exclusive();
|
auto lock = m_lock.lock_exclusive();
|
||||||
@@ -118,11 +233,25 @@ std::optional<winrt::MediaStreamSample> AudioSampleGenerator::TryGetNextSample()
|
|||||||
auto signaledEvent = events[eventIndex];
|
auto signaledEvent = events[eventIndex];
|
||||||
if (signaledEvent == m_endEvent.get())
|
if (signaledEvent == m_endEvent.get())
|
||||||
{
|
{
|
||||||
|
// End was signaled, but check for any remaining samples before returning nullopt
|
||||||
|
auto lock = m_lock.lock_exclusive();
|
||||||
|
if (!m_samples.empty())
|
||||||
|
{
|
||||||
|
std::optional result(m_samples.front());
|
||||||
|
m_samples.pop_front();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
auto lock = m_lock.lock_exclusive();
|
auto lock = m_lock.lock_exclusive();
|
||||||
|
if (m_samples.empty())
|
||||||
|
{
|
||||||
|
// Spurious wake or race - no samples available
|
||||||
|
// If end is signaled, return nullopt
|
||||||
|
return m_endEvent.is_signaled() ? std::nullopt : std::optional<winrt::MediaStreamSample>{};
|
||||||
|
}
|
||||||
std::optional result(m_samples.front());
|
std::optional result(m_samples.front());
|
||||||
m_samples.pop_front();
|
m_samples.pop_front();
|
||||||
return result;
|
return result;
|
||||||
@@ -135,23 +264,349 @@ void AudioSampleGenerator::Start()
|
|||||||
auto expected = false;
|
auto expected = false;
|
||||||
if (m_started.compare_exchange_strong(expected, true))
|
if (m_started.compare_exchange_strong(expected, true))
|
||||||
{
|
{
|
||||||
|
m_endEvent.ResetEvent();
|
||||||
|
m_startEvent.SetEvent();
|
||||||
|
|
||||||
|
// Start loopback capture if available
|
||||||
|
if (m_loopbackCapture)
|
||||||
|
{
|
||||||
|
// Clear any stale samples
|
||||||
|
{
|
||||||
|
auto lock = m_loopbackBufferLock.lock_exclusive();
|
||||||
|
m_loopbackBuffer.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
m_resampleInputBuffer.clear();
|
||||||
|
m_resampleInputPos = 0.0;
|
||||||
|
|
||||||
|
m_loopbackCapture->Start();
|
||||||
|
}
|
||||||
|
|
||||||
m_audioGraph.Start();
|
m_audioGraph.Start();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void AudioSampleGenerator::Stop()
|
void AudioSampleGenerator::Stop()
|
||||||
{
|
{
|
||||||
CheckInitialized();
|
// Stop may be called during teardown even if initialization hasn't completed.
|
||||||
if (m_started.load())
|
// It must never throw.
|
||||||
|
|
||||||
|
if (!m_initialized.load())
|
||||||
{
|
{
|
||||||
m_asyncInitialized.wait();
|
|
||||||
m_audioGraph.Stop();
|
|
||||||
m_endEvent.SetEvent();
|
m_endEvent.SetEvent();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
m_asyncInitialized.wait();
|
||||||
|
|
||||||
|
// Stop loopback capture first
|
||||||
|
if (m_loopbackCapture)
|
||||||
|
{
|
||||||
|
m_loopbackCapture->Stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flush any remaining samples from the loopback capture before stopping the audio graph
|
||||||
|
FlushRemainingAudio();
|
||||||
|
|
||||||
|
// Stop the audio graph - no more quantum callbacks will run
|
||||||
|
m_audioGraph.Stop();
|
||||||
|
|
||||||
|
// Mark as stopped
|
||||||
|
m_started.store(false);
|
||||||
|
|
||||||
|
// Combine all remaining queued samples into one final sample so it can be
|
||||||
|
// returned immediately without waiting for additional TryGetNextSample calls
|
||||||
|
CombineQueuedSamples();
|
||||||
|
|
||||||
|
// NOW signal end event - this allows TryGetNextSample to return remaining
|
||||||
|
// queued samples and then return nullopt
|
||||||
|
m_endEvent.SetEvent();
|
||||||
|
m_audioEvent.SetEvent(); // Also wake any waiting TryGetNextSample
|
||||||
|
|
||||||
|
// DO NOT clear m_loopbackBuffer or m_samples here - allow MediaTranscoder to
|
||||||
|
// consume remaining queued audio samples to avoid audio cutoff at end of recording.
|
||||||
|
// TryGetNextSample() will return nullopt once m_samples is empty and
|
||||||
|
// m_endEvent is signaled. Buffers will be cleaned up on destruction.
|
||||||
|
}
|
||||||
|
|
||||||
|
void AudioSampleGenerator::AppendResampledLoopbackSamples(std::vector<float> const& rawLoopbackSamples, bool flushRemaining)
|
||||||
|
{
|
||||||
|
if (rawLoopbackSamples.empty())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_resampleInputBuffer.insert(m_resampleInputBuffer.end(), rawLoopbackSamples.begin(), rawLoopbackSamples.end());
|
||||||
|
|
||||||
|
if (m_loopbackChannels == 0 || m_graphChannels == 0 || m_resampleRatio <= 0.0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<float> resampledSamples;
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
const uint32_t inputFrames = static_cast<uint32_t>(m_resampleInputBuffer.size() / m_loopbackChannels);
|
||||||
|
if (inputFrames == 0)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!flushRemaining)
|
||||||
|
{
|
||||||
|
if (inputFrames < 2 || (m_resampleInputPos + 1.0) >= inputFrames)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (m_resampleInputPos >= inputFrames)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t inputFrame = static_cast<uint32_t>(m_resampleInputPos);
|
||||||
|
double frac = m_resampleInputPos - inputFrame;
|
||||||
|
uint32_t nextFrame = (inputFrame + 1 < inputFrames) ? (inputFrame + 1) : inputFrame;
|
||||||
|
|
||||||
|
for (uint32_t outCh = 0; outCh < m_graphChannels; outCh++)
|
||||||
|
{
|
||||||
|
float sample = 0.0f;
|
||||||
|
|
||||||
|
if (m_loopbackChannels == m_graphChannels)
|
||||||
|
{
|
||||||
|
uint32_t idx1 = inputFrame * m_loopbackChannels + outCh;
|
||||||
|
uint32_t idx2 = nextFrame * m_loopbackChannels + outCh;
|
||||||
|
float s1 = m_resampleInputBuffer[idx1];
|
||||||
|
float s2 = m_resampleInputBuffer[idx2];
|
||||||
|
sample = static_cast<float>(s1 * (1.0 - frac) + s2 * frac);
|
||||||
|
}
|
||||||
|
else if (m_loopbackChannels > m_graphChannels)
|
||||||
|
{
|
||||||
|
float sum = 0.0f;
|
||||||
|
for (uint32_t inCh = 0; inCh < m_loopbackChannels; inCh++)
|
||||||
|
{
|
||||||
|
uint32_t idx1 = inputFrame * m_loopbackChannels + inCh;
|
||||||
|
uint32_t idx2 = nextFrame * m_loopbackChannels + inCh;
|
||||||
|
float s1 = m_resampleInputBuffer[idx1];
|
||||||
|
float s2 = m_resampleInputBuffer[idx2];
|
||||||
|
sum += static_cast<float>(s1 * (1.0 - frac) + s2 * frac);
|
||||||
|
}
|
||||||
|
sample = sum / m_loopbackChannels;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
uint32_t idx1 = inputFrame * m_loopbackChannels;
|
||||||
|
uint32_t idx2 = nextFrame * m_loopbackChannels;
|
||||||
|
float s1 = m_resampleInputBuffer[idx1];
|
||||||
|
float s2 = m_resampleInputBuffer[idx2];
|
||||||
|
sample = static_cast<float>(s1 * (1.0 - frac) + s2 * frac);
|
||||||
|
}
|
||||||
|
|
||||||
|
resampledSamples.push_back(sample);
|
||||||
|
}
|
||||||
|
|
||||||
|
m_resampleInputPos += m_resampleRatio;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t consumedFrames = static_cast<uint32_t>(m_resampleInputPos);
|
||||||
|
if (consumedFrames > 0)
|
||||||
|
{
|
||||||
|
size_t samplesToErase = static_cast<size_t>(consumedFrames) * m_loopbackChannels;
|
||||||
|
if (samplesToErase >= m_resampleInputBuffer.size())
|
||||||
|
{
|
||||||
|
m_resampleInputBuffer.clear();
|
||||||
|
m_resampleInputPos = 0.0;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
m_resampleInputBuffer.erase(m_resampleInputBuffer.begin(), m_resampleInputBuffer.begin() + samplesToErase);
|
||||||
|
m_resampleInputPos -= consumedFrames;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (flushRemaining)
|
||||||
|
{
|
||||||
|
m_resampleInputBuffer.clear();
|
||||||
|
m_resampleInputPos = 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!resampledSamples.empty())
|
||||||
|
{
|
||||||
|
auto loopbackLock = m_loopbackBufferLock.lock_exclusive();
|
||||||
|
const size_t maxBufferSize = static_cast<size_t>(m_graphSampleRate) * m_graphChannels;
|
||||||
|
|
||||||
|
if (m_loopbackBuffer.size() + resampledSamples.size() > maxBufferSize)
|
||||||
|
{
|
||||||
|
size_t overflow = (m_loopbackBuffer.size() + resampledSamples.size()) - maxBufferSize;
|
||||||
|
if (overflow >= m_loopbackBuffer.size())
|
||||||
|
{
|
||||||
|
m_loopbackBuffer.clear();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
m_loopbackBuffer.erase(m_loopbackBuffer.begin(), m_loopbackBuffer.begin() + overflow);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
m_loopbackBuffer.insert(m_loopbackBuffer.end(), resampledSamples.begin(), resampledSamples.end());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void AudioSampleGenerator::FlushRemainingAudio()
|
||||||
|
{
|
||||||
|
// Called during stop to drain any remaining samples from loopback capture
|
||||||
|
// and convert them to MediaStreamSamples before the audio graph stops.
|
||||||
|
|
||||||
|
if (!m_loopbackCapture)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto lock = m_lock.lock_exclusive();
|
||||||
|
|
||||||
|
// Drain all remaining samples from the loopback capture client
|
||||||
|
std::vector<float> rawLoopbackSamples;
|
||||||
|
{
|
||||||
|
std::vector<float> tempSamples;
|
||||||
|
while (m_loopbackCapture->TryGetSamples(tempSamples))
|
||||||
|
{
|
||||||
|
rawLoopbackSamples.insert(rawLoopbackSamples.end(), tempSamples.begin(), tempSamples.end());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resample and channel-convert the loopback audio to match AudioGraph format
|
||||||
|
if (!rawLoopbackSamples.empty())
|
||||||
|
{
|
||||||
|
AppendResampledLoopbackSamples(rawLoopbackSamples, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now convert everything in m_loopbackBuffer to MediaStreamSamples
|
||||||
|
auto loopbackLock = m_loopbackBufferLock.lock_exclusive();
|
||||||
|
|
||||||
|
if (!m_loopbackBuffer.empty())
|
||||||
|
{
|
||||||
|
uint32_t outputSampleCount = static_cast<uint32_t>(m_loopbackBuffer.size());
|
||||||
|
std::vector<uint8_t> outputData(outputSampleCount * sizeof(float), 0);
|
||||||
|
float* outputFloats = reinterpret_cast<float*>(outputData.data());
|
||||||
|
|
||||||
|
for (uint32_t i = 0; i < outputSampleCount; i++)
|
||||||
|
{
|
||||||
|
float sample = m_loopbackBuffer[i];
|
||||||
|
if (sample > 1.0f) sample = 1.0f;
|
||||||
|
else if (sample < -1.0f) sample = -1.0f;
|
||||||
|
outputFloats[i] = sample;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_loopbackBuffer.clear();
|
||||||
|
|
||||||
|
// Create buffer and sample
|
||||||
|
winrt::Buffer sampleBuffer(outputSampleCount * sizeof(float));
|
||||||
|
memcpy(sampleBuffer.data(), outputData.data(), outputData.size());
|
||||||
|
sampleBuffer.Length(static_cast<uint32_t>(outputData.size()));
|
||||||
|
|
||||||
|
if (sampleBuffer.Length() > 0)
|
||||||
|
{
|
||||||
|
const uint32_t sampleCount = sampleBuffer.Length() / sizeof(float);
|
||||||
|
const uint32_t frames = (m_graphChannels > 0) ? (sampleCount / m_graphChannels) : 0;
|
||||||
|
const int64_t durationTicks = (m_graphSampleRate > 0) ? (static_cast<int64_t>(frames) * 10000000LL / m_graphSampleRate) : 0;
|
||||||
|
const winrt::TimeSpan duration{ durationTicks };
|
||||||
|
|
||||||
|
winrt::TimeSpan timestamp{ 0 };
|
||||||
|
if (m_hasLastSampleTimestamp)
|
||||||
|
{
|
||||||
|
timestamp = winrt::TimeSpan{ m_lastSampleTimestamp.count() + m_lastSampleDuration.count() };
|
||||||
|
}
|
||||||
|
|
||||||
|
auto sample = winrt::MediaStreamSample::CreateFromBuffer(sampleBuffer, timestamp);
|
||||||
|
m_samples.push_back(sample);
|
||||||
|
m_audioEvent.SetEvent();
|
||||||
|
|
||||||
|
m_lastSampleTimestamp = timestamp;
|
||||||
|
m_lastSampleDuration = duration;
|
||||||
|
m_hasLastSampleTimestamp = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void AudioSampleGenerator::CombineQueuedSamples()
|
||||||
|
{
|
||||||
|
// Combine all queued samples into a single sample so it can be returned
|
||||||
|
// immediately in the next TryGetNextSample call. This is critical because
|
||||||
|
// once video ends, the MediaTranscoder may only request one more audio sample.
|
||||||
|
|
||||||
|
auto lock = m_lock.lock_exclusive();
|
||||||
|
|
||||||
|
if (m_samples.size() <= 1)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate total size and collect all sample data
|
||||||
|
size_t totalBytes = 0;
|
||||||
|
std::vector<std::pair<winrt::Windows::Storage::Streams::IBuffer, winrt::Windows::Foundation::TimeSpan>> buffers;
|
||||||
|
winrt::Windows::Foundation::TimeSpan firstTimestamp{ 0 };
|
||||||
|
bool hasFirstTimestamp = false;
|
||||||
|
|
||||||
|
for (auto& sample : m_samples)
|
||||||
|
{
|
||||||
|
auto buffer = sample.Buffer();
|
||||||
|
if (buffer)
|
||||||
|
{
|
||||||
|
totalBytes += buffer.Length();
|
||||||
|
if (!hasFirstTimestamp)
|
||||||
|
{
|
||||||
|
firstTimestamp = sample.Timestamp();
|
||||||
|
hasFirstTimestamp = true;
|
||||||
|
}
|
||||||
|
buffers.push_back({ buffer, sample.Timestamp() });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (totalBytes == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create combined buffer
|
||||||
|
winrt::Buffer combinedBuffer(static_cast<uint32_t>(totalBytes));
|
||||||
|
uint8_t* dest = combinedBuffer.data();
|
||||||
|
uint32_t offset = 0;
|
||||||
|
|
||||||
|
for (auto& [buffer, ts] : buffers)
|
||||||
|
{
|
||||||
|
uint32_t len = buffer.Length();
|
||||||
|
memcpy(dest + offset, buffer.data(), len);
|
||||||
|
offset += len;
|
||||||
|
}
|
||||||
|
combinedBuffer.Length(static_cast<uint32_t>(totalBytes));
|
||||||
|
|
||||||
|
// Create combined sample with first timestamp
|
||||||
|
auto combinedSample = winrt::Windows::Media::Core::MediaStreamSample::CreateFromBuffer(combinedBuffer, firstTimestamp);
|
||||||
|
|
||||||
|
// Clear queue and add combined sample
|
||||||
|
m_samples.clear();
|
||||||
|
m_samples.push_back(combinedSample);
|
||||||
|
|
||||||
|
// Update timestamp tracking
|
||||||
|
const uint32_t sampleCount = static_cast<uint32_t>(totalBytes) / sizeof(float);
|
||||||
|
const uint32_t frames = (m_graphChannels > 0) ? (sampleCount / m_graphChannels) : 0;
|
||||||
|
const int64_t durationTicks = (m_graphSampleRate > 0) ? (static_cast<int64_t>(frames) * 10000000LL / m_graphSampleRate) : 0;
|
||||||
|
m_lastSampleTimestamp = firstTimestamp;
|
||||||
|
m_lastSampleDuration = winrt::Windows::Foundation::TimeSpan{ durationTicks };
|
||||||
|
m_hasLastSampleTimestamp = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void AudioSampleGenerator::OnAudioQuantumStarted(winrt::AudioGraph const& sender, winrt::IInspectable const& args)
|
void AudioSampleGenerator::OnAudioQuantumStarted(winrt::AudioGraph const& sender, winrt::IInspectable const& args)
|
||||||
{
|
{
|
||||||
|
// Don't process if we're not actively recording
|
||||||
|
if (!m_started.load())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
auto lock = m_lock.lock_exclusive();
|
auto lock = m_lock.lock_exclusive();
|
||||||
|
|
||||||
@@ -159,10 +614,101 @@ void AudioSampleGenerator::OnAudioQuantumStarted(winrt::AudioGraph const& sender
|
|||||||
std::optional<winrt::TimeSpan> timestamp = frame.RelativeTime();
|
std::optional<winrt::TimeSpan> timestamp = frame.RelativeTime();
|
||||||
auto audioBuffer = frame.LockBuffer(winrt::AudioBufferAccessMode::Read);
|
auto audioBuffer = frame.LockBuffer(winrt::AudioBufferAccessMode::Read);
|
||||||
|
|
||||||
|
// Get mic audio as a buffer (may be empty if no microphone)
|
||||||
auto sampleBuffer = winrt::Buffer::CreateCopyFromMemoryBuffer(audioBuffer);
|
auto sampleBuffer = winrt::Buffer::CreateCopyFromMemoryBuffer(audioBuffer);
|
||||||
sampleBuffer.Length(audioBuffer.Length());
|
sampleBuffer.Length(audioBuffer.Length());
|
||||||
auto sample = winrt::MediaStreamSample::CreateFromBuffer(sampleBuffer, timestamp.value());
|
|
||||||
m_samples.push_back(sample);
|
// Calculate expected samples per quantum (~10ms at graph sample rate)
|
||||||
|
// AudioGraph uses 10ms quantums by default
|
||||||
|
uint32_t expectedSamplesPerQuantum = (m_graphSampleRate / 100) * m_graphChannels;
|
||||||
|
uint32_t numMicSamples = audioBuffer.Length() / sizeof(float);
|
||||||
|
|
||||||
|
// Drain loopback samples regardless of whether we have mic audio
|
||||||
|
if (m_loopbackCapture)
|
||||||
|
{
|
||||||
|
std::vector<float> rawLoopbackSamples;
|
||||||
|
{
|
||||||
|
std::vector<float> tempSamples;
|
||||||
|
while (m_loopbackCapture->TryGetSamples(tempSamples))
|
||||||
|
{
|
||||||
|
rawLoopbackSamples.insert(rawLoopbackSamples.end(), tempSamples.begin(), tempSamples.end());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resample and channel-convert the loopback audio to match AudioGraph format
|
||||||
|
if (!rawLoopbackSamples.empty())
|
||||||
|
{
|
||||||
|
AppendResampledLoopbackSamples(rawLoopbackSamples);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine the actual number of samples we'll output
|
||||||
|
// Use mic sample count if mic is enabled
|
||||||
|
uint32_t outputSampleCount = m_captureMicrophone ? numMicSamples : expectedSamplesPerQuantum;
|
||||||
|
|
||||||
|
// If microphone is disabled, create a buffer with only loopback audio
|
||||||
|
if (!m_captureMicrophone && outputSampleCount > 0)
|
||||||
|
{
|
||||||
|
// Create a buffer filled with loopback audio or silence
|
||||||
|
std::vector<uint8_t> outputData(outputSampleCount * sizeof(float), 0);
|
||||||
|
float* outputFloats = reinterpret_cast<float*>(outputData.data());
|
||||||
|
|
||||||
|
{
|
||||||
|
auto loopbackLock = m_loopbackBufferLock.lock_exclusive();
|
||||||
|
uint32_t samplesToUse = min(outputSampleCount, static_cast<uint32_t>(m_loopbackBuffer.size()));
|
||||||
|
|
||||||
|
for (uint32_t i = 0; i < samplesToUse; i++)
|
||||||
|
{
|
||||||
|
float sample = m_loopbackBuffer[i];
|
||||||
|
if (sample > 1.0f) sample = 1.0f;
|
||||||
|
else if (sample < -1.0f) sample = -1.0f;
|
||||||
|
outputFloats[i] = sample;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (samplesToUse > 0)
|
||||||
|
{
|
||||||
|
m_loopbackBuffer.erase(m_loopbackBuffer.begin(), m_loopbackBuffer.begin() + samplesToUse);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new buffer with our loopback data
|
||||||
|
sampleBuffer = winrt::Buffer(outputSampleCount * sizeof(float));
|
||||||
|
memcpy(sampleBuffer.data(), outputData.data(), outputData.size());
|
||||||
|
sampleBuffer.Length(static_cast<uint32_t>(outputData.size()));
|
||||||
|
}
|
||||||
|
else if (m_captureMicrophone && numMicSamples > 0)
|
||||||
|
{
|
||||||
|
// Mix loopback into mic samples
|
||||||
|
auto loopbackLock = m_loopbackBufferLock.lock_exclusive();
|
||||||
|
float* bufferData = reinterpret_cast<float*>(sampleBuffer.data());
|
||||||
|
uint32_t samplesToMix = min(numMicSamples, static_cast<uint32_t>(m_loopbackBuffer.size()));
|
||||||
|
|
||||||
|
for (uint32_t i = 0; i < samplesToMix; i++)
|
||||||
|
{
|
||||||
|
float mixed = bufferData[i] + m_loopbackBuffer[i];
|
||||||
|
if (mixed > 1.0f) mixed = 1.0f;
|
||||||
|
else if (mixed < -1.0f) mixed = -1.0f;
|
||||||
|
bufferData[i] = mixed;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (samplesToMix > 0)
|
||||||
|
{
|
||||||
|
m_loopbackBuffer.erase(m_loopbackBuffer.begin(), m_loopbackBuffer.begin() + samplesToMix);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sampleBuffer.Length() > 0)
|
||||||
|
{
|
||||||
|
auto sample = winrt::MediaStreamSample::CreateFromBuffer(sampleBuffer, timestamp.value());
|
||||||
|
m_samples.push_back(sample);
|
||||||
|
|
||||||
|
const uint32_t sampleCount = sampleBuffer.Length() / sizeof(float);
|
||||||
|
const uint32_t frames = (m_graphChannels > 0) ? (sampleCount / m_graphChannels) : 0;
|
||||||
|
const int64_t durationTicks = (m_graphSampleRate > 0) ? (static_cast<int64_t>(frames) * 10000000LL / m_graphSampleRate) : 0;
|
||||||
|
m_lastSampleTimestamp = timestamp.value();
|
||||||
|
m_lastSampleDuration = winrt::TimeSpan{ durationTicks };
|
||||||
|
m_hasLastSampleTimestamp = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
m_audioEvent.SetEvent();
|
m_audioEvent.SetEvent();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include "LoopbackCapture.h"
|
||||||
|
|
||||||
class AudioSampleGenerator
|
class AudioSampleGenerator
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
AudioSampleGenerator();
|
AudioSampleGenerator(bool captureMicrophone = true, bool captureSystemAudio = true);
|
||||||
~AudioSampleGenerator();
|
~AudioSampleGenerator();
|
||||||
|
|
||||||
winrt::Windows::Foundation::IAsyncAction InitializeAsync();
|
winrt::Windows::Foundation::IAsyncAction InitializeAsync();
|
||||||
@@ -18,6 +20,10 @@ private:
|
|||||||
winrt::Windows::Media::Audio::AudioGraph const& sender,
|
winrt::Windows::Media::Audio::AudioGraph const& sender,
|
||||||
winrt::Windows::Foundation::IInspectable const& args);
|
winrt::Windows::Foundation::IInspectable const& args);
|
||||||
|
|
||||||
|
void FlushRemainingAudio();
|
||||||
|
void CombineQueuedSamples();
|
||||||
|
void AppendResampledLoopbackSamples(std::vector<float> const& rawLoopbackSamples, bool flushRemaining = false);
|
||||||
|
|
||||||
void CheckInitialized()
|
void CheckInitialized()
|
||||||
{
|
{
|
||||||
if (!m_initialized.load())
|
if (!m_initialized.load())
|
||||||
@@ -37,12 +43,31 @@ private:
|
|||||||
private:
|
private:
|
||||||
winrt::Windows::Media::Audio::AudioGraph m_audioGraph{ nullptr };
|
winrt::Windows::Media::Audio::AudioGraph m_audioGraph{ nullptr };
|
||||||
winrt::Windows::Media::Audio::AudioDeviceInputNode m_audioInputNode{ nullptr };
|
winrt::Windows::Media::Audio::AudioDeviceInputNode m_audioInputNode{ nullptr };
|
||||||
|
winrt::Windows::Media::Audio::AudioSubmixNode m_submixNode{ nullptr };
|
||||||
winrt::Windows::Media::Audio::AudioFrameOutputNode m_audioOutputNode{ nullptr };
|
winrt::Windows::Media::Audio::AudioFrameOutputNode m_audioOutputNode{ nullptr };
|
||||||
|
|
||||||
|
std::unique_ptr<LoopbackCapture> m_loopbackCapture;
|
||||||
|
std::vector<float> m_loopbackBuffer; // Accumulated loopback samples (resampled to match AudioGraph)
|
||||||
|
wil::srwlock m_loopbackBufferLock;
|
||||||
|
uint32_t m_loopbackChannels = 2;
|
||||||
|
uint32_t m_loopbackSampleRate = 48000;
|
||||||
|
uint32_t m_graphSampleRate = 48000;
|
||||||
|
uint32_t m_graphChannels = 2;
|
||||||
|
double m_resampleRatio = 1.0; // loopbackSampleRate / graphSampleRate
|
||||||
|
winrt::Windows::Foundation::TimeSpan m_lastSampleTimestamp{};
|
||||||
|
winrt::Windows::Foundation::TimeSpan m_lastSampleDuration{};
|
||||||
|
bool m_hasLastSampleTimestamp = false;
|
||||||
|
std::vector<float> m_resampleInputBuffer; // raw loopback samples buffered for resampling
|
||||||
|
double m_resampleInputPos = 0.0; // fractional input frame position for resampling
|
||||||
|
|
||||||
wil::srwlock m_lock;
|
wil::srwlock m_lock;
|
||||||
wil::unique_event m_audioEvent;
|
wil::unique_event m_audioEvent;
|
||||||
wil::unique_event m_endEvent;
|
wil::unique_event m_endEvent;
|
||||||
|
wil::unique_event m_startEvent;
|
||||||
wil::unique_event m_asyncInitialized;
|
wil::unique_event m_asyncInitialized;
|
||||||
std::deque<winrt::Windows::Media::Core::MediaStreamSample> m_samples;
|
std::deque<winrt::Windows::Media::Core::MediaStreamSample> m_samples;
|
||||||
std::atomic<bool> m_initialized = false;
|
std::atomic<bool> m_initialized = false;
|
||||||
std::atomic<bool> m_started = false;
|
std::atomic<bool> m_started = false;
|
||||||
|
bool m_captureMicrophone = true;
|
||||||
|
bool m_captureSystemAudio = true;
|
||||||
};
|
};
|
||||||
@@ -846,7 +846,6 @@ LRESULT CALLBACK DemoTypeHookProc( int nCode, WPARAM wParam, LPARAM lParam )
|
|||||||
if( g_UserDriven )
|
if( g_UserDriven )
|
||||||
{
|
{
|
||||||
// Set baseline indentation to a blocking flag
|
// Set baseline indentation to a blocking flag
|
||||||
// Otherwise indentation seeking will trigger user-driven injection events
|
|
||||||
g_BaselineIndentation = INDENT_SEEK_FLAG;
|
g_BaselineIndentation = INDENT_SEEK_FLAG;
|
||||||
|
|
||||||
// Initialize the injection handler
|
// Initialize the injection handler
|
||||||
|
|||||||
@@ -242,6 +242,13 @@ std::shared_ptr<GifRecordingSession> GifRecordingSession::Create(
|
|||||||
//----------------------------------------------------------------------------
|
//----------------------------------------------------------------------------
|
||||||
HRESULT GifRecordingSession::EncodeFrame(ID3D11Texture2D* frameTexture)
|
HRESULT GifRecordingSession::EncodeFrame(ID3D11Texture2D* frameTexture)
|
||||||
{
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(m_encoderMutex);
|
||||||
|
if (m_encoderReleased)
|
||||||
|
{
|
||||||
|
OutputDebugStringW(L"EncodeFrame called after encoder released.\n");
|
||||||
|
return E_FAIL;
|
||||||
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Create a staging texture for CPU access
|
// Create a staging texture for CPU access
|
||||||
@@ -367,6 +374,7 @@ HRESULT GifRecordingSession::EncodeFrame(ID3D11Texture2D* frameTexture)
|
|||||||
|
|
||||||
// Increment and log frame count
|
// Increment and log frame count
|
||||||
m_frameCount++;
|
m_frameCount++;
|
||||||
|
m_hasAnyFrame.store(true);
|
||||||
OutputDebugStringW((L"GIF Frame #" + std::to_wstring(m_frameCount) + L" fully encoded and committed\n").c_str());
|
OutputDebugStringW((L"GIF Frame #" + std::to_wstring(m_frameCount) + L" fully encoded and committed\n").c_str());
|
||||||
|
|
||||||
return S_OK;
|
return S_OK;
|
||||||
@@ -405,6 +413,12 @@ winrt::IAsyncAction GifRecordingSession::StartAsync()
|
|||||||
{
|
{
|
||||||
captureAttempts++;
|
captureAttempts++;
|
||||||
auto frame = m_frameWait->TryGetNextFrame();
|
auto frame = m_frameWait->TryGetNextFrame();
|
||||||
|
if (!frame && !m_isRecording)
|
||||||
|
{
|
||||||
|
// Recording was stopped while waiting for frame
|
||||||
|
OutputDebugStringW(L"[GIF] Recording stopped during frame wait\n");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
winrt::com_ptr<ID3D11Texture2D> croppedTexture;
|
winrt::com_ptr<ID3D11Texture2D> croppedTexture;
|
||||||
|
|
||||||
@@ -472,8 +486,17 @@ winrt::IAsyncAction GifRecordingSession::StartAsync()
|
|||||||
|
|
||||||
// Wait for the next frame interval
|
// Wait for the next frame interval
|
||||||
co_await winrt::resume_after(std::chrono::milliseconds(1000 / m_frameRate));
|
co_await winrt::resume_after(std::chrono::milliseconds(1000 / m_frameRate));
|
||||||
|
|
||||||
|
// Check again after resuming from sleep
|
||||||
|
if (!m_isRecording || m_closed)
|
||||||
|
{
|
||||||
|
OutputDebugStringW(L"[GIF] Loop exiting after resume_after\n");
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
OutputDebugStringW(L"[GIF] Capture loop exited\n");
|
||||||
|
|
||||||
// Commit the GIF encoder
|
// Commit the GIF encoder
|
||||||
if (m_gifEncoder)
|
if (m_gifEncoder)
|
||||||
{
|
{
|
||||||
@@ -511,6 +534,10 @@ winrt::IAsyncAction GifRecordingSession::StartAsync()
|
|||||||
CloseInternal();
|
CloseInternal();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ensure encoder resources are released in case caller forgets to Close explicitly.
|
||||||
|
ReleaseEncoderResources();
|
||||||
|
OutputDebugStringW(L"[GIF] StartAsync completing, about to co_return\n");
|
||||||
co_return;
|
co_return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -521,18 +548,18 @@ winrt::IAsyncAction GifRecordingSession::StartAsync()
|
|||||||
//----------------------------------------------------------------------------
|
//----------------------------------------------------------------------------
|
||||||
void GifRecordingSession::Close()
|
void GifRecordingSession::Close()
|
||||||
{
|
{
|
||||||
|
OutputDebugStringW(L"[GIF] Close() called\n");
|
||||||
auto expected = false;
|
auto expected = false;
|
||||||
if (m_closed.compare_exchange_strong(expected, true))
|
if (m_closed.compare_exchange_strong(expected, true))
|
||||||
{
|
{
|
||||||
expected = true;
|
OutputDebugStringW(L"[GIF] Setting m_closed = true\n");
|
||||||
if (!m_isRecording.compare_exchange_strong(expected, false))
|
// Signal the capture loop to stop
|
||||||
{
|
m_isRecording = false;
|
||||||
CloseInternal();
|
OutputDebugStringW(L"[GIF] Setting m_isRecording = false\n");
|
||||||
}
|
|
||||||
else
|
// Stop the frame wait to unblock any pending frame acquisition
|
||||||
{
|
m_frameWait->StopCapture();
|
||||||
m_frameWait->StopCapture();
|
OutputDebugStringW(L"[GIF] StopCapture called\n");
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -543,6 +570,42 @@ void GifRecordingSession::Close()
|
|||||||
//----------------------------------------------------------------------------
|
//----------------------------------------------------------------------------
|
||||||
void GifRecordingSession::CloseInternal()
|
void GifRecordingSession::CloseInternal()
|
||||||
{
|
{
|
||||||
|
ReleaseEncoderResources();
|
||||||
|
|
||||||
m_frameWait->StopCapture();
|
m_frameWait->StopCapture();
|
||||||
m_itemClosed.revoke();
|
m_itemClosed.revoke();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//----------------------------------------------------------------------------
|
||||||
|
//
|
||||||
|
// GifRecordingSession::ReleaseEncoderResources
|
||||||
|
// Ensures encoder/stream COM objects release the temp file handle so trim can reopen it.
|
||||||
|
//
|
||||||
|
//----------------------------------------------------------------------------
|
||||||
|
void GifRecordingSession::ReleaseEncoderResources()
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(m_encoderMutex);
|
||||||
|
if (m_encoderReleased)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Commit only if we still own the encoder and it has not been committed; swallow failures.
|
||||||
|
if (m_gifEncoder)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
m_gifEncoder->Commit();
|
||||||
|
}
|
||||||
|
catch (...)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
m_encoderMetadataWriter = nullptr;
|
||||||
|
m_gifEncoder = nullptr;
|
||||||
|
m_wicStream = nullptr;
|
||||||
|
m_wicFactory = nullptr;
|
||||||
|
m_stream = nullptr;
|
||||||
|
m_encoderReleased = true;
|
||||||
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
#include "CaptureFrameWait.h"
|
#include "CaptureFrameWait.h"
|
||||||
#include <d3d11_4.h>
|
#include <d3d11_4.h>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
#include <mutex>
|
||||||
|
|
||||||
class GifRecordingSession : public std::enable_shared_from_this<GifRecordingSession>
|
class GifRecordingSession : public std::enable_shared_from_this<GifRecordingSession>
|
||||||
{
|
{
|
||||||
@@ -27,6 +28,8 @@ public:
|
|||||||
void EnableCursorCapture(bool enable = true) { m_frameWait->EnableCursorCapture(enable); }
|
void EnableCursorCapture(bool enable = true) { m_frameWait->EnableCursorCapture(enable); }
|
||||||
void Close();
|
void Close();
|
||||||
|
|
||||||
|
bool HasCapturedFrames() const { return m_hasAnyFrame.load(); }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
GifRecordingSession(
|
GifRecordingSession(
|
||||||
winrt::Direct3D11::IDirect3DDevice const& device,
|
winrt::Direct3D11::IDirect3DDevice const& device,
|
||||||
@@ -35,6 +38,7 @@ private:
|
|||||||
uint32_t frameRate,
|
uint32_t frameRate,
|
||||||
winrt::Streams::IRandomAccessStream const& stream);
|
winrt::Streams::IRandomAccessStream const& stream);
|
||||||
void CloseInternal();
|
void CloseInternal();
|
||||||
|
void ReleaseEncoderResources();
|
||||||
HRESULT EncodeFrame(ID3D11Texture2D* texture);
|
HRESULT EncodeFrame(ID3D11Texture2D* texture);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
@@ -58,6 +62,9 @@ private:
|
|||||||
|
|
||||||
std::atomic<bool> m_isRecording = false;
|
std::atomic<bool> m_isRecording = false;
|
||||||
std::atomic<bool> m_closed = false;
|
std::atomic<bool> m_closed = false;
|
||||||
|
std::atomic<bool> m_encoderReleased = false;
|
||||||
|
std::atomic<bool> m_hasAnyFrame = false;
|
||||||
|
std::mutex m_encoderMutex;
|
||||||
|
|
||||||
uint32_t m_frameWidth=0;
|
uint32_t m_frameWidth=0;
|
||||||
uint32_t m_frameHeight=0;
|
uint32_t m_frameHeight=0;
|
||||||
|
|||||||
337
src/modules/ZoomIt/ZoomIt/LoopbackCapture.cpp
Normal file
337
src/modules/ZoomIt/ZoomIt/LoopbackCapture.cpp
Normal file
@@ -0,0 +1,337 @@
|
|||||||
|
#include "pch.h"
|
||||||
|
#include "LoopbackCapture.h"
|
||||||
|
#include <functiondiscoverykeys_devpkey.h>
|
||||||
|
|
||||||
|
#pragma comment(lib, "ole32.lib")
|
||||||
|
|
||||||
|
LoopbackCapture::LoopbackCapture()
|
||||||
|
{
|
||||||
|
m_stopEvent.create(wil::EventOptions::ManualReset);
|
||||||
|
m_samplesReadyEvent.create(wil::EventOptions::ManualReset);
|
||||||
|
}
|
||||||
|
|
||||||
|
LoopbackCapture::~LoopbackCapture()
|
||||||
|
{
|
||||||
|
Stop();
|
||||||
|
if (m_pwfx)
|
||||||
|
{
|
||||||
|
CoTaskMemFree(m_pwfx);
|
||||||
|
m_pwfx = nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HRESULT LoopbackCapture::Initialize()
|
||||||
|
{
|
||||||
|
if (m_initialized.load())
|
||||||
|
{
|
||||||
|
return S_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
HRESULT hr = CoCreateInstance(
|
||||||
|
__uuidof(MMDeviceEnumerator),
|
||||||
|
nullptr,
|
||||||
|
CLSCTX_ALL,
|
||||||
|
__uuidof(IMMDeviceEnumerator),
|
||||||
|
m_deviceEnumerator.put_void());
|
||||||
|
if (FAILED(hr))
|
||||||
|
{
|
||||||
|
return hr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the default audio render device (speakers/headphones)
|
||||||
|
hr = m_deviceEnumerator->GetDefaultAudioEndpoint(eRender, eConsole, m_device.put());
|
||||||
|
if (FAILED(hr))
|
||||||
|
{
|
||||||
|
return hr;
|
||||||
|
}
|
||||||
|
|
||||||
|
hr = m_device->Activate(__uuidof(IAudioClient), CLSCTX_ALL, nullptr, m_audioClient.put_void());
|
||||||
|
if (FAILED(hr))
|
||||||
|
{
|
||||||
|
return hr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the mix format
|
||||||
|
hr = m_audioClient->GetMixFormat(&m_pwfx);
|
||||||
|
if (FAILED(hr))
|
||||||
|
{
|
||||||
|
return hr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize audio client in loopback mode
|
||||||
|
// AUDCLNT_STREAMFLAGS_LOOPBACK enables capturing what's being played on the device
|
||||||
|
hr = m_audioClient->Initialize(
|
||||||
|
AUDCLNT_SHAREMODE_SHARED,
|
||||||
|
AUDCLNT_STREAMFLAGS_LOOPBACK,
|
||||||
|
1000000, // 100ms buffer to reduce capture latency
|
||||||
|
0,
|
||||||
|
m_pwfx,
|
||||||
|
nullptr);
|
||||||
|
if (FAILED(hr))
|
||||||
|
{
|
||||||
|
return hr;
|
||||||
|
}
|
||||||
|
|
||||||
|
hr = m_audioClient->GetService(__uuidof(IAudioCaptureClient), m_captureClient.put_void());
|
||||||
|
if (FAILED(hr))
|
||||||
|
{
|
||||||
|
return hr;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_initialized.store(true);
|
||||||
|
return S_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
HRESULT LoopbackCapture::Start()
|
||||||
|
{
|
||||||
|
if (!m_initialized.load())
|
||||||
|
{
|
||||||
|
return E_NOT_VALID_STATE;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_started.load())
|
||||||
|
{
|
||||||
|
return S_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_stopEvent.ResetEvent();
|
||||||
|
|
||||||
|
HRESULT hr = m_audioClient->Start();
|
||||||
|
if (FAILED(hr))
|
||||||
|
{
|
||||||
|
return hr;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_started.store(true);
|
||||||
|
|
||||||
|
// Start capture thread
|
||||||
|
m_captureThread = std::thread(&LoopbackCapture::CaptureThread, this);
|
||||||
|
|
||||||
|
return S_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
void LoopbackCapture::Stop()
|
||||||
|
{
|
||||||
|
if (!m_started.load())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_stopEvent.SetEvent();
|
||||||
|
|
||||||
|
if (m_captureThread.joinable())
|
||||||
|
{
|
||||||
|
m_captureThread.join();
|
||||||
|
}
|
||||||
|
|
||||||
|
DrainCaptureClient();
|
||||||
|
|
||||||
|
if (m_audioClient)
|
||||||
|
{
|
||||||
|
m_audioClient->Stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
m_started.store(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
void LoopbackCapture::DrainCaptureClient()
|
||||||
|
{
|
||||||
|
if (!m_captureClient)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
UINT32 packetLength = 0;
|
||||||
|
HRESULT hr = m_captureClient->GetNextPacketSize(&packetLength);
|
||||||
|
if (FAILED(hr) || packetLength == 0)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
BYTE* pData = nullptr;
|
||||||
|
UINT32 numFramesAvailable = 0;
|
||||||
|
DWORD flags = 0;
|
||||||
|
hr = m_captureClient->GetBuffer(&pData, &numFramesAvailable, &flags, nullptr, nullptr);
|
||||||
|
if (FAILED(hr))
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (numFramesAvailable > 0)
|
||||||
|
{
|
||||||
|
std::vector<float> samples;
|
||||||
|
|
||||||
|
if (m_pwfx->wFormatTag == WAVE_FORMAT_IEEE_FLOAT ||
|
||||||
|
(m_pwfx->wFormatTag == WAVE_FORMAT_EXTENSIBLE &&
|
||||||
|
reinterpret_cast<WAVEFORMATEXTENSIBLE*>(m_pwfx)->SubFormat == KSDATAFORMAT_SUBTYPE_IEEE_FLOAT))
|
||||||
|
{
|
||||||
|
if (flags & AUDCLNT_BUFFERFLAGS_SILENT)
|
||||||
|
{
|
||||||
|
samples.resize(static_cast<size_t>(numFramesAvailable) * m_pwfx->nChannels, 0.0f);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
float* floatData = reinterpret_cast<float*>(pData);
|
||||||
|
samples.assign(floatData, floatData + (static_cast<size_t>(numFramesAvailable) * m_pwfx->nChannels));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (m_pwfx->wFormatTag == WAVE_FORMAT_PCM ||
|
||||||
|
(m_pwfx->wFormatTag == WAVE_FORMAT_EXTENSIBLE &&
|
||||||
|
reinterpret_cast<WAVEFORMATEXTENSIBLE*>(m_pwfx)->SubFormat == KSDATAFORMAT_SUBTYPE_PCM))
|
||||||
|
{
|
||||||
|
if (flags & AUDCLNT_BUFFERFLAGS_SILENT)
|
||||||
|
{
|
||||||
|
samples.resize(static_cast<size_t>(numFramesAvailable) * m_pwfx->nChannels, 0.0f);
|
||||||
|
}
|
||||||
|
else if (m_pwfx->wBitsPerSample == 16)
|
||||||
|
{
|
||||||
|
int16_t* pcmData = reinterpret_cast<int16_t*>(pData);
|
||||||
|
samples.resize(static_cast<size_t>(numFramesAvailable) * m_pwfx->nChannels);
|
||||||
|
for (size_t i = 0; i < samples.size(); i++)
|
||||||
|
{
|
||||||
|
samples[i] = static_cast<float>(pcmData[i]) / 32768.0f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (m_pwfx->wBitsPerSample == 32)
|
||||||
|
{
|
||||||
|
int32_t* pcmData = reinterpret_cast<int32_t*>(pData);
|
||||||
|
samples.resize(static_cast<size_t>(numFramesAvailable) * m_pwfx->nChannels);
|
||||||
|
for (size_t i = 0; i < samples.size(); i++)
|
||||||
|
{
|
||||||
|
samples[i] = static_cast<float>(pcmData[i]) / 2147483648.0f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!samples.empty())
|
||||||
|
{
|
||||||
|
auto lock = m_lock.lock_exclusive();
|
||||||
|
m_sampleQueue.push_back(std::move(samples));
|
||||||
|
m_samplesReadyEvent.SetEvent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hr = m_captureClient->ReleaseBuffer(numFramesAvailable);
|
||||||
|
if (FAILED(hr))
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void LoopbackCapture::CaptureThread()
|
||||||
|
{
|
||||||
|
while (WaitForSingleObject(m_stopEvent.get(), 10) == WAIT_TIMEOUT)
|
||||||
|
{
|
||||||
|
UINT32 packetLength = 0;
|
||||||
|
HRESULT hr = m_captureClient->GetNextPacketSize(&packetLength);
|
||||||
|
if (FAILED(hr))
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (packetLength != 0)
|
||||||
|
{
|
||||||
|
BYTE* pData = nullptr;
|
||||||
|
UINT32 numFramesAvailable = 0;
|
||||||
|
DWORD flags = 0;
|
||||||
|
|
||||||
|
hr = m_captureClient->GetBuffer(&pData, &numFramesAvailable, &flags, nullptr, nullptr);
|
||||||
|
if (FAILED(hr))
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (numFramesAvailable > 0)
|
||||||
|
{
|
||||||
|
std::vector<float> samples;
|
||||||
|
|
||||||
|
// Convert to float samples
|
||||||
|
if (m_pwfx->wFormatTag == WAVE_FORMAT_IEEE_FLOAT ||
|
||||||
|
(m_pwfx->wFormatTag == WAVE_FORMAT_EXTENSIBLE &&
|
||||||
|
reinterpret_cast<WAVEFORMATEXTENSIBLE*>(m_pwfx)->SubFormat == KSDATAFORMAT_SUBTYPE_IEEE_FLOAT))
|
||||||
|
{
|
||||||
|
// Already float format
|
||||||
|
if (flags & AUDCLNT_BUFFERFLAGS_SILENT)
|
||||||
|
{
|
||||||
|
// Insert silence
|
||||||
|
samples.resize(static_cast<size_t>(numFramesAvailable) * m_pwfx->nChannels, 0.0f);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
float* floatData = reinterpret_cast<float*>(pData);
|
||||||
|
samples.assign(floatData, floatData + (static_cast<size_t>(numFramesAvailable) * m_pwfx->nChannels));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (m_pwfx->wFormatTag == WAVE_FORMAT_PCM ||
|
||||||
|
(m_pwfx->wFormatTag == WAVE_FORMAT_EXTENSIBLE &&
|
||||||
|
reinterpret_cast<WAVEFORMATEXTENSIBLE*>(m_pwfx)->SubFormat == KSDATAFORMAT_SUBTYPE_PCM))
|
||||||
|
{
|
||||||
|
// Convert PCM to float
|
||||||
|
if (flags & AUDCLNT_BUFFERFLAGS_SILENT)
|
||||||
|
{
|
||||||
|
samples.resize(static_cast<size_t>(numFramesAvailable) * m_pwfx->nChannels, 0.0f);
|
||||||
|
}
|
||||||
|
else if (m_pwfx->wBitsPerSample == 16)
|
||||||
|
{
|
||||||
|
int16_t* pcmData = reinterpret_cast<int16_t*>(pData);
|
||||||
|
samples.resize(static_cast<size_t>(numFramesAvailable) * m_pwfx->nChannels);
|
||||||
|
for (size_t i = 0; i < samples.size(); i++)
|
||||||
|
{
|
||||||
|
samples[i] = static_cast<float>(pcmData[i]) / 32768.0f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (m_pwfx->wBitsPerSample == 32)
|
||||||
|
{
|
||||||
|
int32_t* pcmData = reinterpret_cast<int32_t*>(pData);
|
||||||
|
samples.resize(static_cast<size_t>(numFramesAvailable) * m_pwfx->nChannels);
|
||||||
|
for (size_t i = 0; i < samples.size(); i++)
|
||||||
|
{
|
||||||
|
samples[i] = static_cast<float>(pcmData[i]) / 2147483648.0f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!samples.empty())
|
||||||
|
{
|
||||||
|
auto lock = m_lock.lock_exclusive();
|
||||||
|
m_sampleQueue.push_back(std::move(samples));
|
||||||
|
m_samplesReadyEvent.SetEvent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hr = m_captureClient->ReleaseBuffer(numFramesAvailable);
|
||||||
|
if (FAILED(hr))
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
hr = m_captureClient->GetNextPacketSize(&packetLength);
|
||||||
|
if (FAILED(hr))
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool LoopbackCapture::TryGetSamples(std::vector<float>& samples)
|
||||||
|
{
|
||||||
|
auto lock = m_lock.lock_exclusive();
|
||||||
|
if (m_sampleQueue.empty())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
samples = std::move(m_sampleQueue.front());
|
||||||
|
m_sampleQueue.pop_front();
|
||||||
|
|
||||||
|
if (m_sampleQueue.empty())
|
||||||
|
{
|
||||||
|
m_samplesReadyEvent.ResetEvent();
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
46
src/modules/ZoomIt/ZoomIt/LoopbackCapture.h
Normal file
46
src/modules/ZoomIt/ZoomIt/LoopbackCapture.h
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <mmdeviceapi.h>
|
||||||
|
#include <audioclient.h>
|
||||||
|
#include <atomic>
|
||||||
|
#include <vector>
|
||||||
|
#include <deque>
|
||||||
|
#include <wil/resource.h>
|
||||||
|
|
||||||
|
class LoopbackCapture
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
LoopbackCapture();
|
||||||
|
~LoopbackCapture();
|
||||||
|
|
||||||
|
HRESULT Initialize();
|
||||||
|
HRESULT Start();
|
||||||
|
void Stop();
|
||||||
|
|
||||||
|
// Returns audio samples in the format: PCM float, stereo, 48kHz
|
||||||
|
bool TryGetSamples(std::vector<float>& samples);
|
||||||
|
|
||||||
|
WAVEFORMATEX* GetFormat() const { return m_pwfx; }
|
||||||
|
uint32_t GetSampleRate() const { return m_pwfx ? m_pwfx->nSamplesPerSec : 48000; }
|
||||||
|
uint32_t GetChannels() const { return m_pwfx ? m_pwfx->nChannels : 2; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
void CaptureThread();
|
||||||
|
void DrainCaptureClient();
|
||||||
|
|
||||||
|
winrt::com_ptr<IMMDeviceEnumerator> m_deviceEnumerator;
|
||||||
|
winrt::com_ptr<IMMDevice> m_device;
|
||||||
|
winrt::com_ptr<IAudioClient> m_audioClient;
|
||||||
|
winrt::com_ptr<IAudioCaptureClient> m_captureClient;
|
||||||
|
WAVEFORMATEX* m_pwfx{ nullptr };
|
||||||
|
|
||||||
|
wil::unique_event m_stopEvent;
|
||||||
|
wil::unique_event m_samplesReadyEvent;
|
||||||
|
std::thread m_captureThread;
|
||||||
|
|
||||||
|
wil::srwlock m_lock;
|
||||||
|
std::deque<std::vector<float>> m_sampleQueue;
|
||||||
|
|
||||||
|
std::atomic<bool> m_initialized{ false };
|
||||||
|
std::atomic<bool> m_started{ false };
|
||||||
|
};
|
||||||
@@ -8,6 +8,579 @@
|
|||||||
//==============================================================================
|
//==============================================================================
|
||||||
#include "pch.h"
|
#include "pch.h"
|
||||||
#include "Utility.h"
|
#include "Utility.h"
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#pragma comment(lib, "uxtheme.lib")
|
||||||
|
|
||||||
|
//----------------------------------------------------------------------------
|
||||||
|
// Dark Mode - Static/Global State
|
||||||
|
//----------------------------------------------------------------------------
|
||||||
|
static bool g_darkModeInitialized = false;
|
||||||
|
static bool g_darkModeEnabled = false;
|
||||||
|
static HBRUSH g_darkBackgroundBrush = nullptr;
|
||||||
|
static HBRUSH g_darkControlBrush = nullptr;
|
||||||
|
static HBRUSH g_darkSurfaceBrush = nullptr;
|
||||||
|
|
||||||
|
// Theme override from registry (defined in ZoomItSettings.h)
|
||||||
|
extern DWORD g_ThemeOverride;
|
||||||
|
|
||||||
|
// Preferred App Mode values for Windows 10/11 dark mode
|
||||||
|
enum class PreferredAppMode
|
||||||
|
{
|
||||||
|
Default,
|
||||||
|
AllowDark,
|
||||||
|
ForceDark,
|
||||||
|
ForceLight,
|
||||||
|
Max
|
||||||
|
};
|
||||||
|
|
||||||
|
// Undocumented ordinals from uxtheme.dll for dark mode support
|
||||||
|
using fnSetPreferredAppMode = PreferredAppMode(WINAPI*)(PreferredAppMode appMode);
|
||||||
|
using fnAllowDarkModeForWindow = bool(WINAPI*)(HWND hWnd, bool allow);
|
||||||
|
using fnShouldAppsUseDarkMode = bool(WINAPI*)();
|
||||||
|
using fnRefreshImmersiveColorPolicyState = void(WINAPI*)();
|
||||||
|
using fnFlushMenuThemes = void(WINAPI*)();
|
||||||
|
|
||||||
|
static fnSetPreferredAppMode pSetPreferredAppMode = nullptr;
|
||||||
|
static fnAllowDarkModeForWindow pAllowDarkModeForWindow = nullptr;
|
||||||
|
static fnShouldAppsUseDarkMode pShouldAppsUseDarkMode = nullptr;
|
||||||
|
static fnRefreshImmersiveColorPolicyState pRefreshImmersiveColorPolicyState = nullptr;
|
||||||
|
static fnFlushMenuThemes pFlushMenuThemes = nullptr;
|
||||||
|
|
||||||
|
//----------------------------------------------------------------------------
|
||||||
|
//
|
||||||
|
// InitializeDarkModeSupport
|
||||||
|
//
|
||||||
|
// Initialize dark mode function pointers from uxtheme.dll
|
||||||
|
//
|
||||||
|
//----------------------------------------------------------------------------
|
||||||
|
static void InitializeDarkModeSupport()
|
||||||
|
{
|
||||||
|
if (g_darkModeInitialized)
|
||||||
|
return;
|
||||||
|
|
||||||
|
g_darkModeInitialized = true;
|
||||||
|
|
||||||
|
HMODULE hUxTheme = GetModuleHandleW(L"uxtheme.dll");
|
||||||
|
if (hUxTheme)
|
||||||
|
{
|
||||||
|
// These are undocumented ordinal exports
|
||||||
|
// Ordinal 135: SetPreferredAppMode (Windows 10 1903+)
|
||||||
|
pSetPreferredAppMode = reinterpret_cast<fnSetPreferredAppMode>(
|
||||||
|
GetProcAddress(hUxTheme, MAKEINTRESOURCEA(135)));
|
||||||
|
// Ordinal 133: AllowDarkModeForWindow
|
||||||
|
pAllowDarkModeForWindow = reinterpret_cast<fnAllowDarkModeForWindow>(
|
||||||
|
GetProcAddress(hUxTheme, MAKEINTRESOURCEA(133)));
|
||||||
|
// Ordinal 132: ShouldAppsUseDarkMode
|
||||||
|
pShouldAppsUseDarkMode = reinterpret_cast<fnShouldAppsUseDarkMode>(
|
||||||
|
GetProcAddress(hUxTheme, MAKEINTRESOURCEA(132)));
|
||||||
|
// Ordinal 104: RefreshImmersiveColorPolicyState
|
||||||
|
pRefreshImmersiveColorPolicyState = reinterpret_cast<fnRefreshImmersiveColorPolicyState>(
|
||||||
|
GetProcAddress(hUxTheme, MAKEINTRESOURCEA(104)));
|
||||||
|
// Ordinal 136: FlushMenuThemes
|
||||||
|
pFlushMenuThemes = reinterpret_cast<fnFlushMenuThemes>(
|
||||||
|
GetProcAddress(hUxTheme, MAKEINTRESOURCEA(136)));
|
||||||
|
|
||||||
|
// Set preferred app mode based on our theme override or system setting
|
||||||
|
// Note: We check g_ThemeOverride directly here because IsDarkModeEnabled
|
||||||
|
// calls InitializeDarkModeSupport, which would cause recursion
|
||||||
|
if (pSetPreferredAppMode)
|
||||||
|
{
|
||||||
|
bool useDarkMode = false;
|
||||||
|
if (g_ThemeOverride == 0)
|
||||||
|
{
|
||||||
|
useDarkMode = false; // Force light
|
||||||
|
}
|
||||||
|
else if (g_ThemeOverride == 1)
|
||||||
|
{
|
||||||
|
useDarkMode = true; // Force dark
|
||||||
|
}
|
||||||
|
else if (pShouldAppsUseDarkMode)
|
||||||
|
{
|
||||||
|
useDarkMode = pShouldAppsUseDarkMode(); // Use system setting
|
||||||
|
}
|
||||||
|
|
||||||
|
if (useDarkMode)
|
||||||
|
{
|
||||||
|
pSetPreferredAppMode(PreferredAppMode::ForceDark);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
pSetPreferredAppMode(PreferredAppMode::ForceLight);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flush menu themes to apply dark mode to context menus
|
||||||
|
if (pFlushMenuThemes)
|
||||||
|
{
|
||||||
|
pFlushMenuThemes();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update cached dark mode state
|
||||||
|
g_darkModeEnabled = false;
|
||||||
|
if (g_ThemeOverride == 0)
|
||||||
|
{
|
||||||
|
g_darkModeEnabled = false;
|
||||||
|
}
|
||||||
|
else if (g_ThemeOverride == 1)
|
||||||
|
{
|
||||||
|
g_darkModeEnabled = true;
|
||||||
|
}
|
||||||
|
else if (pShouldAppsUseDarkMode)
|
||||||
|
{
|
||||||
|
g_darkModeEnabled = pShouldAppsUseDarkMode();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//----------------------------------------------------------------------------
|
||||||
|
//
|
||||||
|
// IsDarkModeEnabled
|
||||||
|
//
|
||||||
|
//----------------------------------------------------------------------------
|
||||||
|
bool IsDarkModeEnabled()
|
||||||
|
{
|
||||||
|
// Check for theme override from registry (0=light, 1=dark, 2+=system)
|
||||||
|
if (g_ThemeOverride == 0)
|
||||||
|
{
|
||||||
|
return false; // Force light mode
|
||||||
|
}
|
||||||
|
else if (g_ThemeOverride == 1)
|
||||||
|
{
|
||||||
|
return true; // Force dark mode
|
||||||
|
}
|
||||||
|
|
||||||
|
InitializeDarkModeSupport();
|
||||||
|
|
||||||
|
// Check the undocumented API first
|
||||||
|
if (pShouldAppsUseDarkMode)
|
||||||
|
{
|
||||||
|
return pShouldAppsUseDarkMode();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: Check registry for system theme preference
|
||||||
|
HKEY hKey;
|
||||||
|
if (RegOpenKeyExW(HKEY_CURRENT_USER,
|
||||||
|
L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize",
|
||||||
|
0, KEY_READ, &hKey) == ERROR_SUCCESS)
|
||||||
|
{
|
||||||
|
DWORD value = 1;
|
||||||
|
DWORD size = sizeof(value);
|
||||||
|
RegQueryValueExW(hKey, L"AppsUseLightTheme", nullptr, nullptr,
|
||||||
|
reinterpret_cast<LPBYTE>(&value), &size);
|
||||||
|
RegCloseKey(hKey);
|
||||||
|
return value == 0; // 0 = dark mode, 1 = light mode
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
//----------------------------------------------------------------------------
|
||||||
|
//
|
||||||
|
// RefreshDarkModeState
|
||||||
|
//
|
||||||
|
//----------------------------------------------------------------------------
|
||||||
|
void RefreshDarkModeState()
|
||||||
|
{
|
||||||
|
InitializeDarkModeSupport();
|
||||||
|
|
||||||
|
if (pRefreshImmersiveColorPolicyState)
|
||||||
|
{
|
||||||
|
pRefreshImmersiveColorPolicyState();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update preferred app mode based on our IsDarkModeEnabled (respects override)
|
||||||
|
bool useDark = IsDarkModeEnabled();
|
||||||
|
if (pSetPreferredAppMode)
|
||||||
|
{
|
||||||
|
if (useDark)
|
||||||
|
{
|
||||||
|
pSetPreferredAppMode(PreferredAppMode::ForceDark);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
pSetPreferredAppMode(PreferredAppMode::ForceLight);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flush menu themes to apply dark mode to context menus
|
||||||
|
if (pFlushMenuThemes)
|
||||||
|
{
|
||||||
|
pFlushMenuThemes();
|
||||||
|
}
|
||||||
|
|
||||||
|
g_darkModeEnabled = useDark;
|
||||||
|
}
|
||||||
|
|
||||||
|
//----------------------------------------------------------------------------
|
||||||
|
//
|
||||||
|
// SetDarkModeForWindow
|
||||||
|
//
|
||||||
|
//----------------------------------------------------------------------------
|
||||||
|
void SetDarkModeForWindow(HWND hWnd, bool enable)
|
||||||
|
{
|
||||||
|
InitializeDarkModeSupport();
|
||||||
|
|
||||||
|
if (pAllowDarkModeForWindow)
|
||||||
|
{
|
||||||
|
pAllowDarkModeForWindow(hWnd, enable);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use DWMWA_USE_IMMERSIVE_DARK_MODE attribute (Windows 10 build 17763+)
|
||||||
|
// Attribute 20 is DWMWA_USE_IMMERSIVE_DARK_MODE
|
||||||
|
BOOL useDarkMode = enable ? TRUE : FALSE;
|
||||||
|
HMODULE hDwmapi = GetModuleHandleW(L"dwmapi.dll");
|
||||||
|
if (hDwmapi)
|
||||||
|
{
|
||||||
|
using fnDwmSetWindowAttribute = HRESULT(WINAPI*)(HWND, DWORD, LPCVOID, DWORD);
|
||||||
|
auto pDwmSetWindowAttribute = reinterpret_cast<fnDwmSetWindowAttribute>(
|
||||||
|
GetProcAddress(hDwmapi, "DwmSetWindowAttribute"));
|
||||||
|
if (pDwmSetWindowAttribute)
|
||||||
|
{
|
||||||
|
// Try attribute 20 first (Windows 11 / newer Windows 10)
|
||||||
|
HRESULT hr = pDwmSetWindowAttribute(hWnd, 20, &useDarkMode, sizeof(useDarkMode));
|
||||||
|
if (FAILED(hr))
|
||||||
|
{
|
||||||
|
// Fall back to attribute 19 (older Windows 10)
|
||||||
|
pDwmSetWindowAttribute(hWnd, 19, &useDarkMode, sizeof(useDarkMode));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//----------------------------------------------------------------------------
|
||||||
|
//
|
||||||
|
// GetDarkModeBrush / GetDarkModeControlBrush / GetDarkModeSurfaceBrush
|
||||||
|
//
|
||||||
|
//----------------------------------------------------------------------------
|
||||||
|
HBRUSH GetDarkModeBrush()
|
||||||
|
{
|
||||||
|
if (!g_darkBackgroundBrush)
|
||||||
|
{
|
||||||
|
g_darkBackgroundBrush = CreateSolidBrush(DarkMode::BackgroundColor);
|
||||||
|
}
|
||||||
|
return g_darkBackgroundBrush;
|
||||||
|
}
|
||||||
|
|
||||||
|
HBRUSH GetDarkModeControlBrush()
|
||||||
|
{
|
||||||
|
if (!g_darkControlBrush)
|
||||||
|
{
|
||||||
|
g_darkControlBrush = CreateSolidBrush(DarkMode::ControlColor);
|
||||||
|
}
|
||||||
|
return g_darkControlBrush;
|
||||||
|
}
|
||||||
|
|
||||||
|
HBRUSH GetDarkModeSurfaceBrush()
|
||||||
|
{
|
||||||
|
if (!g_darkSurfaceBrush)
|
||||||
|
{
|
||||||
|
g_darkSurfaceBrush = CreateSolidBrush(DarkMode::SurfaceColor);
|
||||||
|
}
|
||||||
|
return g_darkSurfaceBrush;
|
||||||
|
}
|
||||||
|
|
||||||
|
//----------------------------------------------------------------------------
|
||||||
|
//
|
||||||
|
// ApplyDarkModeToDialog
|
||||||
|
//
|
||||||
|
//----------------------------------------------------------------------------
|
||||||
|
void ApplyDarkModeToDialog(HWND hDlg)
|
||||||
|
{
|
||||||
|
if (IsDarkModeEnabled())
|
||||||
|
{
|
||||||
|
SetDarkModeForWindow(hDlg, true);
|
||||||
|
|
||||||
|
// Set dark theme for the dialog
|
||||||
|
SetWindowTheme(hDlg, L"DarkMode_Explorer", nullptr);
|
||||||
|
|
||||||
|
// Apply dark theme to common controls (buttons, edit boxes, etc.)
|
||||||
|
EnumChildWindows(hDlg, [](HWND hChild, LPARAM) -> BOOL {
|
||||||
|
wchar_t className[64] = { 0 };
|
||||||
|
GetClassNameW(hChild, className, _countof(className));
|
||||||
|
|
||||||
|
// Apply appropriate theme based on control type
|
||||||
|
if (_wcsicmp(className, L"Button") == 0)
|
||||||
|
{
|
||||||
|
// Check if this is a checkbox or radio button
|
||||||
|
LONG style = GetWindowLong(hChild, GWL_STYLE);
|
||||||
|
LONG buttonType = style & BS_TYPEMASK;
|
||||||
|
if (buttonType == BS_CHECKBOX || buttonType == BS_AUTOCHECKBOX ||
|
||||||
|
buttonType == BS_3STATE || buttonType == BS_AUTO3STATE ||
|
||||||
|
buttonType == BS_RADIOBUTTON || buttonType == BS_AUTORADIOBUTTON)
|
||||||
|
{
|
||||||
|
// Subclass checkbox/radio for dark mode painting - but keep DarkMode_Explorer theme
|
||||||
|
// for proper hit testing (empty theme can break mouse interaction)
|
||||||
|
SetWindowTheme(hChild, L"DarkMode_Explorer", nullptr);
|
||||||
|
SetWindowSubclass(hChild, CheckboxSubclassProc, 2, 0);
|
||||||
|
}
|
||||||
|
else if (buttonType == BS_GROUPBOX)
|
||||||
|
{
|
||||||
|
// Subclass group box for dark mode painting
|
||||||
|
SetWindowTheme(hChild, L"", L"");
|
||||||
|
SetWindowSubclass(hChild, GroupBoxSubclassProc, 4, 0);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
SetWindowTheme(hChild, L"DarkMode_Explorer", nullptr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (_wcsicmp(className, L"Edit") == 0)
|
||||||
|
{
|
||||||
|
// Use empty theme and subclass for dark mode border drawing
|
||||||
|
SetWindowTheme(hChild, L"", L"");
|
||||||
|
SetWindowSubclass(hChild, EditControlSubclassProc, 3, 0);
|
||||||
|
}
|
||||||
|
else if (_wcsicmp(className, L"ComboBox") == 0)
|
||||||
|
{
|
||||||
|
SetWindowTheme(hChild, L"DarkMode_CFD", nullptr);
|
||||||
|
}
|
||||||
|
else if (_wcsicmp(className, L"SysListView32") == 0 ||
|
||||||
|
_wcsicmp(className, L"SysTreeView32") == 0)
|
||||||
|
{
|
||||||
|
SetWindowTheme(hChild, L"DarkMode_Explorer", nullptr);
|
||||||
|
}
|
||||||
|
else if (_wcsicmp(className, L"msctls_trackbar32") == 0)
|
||||||
|
{
|
||||||
|
// Subclass trackbar controls for dark mode painting
|
||||||
|
SetWindowTheme(hChild, L"", L"");
|
||||||
|
SetWindowSubclass(hChild, SliderSubclassProc, 1, 0);
|
||||||
|
}
|
||||||
|
else if (_wcsicmp(className, L"SysTabControl32") == 0)
|
||||||
|
{
|
||||||
|
// Use empty theme for tab control to allow dark background
|
||||||
|
SetWindowTheme(hChild, L"", L"");
|
||||||
|
}
|
||||||
|
else if (_wcsicmp(className, L"msctls_updown32") == 0)
|
||||||
|
{
|
||||||
|
SetWindowTheme(hChild, L"DarkMode_Explorer", nullptr);
|
||||||
|
}
|
||||||
|
else if (_wcsicmp(className, L"msctls_hotkey32") == 0)
|
||||||
|
{
|
||||||
|
// Subclass hotkey controls for dark mode painting
|
||||||
|
SetWindowTheme(hChild, L"", L"");
|
||||||
|
SetWindowSubclass(hChild, HotkeyControlSubclassProc, 1, 0);
|
||||||
|
}
|
||||||
|
else if (_wcsicmp(className, L"Static") == 0)
|
||||||
|
{
|
||||||
|
// Check if this is a text label (not an owner-draw or image control)
|
||||||
|
LONG style = GetWindowLong(hChild, GWL_STYLE);
|
||||||
|
LONG staticType = style & SS_TYPEMASK;
|
||||||
|
|
||||||
|
// Options header uses a dedicated static subclass (to support large title font).
|
||||||
|
// Avoid applying the generic static subclass on top of it.
|
||||||
|
const int controlId = GetDlgCtrlID( hChild );
|
||||||
|
if( controlId == IDC_VERSION || controlId == IDC_COPYRIGHT )
|
||||||
|
{
|
||||||
|
SetWindowTheme( hChild, L"", L"" );
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (staticType == SS_LEFT || staticType == SS_CENTER || staticType == SS_RIGHT ||
|
||||||
|
staticType == SS_LEFTNOWORDWRAP || staticType == SS_SIMPLE)
|
||||||
|
{
|
||||||
|
// Subclass text labels for proper dark mode painting
|
||||||
|
SetWindowTheme(hChild, L"", L"");
|
||||||
|
SetWindowSubclass(hChild, StaticTextSubclassProc, 5, 0);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Other static controls (icons, bitmaps, frames) - just remove theme
|
||||||
|
SetWindowTheme(hChild, L"", L"");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
SetWindowTheme(hChild, L"DarkMode_Explorer", nullptr);
|
||||||
|
}
|
||||||
|
return TRUE;
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Light mode - remove dark mode
|
||||||
|
SetDarkModeForWindow(hDlg, false);
|
||||||
|
SetWindowTheme(hDlg, nullptr, nullptr);
|
||||||
|
|
||||||
|
EnumChildWindows(hDlg, [](HWND hChild, LPARAM) -> BOOL {
|
||||||
|
// Remove subclass from controls
|
||||||
|
wchar_t className[64] = { 0 };
|
||||||
|
GetClassNameW(hChild, className, _countof(className));
|
||||||
|
if (_wcsicmp(className, L"msctls_hotkey32") == 0)
|
||||||
|
{
|
||||||
|
RemoveWindowSubclass(hChild, HotkeyControlSubclassProc, 1);
|
||||||
|
}
|
||||||
|
else if (_wcsicmp(className, L"msctls_trackbar32") == 0)
|
||||||
|
{
|
||||||
|
RemoveWindowSubclass(hChild, SliderSubclassProc, 1);
|
||||||
|
}
|
||||||
|
else if (_wcsicmp(className, L"Button") == 0)
|
||||||
|
{
|
||||||
|
LONG style = GetWindowLong(hChild, GWL_STYLE);
|
||||||
|
LONG buttonType = style & BS_TYPEMASK;
|
||||||
|
if (buttonType == BS_CHECKBOX || buttonType == BS_AUTOCHECKBOX ||
|
||||||
|
buttonType == BS_3STATE || buttonType == BS_AUTO3STATE ||
|
||||||
|
buttonType == BS_RADIOBUTTON || buttonType == BS_AUTORADIOBUTTON)
|
||||||
|
{
|
||||||
|
RemoveWindowSubclass(hChild, CheckboxSubclassProc, 2);
|
||||||
|
}
|
||||||
|
else if (buttonType == BS_GROUPBOX)
|
||||||
|
{
|
||||||
|
RemoveWindowSubclass(hChild, GroupBoxSubclassProc, 4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (_wcsicmp(className, L"Edit") == 0)
|
||||||
|
{
|
||||||
|
RemoveWindowSubclass(hChild, EditControlSubclassProc, 3);
|
||||||
|
}
|
||||||
|
else if (_wcsicmp(className, L"Static") == 0)
|
||||||
|
{
|
||||||
|
RemoveWindowSubclass(hChild, StaticTextSubclassProc, 5);
|
||||||
|
}
|
||||||
|
SetWindowTheme(hChild, nullptr, nullptr);
|
||||||
|
return TRUE;
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//----------------------------------------------------------------------------
|
||||||
|
//
|
||||||
|
// HandleDarkModeCtlColor
|
||||||
|
//
|
||||||
|
//----------------------------------------------------------------------------
|
||||||
|
HBRUSH HandleDarkModeCtlColor(HDC hdc, HWND hCtrl, UINT message)
|
||||||
|
{
|
||||||
|
if (!IsDarkModeEnabled())
|
||||||
|
{
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (message)
|
||||||
|
{
|
||||||
|
case WM_CTLCOLORDLG:
|
||||||
|
SetBkColor(hdc, DarkMode::BackgroundColor);
|
||||||
|
SetTextColor(hdc, DarkMode::TextColor);
|
||||||
|
return GetDarkModeBrush();
|
||||||
|
|
||||||
|
case WM_CTLCOLORSTATIC:
|
||||||
|
SetBkMode(hdc, TRANSPARENT);
|
||||||
|
// Use dimmed color for disabled static controls
|
||||||
|
if (!IsWindowEnabled(hCtrl))
|
||||||
|
{
|
||||||
|
SetTextColor(hdc, RGB(100, 100, 100));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
SetTextColor(hdc, DarkMode::TextColor);
|
||||||
|
}
|
||||||
|
return GetDarkModeBrush();
|
||||||
|
|
||||||
|
case WM_CTLCOLORBTN:
|
||||||
|
SetBkColor(hdc, DarkMode::ControlColor);
|
||||||
|
SetTextColor(hdc, DarkMode::TextColor);
|
||||||
|
return GetDarkModeControlBrush();
|
||||||
|
|
||||||
|
case WM_CTLCOLOREDIT:
|
||||||
|
SetBkColor(hdc, DarkMode::SurfaceColor);
|
||||||
|
SetTextColor(hdc, DarkMode::TextColor);
|
||||||
|
return GetDarkModeSurfaceBrush();
|
||||||
|
|
||||||
|
case WM_CTLCOLORLISTBOX:
|
||||||
|
SetBkColor(hdc, DarkMode::SurfaceColor);
|
||||||
|
SetTextColor(hdc, DarkMode::TextColor);
|
||||||
|
return GetDarkModeSurfaceBrush();
|
||||||
|
}
|
||||||
|
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
//----------------------------------------------------------------------------
|
||||||
|
//
|
||||||
|
// ApplyDarkModeToMenu
|
||||||
|
//
|
||||||
|
// Uses undocumented uxtheme functions to enable dark mode for menus
|
||||||
|
//
|
||||||
|
//----------------------------------------------------------------------------
|
||||||
|
void ApplyDarkModeToMenu(HMENU hMenu)
|
||||||
|
{
|
||||||
|
if (!hMenu)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!IsDarkModeEnabled())
|
||||||
|
{
|
||||||
|
// Light mode - clear any dark background
|
||||||
|
MENUINFO mi = { sizeof(mi) };
|
||||||
|
mi.fMask = MIM_BACKGROUND | MIM_APPLYTOSUBMENUS;
|
||||||
|
mi.hbrBack = nullptr;
|
||||||
|
SetMenuInfo(hMenu, &mi);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For popup menus, we need to use MENUINFO to set the background
|
||||||
|
MENUINFO mi = { sizeof(mi) };
|
||||||
|
mi.fMask = MIM_BACKGROUND | MIM_APPLYTOSUBMENUS;
|
||||||
|
mi.hbrBack = GetDarkModeSurfaceBrush();
|
||||||
|
SetMenuInfo(hMenu, &mi);
|
||||||
|
}
|
||||||
|
|
||||||
|
//----------------------------------------------------------------------------
|
||||||
|
//
|
||||||
|
// RefreshWindowTheme
|
||||||
|
//
|
||||||
|
// Forces a window and all its children to redraw with current theme
|
||||||
|
//
|
||||||
|
//----------------------------------------------------------------------------
|
||||||
|
void RefreshWindowTheme(HWND hWnd)
|
||||||
|
{
|
||||||
|
if (!hWnd)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reapply theme to this window
|
||||||
|
ApplyDarkModeToDialog(hWnd);
|
||||||
|
|
||||||
|
// Force redraw
|
||||||
|
RedrawWindow(hWnd, nullptr, nullptr, RDW_INVALIDATE | RDW_ERASE | RDW_ALLCHILDREN | RDW_FRAME);
|
||||||
|
}
|
||||||
|
|
||||||
|
//----------------------------------------------------------------------------
|
||||||
|
//
|
||||||
|
// CleanupDarkModeResources
|
||||||
|
//
|
||||||
|
//----------------------------------------------------------------------------
|
||||||
|
void CleanupDarkModeResources()
|
||||||
|
{
|
||||||
|
if (g_darkBackgroundBrush)
|
||||||
|
{
|
||||||
|
DeleteObject(g_darkBackgroundBrush);
|
||||||
|
g_darkBackgroundBrush = nullptr;
|
||||||
|
}
|
||||||
|
if (g_darkControlBrush)
|
||||||
|
{
|
||||||
|
DeleteObject(g_darkControlBrush);
|
||||||
|
g_darkControlBrush = nullptr;
|
||||||
|
}
|
||||||
|
if (g_darkSurfaceBrush)
|
||||||
|
{
|
||||||
|
DeleteObject(g_darkSurfaceBrush);
|
||||||
|
g_darkSurfaceBrush = nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//----------------------------------------------------------------------------
|
||||||
|
//
|
||||||
|
// InitializeDarkMode
|
||||||
|
//
|
||||||
|
// Public wrapper to initialize dark mode support early in app startup
|
||||||
|
//
|
||||||
|
//----------------------------------------------------------------------------
|
||||||
|
void InitializeDarkMode()
|
||||||
|
{
|
||||||
|
InitializeDarkModeSupport();
|
||||||
|
}
|
||||||
|
|
||||||
//----------------------------------------------------------------------------
|
//----------------------------------------------------------------------------
|
||||||
//
|
//
|
||||||
@@ -151,3 +724,177 @@ POINT ScalePointInRects( POINT point, const RECT& source, const RECT& target )
|
|||||||
return { targetCenter.x + MulDiv( point.x - sourceCenter.x, targetSize.cx, sourceSize.cx ),
|
return { targetCenter.x + MulDiv( point.x - sourceCenter.x, targetSize.cx, sourceSize.cx ),
|
||||||
targetCenter.y + MulDiv( point.y - sourceCenter.y, targetSize.cy, sourceSize.cy ) };
|
targetCenter.y + MulDiv( point.y - sourceCenter.y, targetSize.cy, sourceSize.cy ) };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//----------------------------------------------------------------------------
|
||||||
|
//
|
||||||
|
// ScaleDialogForDpi
|
||||||
|
//
|
||||||
|
// Scales a dialog and all its child controls for the specified DPI.
|
||||||
|
// oldDpi defaults to DPI_BASELINE (96) for initial scaling.
|
||||||
|
//
|
||||||
|
//----------------------------------------------------------------------------
|
||||||
|
void ScaleDialogForDpi( HWND hDlg, UINT newDpi, UINT oldDpi )
|
||||||
|
{
|
||||||
|
if( newDpi == oldDpi || newDpi == 0 || oldDpi == 0 )
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// With PerMonitorV2, Windows automatically scales dialogs (layout and fonts) when created.
|
||||||
|
// We only need to scale when moving between monitors with different DPIs.
|
||||||
|
// When oldDpi == DPI_BASELINE, this is initial creation and Windows already handled scaling.
|
||||||
|
if( oldDpi == DPI_BASELINE )
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scale the dialog window itself
|
||||||
|
RECT dialogRect;
|
||||||
|
GetWindowRect( hDlg, &dialogRect );
|
||||||
|
int dialogWidth = MulDiv( dialogRect.right - dialogRect.left, newDpi, oldDpi );
|
||||||
|
int dialogHeight = MulDiv( dialogRect.bottom - dialogRect.top, newDpi, oldDpi );
|
||||||
|
SetWindowPos( hDlg, nullptr, 0, 0, dialogWidth, dialogHeight, SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE );
|
||||||
|
|
||||||
|
// Enumerate and scale all child controls
|
||||||
|
HWND hChild = GetWindow( hDlg, GW_CHILD );
|
||||||
|
while( hChild != nullptr )
|
||||||
|
{
|
||||||
|
RECT childRect;
|
||||||
|
GetWindowRect( hChild, &childRect );
|
||||||
|
MapWindowPoints( nullptr, hDlg, reinterpret_cast<LPPOINT>(&childRect), 2 );
|
||||||
|
|
||||||
|
int x = MulDiv( childRect.left, newDpi, oldDpi );
|
||||||
|
int y = MulDiv( childRect.top, newDpi, oldDpi );
|
||||||
|
int width = MulDiv( childRect.right - childRect.left, newDpi, oldDpi );
|
||||||
|
int height = MulDiv( childRect.bottom - childRect.top, newDpi, oldDpi );
|
||||||
|
|
||||||
|
SetWindowPos( hChild, nullptr, x, y, width, height, SWP_NOZORDER | SWP_NOACTIVATE );
|
||||||
|
|
||||||
|
// Scale the font for the control
|
||||||
|
HFONT hFont = reinterpret_cast<HFONT>(SendMessage( hChild, WM_GETFONT, 0, 0 ));
|
||||||
|
if( hFont != nullptr )
|
||||||
|
{
|
||||||
|
LOGFONT lf{};
|
||||||
|
if( GetObject( hFont, sizeof(lf), &lf ) )
|
||||||
|
{
|
||||||
|
lf.lfHeight = MulDiv( lf.lfHeight, newDpi, oldDpi );
|
||||||
|
HFONT hNewFont = CreateFontIndirect( &lf );
|
||||||
|
if( hNewFont )
|
||||||
|
{
|
||||||
|
SendMessage( hChild, WM_SETFONT, reinterpret_cast<WPARAM>(hNewFont), TRUE );
|
||||||
|
// Note: The old font might be shared, so we don't delete it here
|
||||||
|
// The system will clean up fonts when the dialog is destroyed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hChild = GetWindow( hChild, GW_HWNDNEXT );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also scale the dialog's own font
|
||||||
|
HFONT hDialogFont = reinterpret_cast<HFONT>(SendMessage( hDlg, WM_GETFONT, 0, 0 ));
|
||||||
|
if( hDialogFont != nullptr )
|
||||||
|
{
|
||||||
|
LOGFONT lf{};
|
||||||
|
if( GetObject( hDialogFont, sizeof(lf), &lf ) )
|
||||||
|
{
|
||||||
|
lf.lfHeight = MulDiv( lf.lfHeight, newDpi, oldDpi );
|
||||||
|
HFONT hNewFont = CreateFontIndirect( &lf );
|
||||||
|
if( hNewFont )
|
||||||
|
{
|
||||||
|
SendMessage( hDlg, WM_SETFONT, reinterpret_cast<WPARAM>(hNewFont), TRUE );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//----------------------------------------------------------------------------
|
||||||
|
//
|
||||||
|
// ScaleChildControlsForDpi
|
||||||
|
//
|
||||||
|
// Scales a window's direct child controls (and their fonts) for the specified DPI.
|
||||||
|
// Unlike ScaleDialogForDpi, this does not resize the parent window itself.
|
||||||
|
//
|
||||||
|
// This is useful for child dialogs used as tab pages: the tab page window is
|
||||||
|
// already scaled when the parent options dialog is scaled, but the controls
|
||||||
|
// inside the page are not (because they are grandchildren of the options dialog).
|
||||||
|
//
|
||||||
|
//----------------------------------------------------------------------------
|
||||||
|
void ScaleChildControlsForDpi( HWND hParent, UINT newDpi, UINT oldDpi )
|
||||||
|
{
|
||||||
|
if( newDpi == oldDpi || newDpi == 0 || oldDpi == 0 )
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// With PerMonitorV2, Windows automatically scales dialogs (layout and fonts) when created.
|
||||||
|
// We only need to scale when moving between monitors with different DPIs.
|
||||||
|
// When oldDpi == DPI_BASELINE, this is initial creation and Windows already handled scaling.
|
||||||
|
if( oldDpi == DPI_BASELINE )
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
HWND hChild = GetWindow( hParent, GW_CHILD );
|
||||||
|
while( hChild != nullptr )
|
||||||
|
{
|
||||||
|
RECT childRect;
|
||||||
|
GetWindowRect( hChild, &childRect );
|
||||||
|
MapWindowPoints( nullptr, hParent, reinterpret_cast<LPPOINT>(&childRect), 2 );
|
||||||
|
|
||||||
|
int x = MulDiv( childRect.left, newDpi, oldDpi );
|
||||||
|
int y = MulDiv( childRect.top, newDpi, oldDpi );
|
||||||
|
int width = MulDiv( childRect.right - childRect.left, newDpi, oldDpi );
|
||||||
|
int height = MulDiv( childRect.bottom - childRect.top, newDpi, oldDpi );
|
||||||
|
|
||||||
|
SetWindowPos( hChild, nullptr, x, y, width, height, SWP_NOZORDER | SWP_NOACTIVATE );
|
||||||
|
|
||||||
|
// Scale the font for the control
|
||||||
|
HFONT hFont = reinterpret_cast<HFONT>(SendMessage( hChild, WM_GETFONT, 0, 0 ));
|
||||||
|
if( hFont != nullptr )
|
||||||
|
{
|
||||||
|
LOGFONT lf{};
|
||||||
|
if( GetObject( hFont, sizeof(lf), &lf ) )
|
||||||
|
{
|
||||||
|
lf.lfHeight = MulDiv( lf.lfHeight, newDpi, oldDpi );
|
||||||
|
HFONT hNewFont = CreateFontIndirect( &lf );
|
||||||
|
if( hNewFont )
|
||||||
|
{
|
||||||
|
SendMessage( hChild, WM_SETFONT, reinterpret_cast<WPARAM>(hNewFont), TRUE );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hChild = GetWindow( hChild, GW_HWNDNEXT );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//----------------------------------------------------------------------------
|
||||||
|
//
|
||||||
|
// HandleDialogDpiChange
|
||||||
|
//
|
||||||
|
// Handles WM_DPICHANGED message for dialogs. Call this from the dialog's
|
||||||
|
// WndProc when WM_DPICHANGED is received.
|
||||||
|
//
|
||||||
|
//----------------------------------------------------------------------------
|
||||||
|
void HandleDialogDpiChange( HWND hDlg, WPARAM wParam, LPARAM lParam, UINT& currentDpi )
|
||||||
|
{
|
||||||
|
UINT newDpi = HIWORD( wParam );
|
||||||
|
if( newDpi != currentDpi && newDpi != 0 )
|
||||||
|
{
|
||||||
|
const RECT* pSuggestedRect = reinterpret_cast<const RECT*>(lParam);
|
||||||
|
|
||||||
|
// Scale the dialog controls from the current DPI to the new DPI
|
||||||
|
ScaleDialogForDpi( hDlg, newDpi, currentDpi );
|
||||||
|
|
||||||
|
// Move and resize the dialog to the suggested rectangle
|
||||||
|
SetWindowPos( hDlg, nullptr,
|
||||||
|
pSuggestedRect->left,
|
||||||
|
pSuggestedRect->top,
|
||||||
|
pSuggestedRect->right - pSuggestedRect->left,
|
||||||
|
pSuggestedRect->bottom - pSuggestedRect->top,
|
||||||
|
SWP_NOZORDER | SWP_NOACTIVATE );
|
||||||
|
|
||||||
|
currentDpi = newDpi;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,6 +9,10 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "pch.h"
|
#include "pch.h"
|
||||||
|
#include <uxtheme.h>
|
||||||
|
|
||||||
|
// DPI baseline for scaling calculations (dialog units are designed at 96 DPI)
|
||||||
|
constexpr UINT DPI_BASELINE = USER_DEFAULT_SCREEN_DPI;
|
||||||
|
|
||||||
RECT ForceRectInBounds( RECT rect, const RECT& bounds );
|
RECT ForceRectInBounds( RECT rect, const RECT& bounds );
|
||||||
UINT GetDpiForWindowHelper( HWND window );
|
UINT GetDpiForWindowHelper( HWND window );
|
||||||
@@ -16,3 +20,86 @@ RECT GetMonitorRectFromCursor();
|
|||||||
RECT RectFromPointsMinSize( POINT a, POINT b, LONG minSize );
|
RECT RectFromPointsMinSize( POINT a, POINT b, LONG minSize );
|
||||||
int ScaleForDpi( int value, UINT dpi );
|
int ScaleForDpi( int value, UINT dpi );
|
||||||
POINT ScalePointInRects( POINT point, const RECT& source, const RECT& target );
|
POINT ScalePointInRects( POINT point, const RECT& source, const RECT& target );
|
||||||
|
|
||||||
|
// Dialog DPI scaling functions
|
||||||
|
void ScaleDialogForDpi( HWND hDlg, UINT newDpi, UINT oldDpi = DPI_BASELINE );
|
||||||
|
void ScaleChildControlsForDpi( HWND hParent, UINT newDpi, UINT oldDpi = DPI_BASELINE );
|
||||||
|
void HandleDialogDpiChange( HWND hDlg, WPARAM wParam, LPARAM lParam, UINT& currentDpi );
|
||||||
|
|
||||||
|
//----------------------------------------------------------------------------
|
||||||
|
// Dark Mode Support
|
||||||
|
//----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// Dark mode colors
|
||||||
|
namespace DarkMode
|
||||||
|
{
|
||||||
|
// Background colors
|
||||||
|
constexpr COLORREF BackgroundColor = RGB(32, 32, 32);
|
||||||
|
constexpr COLORREF SurfaceColor = RGB(45, 45, 48);
|
||||||
|
constexpr COLORREF ControlColor = RGB(51, 51, 55);
|
||||||
|
|
||||||
|
// Text colors
|
||||||
|
constexpr COLORREF TextColor = RGB(200, 200, 200);
|
||||||
|
constexpr COLORREF DisabledTextColor = RGB(120, 120, 120);
|
||||||
|
constexpr COLORREF LinkColor = RGB(86, 156, 214);
|
||||||
|
|
||||||
|
// Border/accent colors
|
||||||
|
constexpr COLORREF BorderColor = RGB(67, 67, 70);
|
||||||
|
constexpr COLORREF AccentColor = RGB(0, 120, 215);
|
||||||
|
constexpr COLORREF HoverColor = RGB(62, 62, 66);
|
||||||
|
|
||||||
|
// Light mode colors for contrast
|
||||||
|
constexpr COLORREF LightBackgroundColor = RGB(255, 255, 255);
|
||||||
|
constexpr COLORREF LightTextColor = RGB(0, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if system dark mode is enabled
|
||||||
|
bool IsDarkModeEnabled();
|
||||||
|
|
||||||
|
// Refresh dark mode state (call when WM_SETTINGCHANGE received)
|
||||||
|
void RefreshDarkModeState();
|
||||||
|
|
||||||
|
// Enable dark mode title bar for a window
|
||||||
|
void SetDarkModeForWindow(HWND hWnd, bool enable);
|
||||||
|
|
||||||
|
// Apply dark mode to a dialog and enable dark title bar
|
||||||
|
void ApplyDarkModeToDialog(HWND hDlg);
|
||||||
|
|
||||||
|
// Get the appropriate background brush for dark/light mode
|
||||||
|
HBRUSH GetDarkModeBrush();
|
||||||
|
HBRUSH GetDarkModeControlBrush();
|
||||||
|
HBRUSH GetDarkModeSurfaceBrush();
|
||||||
|
|
||||||
|
// Handle WM_CTLCOLOR* messages for dark mode
|
||||||
|
// Returns the brush to use, or nullptr if default handling should be used
|
||||||
|
HBRUSH HandleDarkModeCtlColor(HDC hdc, HWND hCtrl, UINT message);
|
||||||
|
|
||||||
|
// Apply dark mode theme to a popup menu
|
||||||
|
void ApplyDarkModeToMenu(HMENU hMenu);
|
||||||
|
|
||||||
|
// Force redraw of a window and all its children for theme change
|
||||||
|
void RefreshWindowTheme(HWND hWnd);
|
||||||
|
|
||||||
|
// Cleanup dark mode resources (call at app exit)
|
||||||
|
void CleanupDarkModeResources();
|
||||||
|
|
||||||
|
// Initialize dark mode support early in app startup (call before creating windows)
|
||||||
|
void InitializeDarkMode();
|
||||||
|
|
||||||
|
// Subclass procedure for hotkey controls - needs to be accessible from Utility.cpp
|
||||||
|
LRESULT CALLBACK HotkeyControlSubclassProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam, UINT_PTR uIdSubclass, DWORD_PTR dwRefData);
|
||||||
|
|
||||||
|
// Subclass procedure for checkbox controls - needs to be accessible from Utility.cpp
|
||||||
|
LRESULT CALLBACK CheckboxSubclassProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam, UINT_PTR uIdSubclass, DWORD_PTR dwRefData);
|
||||||
|
|
||||||
|
// Subclass procedure for edit controls - needs to be accessible from Utility.cpp
|
||||||
|
LRESULT CALLBACK EditControlSubclassProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam, UINT_PTR uIdSubclass, DWORD_PTR dwRefData);
|
||||||
|
|
||||||
|
// Subclass procedure for group box controls - needs to be accessible from Utility.cpp
|
||||||
|
LRESULT CALLBACK GroupBoxSubclassProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam, UINT_PTR uIdSubclass, DWORD_PTR dwRefData);
|
||||||
|
|
||||||
|
// Subclass procedure for slider/trackbar controls - needs to be accessible from Utility.cpp
|
||||||
|
LRESULT CALLBACK SliderSubclassProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam, UINT_PTR uIdSubclass, DWORD_PTR dwRefData);
|
||||||
|
|
||||||
|
// Subclass procedure for static text controls - needs to be accessible from Utility.cpp
|
||||||
|
LRESULT CALLBACK StaticTextSubclassProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam, UINT_PTR uIdSubclass, DWORD_PTR dwRefData);
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -11,6 +11,12 @@
|
|||||||
#include "CaptureFrameWait.h"
|
#include "CaptureFrameWait.h"
|
||||||
#include "AudioSampleGenerator.h"
|
#include "AudioSampleGenerator.h"
|
||||||
#include <d3d11_4.h>
|
#include <d3d11_4.h>
|
||||||
|
#include <ppltasks.h>
|
||||||
|
#include <atomic>
|
||||||
|
#include <algorithm>
|
||||||
|
#include <chrono>
|
||||||
|
#include <mutex>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
class VideoRecordingSession : public std::enable_shared_from_this<VideoRecordingSession>
|
class VideoRecordingSession : public std::enable_shared_from_this<VideoRecordingSession>
|
||||||
{
|
{
|
||||||
@@ -21,6 +27,7 @@ public:
|
|||||||
RECT const& cropRect,
|
RECT const& cropRect,
|
||||||
uint32_t frameRate,
|
uint32_t frameRate,
|
||||||
bool captureAudio,
|
bool captureAudio,
|
||||||
|
bool captureSystemAudio,
|
||||||
winrt::Streams::IRandomAccessStream const& stream);
|
winrt::Streams::IRandomAccessStream const& stream);
|
||||||
~VideoRecordingSession();
|
~VideoRecordingSession();
|
||||||
|
|
||||||
@@ -28,6 +35,151 @@ public:
|
|||||||
void EnableCursorCapture(bool enable = true) { m_frameWait->EnableCursorCapture(enable); }
|
void EnableCursorCapture(bool enable = true) { m_frameWait->EnableCursorCapture(enable); }
|
||||||
void Close();
|
void Close();
|
||||||
|
|
||||||
|
bool HasCapturedVideoFrames() const { return m_hasVideoSample.load(); }
|
||||||
|
|
||||||
|
// Trim and save functionality
|
||||||
|
static std::wstring ShowSaveDialogWithTrim(
|
||||||
|
HWND hWnd,
|
||||||
|
const std::wstring& suggestedFileName,
|
||||||
|
const std::wstring& originalVideoPath,
|
||||||
|
std::wstring& trimmedVideoPath);
|
||||||
|
|
||||||
|
struct TrimDialogData
|
||||||
|
{
|
||||||
|
struct GifFrame
|
||||||
|
{
|
||||||
|
HBITMAP hBitmap{ nullptr };
|
||||||
|
winrt::Windows::Foundation::TimeSpan start{ 0 };
|
||||||
|
winrt::Windows::Foundation::TimeSpan duration{ 0 };
|
||||||
|
UINT width{ 0 };
|
||||||
|
UINT height{ 0 };
|
||||||
|
};
|
||||||
|
|
||||||
|
std::wstring videoPath;
|
||||||
|
winrt::Windows::Foundation::TimeSpan videoDuration{ 0 };
|
||||||
|
winrt::Windows::Foundation::TimeSpan trimStart{ 0 };
|
||||||
|
winrt::Windows::Foundation::TimeSpan trimEnd{ 0 };
|
||||||
|
winrt::Windows::Foundation::TimeSpan originalTrimStart{ 0 }; // Initial value to detect if trim needed
|
||||||
|
winrt::Windows::Foundation::TimeSpan originalTrimEnd{ 0 }; // Initial value to detect if trim needed
|
||||||
|
winrt::Windows::Foundation::TimeSpan currentPosition{ 0 };
|
||||||
|
// Playback loop anchor. This is set when the user explicitly positions the playhead
|
||||||
|
// (e.g., dragging or using the jog buttons). Pausing/resuming should not change it.
|
||||||
|
winrt::Windows::Foundation::TimeSpan playbackStartPosition{ 0 };
|
||||||
|
bool playbackStartPositionValid{ false };
|
||||||
|
|
||||||
|
// Cached preview frame at playback start position for instant restore when playback stops.
|
||||||
|
HBITMAP hCachedStartFrame{ nullptr };
|
||||||
|
winrt::Windows::Foundation::TimeSpan cachedStartFramePosition{ -1 };
|
||||||
|
|
||||||
|
// When starting playback at a non-zero position, MediaPlayer may briefly report Position==0
|
||||||
|
// before the initial seek is applied. Use this to suppress a one-frame UI jump to 0.
|
||||||
|
std::atomic<bool> pendingInitialSeek{ false };
|
||||||
|
std::atomic<int64_t> pendingInitialSeekTicks{ 0 };
|
||||||
|
winrt::Windows::Media::Editing::MediaComposition composition{ nullptr };
|
||||||
|
winrt::Windows::Media::Playback::MediaPlayer mediaPlayer{ nullptr };
|
||||||
|
winrt::Windows::Storage::StorageFile playbackFile{ nullptr };
|
||||||
|
HBITMAP hPreviewBitmap{ nullptr };
|
||||||
|
HWND hDialog{ nullptr };
|
||||||
|
std::atomic<bool> loadingPreview{ false };
|
||||||
|
std::atomic<int64_t> latestPreviewRequest{ 0 };
|
||||||
|
std::atomic<int64_t> lastRenderedPreview{ -1 };
|
||||||
|
std::atomic<bool> isPlaying{ false };
|
||||||
|
// Monotonic serial used to cancel in-flight StartPlaybackAsync work when the user
|
||||||
|
// immediately pauses after starting playback.
|
||||||
|
std::atomic<uint64_t> playbackCommandSerial{ 0 };
|
||||||
|
std::atomic<bool> frameCopyInProgress{ false };
|
||||||
|
std::atomic<bool> smoothActive{ false };
|
||||||
|
std::atomic<int64_t> smoothBaseTicks{ 0 };
|
||||||
|
std::atomic<int64_t> smoothLastSyncMicroseconds{ 0 };
|
||||||
|
std::atomic<bool> smoothHasNonZeroSample{ false };
|
||||||
|
std::mutex previewBitmapMutex;
|
||||||
|
winrt::event_token frameAvailableToken{};
|
||||||
|
winrt::event_token positionChangedToken{};
|
||||||
|
winrt::event_token stateChangedToken{};
|
||||||
|
winrt::com_ptr<ID3D11Device> previewD3DDevice;
|
||||||
|
winrt::com_ptr<ID3D11DeviceContext> previewD3DContext;
|
||||||
|
winrt::com_ptr<ID3D11Texture2D> previewFrameTexture;
|
||||||
|
winrt::com_ptr<ID3D11Texture2D> previewFrameStaging;
|
||||||
|
bool hoverPlay{ false };
|
||||||
|
bool hoverRewind{ false };
|
||||||
|
bool hoverForward{ false };
|
||||||
|
bool hoverSkipStart{ false };
|
||||||
|
bool hoverSkipEnd{ false };
|
||||||
|
bool hoverVolumeIcon{ false };
|
||||||
|
double volume{ 0.70 }; // Volume level 0.0 to 1.0, initialized from g_TrimDialogVolume in dialog init
|
||||||
|
double previousVolume{ 0.70 }; // Volume before muting, for unmute restoration
|
||||||
|
winrt::Windows::Foundation::TimeSpan previewOverride{ 0 };
|
||||||
|
winrt::Windows::Foundation::TimeSpan positionBeforeOverride{ 0 };
|
||||||
|
bool previewOverrideActive{ false };
|
||||||
|
bool restorePreviewOnRelease{ false };
|
||||||
|
bool playheadPushed{ false };
|
||||||
|
int dialogX{ 0 };
|
||||||
|
int dialogY{ 0 };
|
||||||
|
bool isGif{ false };
|
||||||
|
bool previewBitmapOwned{ true };
|
||||||
|
std::vector<GifFrame> gifFrames;
|
||||||
|
bool gifFramesLoaded{ false };
|
||||||
|
size_t gifLastFrameIndex{ 0 };
|
||||||
|
std::chrono::steady_clock::time_point gifFrameStartTime{}; // When the current GIF frame started displaying
|
||||||
|
|
||||||
|
// Font for time labels
|
||||||
|
HFONT hTimeLabelFont{ nullptr };
|
||||||
|
|
||||||
|
// Mouse tracking for timeline
|
||||||
|
enum DragMode { None, TrimStart, Position, TrimEnd };
|
||||||
|
DragMode dragMode{ None };
|
||||||
|
bool isDragging{ false };
|
||||||
|
int lastPlayheadX{ -1 }; // Track last playhead pixel position for efficient invalidation
|
||||||
|
MMRESULT mmTimerId{ 0 }; // Multimedia timer for smooth MP4 playback
|
||||||
|
|
||||||
|
// Helper to convert time to pixel position
|
||||||
|
int TimeToPixel(winrt::Windows::Foundation::TimeSpan time, int timelineWidth) const
|
||||||
|
{
|
||||||
|
if (timelineWidth <= 0 || videoDuration.count() <= 0)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
double ratio = static_cast<double>(time.count()) / static_cast<double>(videoDuration.count());
|
||||||
|
ratio = std::clamp(ratio, 0.0, 1.0);
|
||||||
|
return static_cast<int>(ratio * timelineWidth);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to convert pixel to time
|
||||||
|
winrt::Windows::Foundation::TimeSpan PixelToTime(int pixel, int timelineWidth) const
|
||||||
|
{
|
||||||
|
if (timelineWidth <= 0 || videoDuration.count() <= 0)
|
||||||
|
{
|
||||||
|
return winrt::Windows::Foundation::TimeSpan{ 0 };
|
||||||
|
}
|
||||||
|
int clampedPixel = std::clamp(pixel, 0, timelineWidth);
|
||||||
|
double ratio = static_cast<double>(clampedPixel) / static_cast<double>(timelineWidth);
|
||||||
|
return winrt::Windows::Foundation::TimeSpan{ static_cast<int64_t>(ratio * videoDuration.count()) };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
static INT_PTR ShowTrimDialog(
|
||||||
|
HWND hParent,
|
||||||
|
const std::wstring& videoPath,
|
||||||
|
winrt::Windows::Foundation::TimeSpan& trimStart,
|
||||||
|
winrt::Windows::Foundation::TimeSpan& trimEnd);
|
||||||
|
|
||||||
|
private:
|
||||||
|
static INT_PTR CALLBACK TrimDialogProc(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam);
|
||||||
|
|
||||||
|
static winrt::Windows::Foundation::IAsyncOperation<winrt::hstring> TrimVideoAsync(
|
||||||
|
const std::wstring& sourceVideoPath,
|
||||||
|
winrt::Windows::Foundation::TimeSpan trimTimeStart,
|
||||||
|
winrt::Windows::Foundation::TimeSpan trimTimeEnd);
|
||||||
|
static winrt::Windows::Foundation::IAsyncOperation<winrt::hstring> TrimGifAsync(
|
||||||
|
const std::wstring& sourceGifPath,
|
||||||
|
winrt::Windows::Foundation::TimeSpan trimTimeStart,
|
||||||
|
winrt::Windows::Foundation::TimeSpan trimTimeEnd);
|
||||||
|
static INT_PTR ShowTrimDialogInternal(
|
||||||
|
HWND hParent,
|
||||||
|
const std::wstring& videoPath,
|
||||||
|
winrt::Windows::Foundation::TimeSpan& trimStart,
|
||||||
|
winrt::Windows::Foundation::TimeSpan& trimEnd);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
VideoRecordingSession(
|
VideoRecordingSession(
|
||||||
winrt::Direct3D11::IDirect3DDevice const& device,
|
winrt::Direct3D11::IDirect3DDevice const& device,
|
||||||
@@ -35,6 +187,7 @@ private:
|
|||||||
RECT const cropRect,
|
RECT const cropRect,
|
||||||
uint32_t frameRate,
|
uint32_t frameRate,
|
||||||
bool captureAudio,
|
bool captureAudio,
|
||||||
|
bool captureSystemAudio,
|
||||||
winrt::Streams::IRandomAccessStream const& stream);
|
winrt::Streams::IRandomAccessStream const& stream);
|
||||||
void CloseInternal();
|
void CloseInternal();
|
||||||
|
|
||||||
@@ -68,4 +221,7 @@ private:
|
|||||||
|
|
||||||
std::atomic<bool> m_isRecording = false;
|
std::atomic<bool> m_isRecording = false;
|
||||||
std::atomic<bool> m_closed = false;
|
std::atomic<bool> m_closed = false;
|
||||||
|
|
||||||
|
// Set once the MediaStreamSource successfully returns at least one video sample.
|
||||||
|
std::atomic<bool> m_hasVideoSample = false;
|
||||||
};
|
};
|
||||||
@@ -113,26 +113,26 @@ END
|
|||||||
// Dialog
|
// Dialog
|
||||||
//
|
//
|
||||||
|
|
||||||
OPTIONS DIALOGEX 0, 0, 279, 325
|
OPTIONS DIALOGEX 0, 0, 299, 325
|
||||||
STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | DS_CENTER | WS_POPUP | WS_CLIPSIBLINGS | WS_CAPTION | WS_SYSMENU
|
STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | DS_CENTER | WS_POPUP | WS_CLIPSIBLINGS | WS_CAPTION | WS_SYSMENU
|
||||||
EXSTYLE WS_EX_CONTROLPARENT
|
EXSTYLE WS_EX_CONTROLPARENT
|
||||||
CAPTION "ZoomIt - Sysinternals: www.sysinternals.com"
|
CAPTION "ZoomIt - Sysinternals: www.sysinternals.com"
|
||||||
FONT 8, "MS Shell Dlg", 0, 0, 0x0
|
FONT 8, "MS Shell Dlg", 0, 0, 0x0
|
||||||
BEGIN
|
BEGIN
|
||||||
DEFPUSHBUTTON "OK",IDOK,166,306,50,14
|
DEFPUSHBUTTON "OK",IDOK,186,306,50,14
|
||||||
PUSHBUTTON "Cancel",IDCANCEL,223,306,50,14
|
PUSHBUTTON "Cancel",IDCANCEL,243,306,50,14
|
||||||
LTEXT "ZoomIt v9.21",IDC_VERSION,42,7,73,10
|
LTEXT "ZoomIt v10.0",IDC_VERSION,42,7,73,10
|
||||||
LTEXT "Copyright © 2006-2025 Mark Russinovich",IDC_COPYRIGHT,42,17,231,8
|
LTEXT "Copyright \251 2006-2026 Mark Russinovich",IDC_COPYRIGHT,42,17,251,8
|
||||||
CONTROL "<a HREF=""https://www.sysinternals.com"">Sysinternals - www.sysinternals.com</a>",IDC_LINK,
|
CONTROL "<a HREF=""https://www.sysinternals.com"">Sysinternals - www.sysinternals.com</a>",IDC_LINK,
|
||||||
"SysLink",WS_TABSTOP,42,26,150,9
|
"SysLink",WS_TABSTOP,42,26,150,9
|
||||||
ICON "APPICON",IDC_STATIC,12,9,20,20
|
ICON "APPICON",IDC_STATIC,12,9,20,20
|
||||||
CONTROL "Show tray icon",IDC_SHOW_TRAY_ICON,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,13,295,105,10
|
CONTROL "Show tray icon",IDC_SHOW_TRAY_ICON,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,13,295,105,10
|
||||||
CONTROL "",IDC_TAB,"SysTabControl32",TCS_MULTILINE | WS_TABSTOP,8,46,265,245
|
CONTROL "",IDC_TAB,"SysTabControl32",TCS_MULTILINE | WS_TABSTOP,8,46,285,247
|
||||||
CONTROL "Run ZoomIt when Windows starts",IDC_AUTOSTART,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,13,309,122,10
|
CONTROL "Run ZoomIt when Windows starts",IDC_AUTOSTART,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,13,309,122,10
|
||||||
END
|
END
|
||||||
|
|
||||||
ADVANCED_BREAK DIALOGEX 0, 0, 209, 219
|
ADVANCED_BREAK DIALOGEX 0, 0, 209, 225
|
||||||
STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | DS_CENTER | WS_POPUP | WS_CAPTION | WS_SYSMENU
|
STYLE DS_SETFONT | DS_MODALFRAME | DS_CENTER | WS_POPUP | WS_CAPTION | WS_SYSMENU
|
||||||
CAPTION "Advanced Break Options"
|
CAPTION "Advanced Break Options"
|
||||||
FONT 8, "MS Shell Dlg", 400, 0, 0x1
|
FONT 8, "MS Shell Dlg", 400, 0, 0x1
|
||||||
BEGIN
|
BEGIN
|
||||||
@@ -158,23 +158,22 @@ BEGIN
|
|||||||
EDITTEXT IDC_BACKGROUND_FILE,62,164,125,12,ES_AUTOHSCROLL | ES_READONLY
|
EDITTEXT IDC_BACKGROUND_FILE,62,164,125,12,ES_AUTOHSCROLL | ES_READONLY
|
||||||
PUSHBUTTON "&...",IDC_BACKGROUND_BROWSE,188,164,13,11
|
PUSHBUTTON "&...",IDC_BACKGROUND_BROWSE,188,164,13,11
|
||||||
CONTROL "Scale to screen:",IDC_CHECK_BACKGROUND_STRETCH,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,58,180,67,10,WS_EX_RIGHT
|
CONTROL "Scale to screen:",IDC_CHECK_BACKGROUND_STRETCH,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,58,180,67,10,WS_EX_RIGHT
|
||||||
DEFPUSHBUTTON "OK",IDOK,97,201,50,14
|
DEFPUSHBUTTON "OK",IDOK,97,199,50,14
|
||||||
PUSHBUTTON "Cancel",IDCANCEL,150,201,50,14
|
PUSHBUTTON "Cancel",IDCANCEL,150,199,50,14
|
||||||
LTEXT "Alarm Sound File:",IDC_STATIC_SOUND_FILE,61,26,56,8
|
LTEXT "Alarm Sound File:",IDC_STATIC_SOUND_FILE,61,26,56,8
|
||||||
LTEXT "Timer Opacity:",IDC_STATIC,8,59,48,8
|
LTEXT "Timer Opacity:",IDC_STATIC,8,59,48,8
|
||||||
LTEXT "Timer Position:",IDC_STATIC,8,77,48,8
|
LTEXT "Timer Position:",IDC_STATIC,8,77,48,8
|
||||||
CONTROL "",IDC_STATIC,"Static",SS_BLACKFRAME | SS_SUNKEN,7,196,193,1,WS_EX_CLIENTEDGE
|
|
||||||
END
|
END
|
||||||
|
|
||||||
ZOOM DIALOGEX 0, 0, 260, 170
|
ZOOM DIALOGEX 0, 0, 260, 170
|
||||||
STYLE DS_SETFONT | DS_FIXEDSYS | DS_CONTROL | WS_CHILD | WS_CLIPSIBLINGS | WS_SYSMENU
|
STYLE DS_SETFONT | DS_CONTROL | WS_CHILD | WS_CLIPSIBLINGS | WS_SYSMENU
|
||||||
FONT 8, "MS Shell Dlg", 400, 0, 0x1
|
FONT 8, "MS Shell Dlg", 400, 0, 0x1
|
||||||
BEGIN
|
BEGIN
|
||||||
CONTROL "",IDC_HOTKEY,"msctls_hotkey32",WS_BORDER | WS_TABSTOP,59,57,80,12
|
CONTROL "",IDC_HOTKEY,"msctls_hotkey32",WS_BORDER | WS_TABSTOP,59,57,80,12
|
||||||
LTEXT "After toggling ZoomIt you can zoom in with the mouse wheel or up and down arrow keys. Exit zoom mode with Escape or by pressing the right mouse button.",IDC_STATIC,7,6,246,26
|
LTEXT "After toggling ZoomIt you can zoom in with the mouse wheel or up and down arrow keys. Exit zoom mode with Escape or by pressing the right mouse button.",IDC_STATIC,7,6,230,26
|
||||||
LTEXT "Zoom Toggle:",IDC_STATIC,7,59,51,8
|
LTEXT "Zoom Toggle:",IDC_STATIC,7,59,51,8
|
||||||
CONTROL "",IDC_ZOOM_SLIDER,"msctls_trackbar32",TBS_AUTOTICKS | TBS_BOTH | TBS_NOTICKS | WS_TABSTOP,53,118,150,15,WS_EX_TRANSPARENT
|
CONTROL "",IDC_ZOOM_SLIDER,"msctls_trackbar32",TBS_AUTOTICKS | TBS_BOTH | TBS_NOTICKS | WS_TABSTOP,53,118,150,15,WS_EX_TRANSPARENT
|
||||||
LTEXT "Specify the initial level of magnification when zooming in:",IDC_STATIC,7,105,215,10
|
LTEXT "Specify the initial level of magnification when zooming in:",IDC_STATIC,7,105,230,10
|
||||||
LTEXT "1.25",IDC_STATIC,52,136,16,8
|
LTEXT "1.25",IDC_STATIC,52,136,16,8
|
||||||
LTEXT "1.5",IDC_STATIC,82,136,12,8
|
LTEXT "1.5",IDC_STATIC,82,136,12,8
|
||||||
LTEXT "1.75",IDC_STATIC,108,136,16,8
|
LTEXT "1.75",IDC_STATIC,108,136,16,8
|
||||||
@@ -183,52 +182,52 @@ BEGIN
|
|||||||
LTEXT "4.0",IDC_STATIC,190,136,12,8
|
LTEXT "4.0",IDC_STATIC,190,136,12,8
|
||||||
CONTROL "Animate zoom in and zoom out:",IDC_ANIMATE_ZOOM,"Button",BS_AUTOCHECKBOX | BS_LEFTTEXT | WS_TABSTOP,7,74,116,10
|
CONTROL "Animate zoom in and zoom out:",IDC_ANIMATE_ZOOM,"Button",BS_AUTOCHECKBOX | BS_LEFTTEXT | WS_TABSTOP,7,74,116,10
|
||||||
CONTROL "Smooth zoomed image:",IDC_SMOOTH_IMAGE,"Button",BS_AUTOCHECKBOX | BS_LEFTTEXT | WS_TABSTOP,7,88,116,10
|
CONTROL "Smooth zoomed image:",IDC_SMOOTH_IMAGE,"Button",BS_AUTOCHECKBOX | BS_LEFTTEXT | WS_TABSTOP,7,88,116,10
|
||||||
LTEXT "Copy a zoomed screen with Ctrl+C or save it by typing Ctrl+S. Crop the copy or save region by entering Ctrl+Shift instead of Ctrl.",IDC_STATIC,7,148,246,17
|
LTEXT "Copy a zoomed screen with Ctrl+C or save it by typing Ctrl+S. Crop the copy or save region by entering Ctrl+Shift instead of Ctrl.",IDC_STATIC,7,148,230,17
|
||||||
LTEXT "Copy a zoomed screen with Ctrl+C or save it by typing Ctrl+S. Crop the copy or save region by entering Ctrl+Shift instead of Ctrl.",IDC_STATIC,6,34,246,18
|
LTEXT "Copy a zoomed screen with Ctrl+C or save it by typing Ctrl+S. Crop the copy or save region by entering Ctrl+Shift instead of Ctrl.",IDC_STATIC,6,34,230,18
|
||||||
END
|
END
|
||||||
|
|
||||||
DRAW DIALOGEX 0, 0, 260, 228
|
DRAW DIALOGEX 0, 0, 260, 228
|
||||||
STYLE DS_SETFONT | DS_FIXEDSYS | DS_CONTROL | WS_CHILD | WS_SYSMENU
|
STYLE DS_SETFONT | DS_CONTROL | WS_CHILD | WS_SYSMENU
|
||||||
FONT 8, "MS Shell Dlg", 400, 0, 0x1
|
FONT 8, "MS Shell Dlg", 400, 0, 0x1
|
||||||
BEGIN
|
BEGIN
|
||||||
LTEXT "Once zoomed, toggle drawing mode by pressing the left mouse button. Undo with Ctrl+Z and all drawing by pressing E. Center the cursor with the space bar. Exit drawing mode by pressing the right mouse button.",IDC_STATIC,7,7,246,24
|
LTEXT "Once zoomed, toggle drawing mode by pressing the left mouse button. Undo with Ctrl+Z and all drawing by pressing E. Center the cursor with the space bar. Exit drawing mode by pressing the right mouse button.",IDC_STATIC,7,7,230,24
|
||||||
LTEXT "Pen Control ",IDC_PEN_CONTROL,7,38,40,8
|
LTEXT "Pen Control ",IDC_PEN_CONTROL,7,38,40,8
|
||||||
LTEXT "Change the pen width by pressing left Ctrl and using the mouse wheel or the up and down arrow keys.",IDC_STATIC,19,48,233,16
|
LTEXT "Change the pen width by pressing left Ctrl and using the mouse wheel or the up and down arrow keys.",IDC_STATIC,19,48,218,16
|
||||||
LTEXT "Colors",IDC_COLORS,7,70,21,8
|
LTEXT "Colors",IDC_COLORS,7,70,21,8
|
||||||
LTEXT "Change the pen color by pressing R (red), G (green), B (blue),\nO (orange), Y (yellow) or P (pink).",IDC_STATIC,19,80,233,16
|
LTEXT "Change the pen color by pressing R (red), G (green), B (blue),\nO (orange), Y (yellow) or P (pink).",IDC_STATIC,19,80,218,16
|
||||||
LTEXT "Highlight and Blur",IDC_HIGHLIGHT_AND_BLUR,7,102,58,8
|
LTEXT "Highlight and Blur",IDC_HIGHLIGHT_AND_BLUR,7,102,58,8
|
||||||
LTEXT "Hold Shift while pressing a color key for a translucent highlighter color. Press X for blur or Shift+X for a stronger blur.",IDC_STATIC,19,113,233,16
|
LTEXT "Hold Shift while pressing a color key for a translucent highlighter color. Press X for blur or Shift+X for a stronger blur.",IDC_STATIC,19,113,218,16
|
||||||
LTEXT "Shapes",IDC_SHAPES,7,134,23,8
|
LTEXT "Shapes",IDC_SHAPES,7,134,23,8
|
||||||
LTEXT "Draw a line by holding down the Shift key, a rectangle with the Ctrl key, an ellipse with the Tab key and an arrow with Shift+Ctrl.",IDC_STATIC,19,144,233,16
|
LTEXT "Draw a line by holding down the Shift key, a rectangle with the Ctrl key, an ellipse with the Tab key and an arrow with Shift+Ctrl.",IDC_STATIC,19,144,218,16
|
||||||
LTEXT "Screen",IDC_SCREEN,7,166,22,8
|
LTEXT "Screen",IDC_SCREEN,7,166,22,8
|
||||||
LTEXT "Clear the screen for a sketch pad by pressing W (white) or K (black). Copy a zoomed screen with Ctrl+C or save it by typing Ctrl+S. Crop the copy or save region by entering Ctrl+Shift instead of Ctrl.",IDC_STATIC,19,176,233,24
|
LTEXT "Clear the screen for a sketch pad by pressing W (white) or K (black). Copy a zoomed screen with Ctrl+C or save it by typing Ctrl+S. Crop the copy or save region by entering Ctrl+Shift instead of Ctrl.",IDC_STATIC,19,176,218,24
|
||||||
CONTROL "",IDC_DRAW_HOTKEY,"msctls_hotkey32",WS_BORDER | WS_TABSTOP,73,207,80,12
|
CONTROL "",IDC_DRAW_HOTKEY,"msctls_hotkey32",WS_BORDER | WS_TABSTOP,73,207,80,12
|
||||||
LTEXT "Draw w/out Zoom:",IDC_STATIC,7,210,63,11
|
LTEXT "Draw w/out Zoom:",IDC_STATIC,7,210,63,11
|
||||||
END
|
END
|
||||||
|
|
||||||
TYPE DIALOGEX 0, 0, 260, 104
|
TYPE DIALOGEX 0, 0, 260, 104
|
||||||
STYLE DS_SETFONT | DS_FIXEDSYS | DS_CONTROL | WS_CHILD | WS_SYSMENU
|
STYLE DS_SETFONT | DS_CONTROL | WS_CHILD | WS_SYSMENU
|
||||||
FONT 8, "MS Shell Dlg", 400, 0, 0x1
|
FONT 8, "MS Shell Dlg", 400, 0, 0x1
|
||||||
BEGIN
|
BEGIN
|
||||||
LTEXT "Once in drawing mode, type 't' to enter typing mode or shift+'t' to enter typing mode with right-aligned input. Exit typing mode by pressing escape or the left mouse button. Use the mouse wheel or up and down arrow keys to change the font size.",IDC_STATIC,7,7,246,32
|
LTEXT "Once in drawing mode, type 't' to enter typing mode or shift+'t' to enter typing mode with right-aligned input. Exit typing mode by pressing escape or the left mouse button. Use the mouse wheel or up and down arrow keys to change the font size.",IDC_STATIC,7,7,230,32
|
||||||
LTEXT "The text color is the current drawing color.",IDC_STATIC,7,47,211,9
|
LTEXT "The text color is the current drawing color.",IDC_STATIC,7,47,230,9
|
||||||
PUSHBUTTON "&Font",IDC_FONT,112,69,41,14
|
PUSHBUTTON "&Font",IDC_FONT,112,69,41,14
|
||||||
GROUPBOX "Text Font",IDC_TEXT_FONT,8,61,99,28
|
GROUPBOX "Sample",IDC_TEXT_FONT,8,61,99,28
|
||||||
END
|
END
|
||||||
|
|
||||||
BREAK DIALOGEX 0, 0, 260, 123
|
BREAK DIALOGEX 0, 0, 260, 123
|
||||||
STYLE DS_SETFONT | DS_FIXEDSYS | DS_CONTROL | WS_CHILD | WS_SYSMENU
|
STYLE DS_SETFONT | DS_CONTROL | WS_CHILD | WS_SYSMENU
|
||||||
FONT 8, "MS Shell Dlg", 400, 0, 0x1
|
FONT 8, "MS Shell Dlg", 400, 0, 0x1
|
||||||
BEGIN
|
BEGIN
|
||||||
CONTROL "",IDC_BREAK_HOTKEY,"msctls_hotkey32",WS_BORDER | WS_TABSTOP,52,67,80,12
|
CONTROL "",IDC_BREAK_HOTKEY,"msctls_hotkey32",WS_BORDER | WS_TABSTOP,52,67,80,12
|
||||||
EDITTEXT IDC_TIMER,31,86,31,13,ES_RIGHT | ES_AUTOHSCROLL | ES_NUMBER
|
EDITTEXT IDC_TIMER,52,86,31,13,ES_RIGHT | ES_AUTOHSCROLL | ES_NUMBER
|
||||||
CONTROL "",IDC_SPIN_TIMER,"msctls_updown32",UDS_SETBUDDYINT | UDS_ALIGNRIGHT | UDS_AUTOBUDDY | UDS_ARROWKEYS | UDS_NOTHOUSANDS,45,86,11,12
|
CONTROL "",IDC_SPIN_TIMER,"msctls_updown32",UDS_SETBUDDYINT | UDS_ALIGNRIGHT | UDS_AUTOBUDDY | UDS_ARROWKEYS | UDS_NOTHOUSANDS,66,86,11,12
|
||||||
LTEXT "minutes",IDC_STATIC,67,88,25,8
|
LTEXT "minutes",IDC_STATIC,88,88,25,8
|
||||||
PUSHBUTTON "&Advanced",IDC_ADVANCED_BREAK,212,102,41,14
|
PUSHBUTTON "&Advanced",IDC_ADVANCED_BREAK,192,102,41,14
|
||||||
LTEXT "Enter timer mode by using the ZoomIt tray icon's Break menu item. Increase and decrease time with the arrow keys. If you Alt-Tab away from the timer window, reactivate it by left-clicking on the ZoomIt tray icon. Exit timer mode with Escape. ",IDC_STATIC,7,7,246,33
|
LTEXT "Enter timer mode by using the ZoomIt tray icon's Break menu item. Increase and decrease time with the arrow keys. If you Alt-Tab away from the timer window, reactivate it by left-clicking on the ZoomIt tray icon. Exit timer mode with Escape. ",IDC_STATIC,7,7,230,33
|
||||||
LTEXT "Start Timer:",IDC_STATIC,7,70,39,8
|
LTEXT "Start Timer:",IDC_STATIC,7,70,39,8
|
||||||
LTEXT "Timer:",IDC_STATIC,7,88,20,8
|
LTEXT "Timer:",IDC_STATIC,7,88,20,8
|
||||||
LTEXT "Change the break timer color using the same keys that the drawing color. The break timer font is the same as text font.",IDC_STATIC,7,45,219,20
|
LTEXT "Change the break timer color using the same keys that the drawing color. The break timer font is the same as text font.",IDC_STATIC,7,45,230,20
|
||||||
CONTROL "Show Time Elapsed After Expiration:",IDC_CHECK_SHOW_EXPIRED,
|
CONTROL "Show Time Elapsed After Expiration:",IDC_CHECK_SHOW_EXPIRED,
|
||||||
"Button",BS_AUTOCHECKBOX | BS_LEFTTEXT | WS_TABSTOP,8,104,132,10
|
"Button",BS_AUTOCHECKBOX | BS_LEFTTEXT | WS_TABSTOP,8,104,132,10
|
||||||
END
|
END
|
||||||
@@ -251,69 +250,90 @@ BEGIN
|
|||||||
END
|
END
|
||||||
|
|
||||||
LIVEZOOM DIALOGEX 0, 0, 260, 134
|
LIVEZOOM DIALOGEX 0, 0, 260, 134
|
||||||
STYLE DS_SETFONT | DS_FIXEDSYS | DS_CONTROL | WS_CHILD | WS_SYSMENU
|
STYLE DS_SETFONT | DS_CONTROL | WS_CHILD | WS_SYSMENU
|
||||||
FONT 8, "MS Shell Dlg", 400, 0, 0x1
|
FONT 8, "MS Shell Dlg", 400, 0, 0x1
|
||||||
BEGIN
|
BEGIN
|
||||||
CONTROL "",IDC_LIVE_HOTKEY,"msctls_hotkey32",WS_BORDER | WS_TABSTOP,69,108,80,12
|
CONTROL "",IDC_LIVE_HOTKEY,"msctls_hotkey32",WS_BORDER | WS_TABSTOP,69,108,80,12
|
||||||
LTEXT "LiveZoom mode is supported on Windows 7 and higher where window updates show while zoomed. ",IDC_STATIC,7,7,246,18
|
LTEXT "LiveZoom mode is supported on Windows 7 and higher where window updates show while zoomed. ",IDC_STATIC,7,7,230,18
|
||||||
LTEXT "LiveZoom Toggle:",IDC_STATIC,7,110,62,8
|
LTEXT "LiveZoom Toggle:",IDC_STATIC,7,110,62,8
|
||||||
LTEXT "To enter and exit LiveZoom, enter the hotkey specified below.",IDC_STATIC,7,94,218,13
|
LTEXT "To enter and exit LiveZoom, enter the hotkey specified below.",IDC_STATIC,7,94,230,13
|
||||||
LTEXT "Note that in LiveZoom you must use Ctrl+Up and Ctrl+Down to control the zoom level. To enter drawing mode, use the standard zoom-without-draw hotkey and then escape to go back to LiveZoom.",IDC_STATIC,7,30,246,27
|
LTEXT "Note that in LiveZoom you must use Ctrl+Up and Ctrl+Down to control the zoom level. To enter drawing mode, use the standard zoom-without-draw hotkey and then escape to go back to LiveZoom.",IDC_STATIC,7,30,230,27
|
||||||
LTEXT "Use LiveDraw to draw and annotate the live desktop. To activate LiveDraw, enter the hotkey with the Shift key in the opposite mode. You can remove LiveDraw annotations by activating LiveDraw and enter the escape key",IDC_STATIC,7,62,246,32
|
LTEXT "Use LiveDraw to draw and annotate the live desktop. To activate LiveDraw, enter the hotkey with the Shift key in the opposite mode. You can remove LiveDraw annotations by activating LiveDraw and enter the escape key",IDC_STATIC,7,62,230,32
|
||||||
END
|
END
|
||||||
|
|
||||||
RECORD DIALOGEX 0, 0, 260, 169
|
RECORD DIALOGEX 0, 0, 260, 181
|
||||||
STYLE DS_SETFONT | DS_FIXEDSYS | DS_CONTROL | WS_CHILD | WS_SYSMENU
|
STYLE DS_SETFONT | DS_FIXEDSYS | DS_CONTROL | WS_CHILD | WS_SYSMENU
|
||||||
FONT 8, "MS Shell Dlg", 400, 0, 0x1
|
FONT 8, "MS Shell Dlg", 400, 0, 0x1
|
||||||
BEGIN
|
BEGIN
|
||||||
CONTROL "",IDC_RECORD_HOTKEY,"msctls_hotkey32",WS_BORDER | WS_TABSTOP,61,96,80,12
|
CONTROL "",IDC_RECORD_HOTKEY,"msctls_hotkey32",WS_BORDER | WS_TABSTOP,61,96,80,12
|
||||||
LTEXT "Record Toggle:",IDC_STATIC,7,98,54,8
|
LTEXT "Record Toggle:",IDC_STATIC,7,98,54,8
|
||||||
LTEXT "Record video of the unzoomed live screen or a static zoomed session by entering the recording hot key and finish the recording by entering it again. ",IDC_STATIC,7,7,246,28
|
LTEXT "Record video of the unzoomed live screen or a static zoomed session by entering the recording hot key and finish the recording by entering it again. ",IDC_STATIC,7,7,248,28
|
||||||
LTEXT "Note: Recording is only available on Windows 10 (version 1903) and higher.",IDC_STATIC,7,77,246,19
|
LTEXT "Note: Recording is only available on Windows 10 (version 1903) and higher.",IDC_STATIC,7,77,249,19
|
||||||
LTEXT "Scaling:",IDC_STATIC,30,115,26,8
|
LTEXT "Scaling:",IDC_STATIC,30,115,26,8
|
||||||
COMBOBOX IDC_RECORD_SCALING,61,114,26,30,CBS_DROPDOWNLIST | CBS_AUTOHSCROLL | CBS_OEMCONVERT | CBS_SORT | WS_VSCROLL | WS_TABSTOP
|
COMBOBOX IDC_RECORD_SCALING,61,114,26,30,CBS_DROPDOWNLIST | CBS_AUTOHSCROLL | CBS_OEMCONVERT | CBS_SORT | WS_VSCROLL | WS_TABSTOP
|
||||||
LTEXT "Format:",IDC_STATIC,30,132,26,8
|
LTEXT "Format:",IDC_STATIC,30,132,26,8
|
||||||
COMBOBOX IDC_RECORD_FORMAT,61,131,60,30,CBS_DROPDOWNLIST | CBS_AUTOHSCROLL | CBS_OEMCONVERT | WS_VSCROLL | WS_TABSTOP
|
COMBOBOX IDC_RECORD_FORMAT,61,131,60,30,CBS_DROPDOWNLIST | CBS_AUTOHSCROLL | CBS_OEMCONVERT | WS_VSCROLL | WS_TABSTOP
|
||||||
LTEXT "Frame Rate:",IDC_STATIC,119,115,44,8,NOT WS_VISIBLE
|
LTEXT "Frame Rate:",IDC_STATIC,119,115,44,8,NOT WS_VISIBLE
|
||||||
COMBOBOX IDC_RECORD_FRAME_RATE,166,114,42,30,CBS_DROPDOWNLIST | CBS_AUTOHSCROLL | CBS_OEMCONVERT | CBS_SORT | NOT WS_VISIBLE | WS_VSCROLL | WS_TABSTOP
|
COMBOBOX IDC_RECORD_FRAME_RATE,166,114,42,30,CBS_DROPDOWNLIST | CBS_AUTOHSCROLL | CBS_OEMCONVERT | CBS_SORT | NOT WS_VISIBLE | WS_VSCROLL | WS_TABSTOP
|
||||||
LTEXT "To crop the portion of the screen that will be recorded, enter the hotkey with the Shift key in the opposite mode. ",IDC_STATIC,7,32,246,19
|
LTEXT "To crop the portion of the screen that will be recorded, enter the hotkey with the Shift key in the opposite mode. ",IDC_STATIC,7,35,245,19
|
||||||
LTEXT "To record a specific window, enter the hotkey with the Alt key in the opposite mode.",IDC_STATIC,7,55,246,19
|
LTEXT "To record a specific window, enter the hotkey with the Alt key in the opposite mode.",IDC_STATIC,7,55,251,19
|
||||||
CONTROL "&Capture audio input:",IDC_CAPTURE_AUDIO,"Button",BS_AUTOCHECKBOX | BS_LEFTTEXT | WS_TABSTOP,7,149,83,10
|
CONTROL "Capture &system audio",IDC_CAPTURE_SYSTEM_AUDIO,"Button",BS_AUTOCHECKBOX | BS_LEFTTEXT | WS_TABSTOP,7,149,83,10
|
||||||
COMBOBOX IDC_MICROPHONE,81,164,172,30,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP
|
CONTROL "&Capture audio input:",IDC_CAPTURE_AUDIO,"Button",BS_AUTOCHECKBOX | BS_LEFTTEXT | WS_TABSTOP,7,161,83,10
|
||||||
LTEXT "Microphone:",IDC_STATIC,32,166,47,8
|
COMBOBOX IDC_MICROPHONE,81,176,152,30,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP
|
||||||
|
LTEXT "Microphone:",IDC_MICROPHONE_LABEL,32,178,47,8
|
||||||
END
|
END
|
||||||
|
|
||||||
SNIP DIALOGEX 0, 0, 260, 68
|
SNIP DIALOGEX 0, 0, 260, 68
|
||||||
STYLE DS_SETFONT | DS_FIXEDSYS | DS_CONTROL | WS_CHILD | WS_CLIPSIBLINGS | WS_SYSMENU
|
STYLE DS_SETFONT | DS_CONTROL | WS_CHILD | WS_CLIPSIBLINGS | WS_SYSMENU
|
||||||
FONT 8, "MS Shell Dlg", 400, 0, 0x1
|
FONT 8, "MS Shell Dlg", 400, 0, 0x1
|
||||||
BEGIN
|
BEGIN
|
||||||
CONTROL "",IDC_SNIP_HOTKEY,"msctls_hotkey32",WS_BORDER | WS_TABSTOP,55,32,80,12
|
CONTROL "",IDC_SNIP_HOTKEY,"msctls_hotkey32",WS_BORDER | WS_TABSTOP,55,32,80,12
|
||||||
LTEXT "Snip Toggle:",IDC_STATIC,7,33,45,8
|
LTEXT "Snip Toggle:",IDC_STATIC,7,33,45,8
|
||||||
LTEXT "Copy a region of the screen to the clipboard or enter the hotkey with the Shift key in the opposite mode to save it to a file. ",IDC_STATIC,7,7,246,19
|
LTEXT "Copy a region of the screen to the clipboard or enter the hotkey with the Shift key in the opposite mode to save it to a file. ",IDC_STATIC,7,7,230,19
|
||||||
END
|
END
|
||||||
|
|
||||||
DEMOTYPE DIALOGEX 0, 0, 259, 249
|
DEMOTYPE DIALOGEX 0, 0, 260, 249
|
||||||
STYLE DS_SETFONT | DS_FIXEDSYS | DS_CONTROL | WS_CHILD | WS_CLIPSIBLINGS | WS_SYSMENU
|
STYLE DS_SETFONT | DS_CONTROL | WS_CHILD | WS_CLIPSIBLINGS | WS_SYSMENU
|
||||||
FONT 8, "MS Shell Dlg", 400, 0, 0x1
|
FONT 8, "MS Shell Dlg", 400, 0, 0x1
|
||||||
BEGIN
|
BEGIN
|
||||||
CONTROL "",IDC_DEMOTYPE_HOTKEY,"msctls_hotkey32",WS_BORDER | WS_TABSTOP,74,154,80,12
|
CONTROL "",IDC_DEMOTYPE_HOTKEY,"msctls_hotkey32",WS_BORDER | WS_TABSTOP,74,154,80,12
|
||||||
LTEXT "DemoType toggle:",IDC_STATIC,7,157,63,8
|
LTEXT "DemoType toggle:",IDC_STATIC,7,157,63,8
|
||||||
PUSHBUTTON "&...",IDC_DEMOTYPE_BROWSE,231,137,16,13
|
PUSHBUTTON "&...",IDC_DEMOTYPE_BROWSE,211,137,16,13
|
||||||
CONTROL "",IDC_DEMOTYPE_SPEED_SLIDER,"msctls_trackbar32",TBS_AUTOTICKS | TBS_BOTH | TBS_NOTICKS | WS_TABSTOP,52,202,150,11,WS_EX_TRANSPARENT
|
CONTROL "",IDC_DEMOTYPE_SPEED_SLIDER,"msctls_trackbar32",TBS_AUTOTICKS | TBS_BOTH | TBS_NOTICKS | WS_TABSTOP,52,202,150,11,WS_EX_TRANSPARENT
|
||||||
CONTROL "Drive input with typing:",IDC_DEMOTYPE_USER_DRIVEN,
|
CONTROL "Drive input with typing:",IDC_DEMOTYPE_USER_DRIVEN,
|
||||||
"Button",BS_AUTOCHECKBOX | BS_LEFTTEXT | WS_TABSTOP,7,173,88,10
|
"Button",BS_AUTOCHECKBOX | BS_LEFTTEXT | WS_TABSTOP,7,173,88,10
|
||||||
LTEXT "DemoType typing speed:",IDC_STATIC,7,189,215,10
|
LTEXT "DemoType typing speed:",IDC_STATIC,7,189,230,10
|
||||||
LTEXT "Slow",IDC_DEMOTYPE_STATIC1,51,213,18,8
|
LTEXT "Slow",IDC_DEMOTYPE_STATIC1,51,213,18,8
|
||||||
LTEXT "Fast",IDC_DEMOTYPE_STATIC2,186,213,17,8
|
LTEXT "Fast",IDC_DEMOTYPE_STATIC2,186,213,17,8
|
||||||
EDITTEXT IDC_DEMOTYPE_FILE,44,137,187,12,ES_AUTOHSCROLL | ES_READONLY
|
EDITTEXT IDC_DEMOTYPE_FILE,44,137,167,12,ES_AUTOHSCROLL | ES_READONLY
|
||||||
LTEXT "Input file:",IDC_STATIC,7,139,32,8
|
LTEXT "Input file:",IDC_STATIC,7,139,32,8
|
||||||
LTEXT "When you reach the end of the file, ZoomIt will reload the file and start at the beginning. Enter the hotkey with the Shift key in the opposite mode to step back to the last [end].",IDC_STATIC,7,108,248,24
|
LTEXT "When you reach the end of the file, ZoomIt will reload the file and start at the beginning. Enter the hotkey with the Shift key in the opposite mode to step back to the last [end].",IDC_STATIC,7,108,230,24
|
||||||
LTEXT "DemoType has ZoomIt type text specified in the input file when you enter the DemoType toggle. Simply separate snippets with the [end] keyword, or you can insert text from the clipboard if it is prefixed with the [start].",IDC_STATIC,7,7,248,24
|
LTEXT "DemoType has ZoomIt type text specified in the input file when you enter the DemoType toggle. Simply separate snippets with the [end] keyword, or you can insert text from the clipboard if it is prefixed with the [start].",IDC_STATIC,7,7,230,24
|
||||||
LTEXT " - Insert pauses with the [pause:n] keyword where 'n' is seconds. ",IDC_STATIC,19,34,212,11
|
LTEXT " - Insert pauses with the [pause:n] keyword where 'n' is seconds. ",IDC_STATIC,19,34,218,11
|
||||||
LTEXT "You can have ZoomIt send text automatically, or select the option to drive input with typing. ZoomIt will block keyboard input while sending output.",IDC_STATIC,7,68,248,16
|
LTEXT "You can have ZoomIt send text automatically, or select the option to drive input with typing. ZoomIt will block keyboard input while sending output.",IDC_STATIC,7,68,230,16
|
||||||
LTEXT "When driving input, hit the space bar to unblock keyboard input at the end of a snippet. In auto mode, control will be returned upon completion.",IDC_STATIC,7,88,248,16
|
LTEXT "When driving input, hit the space bar to unblock keyboard input at the end of a snippet. In auto mode, control will be returned upon completion.",IDC_STATIC,7,88,230,16
|
||||||
LTEXT "- Send text via the clipboard with [paste] and [/paste]. ",IDC_STATIC,23,45,178,8
|
LTEXT "- Send text via the clipboard with [paste] and [/paste]. ",IDC_STATIC,23,45,210,8
|
||||||
LTEXT "- Send keystrokes with [enter], [up], [down], [left], and [right].",IDC_STATIC,23,56,211,8
|
LTEXT "- Send keystrokes with [enter], [up], [down], [left], and [right].",IDC_STATIC,23,56,210,8
|
||||||
|
END
|
||||||
|
|
||||||
|
IDD_VIDEO_TRIM DIALOGEX 0, 0, 521, 380
|
||||||
|
STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | DS_CENTER | WS_POPUP | WS_CAPTION | WS_SYSMENU | WS_THICKFRAME
|
||||||
|
CAPTION "ZoomIt Video Trim"
|
||||||
|
FONT 8, "MS Shell Dlg", 400, 0, 0x1
|
||||||
|
BEGIN
|
||||||
|
LTEXT "",IDC_TRIM_DURATION_LABEL,12,267,160,8
|
||||||
|
CONTROL "",IDC_TRIM_PREVIEW,"Static",SS_OWNERDRAW | SS_NOTIFY,12,12,498,244
|
||||||
|
CTEXT "00:00.000",IDC_TRIM_POSITION_LABEL,155,267,200,8
|
||||||
|
CONTROL "",IDC_TRIM_TIMELINE,"Static",SS_OWNERDRAW | SS_NOTIFY,11,277,498,47,WS_EX_TRANSPARENT
|
||||||
|
CONTROL "",IDC_TRIM_SKIP_START,"Button",BS_OWNERDRAW | WS_TABSTOP,183,327,30,26
|
||||||
|
CONTROL "",IDC_TRIM_REWIND,"Button",BS_OWNERDRAW | WS_TABSTOP,215,327,30,26
|
||||||
|
CONTROL "",IDC_TRIM_PLAY_PAUSE,"Button",BS_OWNERDRAW | WS_TABSTOP,247,325,44,32
|
||||||
|
CONTROL "",IDC_TRIM_FORWARD,"Button",BS_OWNERDRAW | WS_TABSTOP,293,327,30,26
|
||||||
|
CONTROL "",IDC_TRIM_SKIP_END,"Button",BS_OWNERDRAW | WS_TABSTOP,325,327,30,26
|
||||||
|
CONTROL "",IDC_TRIM_VOLUME_ICON,"Static",SS_OWNERDRAW | SS_NOTIFY,365,334,14,12
|
||||||
|
CONTROL "",IDC_TRIM_VOLUME,"msctls_trackbar32",TBS_NOTICKS | WS_TABSTOP,380,333,70,14
|
||||||
|
DEFPUSHBUTTON "OK",IDOK,404,358,50,14
|
||||||
|
PUSHBUTTON "Cancel",IDCANCEL,458,358,50,14
|
||||||
END
|
END
|
||||||
|
|
||||||
|
|
||||||
@@ -327,7 +347,7 @@ GUIDELINES DESIGNINFO
|
|||||||
BEGIN
|
BEGIN
|
||||||
"OPTIONS", DIALOG
|
"OPTIONS", DIALOG
|
||||||
BEGIN
|
BEGIN
|
||||||
RIGHTMARGIN, 273
|
RIGHTMARGIN, 293
|
||||||
BOTTOMMARGIN, 320
|
BOTTOMMARGIN, 320
|
||||||
END
|
END
|
||||||
|
|
||||||
@@ -340,7 +360,6 @@ BEGIN
|
|||||||
"ZOOM", DIALOG
|
"ZOOM", DIALOG
|
||||||
BEGIN
|
BEGIN
|
||||||
LEFTMARGIN, 7
|
LEFTMARGIN, 7
|
||||||
RIGHTMARGIN, 253
|
|
||||||
TOPMARGIN, 7
|
TOPMARGIN, 7
|
||||||
BOTTOMMARGIN, 151
|
BOTTOMMARGIN, 151
|
||||||
END
|
END
|
||||||
@@ -348,7 +367,6 @@ BEGIN
|
|||||||
"DRAW", DIALOG
|
"DRAW", DIALOG
|
||||||
BEGIN
|
BEGIN
|
||||||
LEFTMARGIN, 7
|
LEFTMARGIN, 7
|
||||||
RIGHTMARGIN, 253
|
|
||||||
TOPMARGIN, 7
|
TOPMARGIN, 7
|
||||||
BOTTOMMARGIN, 221
|
BOTTOMMARGIN, 221
|
||||||
END
|
END
|
||||||
@@ -356,7 +374,6 @@ BEGIN
|
|||||||
"TYPE", DIALOG
|
"TYPE", DIALOG
|
||||||
BEGIN
|
BEGIN
|
||||||
LEFTMARGIN, 7
|
LEFTMARGIN, 7
|
||||||
RIGHTMARGIN, 253
|
|
||||||
TOPMARGIN, 7
|
TOPMARGIN, 7
|
||||||
BOTTOMMARGIN, 97
|
BOTTOMMARGIN, 97
|
||||||
END
|
END
|
||||||
@@ -364,7 +381,6 @@ BEGIN
|
|||||||
"BREAK", DIALOG
|
"BREAK", DIALOG
|
||||||
BEGIN
|
BEGIN
|
||||||
LEFTMARGIN, 7
|
LEFTMARGIN, 7
|
||||||
RIGHTMARGIN, 253
|
|
||||||
TOPMARGIN, 7
|
TOPMARGIN, 7
|
||||||
BOTTOMMARGIN, 116
|
BOTTOMMARGIN, 116
|
||||||
END
|
END
|
||||||
@@ -378,7 +394,6 @@ BEGIN
|
|||||||
"LIVEZOOM", DIALOG
|
"LIVEZOOM", DIALOG
|
||||||
BEGIN
|
BEGIN
|
||||||
LEFTMARGIN, 7
|
LEFTMARGIN, 7
|
||||||
RIGHTMARGIN, 253
|
|
||||||
TOPMARGIN, 7
|
TOPMARGIN, 7
|
||||||
BOTTOMMARGIN, 127
|
BOTTOMMARGIN, 127
|
||||||
END
|
END
|
||||||
@@ -386,7 +401,6 @@ BEGIN
|
|||||||
"RECORD", DIALOG
|
"RECORD", DIALOG
|
||||||
BEGIN
|
BEGIN
|
||||||
LEFTMARGIN, 7
|
LEFTMARGIN, 7
|
||||||
RIGHTMARGIN, 253
|
|
||||||
TOPMARGIN, 7
|
TOPMARGIN, 7
|
||||||
BOTTOMMARGIN, 164
|
BOTTOMMARGIN, 164
|
||||||
END
|
END
|
||||||
@@ -394,7 +408,6 @@ BEGIN
|
|||||||
"SNIP", DIALOG
|
"SNIP", DIALOG
|
||||||
BEGIN
|
BEGIN
|
||||||
LEFTMARGIN, 7
|
LEFTMARGIN, 7
|
||||||
RIGHTMARGIN, 253
|
|
||||||
TOPMARGIN, 7
|
TOPMARGIN, 7
|
||||||
BOTTOMMARGIN, 61
|
BOTTOMMARGIN, 61
|
||||||
END
|
END
|
||||||
@@ -402,10 +415,13 @@ BEGIN
|
|||||||
"DEMOTYPE", DIALOG
|
"DEMOTYPE", DIALOG
|
||||||
BEGIN
|
BEGIN
|
||||||
LEFTMARGIN, 7
|
LEFTMARGIN, 7
|
||||||
RIGHTMARGIN, 255
|
|
||||||
TOPMARGIN, 7
|
TOPMARGIN, 7
|
||||||
BOTTOMMARGIN, 205
|
BOTTOMMARGIN, 205
|
||||||
END
|
END
|
||||||
|
|
||||||
|
IDD_VIDEO_TRIM, DIALOG
|
||||||
|
BEGIN
|
||||||
|
END
|
||||||
END
|
END
|
||||||
#endif // APSTUDIO_INVOKED
|
#endif // APSTUDIO_INVOKED
|
||||||
|
|
||||||
@@ -474,6 +490,11 @@ BEGIN
|
|||||||
0
|
0
|
||||||
END
|
END
|
||||||
|
|
||||||
|
IDD_VIDEO_TRIM AFX_DIALOG_LAYOUT
|
||||||
|
BEGIN
|
||||||
|
0
|
||||||
|
END
|
||||||
|
|
||||||
#endif // English (United States) resources
|
#endif // English (United States) resources
|
||||||
/////////////////////////////////////////////////////////////////////////////
|
/////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
|||||||
@@ -216,6 +216,14 @@
|
|||||||
<MultiProcessorCompilation Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">false</MultiProcessorCompilation>
|
<MultiProcessorCompilation Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">false</MultiProcessorCompilation>
|
||||||
<MultiProcessorCompilation Condition="'$(Configuration)|$(Platform)'=='Release|x64'">false</MultiProcessorCompilation>
|
<MultiProcessorCompilation Condition="'$(Configuration)|$(Platform)'=='Release|x64'">false</MultiProcessorCompilation>
|
||||||
</ClCompile>
|
</ClCompile>
|
||||||
|
<ClCompile Include="LoopbackCapture.cpp">
|
||||||
|
<MultiProcessorCompilation Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">false</MultiProcessorCompilation>
|
||||||
|
<MultiProcessorCompilation Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">false</MultiProcessorCompilation>
|
||||||
|
<MultiProcessorCompilation Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'">false</MultiProcessorCompilation>
|
||||||
|
<MultiProcessorCompilation Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'">false</MultiProcessorCompilation>
|
||||||
|
<MultiProcessorCompilation Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">false</MultiProcessorCompilation>
|
||||||
|
<MultiProcessorCompilation Condition="'$(Configuration)|$(Platform)'=='Release|x64'">false</MultiProcessorCompilation>
|
||||||
|
</ClCompile>
|
||||||
<ClCompile Include="$(MSBuildThisFileDirectory)..\..\..\common\sysinternals\dll.c">
|
<ClCompile Include="$(MSBuildThisFileDirectory)..\..\..\common\sysinternals\dll.c">
|
||||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
|
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
|
||||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">NotUsing</PrecompiledHeader>
|
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">NotUsing</PrecompiledHeader>
|
||||||
@@ -293,6 +301,7 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ClInclude Include="AudioSampleGenerator.h" />
|
<ClInclude Include="AudioSampleGenerator.h" />
|
||||||
|
<ClInclude Include="LoopbackCapture.h" />
|
||||||
<ClInclude Include="$(MSBuildThisFileDirectory)..\..\..\common\sysinternals\Eula\Eula.h" />
|
<ClInclude Include="$(MSBuildThisFileDirectory)..\..\..\common\sysinternals\Eula\Eula.h" />
|
||||||
<ClInclude Include="$(MSBuildThisFileDirectory)..\ZoomItModuleInterface\Trace.h" />
|
<ClInclude Include="$(MSBuildThisFileDirectory)..\ZoomItModuleInterface\Trace.h" />
|
||||||
<ClInclude Include="GifRecordingSession.h" />
|
<ClInclude Include="GifRecordingSession.h" />
|
||||||
|
|||||||
@@ -33,6 +33,9 @@
|
|||||||
<ClCompile Include="AudioSampleGenerator.cpp">
|
<ClCompile Include="AudioSampleGenerator.cpp">
|
||||||
<Filter>Source Files</Filter>
|
<Filter>Source Files</Filter>
|
||||||
</ClCompile>
|
</ClCompile>
|
||||||
|
<ClCompile Include="LoopbackCapture.cpp">
|
||||||
|
<Filter>Source Files</Filter>
|
||||||
|
</ClCompile>
|
||||||
<ClCompile Include="DemoType.cpp">
|
<ClCompile Include="DemoType.cpp">
|
||||||
<Filter>Source Files</Filter>
|
<Filter>Source Files</Filter>
|
||||||
</ClCompile>
|
</ClCompile>
|
||||||
@@ -80,6 +83,9 @@
|
|||||||
<ClInclude Include="AudioSampleGenerator.h">
|
<ClInclude Include="AudioSampleGenerator.h">
|
||||||
<Filter>Header Files</Filter>
|
<Filter>Header Files</Filter>
|
||||||
</ClInclude>
|
</ClInclude>
|
||||||
|
<ClInclude Include="LoopbackCapture.h">
|
||||||
|
<Filter>Header Files</Filter>
|
||||||
|
</ClInclude>
|
||||||
<ClInclude Include="DemoType.h">
|
<ClInclude Include="DemoType.h">
|
||||||
<Filter>Header Files</Filter>
|
<Filter>Header Files</Filter>
|
||||||
</ClInclude>
|
</ClInclude>
|
||||||
|
|||||||
@@ -49,8 +49,15 @@ DWORD g_RecordScaling = 100;
|
|||||||
DWORD g_RecordScalingGIF = 50;
|
DWORD g_RecordScalingGIF = 50;
|
||||||
DWORD g_RecordScalingMP4 = 100;
|
DWORD g_RecordScalingMP4 = 100;
|
||||||
RecordingFormat g_RecordingFormat = RecordingFormat::MP4;
|
RecordingFormat g_RecordingFormat = RecordingFormat::MP4;
|
||||||
|
BOOLEAN g_CaptureSystemAudio = TRUE;
|
||||||
BOOLEAN g_CaptureAudio = FALSE;
|
BOOLEAN g_CaptureAudio = FALSE;
|
||||||
TCHAR g_MicrophoneDeviceId[MAX_PATH] = {0};
|
TCHAR g_MicrophoneDeviceId[MAX_PATH] = {0};
|
||||||
|
TCHAR g_RecordingSaveLocationBuffer[MAX_PATH] = {0};
|
||||||
|
TCHAR g_ScreenshotSaveLocationBuffer[MAX_PATH] = {0};
|
||||||
|
DWORD g_ThemeOverride = 2; // 0=light, 1=dark, 2=system default
|
||||||
|
DWORD g_TrimDialogWidth = 0; // 0 means use default; stored in screen pixels
|
||||||
|
DWORD g_TrimDialogHeight = 0; // 0 means use default; stored in screen pixels
|
||||||
|
DWORD g_TrimDialogVolume = 70; // 0-100 volume level for trim dialog preview
|
||||||
|
|
||||||
REG_SETTING RegSettings[] = {
|
REG_SETTING RegSettings[] = {
|
||||||
{ L"ToggleKey", SETTING_TYPE_DWORD, 0, &g_ToggleKey, static_cast<DOUBLE>(g_ToggleKey) },
|
{ L"ToggleKey", SETTING_TYPE_DWORD, 0, &g_ToggleKey, static_cast<DOUBLE>(g_ToggleKey) },
|
||||||
@@ -91,6 +98,13 @@ REG_SETTING RegSettings[] = {
|
|||||||
{ L"RecordScalingGIF", SETTING_TYPE_DWORD, 0, &g_RecordScalingGIF, static_cast<DOUBLE>(g_RecordScalingGIF) },
|
{ L"RecordScalingGIF", SETTING_TYPE_DWORD, 0, &g_RecordScalingGIF, static_cast<DOUBLE>(g_RecordScalingGIF) },
|
||||||
{ L"RecordScalingMP4", SETTING_TYPE_DWORD, 0, &g_RecordScalingMP4, static_cast<DOUBLE>(g_RecordScalingMP4) },
|
{ L"RecordScalingMP4", SETTING_TYPE_DWORD, 0, &g_RecordScalingMP4, static_cast<DOUBLE>(g_RecordScalingMP4) },
|
||||||
{ L"CaptureAudio", SETTING_TYPE_BOOLEAN, 0, &g_CaptureAudio, static_cast<DOUBLE>(g_CaptureAudio) },
|
{ L"CaptureAudio", SETTING_TYPE_BOOLEAN, 0, &g_CaptureAudio, static_cast<DOUBLE>(g_CaptureAudio) },
|
||||||
|
{ L"CaptureSystemAudio", SETTING_TYPE_BOOLEAN, 0, &g_CaptureSystemAudio, static_cast<DOUBLE>(g_CaptureSystemAudio) },
|
||||||
{ L"MicrophoneDeviceId", SETTING_TYPE_STRING, sizeof(g_MicrophoneDeviceId), g_MicrophoneDeviceId, static_cast<DOUBLE>(0) },
|
{ L"MicrophoneDeviceId", SETTING_TYPE_STRING, sizeof(g_MicrophoneDeviceId), g_MicrophoneDeviceId, static_cast<DOUBLE>(0) },
|
||||||
|
{ L"RecordingSaveLocation", SETTING_TYPE_STRING, sizeof(g_RecordingSaveLocationBuffer), g_RecordingSaveLocationBuffer, static_cast<DOUBLE>(0) },
|
||||||
|
{ L"ScreenshotSaveLocation", SETTING_TYPE_STRING, sizeof(g_ScreenshotSaveLocationBuffer), g_ScreenshotSaveLocationBuffer, static_cast<DOUBLE>(0) },
|
||||||
|
{ L"Theme", SETTING_TYPE_DWORD, 0, &g_ThemeOverride, static_cast<DOUBLE>(g_ThemeOverride) },
|
||||||
|
{ L"TrimDialogWidth", SETTING_TYPE_DWORD, 0, &g_TrimDialogWidth, static_cast<DOUBLE>(0) },
|
||||||
|
{ L"TrimDialogHeight", SETTING_TYPE_DWORD, 0, &g_TrimDialogHeight, static_cast<DOUBLE>(0) },
|
||||||
|
{ L"TrimDialogVolume", SETTING_TYPE_DWORD, 0, &g_TrimDialogVolume, static_cast<DOUBLE>(g_TrimDialogVolume) },
|
||||||
{ NULL, SETTING_TYPE_DWORD, 0, NULL, static_cast<DOUBLE>(0) }
|
{ NULL, SETTING_TYPE_DWORD, 0, NULL, static_cast<DOUBLE>(0) }
|
||||||
};
|
};
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -7,8 +7,10 @@
|
|||||||
#include <shlobj.h>
|
#include <shlobj.h>
|
||||||
#include <tchar.h>
|
#include <tchar.h>
|
||||||
#include <wincodec.h>
|
#include <wincodec.h>
|
||||||
|
#include <shcore.h>
|
||||||
#include <magnification.h>
|
#include <magnification.h>
|
||||||
#include <Uxtheme.h>
|
#include <Uxtheme.h>
|
||||||
|
#include <vssym32.h>
|
||||||
#include <math.h>
|
#include <math.h>
|
||||||
#include <shellapi.h>
|
#include <shellapi.h>
|
||||||
#include <shlwapi.h>
|
#include <shlwapi.h>
|
||||||
@@ -41,12 +43,15 @@
|
|||||||
#include <winrt/Windows.Graphics.DirectX.Direct3d11.h>
|
#include <winrt/Windows.Graphics.DirectX.Direct3d11.h>
|
||||||
#include <winrt/Windows.Media.h>
|
#include <winrt/Windows.Media.h>
|
||||||
#include <winrt/Windows.Media.Core.h>
|
#include <winrt/Windows.Media.Core.h>
|
||||||
|
#include <winrt/Windows.Media.Editing.h>
|
||||||
|
#include <winrt/Windows.Media.Playback.h>
|
||||||
#include <winrt/Windows.Media.Transcoding.h>
|
#include <winrt/Windows.Media.Transcoding.h>
|
||||||
#include <winrt/Windows.Media.MediaProperties.h>
|
#include <winrt/Windows.Media.MediaProperties.h>
|
||||||
#include <winrt/Windows.Media.Devices.h>
|
#include <winrt/Windows.Media.Devices.h>
|
||||||
#include <winrt/Windows.Storage.h>
|
#include <winrt/Windows.Storage.h>
|
||||||
#include <winrt/Windows.Storage.Streams.h>
|
#include <winrt/Windows.Storage.Streams.h>
|
||||||
#include <winrt/Windows.Storage.Pickers.h>
|
#include <winrt/Windows.Storage.Pickers.h>
|
||||||
|
#include <winrt/Windows.Storage.FileProperties.h>
|
||||||
#include <winrt/Windows.Devices.Enumeration.h>
|
#include <winrt/Windows.Devices.Enumeration.h>
|
||||||
|
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
@@ -69,6 +74,9 @@
|
|||||||
#include <d3d11_4.h>
|
#include <d3d11_4.h>
|
||||||
#include <dxgi1_6.h>
|
#include <dxgi1_6.h>
|
||||||
#include <d2d1_3.h>
|
#include <d2d1_3.h>
|
||||||
|
#include <mfapi.h>
|
||||||
|
#include <mfidl.h>
|
||||||
|
#include <mfreadwrite.h>
|
||||||
|
|
||||||
|
|
||||||
// STL
|
// STL
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
// Non-localizable
|
// Non-localizable
|
||||||
//////////////////////////////
|
//////////////////////////////
|
||||||
#define IDC_AUDIO 117
|
#define IDC_AUDIO 117
|
||||||
|
#define IDD_VIDEO_TRIM 119
|
||||||
#define IDC_LINK 1000
|
#define IDC_LINK 1000
|
||||||
#define IDC_ALT 1001
|
#define IDC_ALT 1001
|
||||||
#define IDC_CTRL 1002
|
#define IDC_CTRL 1002
|
||||||
@@ -94,9 +95,22 @@
|
|||||||
#define IDC_DEMOTYPE_STATIC2 1074
|
#define IDC_DEMOTYPE_STATIC2 1074
|
||||||
#define IDC_COPYRIGHT 1075
|
#define IDC_COPYRIGHT 1075
|
||||||
#define IDC_RECORD_FORMAT 1076
|
#define IDC_RECORD_FORMAT 1076
|
||||||
|
#define IDC_TRIM_POSITION_LABEL 1087
|
||||||
|
#define IDC_TRIM_PREVIEW 1088
|
||||||
|
#define IDC_TRIM_TIMELINE 1089
|
||||||
|
#define IDC_TRIM_PLAY_PAUSE 1090
|
||||||
|
#define IDC_TRIM_REWIND 1091
|
||||||
|
#define IDC_TRIM_FORWARD 1092
|
||||||
|
#define IDC_TRIM_DURATION_LABEL 1094
|
||||||
|
#define IDC_TRIM_SKIP_START 1095
|
||||||
|
#define IDC_TRIM_SKIP_END 1096
|
||||||
|
#define IDC_TRIM_VOLUME 1097
|
||||||
|
#define IDC_TRIM_VOLUME_ICON 1098
|
||||||
#define IDC_PEN_WIDTH 1105
|
#define IDC_PEN_WIDTH 1105
|
||||||
#define IDC_TIMER 1106
|
#define IDC_TIMER 1106
|
||||||
#define IDC_SMOOTH_IMAGE 1107
|
#define IDC_SMOOTH_IMAGE 1107
|
||||||
|
#define IDC_CAPTURE_SYSTEM_AUDIO 1108
|
||||||
|
#define IDC_MICROPHONE_LABEL 1109
|
||||||
#define IDC_SAVE 40002
|
#define IDC_SAVE 40002
|
||||||
#define IDC_COPY 40004
|
#define IDC_COPY 40004
|
||||||
#define IDC_RECORD 40006
|
#define IDC_RECORD 40006
|
||||||
@@ -109,9 +123,9 @@
|
|||||||
//
|
//
|
||||||
#ifdef APSTUDIO_INVOKED
|
#ifdef APSTUDIO_INVOKED
|
||||||
#ifndef APSTUDIO_READONLY_SYMBOLS
|
#ifndef APSTUDIO_READONLY_SYMBOLS
|
||||||
#define _APS_NEXT_RESOURCE_VALUE 118
|
#define _APS_NEXT_RESOURCE_VALUE 120
|
||||||
#define _APS_NEXT_COMMAND_VALUE 40013
|
#define _APS_NEXT_COMMAND_VALUE 40013
|
||||||
#define _APS_NEXT_CONTROL_VALUE 1078
|
#define _APS_NEXT_CONTROL_VALUE 1099
|
||||||
#define _APS_NEXT_SYMED_VALUE 101
|
#define _APS_NEXT_SYMED_VALUE 101
|
||||||
#endif
|
#endif
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -89,6 +89,8 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
|||||||
|
|
||||||
public StringProperty RecordFormat { get; set; }
|
public StringProperty RecordFormat { get; set; }
|
||||||
|
|
||||||
|
public BoolProperty CaptureSystemAudio { get; set; }
|
||||||
|
|
||||||
public BoolProperty CaptureAudio { get; set; }
|
public BoolProperty CaptureAudio { get; set; }
|
||||||
|
|
||||||
public StringProperty MicrophoneDeviceId { get; set; }
|
public StringProperty MicrophoneDeviceId { get; set; }
|
||||||
|
|||||||
@@ -285,6 +285,9 @@
|
|||||||
<ComboBoxItem>MP4</ComboBoxItem>
|
<ComboBoxItem>MP4</ComboBoxItem>
|
||||||
</ComboBox>
|
</ComboBox>
|
||||||
</tkcontrols:SettingsCard>
|
</tkcontrols:SettingsCard>
|
||||||
|
<tkcontrols:SettingsCard Name="ZoomItRecordCaptureSystemAudio" ContentAlignment="Left">
|
||||||
|
<CheckBox x:Uid="ZoomIt_Record_CaptureSystemAudio" IsChecked="{x:Bind ViewModel.RecordCaptureSystemAudio, Mode=TwoWay}" />
|
||||||
|
</tkcontrols:SettingsCard>
|
||||||
<tkcontrols:SettingsCard Name="ZoomItRecordCaptureAudio" ContentAlignment="Left">
|
<tkcontrols:SettingsCard Name="ZoomItRecordCaptureAudio" ContentAlignment="Left">
|
||||||
<CheckBox x:Uid="ZoomIt_Record_CaptureAudio" IsChecked="{x:Bind ViewModel.RecordCaptureAudio, Mode=TwoWay}" />
|
<CheckBox x:Uid="ZoomIt_Record_CaptureAudio" IsChecked="{x:Bind ViewModel.RecordCaptureAudio, Mode=TwoWay}" />
|
||||||
</tkcontrols:SettingsCard>
|
</tkcontrols:SettingsCard>
|
||||||
|
|||||||
@@ -5098,6 +5098,9 @@ The break timer font matches the text font.</value>
|
|||||||
<data name="ZoomIt_Record_Format.Header" xml:space="preserve">
|
<data name="ZoomIt_Record_Format.Header" xml:space="preserve">
|
||||||
<value>Format</value>
|
<value>Format</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="ZoomIt_Record_CaptureSystemAudio.Content" xml:space="preserve">
|
||||||
|
<value>Capture system audio</value>
|
||||||
|
</data>
|
||||||
<data name="ZoomIt_Record_CaptureAudio.Content" xml:space="preserve">
|
<data name="ZoomIt_Record_CaptureAudio.Content" xml:space="preserve">
|
||||||
<value>Capture audio input</value>
|
<value>Capture audio input</value>
|
||||||
</data>
|
</data>
|
||||||
|
|||||||
@@ -850,6 +850,20 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool RecordCaptureSystemAudio
|
||||||
|
{
|
||||||
|
get => _zoomItSettings.Properties.CaptureSystemAudio.Value;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (_zoomItSettings.Properties.CaptureSystemAudio.Value != value)
|
||||||
|
{
|
||||||
|
_zoomItSettings.Properties.CaptureSystemAudio.Value = value;
|
||||||
|
OnPropertyChanged(nameof(RecordCaptureSystemAudio));
|
||||||
|
NotifySettingsChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public bool RecordCaptureAudio
|
public bool RecordCaptureAudio
|
||||||
{
|
{
|
||||||
get => _zoomItSettings.Properties.CaptureAudio.Value;
|
get => _zoomItSettings.Properties.CaptureAudio.Value;
|
||||||
|
|||||||
Reference in New Issue
Block a user