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:
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)];
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user