Apply clang-format across the whole tree

Run clang-format (the repo's .clang-format: LLVM base, 120 cols, tabs,
Allman functions) over every source file so the tree is formatter-clean.
Whitespace only -- no behavior change; full x64 + x86 suites pass.

Also set SortIncludes: false in .clang-format. Windows include order is
load-bearing (windows.h must precede tlhelp32.h / mmreg.h / xinput.h /
dinput.h; winsock2.h must precede windows.h), and the default
alphabetical sort reorders tlhelp32.h ahead of windows.h -- a build
break. Leaving order alone keeps the manual, correct grouping.
This commit is contained in:
2026-07-12 11:52:53 +02:00
parent c684a15fb9
commit 30eccf749d
155 changed files with 3333 additions and 6171 deletions

View File

@@ -24,8 +24,7 @@
#include <cstdint>
#include <vector>
namespace coop
{
namespace coop {
// The standard sample rates a shared-mode WASAPI stream realistically uses. Candidates are this
// set; a non-standard true rate is out of scope (and would show as low-confidence either way).
@@ -35,30 +34,25 @@ inline const std::vector<unsigned>& standard_audio_rates()
return rates;
}
struct RateCorrelation
{
bool ok = false; // a confident pick was made (winner clears the threshold AND beats the runner-up)
unsigned rate = 0; // best candidate rate (Hz)
double score = 0.0; // alignment score of the winner, in [0,1] (1 = perfect)
struct RateCorrelation {
bool ok = false; // a confident pick was made (winner clears the threshold AND beats the runner-up)
unsigned rate = 0; // best candidate rate (Hz)
double score = 0.0; // alignment score of the winner, in [0,1] (1 = perfect)
double runner_up = 0.0; // score of the second-best candidate (for separation)
};
namespace correlate_detail
{
namespace correlate_detail {
// Average interleaved float frames down to a single mono channel.
inline void downmix(const float* interleaved, std::size_t frames, unsigned channels, std::vector<float>& out)
{
out.resize(frames);
if (channels == 0)
{
if (channels == 0) {
channels = 1;
}
for (std::size_t i = 0; i < frames; ++i)
{
for (std::size_t i = 0; i < frames; ++i) {
float sum = 0.0f;
for (unsigned c = 0; c < channels; ++c)
{
for (unsigned c = 0; c < channels; ++c) {
sum += interleaved[i * channels + c];
}
out[i] = sum / static_cast<float>(channels);
@@ -66,24 +60,20 @@ inline void downmix(const float* interleaved, std::size_t frames, unsigned chann
}
// Linear-resample a mono signal from src_rate to dst_rate.
inline void resample_linear(const std::vector<float>& in, unsigned src_rate, unsigned dst_rate,
std::vector<float>& out)
inline void resample_linear(const std::vector<float>& in, unsigned src_rate, unsigned dst_rate, std::vector<float>& out)
{
if (src_rate == 0 || dst_rate == 0 || in.empty())
{
if (src_rate == 0 || dst_rate == 0 || in.empty()) {
out.clear();
return;
}
if (src_rate == dst_rate)
{
if (src_rate == dst_rate) {
out = in;
return;
}
const double step = static_cast<double>(src_rate) / static_cast<double>(dst_rate);
const std::size_t out_n = static_cast<std::size_t>(static_cast<double>(in.size()) / step);
out.resize(out_n);
for (std::size_t i = 0; i < out_n; ++i)
{
for (std::size_t i = 0; i < out_n; ++i) {
const double pos = static_cast<double>(i) * step;
const std::size_t j = static_cast<std::size_t>(pos);
const double frac = pos - static_cast<double>(j);
@@ -97,29 +87,24 @@ inline void resample_linear(const std::vector<float>& in, unsigned src_rate, uns
// alignment search (the envelope/content alignment doesn't need full bandwidth).
inline void decimate(const std::vector<float>& in, unsigned rate, unsigned corr_rate, std::vector<float>& out)
{
if (rate <= corr_rate || in.empty())
{
if (rate <= corr_rate || in.empty()) {
out = in;
return;
}
const double factor = static_cast<double>(rate) / static_cast<double>(corr_rate);
const std::size_t out_n = static_cast<std::size_t>(static_cast<double>(in.size()) / factor);
out.resize(out_n);
for (std::size_t i = 0; i < out_n; ++i)
{
for (std::size_t i = 0; i < out_n; ++i) {
const std::size_t lo = static_cast<std::size_t>(static_cast<double>(i) * factor);
std::size_t hi = static_cast<std::size_t>(static_cast<double>(i + 1) * factor);
if (hi <= lo)
{
if (hi <= lo) {
hi = lo + 1;
}
if (hi > in.size())
{
if (hi > in.size()) {
hi = in.size();
}
float sum = 0.0f;
for (std::size_t k = lo; k < hi; ++k)
{
for (std::size_t k = lo; k < hi; ++k) {
sum += in[k];
}
out[i] = sum / static_cast<float>(hi - lo);
@@ -133,29 +118,24 @@ inline double ncc(const std::vector<float>& a, const std::vector<float>& b, long
{
double sa = 0.0, sb = 0.0;
std::size_t n = 0;
for (std::size_t i = start; i < start + len && i < a.size(); ++i)
{
for (std::size_t i = start; i < start + len && i < a.size(); ++i) {
const long bi = static_cast<long>(i) + lag;
if (bi < 0 || static_cast<std::size_t>(bi) >= b.size())
{
if (bi < 0 || static_cast<std::size_t>(bi) >= b.size()) {
continue;
}
sa += a[i];
sb += b[bi];
++n;
}
if (n < 8)
{
if (n < 8) {
return 0.0;
}
const double ma = sa / static_cast<double>(n);
const double mb = sb / static_cast<double>(n);
double num = 0.0, da = 0.0, db = 0.0;
for (std::size_t i = start; i < start + len && i < a.size(); ++i)
{
for (std::size_t i = start; i < start + len && i < a.size(); ++i) {
const long bi = static_cast<long>(i) + lag;
if (bi < 0 || static_cast<std::size_t>(bi) >= b.size())
{
if (bi < 0 || static_cast<std::size_t>(bi) >= b.size()) {
continue;
}
const double xa = a[i] - ma;
@@ -164,8 +144,7 @@ inline double ncc(const std::vector<float>& a, const std::vector<float>& b, long
da += xa * xa;
db += xb * xb;
}
if (da < 1e-9 || db < 1e-9)
{
if (da < 1e-9 || db < 1e-9) {
return 0.0;
}
return num / std::sqrt(da * db);
@@ -188,11 +167,9 @@ inline double aligned_score(const std::vector<float>& a, const std::vector<float
const std::size_t mid_len = n / 2;
double best = -2.0;
long best_lag = 0;
for (long lag = -max_lag; lag <= max_lag; ++lag)
{
for (long lag = -max_lag; lag <= max_lag; ++lag) {
const double c = ncc(a, b, lag, mid_start, mid_len);
if (c > best)
{
if (c > best) {
best = c;
best_lag = lag;
}
@@ -218,8 +195,7 @@ inline RateCorrelation correlate_rate(const std::vector<float>& hook_mono, const
{
using namespace correlate_detail;
RateCorrelation result;
if (hook_mono.empty() || loop_mono.empty() || device_rate == 0)
{
if (hook_mono.empty() || loop_mono.empty() || device_rate == 0) {
return result;
}
constexpr unsigned kCorrRate = 8000; // alignment search rate (Nyquist 4 kHz -- plenty for content)
@@ -230,19 +206,15 @@ inline RateCorrelation correlate_rate(const std::vector<float>& hook_mono, const
double best = -1.0, second = -1.0;
unsigned best_rate = 0;
std::vector<float> resampled, hook_ds;
for (unsigned cand : candidates)
{
for (unsigned cand : candidates) {
resample_linear(hook_mono, cand, device_rate, resampled); // treat hook as sampled at `cand`
decimate(resampled, device_rate, kCorrRate, hook_ds);
const double s = aligned_score(hook_ds, loop_ds, kCorrRate);
if (s > best)
{
if (s > best) {
second = best;
best = s;
best_rate = cand;
}
else if (s > second)
{
} else if (s > second) {
second = s;
}
}
@@ -250,7 +222,8 @@ inline RateCorrelation correlate_rate(const std::vector<float>& hook_mono, const
result.rate = best_rate;
result.score = best < 0.0 ? 0.0 : best;
result.runner_up = second < 0.0 ? 0.0 : second;
result.ok = result.score >= min_score && (result.runner_up <= 1e-6 || result.score >= result.runner_up * separation);
result.ok =
result.score >= min_score && (result.runner_up <= 1e-6 || result.score >= result.runner_up * separation);
return result;
}
@@ -266,8 +239,7 @@ inline RateCorrelation correlate_rate(const std::vector<float>& hook_mono, const
inline constexpr unsigned kWaveFormatPcm = 1; // WAVE_FORMAT_PCM
inline constexpr unsigned kWaveFormatFloat = 3; // WAVE_FORMAT_IEEE_FLOAT
struct LayoutCandidate
{
struct LayoutCandidate {
unsigned channels;
unsigned bits;
unsigned tag; // kWaveFormatPcm / kWaveFormatFloat
@@ -278,16 +250,14 @@ struct LayoutCandidate
inline const std::vector<LayoutCandidate>& standard_audio_layouts()
{
static const std::vector<LayoutCandidate> v = {
{2, 32, kWaveFormatFloat}, {1, 32, kWaveFormatFloat}, {6, 32, kWaveFormatFloat},
{8, 32, kWaveFormatFloat}, {4, 32, kWaveFormatFloat}, {2, 16, kWaveFormatPcm},
{1, 16, kWaveFormatPcm}, {6, 16, kWaveFormatPcm}, {8, 16, kWaveFormatPcm},
{4, 16, kWaveFormatPcm},
{2, 32, kWaveFormatFloat}, {1, 32, kWaveFormatFloat}, {6, 32, kWaveFormatFloat}, {8, 32, kWaveFormatFloat},
{4, 32, kWaveFormatFloat}, {2, 16, kWaveFormatPcm}, {1, 16, kWaveFormatPcm}, {6, 16, kWaveFormatPcm},
{8, 16, kWaveFormatPcm}, {4, 16, kWaveFormatPcm},
};
return v;
}
struct FormatCorrelation
{
struct FormatCorrelation {
bool ok = false;
unsigned rate = 0;
unsigned channels = 0;
@@ -302,15 +272,13 @@ struct FormatCorrelation
// padding is stale staging-buffer bytes, so the host must extract the real `count*real_block` bytes
// per buffer (and concatenate) before de-interleaving -- otherwise the padding scrambles the audio.
// This carries that self-describing capture: `bytes` holds counts[i]*stride bytes per chunk.
struct ChunkedCapture
{
struct ChunkedCapture {
unsigned stride = 0; // bytes per frame as pushed (the guessed/device block_align)
std::vector<std::uint32_t> counts; // real frame count of each chunk
std::vector<std::uint8_t> bytes; // concatenated, counts[i]*stride bytes per chunk
};
namespace correlate_detail
{
namespace correlate_detail {
// De-interleave raw bytes under (channels/bits/tag) and average to mono float.
inline void decode_layout(const std::uint8_t* bytes, std::size_t n, const LayoutCandidate& fmt,
std::vector<float>& mono)
@@ -318,33 +286,25 @@ inline void decode_layout(const std::uint8_t* bytes, std::size_t n, const Layout
mono.clear();
const unsigned ch = fmt.channels == 0 ? 1 : fmt.channels;
const unsigned bps = fmt.bits / 8;
if (bps == 0)
{
if (bps == 0) {
return;
}
const std::size_t frame = static_cast<std::size_t>(ch) * bps;
const std::size_t frames = n / frame;
mono.resize(frames);
const bool is_float = fmt.tag == kWaveFormatFloat;
for (std::size_t i = 0; i < frames; ++i)
{
for (std::size_t i = 0; i < frames; ++i) {
double sum = 0.0;
for (unsigned c = 0; c < ch; ++c)
{
for (unsigned c = 0; c < ch; ++c) {
const std::uint8_t* p = bytes + i * frame + static_cast<std::size_t>(c) * bps;
float s = 0.0f;
if (is_float && fmt.bits == 32)
{
if (is_float && fmt.bits == 32) {
std::memcpy(&s, p, 4);
}
else if (fmt.bits == 16)
{
} else if (fmt.bits == 16) {
std::int16_t v;
std::memcpy(&v, p, 2);
s = v / 32768.0f;
}
else if (fmt.bits == 32)
{
} else if (fmt.bits == 32) {
std::int32_t v;
std::memcpy(&v, p, 4);
s = static_cast<float>(v / 2147483648.0);
@@ -372,58 +332,50 @@ inline FormatCorrelation correlate_format(const ChunkedCapture& hook, const std:
double min_margin = 0.04)
{
FormatCorrelation result;
if (hook.stride == 0 || hook.counts.empty() || loop_mono.empty() || device_rate == 0)
{
if (hook.stride == 0 || hook.counts.empty() || loop_mono.empty() || device_rate == 0) {
return result;
}
double best = -1.0, second = -1.0;
std::vector<std::uint8_t> clean;
std::vector<float> hook_mono;
for (const LayoutCandidate& layout : layouts)
{
for (const LayoutCandidate& layout : layouts) {
const unsigned real_block = layout.channels * (layout.bits / 8);
if (real_block == 0 || real_block > hook.stride)
{
if (real_block == 0 || real_block > hook.stride) {
continue; // can't extract a frame larger than what was pushed (the guess is the max)
}
// Pull the real count*real_block bytes out of each padded chunk and concatenate -> contiguous
// audio for this candidate layout (the padding, which is stale staging bytes, is dropped).
clean.clear();
std::size_t off = 0;
for (std::uint32_t count : hook.counts)
{
for (std::uint32_t count : hook.counts) {
const std::size_t chunk_bytes = static_cast<std::size_t>(count) * hook.stride;
const std::size_t take = static_cast<std::size_t>(count) * real_block;
if (off + chunk_bytes <= hook.bytes.size())
{
if (off + chunk_bytes <= hook.bytes.size()) {
clean.insert(clean.end(), hook.bytes.begin() + off, hook.bytes.begin() + off + take);
}
off += chunk_bytes;
}
correlate_detail::decode_layout(clean.data(), clean.size(), layout, hook_mono);
if (hook_mono.size() < device_rate / 5)
{
if (hook_mono.size() < device_rate / 5) {
continue; // this layout yields too little audio to judge
}
const RateCorrelation rc = correlate_rate(hook_mono, loop_mono, device_rate, rates, /*min_score=*/0.0,
/*separation=*/1.0);
if (rc.score > best)
{
/*separation=*/1.0);
if (rc.score > best) {
second = best;
best = rc.score;
result.rate = rc.rate;
result.channels = layout.channels;
result.bits = layout.bits;
result.tag = layout.tag;
}
else if (rc.score > second)
{
} else if (rc.score > second) {
second = rc.score;
}
}
result.score = best < 0.0 ? 0.0 : best;
result.runner_up = second < 0.0 ? 0.0 : second;
result.ok = result.score >= min_score && (result.runner_up <= 1e-6 || result.score - result.runner_up >= min_margin);
result.ok =
result.score >= min_score && (result.runner_up <= 1e-6 || result.score - result.runner_up >= min_margin);
return result;
}

View File

@@ -17,8 +17,7 @@
#include <cstring>
#include <string>
namespace coop
{
namespace coop {
// 'AURG' little-endian; sanity-checks the mapping before either side trusts it.
inline constexpr std::uint32_t kAudioRingMagic = 0x47525541u;
@@ -30,8 +29,7 @@ inline constexpr std::uint32_t kAudioRingVersion = 2;
// fields in the header. The host writes the fields then bumps op_seq; the hook applies
// the command once per new op_seq. Lets the Audio panel re-measure a stream's rate or
// override its format when detection is wrong/unrecoverable.
enum AudioRingOp : std::uint32_t
{
enum AudioRingOp : std::uint32_t {
AudioRingOp_None = 0,
AudioRingOp_Remeasure = 1, // re-run the sample-rate measurement for this stream
AudioRingOp_Override = 2, // adopt the op_rate/channels/bits/format_tag verbatim
@@ -48,8 +46,7 @@ inline constexpr std::uint32_t kAudioRingCapacity = 1u << 20;
// the format fields are written once by the producer *before* it publishes
// format_valid (release), and read by the consumer *after* it observes
// format_valid (acquire), so they need no atomicity of their own.
struct AudioRingHeader
{
struct AudioRingHeader {
std::uint32_t magic;
std::uint32_t version;
@@ -67,7 +64,7 @@ struct AudioRingHeader
std::uint32_t sample_rate;
std::uint32_t channels;
std::uint32_t bits;
std::uint32_t format_tag; // WAVE_FORMAT_* (PCM=1, IEEE_FLOAT=3, EXTENSIBLE=0xFFFE)
std::uint32_t format_tag; // WAVE_FORMAT_* (PCM=1, IEEE_FLOAT=3, EXTENSIBLE=0xFFFE)
std::uint32_t block_align; // bytes per frame (all channels)
std::uint32_t capacity; // bytes in the trailing data region
@@ -176,8 +173,7 @@ inline bool audio_ring_push(AudioRingHeader& h, const void* src, std::uint32_t b
const std::uint64_t w = h.write_pos.load(std::memory_order_relaxed);
const std::uint64_t r = h.read_pos.load(std::memory_order_acquire);
const std::uint32_t used = static_cast<std::uint32_t>(w - r);
if (bytes > h.capacity - used)
{
if (bytes > h.capacity - used) {
h.overruns.fetch_add(1, std::memory_order_relaxed);
return false;
}
@@ -185,8 +181,7 @@ inline bool audio_ring_push(AudioRingHeader& h, const void* src, std::uint32_t b
const std::uint32_t off = static_cast<std::uint32_t>(w % h.capacity);
const std::uint32_t first = std::min(bytes, h.capacity - off);
std::memcpy(data + off, src, first);
if (bytes > first)
{
if (bytes > first) {
std::memcpy(data, static_cast<const std::uint8_t*>(src) + first, bytes - first);
}
h.write_pos.store(w + bytes, std::memory_order_release);
@@ -222,8 +217,7 @@ inline std::uint32_t audio_ring_pop(AudioRingHeader& h, void* dst, std::uint32_t
const std::uint32_t off = static_cast<std::uint32_t>(r % h.capacity);
const std::uint32_t first = std::min(bytes, h.capacity - off);
std::memcpy(dst, data + off, first);
if (bytes > first)
{
if (bytes > first) {
std::memcpy(static_cast<std::uint8_t*>(dst) + first, data, bytes - first);
}
h.read_pos.store(r + bytes, std::memory_order_release);
@@ -234,8 +228,7 @@ inline std::uint32_t audio_ring_pop(AudioRingHeader& h, void* dst, std::uint32_t
// bumps op_seq (release) so the hook applies it exactly once. For a re-measure the
// rate/channels/bits are ignored.
inline void audio_ring_post_op(AudioRingHeader& h, std::uint32_t kind, std::uint32_t rate = 0,
std::uint32_t channels = 0, std::uint32_t bits = 0,
std::uint32_t format_tag = 0)
std::uint32_t channels = 0, std::uint32_t bits = 0, std::uint32_t format_tag = 0)
{
h.op_kind = kind;
h.op_rate = rate;
@@ -246,8 +239,7 @@ inline void audio_ring_post_op(AudioRingHeader& h, std::uint32_t kind, std::uint
}
// One operator command read back by the hook.
struct AudioRingOpCmd
{
struct AudioRingOpCmd {
std::uint32_t kind = AudioRingOp_None;
std::uint32_t rate = 0;
std::uint32_t channels = 0;
@@ -261,8 +253,7 @@ struct AudioRingOpCmd
inline std::uint32_t audio_ring_poll_op(AudioRingHeader& h, std::uint32_t& last_seq, AudioRingOpCmd& out)
{
const std::uint32_t seq = h.op_seq.load(std::memory_order_acquire);
if (seq == last_seq)
{
if (seq == last_seq) {
return AudioRingOp_None;
}
last_seq = seq;
@@ -280,8 +271,7 @@ inline std::uint32_t audio_ring_poll_op(AudioRingHeader& h, std::uint32_t& last_
inline std::wstring audio_ring_name(unsigned long target_pid, unsigned index = 0)
{
std::wstring name = std::wstring(kAudioRingPrefix) + std::to_wstring(target_pid);
if (index != 0)
{
if (index != 0) {
name += L"_" + std::to_wstring(index);
}
return name;

View File

@@ -9,8 +9,7 @@
#include <windows.h> // USER_DEFAULT_SCREEN_DPI (== 96, the 100%-scale baseline)
namespace coop
{
namespace coop {
// ImGui's built-in default font (ProggyClean) rasterizes at this pixel size at 100% scale. Named once
// here so the DPI math scales from a single owned constant instead of a bare 13 sprinkled around.
@@ -26,8 +25,7 @@ inline constexpr float kMaxUiScale = 8.0f;
// callers never derive a zero-size font.
inline float dpi_scale_from(unsigned dpi)
{
if (dpi == 0)
{
if (dpi == 0) {
dpi = USER_DEFAULT_SCREEN_DPI;
}
const float scale = static_cast<float>(dpi) / static_cast<float>(USER_DEFAULT_SCREEN_DPI);

View File

@@ -15,8 +15,7 @@
#include <cstring>
#include <string>
namespace coop
{
namespace coop {
// 'CLOG' little-endian.
inline constexpr std::uint32_t kLogRingMagic = 0x474F4C43u;
@@ -25,33 +24,30 @@ inline constexpr std::uint32_t kLogRingVersion = 1;
// Per-pid mapping name, mirroring the other channels: coop_log_<pid>.
inline constexpr wchar_t kLogRingPrefix[] = L"Local\\coop_log_";
inline constexpr std::uint32_t kLogMsgLen = 192; // chars per line (incl. NUL)
inline constexpr std::uint32_t kLogMsgLen = 192; // chars per line (incl. NUL)
inline constexpr std::uint32_t kLogCapacity = 1024; // ring records
// Severity of a log line; drives the host Log window's colour. Stored in
// LogRecord::level. Info is 0 so existing/zero-filled records read as Info.
enum LogLevel : std::uint32_t
{
enum LogLevel : std::uint32_t {
LogLevel_Info = 0,
LogLevel_Warn = 1,
LogLevel_Error = 2,
};
struct LogRecord
{
struct LogRecord {
std::atomic<std::uint64_t> seq; // 0 = empty; else (global index + 1) once written
std::uint32_t pid;
std::uint32_t level; // LogLevel
std::uint32_t level; // LogLevel
std::uint64_t millis; // producer timestamp (GetTickCount64)
char text[kLogMsgLen];
};
struct LogRing
{
struct LogRing {
std::uint32_t magic;
std::uint32_t version;
std::uint32_t capacity; // number of records
std::uint32_t msg_len; // kLogMsgLen (sanity)
std::uint32_t capacity; // number of records
std::uint32_t msg_len; // kLogMsgLen (sanity)
std::atomic<std::uint64_t> write_index; // total records ever claimed (free-running)
std::uint8_t reserved[32];
// LogRecord records[capacity] follows immediately.
@@ -83,13 +79,11 @@ inline void log_ring_init(LogRing& r, std::uint32_t capacity)
inline bool log_ring_valid(const LogRing& r)
{
return r.magic == kLogRingMagic && r.version == kLogRingVersion && r.capacity != 0 &&
r.msg_len == kLogMsgLen;
return r.magic == kLogRingMagic && r.version == kLogRingVersion && r.capacity != 0 && r.msg_len == kLogMsgLen;
}
// Producer (hook): append a line at severity `level` (LogLevel). Multi-producer safe.
inline void log_ring_push(LogRing& r, std::uint32_t pid, std::uint32_t level, std::uint64_t millis,
const char* text)
inline void log_ring_push(LogRing& r, std::uint32_t pid, std::uint32_t level, std::uint64_t millis, const char* text)
{
const std::uint64_t idx = r.write_index.fetch_add(1, std::memory_order_acq_rel);
LogRecord& rec = log_ring_records(&r)[idx % r.capacity];
@@ -115,23 +109,19 @@ template <typename F>
inline void log_ring_drain(LogRing& r, std::uint64_t& cursor, F&& emit)
{
const std::uint64_t w = r.write_index.load(std::memory_order_acquire);
if (w <= cursor)
{
if (w <= cursor) {
return;
}
const std::uint64_t lo = (w > r.capacity) ? (w - r.capacity) : 0;
std::uint64_t i = cursor < lo ? lo : cursor; // skip records already overwritten
LogRecord* recs = log_ring_records(&r);
for (; i < w; ++i)
{
for (; i < w; ++i) {
LogRecord& rec = recs[i % r.capacity];
const std::uint64_t s1 = rec.seq.load(std::memory_order_acquire);
if (s1 <= i)
{
if (s1 <= i) {
break; // generation i not written yet (in-flight, or being overwritten); retry next call
}
if (s1 != i + 1)
{
if (s1 != i + 1) {
continue; // s1 > i+1: overwritten by a later generation before we got here; lost, skip
}
// Seqlock read: copy the record out, then re-check seq. A producer overwriting this slot stores
@@ -142,8 +132,7 @@ inline void log_ring_drain(LogRing& r, std::uint64_t& cursor, F&& emit)
snap.millis = rec.millis;
std::memcpy(snap.text, rec.text, kLogMsgLen);
std::atomic_thread_fence(std::memory_order_acquire);
if (rec.seq.load(std::memory_order_relaxed) == i + 1)
{
if (rec.seq.load(std::memory_order_relaxed) == i + 1) {
emit(snap); // consistent snapshot
}
// else: overwritten while we copied -> skip (lost)

View File

@@ -7,8 +7,7 @@
#include <cstddef>
#include <cstdint>
namespace coop
{
namespace coop {
// Bump whenever the layout of SharedBlock or CoopPadState changes. The hook
// refuses to attach to a host with a mismatched version.
@@ -27,12 +26,11 @@ inline constexpr wchar_t kSharedMemoryPrefix[] = L"Local\\coop_ipc_";
// One controller's state, laid out to map 1:1 onto XINPUT_GAMEPAD plus the
// metadata the hook needs. Field names/types match XINPUT_GAMEPAD so the hook
// can memcpy the trailing region straight into an XINPUT_STATE.
struct CoopPadState
{
struct CoopPadState {
std::uint8_t connected; // 1 if a guest/host pad is mapped to this slot
std::uint8_t reserved[3];
std::uint32_t packet; // bumps on change -> XINPUT_STATE::dwPacketNumber
std::uint16_t buttons; // XINPUT_GAMEPAD_* bitmask
std::uint32_t packet; // bumps on change -> XINPUT_STATE::dwPacketNumber
std::uint16_t buttons; // XINPUT_GAMEPAD_* bitmask
std::uint8_t left_trigger;
std::uint8_t right_trigger;
std::int16_t thumb_lx;
@@ -56,30 +54,27 @@ inline constexpr std::uint32_t kMaxAudioStreams = 4;
// we injected (the common case) was never seen at Initialize, so its format starts as a
// guess (the device mix format) and its true sample rate is measured from the render
// cadence; a stream we watched get created carries its exact Initialize format.
enum AudioFormatState : std::uint32_t
{
AudioFormat_Unknown = 0, // no format determined yet
AudioFormat_Exact = 1, // taken from the game's own IAudioClient::Initialize
AudioFormat_Measuring = 2, // guessed (device mix format); true sample rate being measured
AudioFormat_Measured = 3, // guessed rate measured (consensus on a standard rate); ch/bits assumed
enum AudioFormatState : std::uint32_t {
AudioFormat_Unknown = 0, // no format determined yet
AudioFormat_Exact = 1, // taken from the game's own IAudioClient::Initialize
AudioFormat_Measuring = 2, // guessed (device mix format); true sample rate being measured
AudioFormat_Measured = 3, // guessed rate measured (consensus on a standard rate); ch/bits assumed
AudioFormat_LowConfidence = 4, // rate never reached consensus; best estimate published -- verify/override
AudioFormat_Override = 5, // operator set this format manually (see the per-stream op channel)
AudioFormat_Override = 5, // operator set this format manually (see the per-stream op channel)
};
struct AudioStreamInfo
{
std::uint32_t is_primary; // 1 = the stream the hook captures/silences
struct AudioStreamInfo {
std::uint32_t is_primary; // 1 = the stream the hook captures/silences
std::uint32_t sample_rate;
std::uint16_t channels;
std::uint16_t bits;
std::uint32_t format_tag; // WAVE_FORMAT_* of this stream
std::uint32_t format_tag; // WAVE_FORMAT_* of this stream
std::uint64_t frames_rendered;
std::uint32_t format_state; // AudioFormatState: how the format above was determined
};
// Orthogonal hook subsystems the host can install/remove independently.
enum HookSubsystem : std::uint32_t
{
enum HookSubsystem : std::uint32_t {
HookSubsys_Input = 0, // XInput hooks (forward the guest pad)
HookSubsys_Focus = 1, // focus spoof (keep the game running unfocused)
HookSubsys_Audio = 2, // WASAPI render-hook (audio mirror without echo)
@@ -93,17 +88,15 @@ inline constexpr std::uint32_t kMaxHookEntries = 24;
// One installed hook, for the Injection panel's hook list. POD diagnostics, like
// AudioStreamInfo: the hook is the sole writer; benign cross-process races are ok.
struct HookEntry
{
char name[40]; // e.g. "XInputGetState"
struct HookEntry {
char name[40]; // e.g. "XInputGetState"
std::uint32_t subsystem; // HookSubsystem
std::uint32_t installed; // 1 if currently hooked
std::uint64_t calls; // cumulative times the detour ran
};
// Indices into HookStatus::focus_query_calls.
enum FocusApi : std::uint32_t
{
enum FocusApi : std::uint32_t {
FocusApi_Foreground = 0, // GetForegroundWindow
FocusApi_Active = 1, // GetActiveWindow
FocusApi_Focus = 2, // GetFocus
@@ -115,17 +108,16 @@ enum FocusApi : std::uint32_t
// polling, does it use the focus APIs, and does it read input through a
// focus-gated path (Raw Input / DirectInput)? Diagnostics only, so the non-atomic
// fields tolerate benign cross-process races.
struct HookStatus
{
std::atomic<std::uint32_t> heartbeat; // DLL bumps ~4x/sec while alive
std::atomic<std::uint64_t> get_state_calls[kMaxPads]; // XInputGetState/Ex per slot
std::atomic<std::uint64_t> get_caps_calls[kMaxPads]; // XInputGetCapabilities per slot
struct HookStatus {
std::atomic<std::uint32_t> heartbeat; // DLL bumps ~4x/sec while alive
std::atomic<std::uint64_t> get_state_calls[kMaxPads]; // XInputGetState/Ex per slot
std::atomic<std::uint64_t> get_caps_calls[kMaxPads]; // XInputGetCapabilities per slot
std::atomic<std::uint64_t> focus_query_calls[FocusApi_Count]; // focus API calls, see FocusApi
std::uint32_t attached; // 1 once XInput hooks are installed
std::uint32_t focus_spoof; // 1 once focus spoofing is active
std::uint32_t game_pid; // the DLL's own pid (sanity check)
std::uint64_t game_hwnd; // window the DLL subclassed (0 if none yet)
std::uint32_t attached; // 1 once XInput hooks are installed
std::uint32_t focus_spoof; // 1 once focus spoofing is active
std::uint32_t game_pid; // the DLL's own pid (sanity check)
std::uint64_t game_hwnd; // window the DLL subclassed (0 if none yet)
// Input-path diagnostics: which focus-gated mechanism (if any) the game uses.
std::uint32_t raw_input_registered; // process has any Raw Input registration
@@ -141,8 +133,8 @@ struct HookStatus
// Audio render-hook diagnostics. Stream counting runs whenever the DLL is
// injected, independent of whether audio mirroring is enabled, so a
// multi-stream game is visible before/without turning the mirror on.
std::uint32_t audio_streams_seen; // distinct render clients ever created
AudioStreamInfo audio_streams[kMaxAudioStreams]; // per-slot detail, [0] is primary
std::uint32_t audio_streams_seen; // distinct render clients ever created
AudioStreamInfo audio_streams[kMaxAudioStreams]; // per-slot detail, [0] is primary
// Hook registry: every individual hook the DLL has installed, with a running
// call count. Lets the Injection panel list exactly what's hooked and how busy.
@@ -163,8 +155,7 @@ struct HookStatus
// Host -> hook control channel. The host requests which hook subsystems should be
// installed; the hook reconciles each tick. 0 = install (the zero-filled default,
// so a fresh mapping installs everything as before), 1 = remove.
struct HookControl
{
struct HookControl {
std::atomic<std::uint32_t> subsystem_disabled[HookSubsys_Count];
// Cursor handling for cursor-clipping games (part of the Focus subsystem).
@@ -181,17 +172,16 @@ struct HookControl
// is the sole writer. `generation` bumps on every published frame (0 = nothing
// shared yet); width/height/format describe the currently shared texture, so the
// host reopens it whenever they change. The keyed mutex uses key 0 on both sides.
struct VideoShare
{
struct VideoShare {
std::atomic<std::uint32_t> generation; // bumps per published frame; 0 = none yet
std::uint32_t width; // shared texture dimensions / DXGI format
std::uint32_t height;
std::uint32_t format; // DXGI_FORMAT of the shared texture
std::uint64_t present_calls; // cumulative Present() detours (diagnostic)
std::int64_t present_qpc; // QueryPerformanceCounter at the last publish
std::uint64_t frames_dropped; // cumulative captures skipped because the shared
// keyed mutex was busy (host mid-copy) -- a frame
// the game produced that never reached the mirror
std::uint32_t format; // DXGI_FORMAT of the shared texture
std::uint64_t present_calls; // cumulative Present() detours (diagnostic)
std::int64_t present_qpc; // QueryPerformanceCounter at the last publish
std::uint64_t frames_dropped; // cumulative captures skipped because the shared
// keyed mutex was busy (host mid-copy) -- a frame
// the game produced that never reached the mirror
// present_calls / frames_dropped stay plain uint64_t (POD layout) but are read/written via
// std::atomic_ref so the host's cross-process read isn't torn (an x86 DLL stores 64 bits in two
// halves). Kept as fields, not std::atomic, only so the layout/offset asserts stay simple.
@@ -203,8 +193,7 @@ struct VideoShare
// the matching window messages to the game, and maintains a synthesized state the
// GetAsyncKeyState/GetKeyboardState/GetCursorPos hooks report to polling games.
enum MkbEventType : std::uint32_t
{
enum MkbEventType : std::uint32_t {
Mkb_KeyDown = 0, // code = Win32 virtual-key
Mkb_KeyUp = 1, // code = Win32 virtual-key
Mkb_Char = 2, // code = UTF-16 code unit (WM_CHAR)
@@ -213,8 +202,7 @@ enum MkbEventType : std::uint32_t
Mkb_Wheel = 5, // code = signed wheel delta (WHEEL_DELTA units); x,y = game client px
};
struct MkbEvent
{
struct MkbEvent {
std::uint32_t type; // MkbEventType
std::uint32_t code; // see per-type meaning above
std::int32_t x; // game-client x (mouse events)
@@ -227,8 +215,7 @@ static_assert(sizeof(MkbEvent) == 16, "MkbEvent must stay byte-identical across
inline constexpr std::uint32_t kMkbQueueSize = 128;
// Lock-free SPSC ring: host produces, hook consumes. Free-running 32-bit indices.
struct MkbRing
{
struct MkbRing {
std::atomic<std::uint32_t> head; // producer (host) write position
std::atomic<std::uint32_t> tail; // consumer (hook) read position
MkbEvent events[kMkbQueueSize];
@@ -244,8 +231,7 @@ inline constexpr std::uint64_t kVideoMutexKey = 0;
// Top-level shared block. The host is the sole writer of pad state; the hook is
// the sole reader. A seqlock (even = stable, odd = write in progress) lets the
// reader grab a torn-free snapshot without a kernel lock on the hot path.
struct SharedBlock
{
struct SharedBlock {
std::uint32_t magic;
std::uint32_t version;
std::uint32_t pad_count; // number of populated slots, <= kMaxPads
@@ -297,20 +283,17 @@ static_assert(sizeof(MkbRing) == 2056, "MkbRing size changed -- wire-protocol ch
// Writer side: publish a fresh set of pad states. Called from the host.
inline void publish_pads(SharedBlock& block, const CoopPadState* pads, std::uint32_t count)
{
if (count > kMaxPads)
{
if (count > kMaxPads) {
count = kMaxPads;
}
const std::uint32_t seq = block.sequence.load(std::memory_order_relaxed);
block.sequence.store(seq + 1, std::memory_order_release); // -> odd: write begins
std::atomic_thread_fence(std::memory_order_release);
block.pad_count = count;
for (std::uint32_t i = 0; i < count; ++i)
{
for (std::uint32_t i = 0; i < count; ++i) {
block.pads[i] = pads[i];
}
for (std::uint32_t i = count; i < kMaxPads; ++i)
{
for (std::uint32_t i = count; i < kMaxPads; ++i) {
block.pads[i] = CoopPadState{};
}
block.sequence.store(seq + 2, std::memory_order_release); // -> even: write done
@@ -320,26 +303,21 @@ inline void publish_pads(SharedBlock& block, const CoopPadState* pads, std::uint
// if a write is in flight; bounded so a crashed writer can't hang the game.
inline bool read_pads(const SharedBlock& block, CoopPadState (&out)[kMaxPads], std::uint32_t& out_count)
{
for (int attempt = 0; attempt < 64; ++attempt)
{
for (int attempt = 0; attempt < 64; ++attempt) {
const std::uint32_t before = block.sequence.load(std::memory_order_acquire);
if (before & 1u)
{
if (before & 1u) {
continue; // writer mid-update, retry
}
std::uint32_t count = block.pad_count;
if (count > kMaxPads)
{
if (count > kMaxPads) {
count = kMaxPads;
}
for (std::uint32_t i = 0; i < kMaxPads; ++i)
{
for (std::uint32_t i = 0; i < kMaxPads; ++i) {
out[i] = block.pads[i];
}
std::atomic_thread_fence(std::memory_order_acquire);
const std::uint32_t after = block.sequence.load(std::memory_order_acquire);
if (before == after)
{
if (before == after) {
out_count = count;
return true;
}
@@ -354,8 +332,7 @@ inline bool push_mkb_event(MkbRing& ring, const MkbEvent& ev)
{
const std::uint32_t head = ring.head.load(std::memory_order_relaxed);
const std::uint32_t tail = ring.tail.load(std::memory_order_acquire);
if (head - tail >= kMkbQueueSize)
{
if (head - tail >= kMkbQueueSize) {
return false; // full -> drop (host should always drain faster than it fills)
}
ring.events[head & (kMkbQueueSize - 1)] = ev;
@@ -368,8 +345,7 @@ inline bool pop_mkb_event(MkbRing& ring, MkbEvent& out)
{
const std::uint32_t tail = ring.tail.load(std::memory_order_relaxed);
const std::uint32_t head = ring.head.load(std::memory_order_acquire);
if (tail == head)
{
if (tail == head) {
return false; // empty
}
out = ring.events[tail & (kMkbQueueSize - 1)];

View File

@@ -9,26 +9,20 @@
#include "coop/protocol.hpp"
namespace coop
{
namespace coop {
class SharedMemory
{
public:
class SharedMemory {
public:
SharedMemory() = default;
SharedMemory(const SharedMemory&) = delete;
SharedMemory& operator=(const SharedMemory&) = delete;
SharedMemory(SharedMemory&& other) noexcept
{
*this = std::move(other);
}
SharedMemory(SharedMemory&& other) noexcept { *this = std::move(other); }
SharedMemory& operator=(SharedMemory&& other) noexcept
{
if (this != &other)
{
if (this != &other) {
reset();
mapping_ = std::exchange(other.mapping_, nullptr);
view_ = std::exchange(other.view_, nullptr);
@@ -37,19 +31,15 @@ public:
return *this;
}
~SharedMemory()
{
reset();
}
~SharedMemory() { reset(); }
// Host side: create (or open if it already exists) the named section.
bool create(const std::wstring& name, std::size_t size)
{
reset();
mapping_ = CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr, PAGE_READWRITE, 0,
static_cast<DWORD>(size), name.c_str());
if (mapping_ == nullptr)
{
mapping_ = CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr, PAGE_READWRITE, 0, static_cast<DWORD>(size),
name.c_str());
if (mapping_ == nullptr) {
return false;
}
return map(size);
@@ -60,8 +50,7 @@ public:
{
reset();
mapping_ = OpenFileMappingW(FILE_MAP_ALL_ACCESS, FALSE, name.c_str());
if (mapping_ == nullptr)
{
if (mapping_ == nullptr) {
return false;
}
return map(size);
@@ -69,23 +58,18 @@ public:
void reset()
{
if (view_ != nullptr)
{
if (view_ != nullptr) {
UnmapViewOfFile(view_);
view_ = nullptr;
}
if (mapping_ != nullptr)
{
if (mapping_ != nullptr) {
CloseHandle(mapping_);
mapping_ = nullptr;
}
size_ = 0;
}
[[nodiscard]] bool valid() const
{
return view_ != nullptr;
}
[[nodiscard]] bool valid() const { return view_ != nullptr; }
template <typename T>
[[nodiscard]] T* as() const
@@ -93,22 +77,15 @@ public:
return static_cast<T*>(view_);
}
[[nodiscard]] void* data() const
{
return view_;
}
[[nodiscard]] void* data() const { return view_; }
[[nodiscard]] std::size_t size() const
{
return size_;
}
[[nodiscard]] std::size_t size() const { return size_; }
private:
private:
bool map(std::size_t size)
{
view_ = MapViewOfFile(mapping_, FILE_MAP_ALL_ACCESS, 0, 0, size);
if (view_ == nullptr)
{
if (view_ == nullptr) {
CloseHandle(mapping_);
mapping_ = nullptr;
return false;

View File

@@ -21,8 +21,7 @@
#include <cstdint>
#include <vector>
namespace coop
{
namespace coop {
// WAVE_FORMAT_* tags we decode (kept local to avoid an mmreg.h dependency, matching audio_mix.hpp).
inline constexpr std::uint32_t kToneFormatPcm = 1;
@@ -30,37 +29,35 @@ inline constexpr std::uint32_t kToneFormatFloat = 3;
// One channel's worth of measured fidelity. Fields are NaN/0 when not applicable
// (e.g. pitch metrics need a known expected_hz > 0).
struct ToneReport
{
bool valid = false; // enough samples to analyze
unsigned sample_rate = 0; // the rate the samples are interpreted at (the *declared* rate)
std::size_t frames = 0; // mono frames analyzed
struct ToneReport {
bool valid = false; // enough samples to analyze
unsigned sample_rate = 0; // the rate the samples are interpreted at (the *declared* rate)
std::size_t frames = 0; // mono frames analyzed
double duration_sec = 0.0;
// --- Level ---
double rms = 0.0; // 0..1
double peak = 0.0; // 0..1
double clipped_fraction = 0.0; // fraction of samples at >= 0.999 full-scale
double rms = 0.0; // 0..1
double peak = 0.0; // 0..1
double clipped_fraction = 0.0; // fraction of samples at >= 0.999 full-scale
// --- Pitch (needs a known input tone frequency) ---
double expected_hz = 0.0; // the tone frequency that was played
double dominant_hz = 0.0; // the fundamental we recovered
double pitch_error_ratio = 0.0; // dominant / expected (1.0 = perfect)
double pitch_error_cents = 0.0; // 1200*log2(ratio); +/- ~10 cents starts to be audible
double expected_hz = 0.0; // the tone frequency that was played
double dominant_hz = 0.0; // the fundamental we recovered
double pitch_error_ratio = 0.0; // dominant / expected (1.0 = perfect)
double pitch_error_cents = 0.0; // 1200*log2(ratio); +/- ~10 cents starts to be audible
// --- Spectral purity (tone mode) ---
double snr_db = 0.0; // fundamental power vs everything else (DC + harmonics excluded from "signal")
double thd_percent = 0.0; // harmonics 2..6 vs fundamental
double snr_db = 0.0; // fundamental power vs everything else (DC + harmonics excluded from "signal")
double thd_percent = 0.0; // harmonics 2..6 vs fundamental
// --- Time-domain defects (content-agnostic) ---
unsigned glitch_count = 0; // discontinuity events (clicks): big isolated sample jumps
unsigned glitch_count = 0; // discontinuity events (clicks): big isolated sample jumps
double glitch_rate_per_sec = 0.0;
unsigned dropout_count = 0; // gaps: stretches that fall near-silent mid-signal
double dropout_ms = 0.0; // total duration of those gaps
unsigned dropout_count = 0; // gaps: stretches that fall near-silent mid-signal
double dropout_ms = 0.0; // total duration of those gaps
};
namespace detail
{
namespace detail {
inline constexpr double kPi = 3.14159265358979323846;
@@ -68,28 +65,22 @@ inline constexpr double kPi = 3.14159265358979323846;
inline void fft(std::vector<std::complex<double>>& a)
{
const std::size_t n = a.size();
for (std::size_t i = 1, j = 0; i < n; ++i)
{
for (std::size_t i = 1, j = 0; i < n; ++i) {
std::size_t bit = n >> 1;
for (; (j & bit) != 0; bit >>= 1)
{
for (; (j & bit) != 0; bit >>= 1) {
j ^= bit;
}
j ^= bit;
if (i < j)
{
if (i < j) {
std::swap(a[i], a[j]);
}
}
for (std::size_t len = 2; len <= n; len <<= 1)
{
for (std::size_t len = 2; len <= n; len <<= 1) {
const double ang = -2.0 * kPi / static_cast<double>(len);
const std::complex<double> wlen(std::cos(ang), std::sin(ang));
for (std::size_t i = 0; i < n; i += len)
{
for (std::size_t i = 0; i < n; i += len) {
std::complex<double> w(1.0, 0.0);
for (std::size_t k = 0; k < len / 2; ++k)
{
for (std::size_t k = 0; k < len / 2; ++k) {
const std::complex<double> u = a[i + k];
const std::complex<double> v = a[i + k + len / 2] * w;
a[i + k] = u + v;
@@ -104,8 +95,7 @@ inline void fft(std::vector<std::complex<double>>& a)
inline std::size_t floor_pow2(std::size_t n)
{
std::size_t p = 1;
while ((p << 1) != 0 && (p << 1) <= n)
{
while ((p << 1) != 0 && (p << 1) <= n) {
p <<= 1;
}
return n == 0 ? 0 : p;
@@ -120,27 +110,21 @@ inline std::vector<float> decode_channel(const std::uint8_t* pcm, std::size_t by
std::uint32_t bits, std::uint32_t channels, std::uint32_t channel = 0)
{
std::vector<float> out;
if (pcm == nullptr || channels == 0 || channel >= channels)
{
if (pcm == nullptr || channels == 0 || channel >= channels) {
return out;
}
if (format_tag == kToneFormatFloat && bits == 32)
{
if (format_tag == kToneFormatFloat && bits == 32) {
const std::size_t frames = bytes / (channels * 4);
out.reserve(frames);
const auto* f = reinterpret_cast<const float*>(pcm);
for (std::size_t i = 0; i < frames; ++i)
{
for (std::size_t i = 0; i < frames; ++i) {
out.push_back(f[i * channels + channel]);
}
}
else if (format_tag == kToneFormatPcm && bits == 16)
{
} else if (format_tag == kToneFormatPcm && bits == 16) {
const std::size_t frames = bytes / (channels * 2);
out.reserve(frames);
const auto* s = reinterpret_cast<const std::int16_t*>(pcm);
for (std::size_t i = 0; i < frames; ++i)
{
for (std::size_t i = 0; i < frames; ++i) {
out.push_back(static_cast<float>(s[i * channels + channel]) / 32768.0f);
}
}
@@ -150,15 +134,13 @@ inline std::vector<float> decode_channel(const std::uint8_t* pcm, std::size_t by
// Analyze a single channel of normalized float samples. `expected_hz` is the known
// input tone frequency (pass 0 to skip the pitch/SNR/THD metrics for non-tone audio;
// the click/dropout/level metrics still apply).
inline ToneReport analyze_tone(const float* samples, std::size_t frames, unsigned sample_rate,
double expected_hz)
inline ToneReport analyze_tone(const float* samples, std::size_t frames, unsigned sample_rate, double expected_hz)
{
ToneReport r;
r.sample_rate = sample_rate;
r.frames = frames;
r.expected_hz = expected_hz;
if (samples == nullptr || frames < 64 || sample_rate == 0)
{
if (samples == nullptr || frames < 64 || sample_rate == 0) {
return r;
}
r.valid = true;
@@ -168,14 +150,12 @@ inline ToneReport analyze_tone(const float* samples, std::size_t frames, unsigne
double sumsq = 0.0;
double peak = 0.0;
std::size_t clipped = 0;
for (std::size_t i = 0; i < frames; ++i)
{
for (std::size_t i = 0; i < frames; ++i) {
const double x = samples[i];
sumsq += x * x;
const double a = std::fabs(x);
peak = std::max(peak, a);
if (a >= 0.999)
{
if (a >= 0.999) {
++clipped;
}
}
@@ -188,11 +168,9 @@ inline ToneReport analyze_tone(const float* samples, std::size_t frames, unsigne
// step. Use the median |first difference| as a robust scale (immune to the tone's own
// slope and to a few outliers), and flag steps beyond 8x it. Group samples within a
// short refractory window into one event so a single click isn't counted many times.
if (frames >= 3)
{
if (frames >= 3) {
std::vector<float> diff(frames - 1);
for (std::size_t i = 1; i < frames; ++i)
{
for (std::size_t i = 1; i < frames; ++i) {
diff[i - 1] = std::fabs(samples[i] - samples[i - 1]);
}
std::vector<float> sorted(diff);
@@ -202,12 +180,9 @@ inline ToneReport analyze_tone(const float* samples, std::size_t frames, unsigne
const std::size_t refractory = std::max<std::size_t>(sample_rate / 1000, 8); // ~1 ms
std::size_t last_event_end = 0;
bool have_event = false;
for (std::size_t i = 0; i < diff.size(); ++i)
{
if (diff[i] > thresh)
{
if (!have_event || i > last_event_end)
{
for (std::size_t i = 0; i < diff.size(); ++i) {
if (diff[i] > thresh) {
if (!have_event || i > last_event_end) {
++r.glitch_count;
}
have_event = true;
@@ -220,43 +195,35 @@ inline ToneReport analyze_tone(const float* samples, std::size_t frames, unsigne
// --- Dropout detection: stretches that fall near-silent in an otherwise active signal ---
// Slide a ~5 ms window; flag windows whose RMS drops below 8% of the global RMS. Only
// meaningful when the signal is actually present (global RMS above a small floor).
if (r.rms > 1e-4)
{
if (r.rms > 1e-4) {
const std::size_t win = std::max<std::size_t>(sample_rate * 5 / 1000, 16); // ~5 ms
const std::size_t hop = std::max<std::size_t>(win / 2, 1);
const double silence_thresh = 0.08 * r.rms;
bool in_gap = false;
std::size_t gap_first = 0; // first silent window's start sample
std::size_t gap_last = 0; // last silent window's end sample
std::size_t gap_first = 0; // first silent window's start sample
std::size_t gap_last = 0; // last silent window's end sample
std::size_t total_silent_samples = 0;
auto close_gap = [&]() {
if (in_gap)
{
if (in_gap) {
total_silent_samples += (gap_last - gap_first);
in_gap = false;
}
};
for (std::size_t start = 0; start + win <= frames; start += hop)
{
for (std::size_t start = 0; start + win <= frames; start += hop) {
double ws = 0.0;
for (std::size_t i = 0; i < win; ++i)
{
for (std::size_t i = 0; i < win; ++i) {
const double x = samples[start + i];
ws += x * x;
}
const double wr = std::sqrt(ws / static_cast<double>(win));
if (wr < silence_thresh)
{
if (!in_gap)
{
if (wr < silence_thresh) {
if (!in_gap) {
++r.dropout_count;
gap_first = start;
in_gap = true;
}
gap_last = start + win;
}
else
{
} else {
close_gap();
}
}
@@ -267,23 +234,19 @@ inline ToneReport analyze_tone(const float* samples, std::size_t frames, unsigne
}
// --- Spectral analysis (pitch / SNR / THD), Hann-windowed FFT ---
if (expected_hz > 0.0)
{
if (expected_hz > 0.0) {
std::size_t n = detail::floor_pow2(frames);
n = std::min<std::size_t>(n, std::size_t(1) << 18); // cap cost (~5 s @ 48k)
if (n >= 1024)
{
if (n >= 1024) {
std::vector<std::complex<double>> buf(n);
for (std::size_t i = 0; i < n; ++i)
{
for (std::size_t i = 0; i < n; ++i) {
const double w = 0.5 - 0.5 * std::cos(2.0 * detail::kPi * i / (n - 1)); // Hann
buf[i] = std::complex<double>(samples[i] * w, 0.0);
}
detail::fft(buf);
const std::size_t half = n / 2;
std::vector<double> mag(half);
for (std::size_t i = 0; i < half; ++i)
{
for (std::size_t i = 0; i < half; ++i) {
mag[i] = std::abs(buf[i]);
}
const double bin_hz = static_cast<double>(sample_rate) / static_cast<double>(n);
@@ -291,31 +254,26 @@ inline ToneReport analyze_tone(const float* samples, std::size_t frames, unsigne
// Peak bin, ignoring DC/very low bins (skip < 20 Hz).
std::size_t lo = std::max<std::size_t>(static_cast<std::size_t>(20.0 / bin_hz), 1);
std::size_t peak_bin = lo;
for (std::size_t i = lo; i < half; ++i)
{
if (mag[i] > mag[peak_bin])
{
for (std::size_t i = lo; i < half; ++i) {
if (mag[i] > mag[peak_bin]) {
peak_bin = i;
}
}
// Quadratic (parabolic) interpolation on log-magnitude for a sub-bin estimate
// (accurate for a Hann-windowed peak).
double delta = 0.0;
if (peak_bin > 0 && peak_bin + 1 < half)
{
if (peak_bin > 0 && peak_bin + 1 < half) {
const double a = std::log(mag[peak_bin - 1] + 1e-30);
const double b = std::log(mag[peak_bin] + 1e-30);
const double c = std::log(mag[peak_bin + 1] + 1e-30);
const double denom = (a - 2.0 * b + c);
if (std::fabs(denom) > 1e-30)
{
if (std::fabs(denom) > 1e-30) {
delta = 0.5 * (a - c) / denom;
delta = std::max(-0.5, std::min(0.5, delta));
}
}
r.dominant_hz = (static_cast<double>(peak_bin) + delta) * bin_hz;
if (r.dominant_hz > 0.0)
{
if (r.dominant_hz > 0.0) {
r.pitch_error_ratio = r.dominant_hz / expected_hz;
r.pitch_error_cents = 1200.0 * std::log2(r.pitch_error_ratio);
}
@@ -328,10 +286,8 @@ inline ToneReport analyze_tone(const float* samples, std::size_t frames, unsigne
auto lobe_power = [&](double hz, long half_w) {
const long center = static_cast<long>(std::lround(hz / bin_hz));
double p = 0.0;
for (long k = center - half_w; k <= center + half_w; ++k)
{
if (k >= 0 && static_cast<std::size_t>(k) < half)
{
for (long k = center - half_w; k <= center + half_w; ++k) {
if (k >= 0 && static_cast<std::size_t>(k) < half) {
p += mag[k] * mag[k];
}
}
@@ -339,8 +295,7 @@ inline ToneReport analyze_tone(const float* samples, std::size_t frames, unsigne
};
double total_power = 0.0;
for (std::size_t i = lo; i < half; ++i)
{
for (std::size_t i = lo; i < half; ++i) {
total_power += mag[i] * mag[i];
}
const double fund_power = lobe_power(r.dominant_hz, 8);
@@ -348,11 +303,9 @@ inline ToneReport analyze_tone(const float* samples, std::size_t frames, unsigne
r.snr_db = 10.0 * std::log10(std::max(fund_power, 1e-30) / residual);
double harm_power = 0.0;
for (int h = 2; h <= 6; ++h)
{
for (int h = 2; h <= 6; ++h) {
const double hz = r.dominant_hz * h;
if (hz < (sample_rate / 2.0))
{
if (hz < (sample_rate / 2.0)) {
harm_power += lobe_power(hz, 3);
}
}

View File

@@ -9,8 +9,7 @@
#include <windows.h>
namespace coop
{
namespace coop {
// Directory of the current executable, with a trailing separator.
inline std::wstring exe_directory()
@@ -28,21 +27,17 @@ inline std::wstring exe_directory()
inline std::wstring deployed_artifact_path(const wchar_t* name)
{
const std::wstring here = exe_directory() + name;
if (GetFileAttributesW(here.c_str()) != INVALID_FILE_ATTRIBUTES)
{
if (GetFileAttributesW(here.c_str()) != INVALID_FILE_ATTRIBUTES) {
return here;
}
std::wstring dir = exe_directory();
if (!dir.empty())
{
if (!dir.empty()) {
dir.pop_back(); // drop the trailing separator before going up a level
}
const std::size_t slash = dir.find_last_of(L"\\/");
if (slash != std::wstring::npos)
{
if (slash != std::wstring::npos) {
const std::wstring up = dir.substr(0, slash + 1) + name;
if (GetFileAttributesW(up.c_str()) != INVALID_FILE_ATTRIBUTES)
{
if (GetFileAttributesW(up.c_str()) != INVALID_FILE_ATTRIBUTES) {
return up;
}
}

View File

@@ -12,20 +12,17 @@
#include <string>
#include <vector>
namespace coop
{
namespace coop {
struct WavData
{
struct WavData {
std::uint32_t sample_rate = 0;
std::uint32_t channels = 0;
std::uint32_t bits = 0;
std::uint32_t format_tag = 0; // 1 = PCM, 3 = IEEE float
std::uint32_t format_tag = 0; // 1 = PCM, 3 = IEEE float
std::vector<std::uint8_t> pcm; // interleaved frames
};
namespace detail
{
namespace detail {
inline void wav_put_u32(std::vector<std::uint8_t>& b, std::uint32_t v)
{
b.push_back(v & 0xFF);
@@ -75,12 +72,11 @@ inline bool wav_write(const std::wstring& path, const void* pcm, std::size_t byt
detail::wav_put_u32(hdr, static_cast<std::uint32_t>(bytes));
FILE* f = nullptr;
if (_wfopen_s(&f, path.c_str(), L"wb") != 0 || f == nullptr)
{
if (_wfopen_s(&f, path.c_str(), L"wb") != 0 || f == nullptr) {
return false;
}
const bool ok = std::fwrite(hdr.data(), 1, hdr.size(), f) == hdr.size() &&
(bytes == 0 || std::fwrite(pcm, 1, bytes, f) == bytes);
const bool ok = std::fwrite(hdr.data(), 1, hdr.size(), f) == hdr.size()
&& (bytes == 0 || std::fwrite(pcm, 1, bytes, f) == bytes);
std::fclose(f);
return ok;
}
@@ -89,44 +85,37 @@ inline bool wav_write(const std::wstring& path, const void* pcm, std::size_t byt
inline bool wav_read(const std::wstring& path, WavData& out)
{
FILE* f = nullptr;
if (_wfopen_s(&f, path.c_str(), L"rb") != 0 || f == nullptr)
{
if (_wfopen_s(&f, path.c_str(), L"rb") != 0 || f == nullptr) {
return false;
}
std::fseek(f, 0, SEEK_END);
const long size = std::ftell(f);
std::fseek(f, 0, SEEK_SET);
if (size < 44)
{
if (size < 44) {
std::fclose(f);
return false;
}
std::vector<std::uint8_t> all(static_cast<std::size_t>(size));
const bool read_ok = std::fread(all.data(), 1, all.size(), f) == all.size();
std::fclose(f);
if (!read_ok || std::memcmp(all.data(), "RIFF", 4) != 0 || std::memcmp(all.data() + 8, "WAVE", 4) != 0)
{
if (!read_ok || std::memcmp(all.data(), "RIFF", 4) != 0 || std::memcmp(all.data() + 8, "WAVE", 4) != 0) {
return false;
}
// Walk chunks for "fmt " and "data".
std::size_t pos = 12;
bool have_fmt = false, have_data = false;
while (pos + 8 <= all.size())
{
while (pos + 8 <= all.size()) {
const std::uint8_t* p = all.data() + pos;
const std::uint32_t chunk_size = detail::wav_get_u32(p + 4);
const std::size_t body = pos + 8;
if (std::memcmp(p, "fmt ", 4) == 0 && body + 16 <= all.size())
{
if (std::memcmp(p, "fmt ", 4) == 0 && body + 16 <= all.size()) {
out.format_tag = detail::wav_get_u16(all.data() + body + 0);
out.channels = detail::wav_get_u16(all.data() + body + 2);
out.sample_rate = detail::wav_get_u32(all.data() + body + 4);
out.bits = detail::wav_get_u16(all.data() + body + 14);
have_fmt = true;
}
else if (std::memcmp(p, "data", 4) == 0)
{
} else if (std::memcmp(p, "data", 4) == 0) {
const std::size_t avail = all.size() - body;
const std::size_t n = std::min<std::size_t>(chunk_size, avail);
out.pcm.assign(all.begin() + body, all.begin() + body + n);
@@ -136,8 +125,7 @@ inline bool wav_read(const std::wstring& path, WavData& out)
// wrap `pos` on a 32-bit size_t (x86) and spin the loop on garbage, and there's nothing valid
// past a chunk that claims more than the file holds anyway.
const std::size_t advance = static_cast<std::size_t>(chunk_size) + (chunk_size & 1);
if (advance > all.size() - body)
{
if (advance > all.size() - body) {
break;
}
pos = body + advance;