Deduplicate the host audio/UI code and scrub history from comments
Extract a RAII RenderEndpoint that owns the enumerator/endpoint/client/ render-event/buffer for both AudioMirror::run_hooked and run_loopback, replacing two hand-rolled setup+teardown blocks and their two identical fail lambdas with one set_error helper; every COM object now frees on each exit path automatically. Route audio_format_verifier's mono decode through the shared correlate_detail::decode_layout instead of a second copy, and read the debug env var via GetEnvironmentVariableA (drops the getenv C4996). Fold the near-identical read_frame/read_pixel staging-copy setup in SharedTextureSource into one map_staging_copy, and the three separate case-insensitive filter helpers (log/injection panels) into ui/text_match.hpp. Comments: drop game-name anecdotes and "the old code"/version phrasing; genericize the override-file example; fix a stale heartbeat-interval note. Behavior unchanged (host tests + mock_game_test pass).
This commit is contained in:
@@ -47,48 +47,12 @@ ScalarFormat resolve(const WAVEFORMATEX* wfx)
|
|||||||
return f;
|
return f;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Decode interleaved PCM (`channels`/`bits`/`tag`) into per-channel float samples, then average to
|
// Decode interleaved PCM at the resolved scalar format down to mono float, via the shared
|
||||||
// mono. Handles float32 and 16/32-bit PCM (the formats WASAPI shared-mode streams use).
|
// de-interleaver the correlation core uses (WAVE_FORMAT_* values match its layout tags).
|
||||||
std::vector<float> to_mono(const std::vector<BYTE>& bytes, const ScalarFormat& fmt)
|
std::vector<float> to_mono(const std::vector<BYTE>& bytes, const ScalarFormat& fmt)
|
||||||
{
|
{
|
||||||
std::vector<float> mono;
|
std::vector<float> mono;
|
||||||
const unsigned ch = fmt.channels == 0 ? 1 : fmt.channels;
|
correlate_detail::decode_layout(bytes.data(), bytes.size(), {fmt.channels, fmt.bits, fmt.tag}, mono);
|
||||||
const unsigned bps = fmt.bits / 8;
|
|
||||||
if (bps == 0)
|
|
||||||
{
|
|
||||||
return mono;
|
|
||||||
}
|
|
||||||
const std::size_t frame = static_cast<std::size_t>(ch) * bps;
|
|
||||||
const std::size_t frames = bytes.size() / frame;
|
|
||||||
mono.resize(frames);
|
|
||||||
const bool is_float = fmt.tag == WAVE_FORMAT_IEEE_FLOAT;
|
|
||||||
for (std::size_t i = 0; i < frames; ++i)
|
|
||||||
{
|
|
||||||
double sum = 0.0;
|
|
||||||
for (unsigned c = 0; c < ch; ++c)
|
|
||||||
{
|
|
||||||
const BYTE* p = bytes.data() + i * frame + static_cast<std::size_t>(c) * bps;
|
|
||||||
float s = 0.0f;
|
|
||||||
if (is_float && fmt.bits == 32)
|
|
||||||
{
|
|
||||||
std::memcpy(&s, p, 4);
|
|
||||||
}
|
|
||||||
else if (fmt.bits == 16)
|
|
||||||
{
|
|
||||||
std::int16_t v;
|
|
||||||
std::memcpy(&v, p, 2);
|
|
||||||
s = v / 32768.0f;
|
|
||||||
}
|
|
||||||
else if (fmt.bits == 32)
|
|
||||||
{
|
|
||||||
std::int32_t v;
|
|
||||||
std::memcpy(&v, p, 4);
|
|
||||||
s = static_cast<float>(v / 2147483648.0);
|
|
||||||
}
|
|
||||||
sum += s;
|
|
||||||
}
|
|
||||||
mono[i] = static_cast<float>(sum / ch);
|
|
||||||
}
|
|
||||||
return mono;
|
return mono;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -201,7 +165,8 @@ FormatVerification verify_stream_format(DWORD pid, AudioRingHeader* ring, unsign
|
|||||||
const std::size_t hook_frames = dev_block != 0 ? cap.bytes.size() / dev_block : 0;
|
const std::size_t hook_frames = dev_block != 0 ? cap.bytes.size() / dev_block : 0;
|
||||||
CoTaskMemFree(dev_wfx);
|
CoTaskMemFree(dev_wfx);
|
||||||
|
|
||||||
if (const char* dbg = std::getenv("COOP_VERIFY_DEBUG"); dbg != nullptr && dbg[0] == '1')
|
char dbg[2] = {};
|
||||||
|
if (GetEnvironmentVariableA("COOP_VERIFY_DEBUG", dbg, sizeof(dbg)) > 0 && dbg[0] == '1')
|
||||||
{
|
{
|
||||||
std::fprintf(stderr, "[verify] dev=%uHz/%uch/%ubit blk=%u chunks=%zu hook_frames=%zu loop=%zu layout=%d\n",
|
std::fprintf(stderr, "[verify] dev=%uHz/%uch/%ubit blk=%u chunks=%zu hook_frames=%zu loop=%zu layout=%d\n",
|
||||||
dev.rate, dev.channels, dev.bits, dev_block, cap.counts.size(), hook_frames, loop_mono.size(),
|
dev.rate, dev.channels, dev.bits, dev_block, cap.counts.size(), hook_frames, loop_mono.size(),
|
||||||
@@ -214,7 +179,7 @@ FormatVerification verify_stream_format(DWORD pid, AudioRingHeader* ring, unsign
|
|||||||
|
|
||||||
if (recover_layout)
|
if (recover_layout)
|
||||||
{
|
{
|
||||||
// Step (b): recover channels + bit depth too, by trying candidate de-interleavings of the
|
// Recover channels + bit depth too, by trying candidate de-interleavings of the
|
||||||
// (de-padded) hook bytes and keeping whichever (layout, rate) correlates with the loopback.
|
// (de-padded) hook bytes and keeping whichever (layout, rate) correlates with the loopback.
|
||||||
const FormatCorrelation fc =
|
const FormatCorrelation fc =
|
||||||
correlate_format(cap, loop_mono, dev.rate, standard_audio_rates(), standard_audio_layouts());
|
correlate_format(cap, loop_mono, dev.rate, standard_audio_rates(), standard_audio_layouts());
|
||||||
@@ -228,7 +193,7 @@ FormatVerification verify_stream_format(DWORD pid, AudioRingHeader* ring, unsign
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Step (a): rate only, assuming the hook layout matches the device (common stereo case), so
|
// Rate only, assuming the hook layout matches the device (common stereo case), so
|
||||||
// the de-padded payload is already clean device-layout audio.
|
// the de-padded payload is already clean device-layout audio.
|
||||||
const std::vector<float> hook_mono = to_mono(cap.bytes, dev);
|
const std::vector<float> hook_mono = to_mono(cap.bytes, dev);
|
||||||
const RateCorrelation rc = correlate_rate(hook_mono, loop_mono, dev.rate, standard_audio_rates());
|
const RateCorrelation rc = correlate_rate(hook_mono, loop_mono, dev.rate, standard_audio_rates());
|
||||||
|
|||||||
@@ -100,6 +100,112 @@ struct ByteRing
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// The default render endpoint plus an event-driven render client, shared by the hooked and
|
||||||
|
// loopback mirror paths. RAII: everything acquired is released on destruction.
|
||||||
|
struct RenderEndpoint
|
||||||
|
{
|
||||||
|
IMMDeviceEnumerator* enumerator = nullptr;
|
||||||
|
IMMDevice* endpoint = nullptr;
|
||||||
|
IAudioClient* client = nullptr;
|
||||||
|
IAudioRenderClient* render = nullptr;
|
||||||
|
HANDLE event = nullptr;
|
||||||
|
UINT32 buffer_frames = 0;
|
||||||
|
|
||||||
|
RenderEndpoint() = default;
|
||||||
|
RenderEndpoint(const RenderEndpoint&) = delete;
|
||||||
|
RenderEndpoint& operator=(const RenderEndpoint&) = delete;
|
||||||
|
|
||||||
|
~RenderEndpoint()
|
||||||
|
{
|
||||||
|
if (render)
|
||||||
|
{
|
||||||
|
render->Release();
|
||||||
|
}
|
||||||
|
if (client)
|
||||||
|
{
|
||||||
|
client->Release();
|
||||||
|
}
|
||||||
|
if (endpoint)
|
||||||
|
{
|
||||||
|
endpoint->Release();
|
||||||
|
}
|
||||||
|
if (enumerator)
|
||||||
|
{
|
||||||
|
enumerator->Release();
|
||||||
|
}
|
||||||
|
if (event)
|
||||||
|
{
|
||||||
|
CloseHandle(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bring up the default endpoint, an (uninitialized) IAudioClient, and the render event.
|
||||||
|
// On failure reports the failing step through `fail(step, hr)` and returns false.
|
||||||
|
template <typename Fail>
|
||||||
|
bool activate(Fail&& fail)
|
||||||
|
{
|
||||||
|
HRESULT hr = CoCreateInstance(__uuidof(MMDeviceEnumerator), nullptr, CLSCTX_ALL,
|
||||||
|
__uuidof(IMMDeviceEnumerator), reinterpret_cast<void**>(&enumerator));
|
||||||
|
if (FAILED(hr))
|
||||||
|
{
|
||||||
|
fail("CoCreateInstance(MMDeviceEnumerator)", hr);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
hr = enumerator->GetDefaultAudioEndpoint(eRender, eConsole, &endpoint);
|
||||||
|
if (FAILED(hr))
|
||||||
|
{
|
||||||
|
fail("GetDefaultAudioEndpoint", hr);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
hr = endpoint->Activate(__uuidof(IAudioClient), CLSCTX_ALL, nullptr, reinterpret_cast<void**>(&client));
|
||||||
|
if (FAILED(hr))
|
||||||
|
{
|
||||||
|
fail("Activate render client", hr);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
event = CreateEventW(nullptr, FALSE, FALSE, nullptr);
|
||||||
|
if (!event)
|
||||||
|
{
|
||||||
|
fail("CreateEvent(render)", HRESULT_FROM_WIN32(GetLastError()));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize the client shared-mode at `fmt`. Returned (not reported) so the hooked path
|
||||||
|
// can treat an unrenderable game format as a quiet fall-back rather than an error.
|
||||||
|
HRESULT initialize(const WAVEFORMATEX* fmt, DWORD flags)
|
||||||
|
{
|
||||||
|
constexpr REFERENCE_TIME kRenderBuffer = 30 * 10000; // 30 ms, in 100-ns units
|
||||||
|
return client->Initialize(AUDCLNT_SHAREMODE_SHARED, flags, kRenderBuffer, 0, fmt, nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wire the render event and fetch the render service + buffer size (post-Initialize).
|
||||||
|
template <typename Fail>
|
||||||
|
bool wire(Fail&& fail)
|
||||||
|
{
|
||||||
|
HRESULT hr = client->SetEventHandle(event);
|
||||||
|
if (FAILED(hr))
|
||||||
|
{
|
||||||
|
fail("Render SetEventHandle", hr);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
hr = client->GetService(__uuidof(IAudioRenderClient), reinterpret_cast<void**>(&render));
|
||||||
|
if (FAILED(hr))
|
||||||
|
{
|
||||||
|
fail("GetService(RenderClient)", hr);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
hr = client->GetBufferSize(&buffer_frames);
|
||||||
|
if (FAILED(hr))
|
||||||
|
{
|
||||||
|
fail("GetBufferSize", hr);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
AudioMirror::~AudioMirror()
|
AudioMirror::~AudioMirror()
|
||||||
@@ -125,6 +231,13 @@ void AudioMirror::set_status(std::string s)
|
|||||||
status_ = std::move(s);
|
status_ = std::move(s);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void AudioMirror::set_error(const char* step, HRESULT hr)
|
||||||
|
{
|
||||||
|
char buf[160];
|
||||||
|
std::snprintf(buf, sizeof(buf), "%s (0x%08lX)", step, static_cast<unsigned long>(hr));
|
||||||
|
set_status(buf);
|
||||||
|
}
|
||||||
|
|
||||||
void AudioMirror::set_fallback_reason(std::string s)
|
void AudioMirror::set_fallback_reason(std::string s)
|
||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> lock(status_mutex_);
|
std::lock_guard<std::mutex> lock(status_mutex_);
|
||||||
@@ -429,77 +542,32 @@ AudioMirror::HookedResult AudioMirror::run_hooked(AudioRingHeader* const* rings)
|
|||||||
}
|
}
|
||||||
auto* fmt = reinterpret_cast<WAVEFORMATEX*>(&wfx);
|
auto* fmt = reinterpret_cast<WAVEFORMATEX*>(&wfx);
|
||||||
|
|
||||||
IMMDeviceEnumerator* enumerator = nullptr;
|
RenderEndpoint ep;
|
||||||
IMMDevice* endpoint = nullptr;
|
|
||||||
IAudioClient* render_client = nullptr;
|
|
||||||
IAudioRenderClient* render = nullptr;
|
|
||||||
HANDLE render_event = nullptr;
|
|
||||||
bool started = false;
|
bool started = false;
|
||||||
HookedResult result = HookedResult::Stopped;
|
HookedResult result = HookedResult::Stopped;
|
||||||
|
auto fail = [this](const char* step, HRESULT hr) { set_error(step, hr); };
|
||||||
auto fail = [&](const char* msg, HRESULT hr) {
|
|
||||||
char buf[160];
|
|
||||||
std::snprintf(buf, sizeof(buf), "%s (0x%08lX)", msg, static_cast<unsigned long>(hr));
|
|
||||||
set_status(buf);
|
|
||||||
};
|
|
||||||
|
|
||||||
do
|
do
|
||||||
{
|
{
|
||||||
HRESULT hr = CoCreateInstance(__uuidof(MMDeviceEnumerator), nullptr, CLSCTX_ALL,
|
if (!ep.activate(fail))
|
||||||
__uuidof(IMMDeviceEnumerator), reinterpret_cast<void**>(&enumerator));
|
|
||||||
if (FAILED(hr))
|
|
||||||
{
|
{
|
||||||
fail("CoCreateInstance(MMDeviceEnumerator)", hr);
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
hr = enumerator->GetDefaultAudioEndpoint(eRender, eConsole, &endpoint);
|
|
||||||
if (FAILED(hr))
|
|
||||||
{
|
|
||||||
fail("GetDefaultAudioEndpoint", hr);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
hr = endpoint->Activate(__uuidof(IAudioClient), CLSCTX_ALL, nullptr,
|
|
||||||
reinterpret_cast<void**>(&render_client));
|
|
||||||
if (FAILED(hr))
|
|
||||||
{
|
|
||||||
fail("Activate render client", hr);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
render_event = CreateEventW(nullptr, FALSE, FALSE, nullptr);
|
|
||||||
if (!render_event)
|
|
||||||
{
|
|
||||||
fail("CreateEvent(render)", HRESULT_FROM_WIN32(GetLastError()));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
constexpr REFERENCE_TIME kRenderBuffer = 30 * 10000; // 30 ms
|
|
||||||
const DWORD flags = AUDCLNT_STREAMFLAGS_EVENTCALLBACK | AUDCLNT_STREAMFLAGS_AUTOCONVERTPCM |
|
const DWORD flags = AUDCLNT_STREAMFLAGS_EVENTCALLBACK | AUDCLNT_STREAMFLAGS_AUTOCONVERTPCM |
|
||||||
AUDCLNT_STREAMFLAGS_SRC_DEFAULT_QUALITY;
|
AUDCLNT_STREAMFLAGS_SRC_DEFAULT_QUALITY;
|
||||||
hr = render_client->Initialize(AUDCLNT_SHAREMODE_SHARED, flags, kRenderBuffer, 0, fmt, nullptr);
|
if (FAILED(ep.initialize(fmt, flags)))
|
||||||
if (FAILED(hr))
|
|
||||||
{
|
{
|
||||||
// The game's format isn't renderable here (rare). Bail to loopback.
|
// The game's format isn't renderable here (rare). Bail to loopback.
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
started = true; // past the point where falling back is clean
|
started = true; // past the point where falling back is clean
|
||||||
if (FAILED(hr = render_client->SetEventHandle(render_event)))
|
if (!ep.wire(fail))
|
||||||
{
|
{
|
||||||
fail("Render SetEventHandle", hr);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (FAILED(hr = render_client->GetService(__uuidof(IAudioRenderClient),
|
|
||||||
reinterpret_cast<void**>(&render))))
|
|
||||||
{
|
|
||||||
fail("GetService(RenderClient)", hr);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
UINT32 render_frames = 0;
|
|
||||||
if (FAILED(hr = render_client->GetBufferSize(&render_frames)))
|
|
||||||
{
|
|
||||||
fail("GetBufferSize", hr);
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
IAudioClient* render_client = ep.client;
|
||||||
|
IAudioRenderClient* render = ep.render;
|
||||||
|
const UINT32 render_frames = ep.buffer_frames;
|
||||||
|
|
||||||
sample_rate_.store(rate, std::memory_order_relaxed);
|
sample_rate_.store(rate, std::memory_order_relaxed);
|
||||||
channels_.store(channels, std::memory_order_relaxed);
|
channels_.store(channels, std::memory_order_relaxed);
|
||||||
@@ -514,9 +582,9 @@ AudioMirror::HookedResult AudioMirror::run_hooked(AudioRingHeader* const* rings)
|
|||||||
std::vector<BYTE> temp(static_cast<size_t>(render_frames) * frame_bytes);
|
std::vector<BYTE> temp(static_cast<size_t>(render_frames) * frame_bytes);
|
||||||
std::vector<float> acc(static_cast<size_t>(render_frames) * channels);
|
std::vector<float> acc(static_cast<size_t>(render_frames) * channels);
|
||||||
|
|
||||||
if (FAILED(hr = render_client->Start()))
|
if (const HRESULT hr = render_client->Start(); FAILED(hr))
|
||||||
{
|
{
|
||||||
fail("Render Start", hr);
|
set_error("Render Start", hr);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -526,7 +594,7 @@ AudioMirror::HookedResult AudioMirror::run_hooked(AudioRingHeader* const* rings)
|
|||||||
source_.store(Source::Hooked, std::memory_order_relaxed);
|
source_.store(Source::Hooked, std::memory_order_relaxed);
|
||||||
running_.store(true, std::memory_order_release);
|
running_.store(true, std::memory_order_release);
|
||||||
|
|
||||||
HANDLE waits[2] = {stop_event_, render_event};
|
HANDLE waits[2] = {stop_event_, ep.event};
|
||||||
for (;;)
|
for (;;)
|
||||||
{
|
{
|
||||||
const DWORD w = WaitForMultipleObjects(2, waits, FALSE, 200);
|
const DWORD w = WaitForMultipleObjects(2, waits, FALSE, 200);
|
||||||
@@ -614,27 +682,6 @@ AudioMirror::HookedResult AudioMirror::run_hooked(AudioRingHeader* const* rings)
|
|||||||
enable_capture(rings, false);
|
enable_capture(rings, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (render)
|
|
||||||
{
|
|
||||||
render->Release();
|
|
||||||
}
|
|
||||||
if (render_client)
|
|
||||||
{
|
|
||||||
render_client->Release();
|
|
||||||
}
|
|
||||||
if (endpoint)
|
|
||||||
{
|
|
||||||
endpoint->Release();
|
|
||||||
}
|
|
||||||
if (enumerator)
|
|
||||||
{
|
|
||||||
enumerator->Release();
|
|
||||||
}
|
|
||||||
if (render_event)
|
|
||||||
{
|
|
||||||
CloseHandle(render_event);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!started)
|
if (!started)
|
||||||
{
|
{
|
||||||
// Never got a working render client; let the caller try loopback. Capture is
|
// Never got a working render client; let the caller try loopback. Capture is
|
||||||
@@ -649,45 +696,20 @@ bool AudioMirror::run_loopback(DWORD pid, AudioRingHeader* promote_ring)
|
|||||||
source_.store(Source::Loopback, std::memory_order_relaxed);
|
source_.store(Source::Loopback, std::memory_order_relaxed);
|
||||||
bool promote = false;
|
bool promote = false;
|
||||||
|
|
||||||
IMMDeviceEnumerator* enumerator = nullptr;
|
RenderEndpoint ep;
|
||||||
IMMDevice* endpoint = nullptr;
|
|
||||||
IAudioClient* render_client = nullptr;
|
|
||||||
IAudioRenderClient* render = nullptr;
|
|
||||||
WAVEFORMATEX* fmt = nullptr;
|
WAVEFORMATEX* fmt = nullptr;
|
||||||
HANDLE render_event = nullptr;
|
|
||||||
ProcessLoopbackCapture capture;
|
ProcessLoopbackCapture capture;
|
||||||
|
auto fail = [this](const char* step, HRESULT hr) { set_error(step, hr); };
|
||||||
auto fail = [&](const char* msg, HRESULT hr) {
|
|
||||||
char buf[160];
|
|
||||||
std::snprintf(buf, sizeof(buf), "%s (0x%08lX)", msg, static_cast<unsigned long>(hr));
|
|
||||||
set_status(buf);
|
|
||||||
};
|
|
||||||
|
|
||||||
do
|
do
|
||||||
{
|
{
|
||||||
HRESULT hr = CoCreateInstance(__uuidof(MMDeviceEnumerator), nullptr, CLSCTX_ALL,
|
if (!ep.activate(fail))
|
||||||
__uuidof(IMMDeviceEnumerator), reinterpret_cast<void**>(&enumerator));
|
|
||||||
if (FAILED(hr))
|
|
||||||
{
|
{
|
||||||
fail("CoCreateInstance(MMDeviceEnumerator)", hr);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
hr = enumerator->GetDefaultAudioEndpoint(eRender, eConsole, &endpoint);
|
|
||||||
if (FAILED(hr))
|
|
||||||
{
|
|
||||||
fail("GetDefaultAudioEndpoint", hr);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
hr = endpoint->Activate(__uuidof(IAudioClient), CLSCTX_ALL, nullptr,
|
|
||||||
reinterpret_cast<void**>(&render_client));
|
|
||||||
if (FAILED(hr))
|
|
||||||
{
|
|
||||||
fail("Activate render client", hr);
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
// Capture and render share one format (the output endpoint's mix format);
|
// Capture and render share one format (the output endpoint's mix format);
|
||||||
// WASAPI converts the captured process audio into it.
|
// WASAPI converts the captured process audio into it.
|
||||||
hr = render_client->GetMixFormat(&fmt);
|
HRESULT hr = ep.client->GetMixFormat(&fmt);
|
||||||
if (FAILED(hr))
|
if (FAILED(hr))
|
||||||
{
|
{
|
||||||
fail("GetMixFormat", hr);
|
fail("GetMixFormat", hr);
|
||||||
@@ -696,40 +718,19 @@ bool AudioMirror::run_loopback(DWORD pid, AudioRingHeader* promote_ring)
|
|||||||
sample_rate_.store(fmt->nSamplesPerSec, std::memory_order_relaxed);
|
sample_rate_.store(fmt->nSamplesPerSec, std::memory_order_relaxed);
|
||||||
channels_.store(fmt->nChannels, std::memory_order_relaxed);
|
channels_.store(fmt->nChannels, std::memory_order_relaxed);
|
||||||
|
|
||||||
render_event = CreateEventW(nullptr, FALSE, FALSE, nullptr);
|
hr = ep.initialize(fmt, AUDCLNT_STREAMFLAGS_EVENTCALLBACK);
|
||||||
if (!render_event)
|
|
||||||
{
|
|
||||||
fail("CreateEvent(render)", HRESULT_FROM_WIN32(GetLastError()));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
constexpr REFERENCE_TIME kRenderBuffer = 30 * 10000; // 30 ms, in 100-ns units
|
|
||||||
hr = render_client->Initialize(AUDCLNT_SHAREMODE_SHARED, AUDCLNT_STREAMFLAGS_EVENTCALLBACK,
|
|
||||||
kRenderBuffer, 0, fmt, nullptr);
|
|
||||||
if (FAILED(hr))
|
if (FAILED(hr))
|
||||||
{
|
{
|
||||||
fail("Render Initialize", hr);
|
fail("Render Initialize", hr);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
hr = render_client->SetEventHandle(render_event);
|
if (!ep.wire(fail))
|
||||||
if (FAILED(hr))
|
|
||||||
{
|
{
|
||||||
fail("Render SetEventHandle", hr);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
hr = render_client->GetService(__uuidof(IAudioRenderClient), reinterpret_cast<void**>(&render));
|
|
||||||
if (FAILED(hr))
|
|
||||||
{
|
|
||||||
fail("GetService(RenderClient)", hr);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
UINT32 render_frames = 0;
|
|
||||||
hr = render_client->GetBufferSize(&render_frames);
|
|
||||||
if (FAILED(hr))
|
|
||||||
{
|
|
||||||
fail("GetBufferSize", hr);
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
IAudioClient* render_client = ep.client;
|
||||||
|
IAudioRenderClient* render = ep.render;
|
||||||
|
const UINT32 render_frames = ep.buffer_frames;
|
||||||
|
|
||||||
const size_t frame_bytes = fmt->nBlockAlign;
|
const size_t frame_bytes = fmt->nBlockAlign;
|
||||||
ByteRing ring;
|
ByteRing ring;
|
||||||
@@ -760,7 +761,7 @@ bool AudioMirror::run_loopback(DWORD pid, AudioRingHeader* promote_ring)
|
|||||||
set_status(st);
|
set_status(st);
|
||||||
running_.store(true, std::memory_order_release);
|
running_.store(true, std::memory_order_release);
|
||||||
|
|
||||||
HANDLE waits[2] = {stop_event_, render_event};
|
HANDLE waits[2] = {stop_event_, ep.event};
|
||||||
for (;;)
|
for (;;)
|
||||||
{
|
{
|
||||||
const DWORD w = WaitForMultipleObjects(2, waits, FALSE, 200);
|
const DWORD w = WaitForMultipleObjects(2, waits, FALSE, 200);
|
||||||
@@ -809,31 +810,10 @@ bool AudioMirror::run_loopback(DWORD pid, AudioRingHeader* promote_ring)
|
|||||||
} while (false);
|
} while (false);
|
||||||
|
|
||||||
capture.stop();
|
capture.stop();
|
||||||
|
|
||||||
if (render)
|
|
||||||
{
|
|
||||||
render->Release();
|
|
||||||
}
|
|
||||||
if (render_client)
|
|
||||||
{
|
|
||||||
render_client->Release();
|
|
||||||
}
|
|
||||||
if (endpoint)
|
|
||||||
{
|
|
||||||
endpoint->Release();
|
|
||||||
}
|
|
||||||
if (enumerator)
|
|
||||||
{
|
|
||||||
enumerator->Release();
|
|
||||||
}
|
|
||||||
if (fmt)
|
if (fmt)
|
||||||
{
|
{
|
||||||
CoTaskMemFree(fmt);
|
CoTaskMemFree(fmt);
|
||||||
}
|
}
|
||||||
if (render_event)
|
|
||||||
{
|
|
||||||
CloseHandle(render_event);
|
|
||||||
}
|
|
||||||
return promote; // true = hook caught up, caller should switch to hooked
|
return promote; // true = hook caught up, caller should switch to hooked
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
// THIS host's audio) carries the real game's sound, re-rendering it on the
|
// THIS host's audio) carries the real game's sound, re-rendering it on the
|
||||||
// default output endpoint.
|
// default output endpoint.
|
||||||
//
|
//
|
||||||
// Two source paths (see docs/audio-render-hook-plan.md):
|
// Two source paths:
|
||||||
// - Hooked: the injected render-hook copies the game's frames into a shared
|
// - Hooked: the injected render-hook copies the game's frames into a shared
|
||||||
// audio ring AND silences the game locally, so there is no echo. Preferred.
|
// audio ring AND silences the game locally, so there is no echo. Preferred.
|
||||||
// - Loopback: WASAPI process-loopback capture of the game (the game still
|
// - Loopback: WASAPI process-loopback capture of the game (the game still
|
||||||
@@ -128,6 +128,7 @@ private:
|
|||||||
void drain_ops(); // audio thread: post queued operator ops to the session rings
|
void drain_ops(); // audio thread: post queued operator ops to the session rings
|
||||||
bool stop_requested() const;
|
bool stop_requested() const;
|
||||||
void set_status(std::string s);
|
void set_status(std::string s);
|
||||||
|
void set_error(const char* step, HRESULT hr); // status = "<step> (0x<hr>)"
|
||||||
void set_fallback_reason(std::string s);
|
void set_fallback_reason(std::string s);
|
||||||
|
|
||||||
std::thread thread_;
|
std::thread thread_;
|
||||||
|
|||||||
@@ -23,10 +23,8 @@ std::wstring to_lower(std::wstring s)
|
|||||||
return s;
|
return s;
|
||||||
}
|
}
|
||||||
|
|
||||||
// UTF-8 round-trip so a non-ASCII image name (e.g. a CJK game exe) survives persist/reload and can't
|
// UTF-8 round-trip so a non-ASCII image name (e.g. a CJK game exe) survives persist/reload and
|
||||||
// collide with another name in its high bits. The old `c & 0x7F` mask was lossy and not a true
|
// can't collide with another name; for ASCII names (the common case) UTF-8 is byte-identical.
|
||||||
// inverse of widen; for ASCII names (the common case) UTF-8 is byte-identical, so existing override
|
|
||||||
// files stay compatible.
|
|
||||||
std::string narrow(const std::wstring& w)
|
std::string narrow(const std::wstring& w)
|
||||||
{
|
{
|
||||||
if (w.empty())
|
if (w.empty())
|
||||||
|
|||||||
@@ -5,8 +5,8 @@
|
|||||||
// truth). A human-readable one-line-per-game text file:
|
// truth). A human-readable one-line-per-game text file:
|
||||||
//
|
//
|
||||||
// # CoopAllTheThings per-game audio overrides
|
// # CoopAllTheThings per-game audio overrides
|
||||||
// brotato.exe = 44100 2 32 float
|
// <image.exe> = 44100 2 32 float
|
||||||
// snb.exe = 48000 2 16 pcm
|
// <image.exe> = 48000 2 16 pcm
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
|
|||||||
@@ -12,11 +12,11 @@
|
|||||||
// 3. Re-prime (rebuild the cushion) ONLY on a genuine starvation: the device buffer fully
|
// 3. Re-prime (rebuild the cushion) ONLY on a genuine starvation: the device buffer fully
|
||||||
// drained AND the ring is empty. Crucially, do NOT re-prime on a mere partial fill.
|
// drained AND the ring is empty. Crucially, do NOT re-prime on a mere partial fill.
|
||||||
//
|
//
|
||||||
// Rule 3 is the whole point. The original code re-primed whenever it couldn't completely fill
|
// Rule 3 is the whole point. Re-priming whenever a tick can't completely fill the free buffer
|
||||||
// the free buffer space that tick (`to_write < avail`); that withholds the feed until ~30 ms
|
// space (`to_write < avail`) withholds the feed until ~30 ms has rebuffered, which DRAINS the
|
||||||
// has rebuffered, which DRAINS the device and manufactures the very ~30 ms silence gap it meant
|
// device and manufactures the very ~30 ms silence gap it means to avoid -- turning a one-frame
|
||||||
// to avoid -- turning a one-frame ring dip into a full drop-out. On a jittery game that fired
|
// ring dip into a full drop-out. On a jittery game that fires constantly, producing choppy /
|
||||||
// constantly, producing the choppy / "metallic" mirror audio. coop_audio_validate quantifies it.
|
// "metallic" mirror audio. coop_audio_validate quantifies the defect.
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
|||||||
@@ -298,8 +298,8 @@ void AudioPanel::draw_ui(const HookStatusView& status, bool debug_details)
|
|||||||
|
|
||||||
// --- Render-stream view -----------------------------------------------
|
// --- Render-stream view -----------------------------------------------
|
||||||
// The hook counts every render stream the game creates, even with mirroring
|
// The hook counts every render stream the game creates, even with mirroring
|
||||||
// off. v1 captures only the first ("primary"); the count makes a multi-stream
|
// off. The count makes a multi-stream game obvious; the per-stream table
|
||||||
// game obvious, and the per-stream table (debug details) shows why.
|
// (debug details) shows each stream's format/provenance/activity.
|
||||||
ImGui::Separator();
|
ImGui::Separator();
|
||||||
ImGui::Text("Render streams: %u", status.audio_streams_seen);
|
ImGui::Text("Render streams: %u", status.audio_streams_seen);
|
||||||
if (status.audio_streams_seen > kMaxAudioStreams)
|
if (status.audio_streams_seen > kMaxAudioStreams)
|
||||||
|
|||||||
@@ -93,27 +93,33 @@ bool SharedTextureSource::reopen(unsigned long pid, const VideoShareView& share)
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool SharedTextureSource::read_frame(std::vector<std::uint8_t>& out, std::uint32_t& w, std::uint32_t& h)
|
bool SharedTextureSource::map_staging_copy(Microsoft::WRL::ComPtr<ID3D11Texture2D>& staging,
|
||||||
|
D3D11_MAPPED_SUBRESOURCE& map, D3D11_TEXTURE2D_DESC& desc)
|
||||||
{
|
{
|
||||||
if (private_ == nullptr || ctx_ == nullptr || device_ == nullptr)
|
if (private_ == nullptr || ctx_ == nullptr || device_ == nullptr)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
D3D11_TEXTURE2D_DESC desc{};
|
|
||||||
private_->GetDesc(&desc);
|
private_->GetDesc(&desc);
|
||||||
D3D11_TEXTURE2D_DESC staging_desc = desc;
|
D3D11_TEXTURE2D_DESC staging_desc = desc;
|
||||||
staging_desc.Usage = D3D11_USAGE_STAGING;
|
staging_desc.Usage = D3D11_USAGE_STAGING;
|
||||||
staging_desc.BindFlags = 0;
|
staging_desc.BindFlags = 0;
|
||||||
staging_desc.CPUAccessFlags = D3D11_CPU_ACCESS_READ;
|
staging_desc.CPUAccessFlags = D3D11_CPU_ACCESS_READ;
|
||||||
staging_desc.MiscFlags = 0;
|
staging_desc.MiscFlags = 0;
|
||||||
Microsoft::WRL::ComPtr<ID3D11Texture2D> staging;
|
|
||||||
if (FAILED(device_->CreateTexture2D(&staging_desc, nullptr, &staging)))
|
if (FAILED(device_->CreateTexture2D(&staging_desc, nullptr, &staging)))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
ctx_->CopyResource(staging.Get(), private_.Get());
|
ctx_->CopyResource(staging.Get(), private_.Get());
|
||||||
|
return SUCCEEDED(ctx_->Map(staging.Get(), 0, D3D11_MAP_READ, 0, &map));
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SharedTextureSource::read_frame(std::vector<std::uint8_t>& out, std::uint32_t& w, std::uint32_t& h)
|
||||||
|
{
|
||||||
|
Microsoft::WRL::ComPtr<ID3D11Texture2D> staging;
|
||||||
D3D11_MAPPED_SUBRESOURCE map{};
|
D3D11_MAPPED_SUBRESOURCE map{};
|
||||||
if (FAILED(ctx_->Map(staging.Get(), 0, D3D11_MAP_READ, 0, &map)))
|
D3D11_TEXTURE2D_DESC desc{};
|
||||||
|
if (!map_staging_copy(staging, map, desc))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -132,30 +138,16 @@ bool SharedTextureSource::read_frame(std::vector<std::uint8_t>& out, std::uint32
|
|||||||
|
|
||||||
bool SharedTextureSource::read_pixel(std::uint32_t x, std::uint32_t y, std::uint8_t out[4])
|
bool SharedTextureSource::read_pixel(std::uint32_t x, std::uint32_t y, std::uint8_t out[4])
|
||||||
{
|
{
|
||||||
if (private_ == nullptr || ctx_ == nullptr || device_ == nullptr)
|
Microsoft::WRL::ComPtr<ID3D11Texture2D> staging;
|
||||||
|
D3D11_MAPPED_SUBRESOURCE map{};
|
||||||
|
D3D11_TEXTURE2D_DESC desc{};
|
||||||
|
if (!map_staging_copy(staging, map, desc))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
D3D11_TEXTURE2D_DESC desc{};
|
|
||||||
private_->GetDesc(&desc);
|
|
||||||
if (x >= desc.Width || y >= desc.Height)
|
if (x >= desc.Width || y >= desc.Height)
|
||||||
{
|
{
|
||||||
return false;
|
ctx_->Unmap(staging.Get(), 0);
|
||||||
}
|
|
||||||
D3D11_TEXTURE2D_DESC staging_desc = desc;
|
|
||||||
staging_desc.Usage = D3D11_USAGE_STAGING;
|
|
||||||
staging_desc.BindFlags = 0;
|
|
||||||
staging_desc.CPUAccessFlags = D3D11_CPU_ACCESS_READ;
|
|
||||||
staging_desc.MiscFlags = 0;
|
|
||||||
Microsoft::WRL::ComPtr<ID3D11Texture2D> staging;
|
|
||||||
if (FAILED(device_->CreateTexture2D(&staging_desc, nullptr, &staging)))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
ctx_->CopyResource(staging.Get(), private_.Get());
|
|
||||||
D3D11_MAPPED_SUBRESOURCE map{};
|
|
||||||
if (FAILED(ctx_->Map(staging.Get(), 0, D3D11_MAP_READ, 0, &map)))
|
|
||||||
{
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const auto* px = static_cast<const std::uint8_t*>(map.pData) + static_cast<std::size_t>(y) * map.RowPitch +
|
const auto* px = static_cast<const std::uint8_t*>(map.pData) + static_cast<std::size_t>(y) * map.RowPitch +
|
||||||
|
|||||||
@@ -68,6 +68,10 @@ public:
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
bool reopen(unsigned long pid, const VideoShareView& share);
|
bool reopen(unsigned long pid, const VideoShareView& share);
|
||||||
|
// Copy the private texture into a fresh CPU staging texture and map it (shared body of
|
||||||
|
// read_frame/read_pixel). On success the caller reads via `map` and must Unmap `staging`.
|
||||||
|
bool map_staging_copy(Microsoft::WRL::ComPtr<ID3D11Texture2D>& staging, D3D11_MAPPED_SUBRESOURCE& map,
|
||||||
|
D3D11_TEXTURE2D_DESC& desc);
|
||||||
|
|
||||||
Microsoft::WRL::ComPtr<ID3D11Device1> device_;
|
Microsoft::WRL::ComPtr<ID3D11Device1> device_;
|
||||||
Microsoft::WRL::ComPtr<ID3D11DeviceContext> ctx_;
|
Microsoft::WRL::ComPtr<ID3D11DeviceContext> ctx_;
|
||||||
|
|||||||
@@ -15,8 +15,8 @@ bool hook_dll_alive(unsigned long pid, int timeout_ms)
|
|||||||
// The per-pid section exists only while someone holds it; a connected DLL keeps it alive across
|
// The per-pid section exists only while someone holds it; a connected DLL keeps it alive across
|
||||||
// a host restart. Open it (don't create), then confirm the DLL's worker is actually beating --
|
// a host restart. Open it (don't create), then confirm the DLL's worker is actually beating --
|
||||||
// a stale section with a dead worker (heartbeat frozen) must read as not-alive so we inject fresh.
|
// a stale section with a dead worker (heartbeat frozen) must read as not-alive so we inject fresh.
|
||||||
// Poll rather than sample once: the worker only beats ~every 250 ms, so a single short read can
|
// Poll rather than sample once: the worker only beats ~4x/s, so a single short read can straddle
|
||||||
// straddle a gap and miss it; return the instant a beat lands, and give up after the timeout.
|
// a gap and miss it; return the instant a beat lands, and give up after the timeout.
|
||||||
SharedMemory shm;
|
SharedMemory shm;
|
||||||
if (!shm.open(shared_memory_name(pid), sizeof(SharedBlock)))
|
if (!shm.open(shared_memory_name(pid), sizeof(SharedBlock)))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
#include "inject/dll_probe.hpp"
|
#include "inject/dll_probe.hpp"
|
||||||
#include "inject/injector.hpp"
|
#include "inject/injector.hpp"
|
||||||
#include "ui/app_chrome.hpp"
|
#include "ui/app_chrome.hpp"
|
||||||
|
#include "ui/text_match.hpp"
|
||||||
|
|
||||||
namespace coop
|
namespace coop
|
||||||
{
|
{
|
||||||
@@ -32,41 +33,15 @@ std::string narrow(const std::wstring& w)
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Case-insensitive equality of two image names (e.g. "snb.exe").
|
// Case-insensitive equality of two image names (e.g. "game.exe").
|
||||||
bool iequals_name(const std::wstring& a, const std::wstring& b)
|
bool iequals_name(const std::wstring& a, const std::wstring& b)
|
||||||
{
|
{
|
||||||
const std::string x = narrow(a), y = narrow(b);
|
return _wcsicmp(a.c_str(), b.c_str()) == 0;
|
||||||
if (x.size() != y.size())
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
for (std::size_t i = 0; i < x.size(); ++i)
|
|
||||||
{
|
|
||||||
if (::tolower(static_cast<unsigned char>(x[i])) != ::tolower(static_cast<unsigned char>(y[i])))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool contains_ci(const std::wstring& haystack, const char* needle_utf8)
|
bool contains_ci_w(const std::wstring& haystack, const char* needle_utf8)
|
||||||
{
|
{
|
||||||
if (needle_utf8 == nullptr || needle_utf8[0] == '\0')
|
return contains_ci(narrow(haystack), needle_utf8);
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
const std::string hay = narrow(haystack);
|
|
||||||
std::string h = hay, n = needle_utf8;
|
|
||||||
for (char& c : h)
|
|
||||||
{
|
|
||||||
c = static_cast<char>(::tolower(static_cast<unsigned char>(c)));
|
|
||||||
}
|
|
||||||
for (char& c : n)
|
|
||||||
{
|
|
||||||
c = static_cast<char>(::tolower(static_cast<unsigned char>(c)));
|
|
||||||
}
|
|
||||||
return h.find(n) != std::string::npos;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Absolute path to coop_hook.dll, assumed to sit next to the host executable.
|
// Absolute path to coop_hook.dll, assumed to sit next to the host executable.
|
||||||
@@ -517,8 +492,8 @@ void InjectionPanel::draw_subsystem_controls(const HookStatusView& status)
|
|||||||
ImGui::PopID();
|
ImGui::PopID();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cursor release is a Focus sub-option for games that clip/recenter the mouse
|
// Cursor release is a Focus sub-option for games that clip/recenter the mouse,
|
||||||
// (e.g. Trails through Daybreak), which would otherwise trap the operator.
|
// which would otherwise trap the operator.
|
||||||
if (ImGui::Checkbox("Release operator cursor (free the game's clip) [F2]", &release_cursor_))
|
if (ImGui::Checkbox("Release operator cursor (free the game's clip) [F2]", &release_cursor_))
|
||||||
{
|
{
|
||||||
server_.set_cursor_clip_allowed(!release_cursor_);
|
server_.set_cursor_clip_allowed(!release_cursor_);
|
||||||
@@ -690,7 +665,7 @@ void InjectionPanel::draw(bool debug_details)
|
|||||||
{
|
{
|
||||||
for (const WindowEntry& w : windows_)
|
for (const WindowEntry& w : windows_)
|
||||||
{
|
{
|
||||||
if (!contains_ci(w.title, window_filter_) && !contains_ci(w.exe_name, window_filter_))
|
if (!contains_ci_w(w.title, window_filter_) && !contains_ci_w(w.exe_name, window_filter_))
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -718,7 +693,7 @@ void InjectionPanel::draw(bool debug_details)
|
|||||||
{
|
{
|
||||||
for (const ProcessEntry& entry : processes_)
|
for (const ProcessEntry& entry : processes_)
|
||||||
{
|
{
|
||||||
if (!contains_ci(entry.exe_name, filter_))
|
if (!contains_ci_w(entry.exe_name, filter_))
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,37 +1,16 @@
|
|||||||
#include "log_panel.hpp"
|
#include "log_panel.hpp"
|
||||||
|
|
||||||
#include <algorithm>
|
|
||||||
#include <cctype>
|
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
#include <cstring>
|
|
||||||
#include <string>
|
|
||||||
|
|
||||||
#include "imgui.h"
|
#include "imgui.h"
|
||||||
|
|
||||||
#include "injection_panel.hpp"
|
#include "injection_panel.hpp"
|
||||||
#include "ui/app_chrome.hpp"
|
#include "ui/app_chrome.hpp"
|
||||||
|
#include "ui/text_match.hpp"
|
||||||
|
|
||||||
namespace coop
|
namespace coop
|
||||||
{
|
{
|
||||||
|
|
||||||
namespace
|
|
||||||
{
|
|
||||||
// Case-insensitive substring test, so the log filter matches regardless of case (consistent with the
|
|
||||||
// rest of the app, where "error" should find "ERROR").
|
|
||||||
bool icontains(const std::string& hay, const char* needle)
|
|
||||||
{
|
|
||||||
if (needle == nullptr || needle[0] == '\0')
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
auto lower = [](std::string s) {
|
|
||||||
std::transform(s.begin(), s.end(), s.begin(), [](unsigned char c) { return static_cast<char>(std::tolower(c)); });
|
|
||||||
return s;
|
|
||||||
};
|
|
||||||
return lower(hay).find(lower(needle)) != std::string::npos;
|
|
||||||
}
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
void LogPanel::add_line(const LogRecord& rec)
|
void LogPanel::add_line(const LogRecord& rec)
|
||||||
{
|
{
|
||||||
if (first_millis_ == 0)
|
if (first_millis_ == 0)
|
||||||
@@ -76,7 +55,7 @@ void LogPanel::draw()
|
|||||||
const bool has_filter = filter_[0] != '\0';
|
const bool has_filter = filter_[0] != '\0';
|
||||||
for (const Line& line : lines_)
|
for (const Line& line : lines_)
|
||||||
{
|
{
|
||||||
if (has_filter && !icontains(line.text, filter_))
|
if (has_filter && !contains_ci(line.text, filter_))
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|||||||
31
host/src/ui/text_match.hpp
Normal file
31
host/src/ui/text_match.hpp
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
// Small case-insensitive text helpers shared by the host panels' filter boxes, so
|
||||||
|
// "error" matches "ERROR" everywhere consistently. ASCII-fold only (the UI filters
|
||||||
|
// are typed ASCII); non-ASCII bytes compare as-is.
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cctype>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace coop
|
||||||
|
{
|
||||||
|
|
||||||
|
inline std::string ascii_lower(std::string s)
|
||||||
|
{
|
||||||
|
for (char& c : s)
|
||||||
|
{
|
||||||
|
c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
|
||||||
|
}
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
// True when `needle` is empty or a case-insensitive substring of `haystack`.
|
||||||
|
inline bool contains_ci(const std::string& haystack, const char* needle)
|
||||||
|
{
|
||||||
|
if (needle == nullptr || needle[0] == '\0')
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return ascii_lower(haystack).find(ascii_lower(needle)) != std::string::npos;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace coop
|
||||||
Reference in New Issue
Block a user