Ψ-W2-T2 remediation: atomic temp+rename collapse write, honest unknown-channel fallback, [verify — DAW] markers, corrected+filed bake-collapse deferral, observable collapse message, quiet-NaN test
This commit is contained in:
@@ -19,8 +19,9 @@ Per-module detail — what each file owns, its invariants — lives in the twent
|
||||
paths anywhere in the index.
|
||||
- **Material:** must handle full-mix/stem bounces, chops/one-shots, and
|
||||
single-cycle/wavetable grabs equally. That means exact sample-accurate bounds,
|
||||
explicit tail control, channel-count preservation, and loop/zero-crossing
|
||||
handling all matter from day one.
|
||||
explicit tail control, correct channel handling (the exact-bounds channel rule
|
||||
under Precision invariants), and loop/zero-crossing handling all matter from day
|
||||
one.
|
||||
|
||||
## One-time submodule setup
|
||||
|
||||
@@ -206,7 +207,7 @@ Plan-style docs live under `docs/`:
|
||||
- **Null test:** a dry offline capture of a range, re-inserted at its source position, nulls to silence against the source — the tool's trust anchor. Ship as a verification action. (Verification action cut per `docs/product/provenance.md` — manual verification only.)
|
||||
- **Bit-identical repeats:** identical offline capture requests produce identical files.
|
||||
- **Non-destructive:** capture never mutates source items or tracks; the realtime backend's temp track is created and removed cleanly, and source routing is restored.
|
||||
- **Exact bounds:** no rounding of the requested range; no added silence unless a tail is explicitly requested; **no lossy channel fold** — summing or averaging differing channels is forbidden. The one permitted collapse is lossless: a new capture whose channels are bit-identical per frame (float bit patterns, never an epsilon) lands as a 1-channel file, with `Sample::channelCount` and the file's `fmt` written together so the two can never disagree. Frame count, sample rate and bit depth are untouched by it. Never retroactive — existing entries and files are never rewritten — and ingest is excluded, because an imported file is the user's bytes, not our capture. The superseded wording ("channel count preserved") was already untrue in the other direction: a mono source renders at `RENDER_CHANNELS = 2`.
|
||||
- **Exact bounds:** no rounding of the requested range; no added silence unless a tail is explicitly requested; **no lossy channel fold** — summing or averaging differing channels is forbidden. The one permitted collapse is lossless: a new capture whose channels are bit-identical per frame (float bit patterns, never an epsilon) lands as a 1-channel file, with `Sample::channelCount` and the file's `fmt` written together so the two can never disagree. Frame count, sample rate and bit depth are untouched by it. Never retroactive — existing entries and files are never rewritten — and ingest is excluded, because an imported file is the user's bytes, not our capture. The superseded wording ("channel count preserved") was already untrue in the other direction: a mono source renders at `RENDER_CHANNELS = 2`. `[verify — DAW]` "lossless" here is a file-bytes property; whether REAPER sums a 1-channel item on a stereo track at the same unity gain as a dual-mono 2-channel item (pan law, mono spread) — the null test's actual playback-chain property — is unconfirmed.
|
||||
- **Relative paths only** in the persisted `BankIndex`.
|
||||
- **Capture FX scope:** two scopes only — item = item/take FX only; track = item FX + the selected track's own track FX. There is no master scope (to capture the master, render a track instead). For both scopes, the out-of-scope chain (ancestors + master track, plus the item's own track for item scope) has its FX, gain, and pan/width/pan-law/mode neutralized to unity — the master track is bypassed as out-of-scope chain, not captured as a scope. Range (time selection or razor) is orthogonal.
|
||||
|
||||
|
||||
+5
-1
@@ -2661,7 +2661,11 @@ LOSSY fold; this collapse is lossless by predicate) but contradicted in text. Th
|
||||
amendment: channel count is preserved except that bit-identical channels may collapse
|
||||
losslessly to mono; a lossy fold remains forbidden. Noted in the amendment: the old text
|
||||
was already untrue in the other direction — a mono source renders at `RENDER_CHANNELS=2`
|
||||
today (`capture_orchestrator.cpp:227`, hardcoded and never measured).
|
||||
today (`capture_orchestrator.cpp:227`, hardcoded and never measured). `[verify — DAW]`
|
||||
"lossless" is proven at the file-bytes level; it is not the same claim as the null
|
||||
test's playback-chain property (whether REAPER sums a 1-channel item on a stereo track
|
||||
at the same unity gain as a dual-mono 2-channel item) — see the acceptance criteria's
|
||||
own `[verify — DAW]` on that bullet below.
|
||||
|
||||
**Surface boundary — owns:** `core/capture/wav_codec` (the pure bit-identity predicate +
|
||||
collapse plan, with `wav_codec` unit tests), `shell/capture/capture.cpp` (the post-render
|
||||
|
||||
@@ -577,3 +577,42 @@ select/move the neighbour, or capture at track scope instead.
|
||||
**Done looks like.** Nothing to do — recorded so a future reviewer does not read the
|
||||
non-isolation as an oversight and re-propose closing it against the recipe's stated
|
||||
tracks-and-range-only shape.
|
||||
|
||||
## Resample-bake landings don't apply the lossless mono collapse to a dual-mono render
|
||||
|
||||
**Context (surfaced by Ψ-W2-T2, mono-collapse).** The collapse (`collapseCapturedFileToMono`
|
||||
/ `core/capture/wav_codec::collapseToMono`) ships for every extension capture path —
|
||||
offline, realtime, batch, recapture — but not for `bake_land.cpp`'s `landOne`, the
|
||||
resample bake's landing function. A dead-center instrument render (the common case
|
||||
that motivated Ψ.6 in the first place) is exactly the dual-mono shape the predicate
|
||||
collapses, so an un-collapsed bake keeps paying for the second channel it doesn't need.
|
||||
|
||||
**Not deferred for the reason once given.** `landOne` reads the staged file into `bytes`
|
||||
once (`bake_land.cpp:101`), parses its layout (`:105`), hashes it (`:126`), derives the
|
||||
channel count twice (`:131`, `:178`), and writes it (`:165`) — all from that same one
|
||||
buffer, so collapsing `bytes` right after the layout parse would keep the hash, the
|
||||
channel count, and the written file consistent by construction; there is no ordering
|
||||
hazard here to defer around.
|
||||
|
||||
**The real reason.** `bake_land.cpp` is Phase Ξ's freshly-landed surface
|
||||
(Ξ-W2-T1, the resample bake chain) and another team is actively remediating it. Landing
|
||||
a mutation there now would cross tracks mid-remediation for no urgent gain — the mono
|
||||
propagation this item would add is a size win, not a correctness one.
|
||||
|
||||
**A mono capture already propagates through the bake for free**, so this item is scoped
|
||||
to the dual-mono-*render* case only: `runBake` / `instrument_bake.cpp` already renders
|
||||
however many channels the dialed sound has, and `bake_render.cpp:38` reads
|
||||
`sample.channelCount()` off that render rather than hardcoding 2 — a mono-programmed
|
||||
sound already bakes to a mono file today, with no change needed.
|
||||
|
||||
**Intended fix.** Once `bake_land.cpp` is quiet, call `collapseToMono` on the staged
|
||||
`bytes` in `landOne` right after the layout parse (`:105`) and before the hash (`:126`),
|
||||
matching the offline/realtime insertion point (post-parse, pre-identity-read).
|
||||
|
||||
**Priority / risk.** Low — a size optimization on an already-correct path, not a
|
||||
precision-invariant gap; the bake's dual-mono case still lands as a valid (if larger)
|
||||
stereo file today.
|
||||
|
||||
**Done looks like.** A dead-center instrument bake lands as a 1-channel file with
|
||||
`Sample::channelCount` matching, the same way an offline dead-center capture does; a
|
||||
true-stereo bake is byte-identical to today's output.
|
||||
|
||||
@@ -89,6 +89,14 @@ Detail specific to these pure modules:
|
||||
which is what the bit-identical-repeats invariant actually asks for. Do not "fix"
|
||||
this by hashing pre-collapse — that would make two entries with different audio
|
||||
layouts share one identity.
|
||||
- **The collapse's minimal rebuild also drops `bext`/iXML/LIST — a source-position
|
||||
consequence, not only a hashing one.** REAPER's renderer writes a `bext` time
|
||||
reference, and REAPER's own import paths can position an item at that BWF timestamp,
|
||||
so a collapsed capture loses it while a declined (non-collapsed) capture from the same
|
||||
action keeps it — two captures from one action behave differently on re-import.
|
||||
`shell/capture/insert.cpp` is unaffected (it drives `SetEditCurPos` + `InsertMedia`
|
||||
rather than reading BWF), so this is not a defect in the shipped insert path.
|
||||
Accepted, not verified against a DAW re-import: `[verify — DAW]`.
|
||||
- `tail_control`'s `kDefaultManualTailMs`/`kManualStepMs` and
|
||||
`render_settings`'s `kMaxTailMs`/`kAutoTrimThresholdDb` are separate constants
|
||||
in separate files by design (panel-facing default/step vs. runaway-guard cap)
|
||||
|
||||
@@ -284,8 +284,13 @@ MonoCollapse collapseToMono(const std::vector<std::uint8_t>& bytes) {
|
||||
}
|
||||
}
|
||||
|
||||
// float -> double -> float round-trips exactly (double represents every float),
|
||||
// so channel 0 reaches the rebuilt file unaltered.
|
||||
// float -> double -> float round-trips exactly for every finite value and for
|
||||
// +-0/+-infinity (double represents every float bit pattern in those classes), so
|
||||
// channel 0 reaches the rebuilt file unaltered. The one hole: a signaling NaN is
|
||||
// quieted by the float->double promotion, so an identical-bit sNaN pair could
|
||||
// collapse to a different bit pattern than it started with. Not reachable from
|
||||
// REAPER-rendered audio, but the bit-identical predicate above admits NaN inputs,
|
||||
// so this rebuild is not exempt from the claim it makes.
|
||||
std::vector<double> mono(frames);
|
||||
for (std::size_t f = 0; f < frames; ++f)
|
||||
mono[f] = static_cast<double>(pcm[f * stride]);
|
||||
|
||||
@@ -113,7 +113,8 @@ struct MonoCollapse {
|
||||
// The rebuild is a canonical minimal WAV, so non-audio chunks (a renderer's `bext`
|
||||
// timestamp, iXML, LIST) do not survive it. That much hashWavContent already skips —
|
||||
// but the collapse rewrites the `fmt ` body and the `data` payload too, which moves
|
||||
// the file's content identity; see this directory's CLAUDE.md for what that costs.
|
||||
// the file's content identity; see this directory's CLAUDE.md for what that costs,
|
||||
// including the bext/source-position consequence beyond hashing.
|
||||
MonoCollapse collapseToMono(const std::vector<std::uint8_t>& bytes);
|
||||
|
||||
// --- Content identity (dedup hashes) -----------------------------------------
|
||||
|
||||
@@ -89,8 +89,10 @@ struct Sample {
|
||||
double wetDry = 1.0; // 1.0 = fully wet, 0.0 = fully dry
|
||||
|
||||
// Channels in the file this entry names — equal to its `fmt ` count by
|
||||
// construction, which is what makes the instrument's mono/stereo read-out and
|
||||
// its waveform lane count agree with the audio. 0 = unknown (pre-field entry).
|
||||
// construction on every path that measures it, which is what makes the
|
||||
// instrument's mono/stereo-toggle default agree with the audio (the waveform
|
||||
// lane count reads the decoded file directly, not this field). 0 = unknown —
|
||||
// a pre-field entry, or a capture whose file could not be parsed to measure it.
|
||||
int channelCount = 0;
|
||||
int sampleRate = 0;
|
||||
|
||||
|
||||
@@ -61,7 +61,7 @@ detail not covered there:
|
||||
- `realtime_lifecycle` (`shell/capture`) — the in-flight realtime-capture state machine + globals (Q-W3 hoist): the action starts it, `OnTimer` drives it per tick via `DriveRealtimeCapture` (a single-pointer-test idle fast path — load-bearing hot-path guardrail), `CommitRealtimeResult` lands a finished capture in the bank, `AbortRealtimeCaptureForUnload` tears down cleanly on extension unload.
|
||||
- `capture_realtime_shell` (`shell/capture`) — the async realtime-record backend surface (Q-W6 split of the former fat `capture.h`): `RealtimeRecordBackend::begin`/`tick`/`abort`, transport-driven across timer ticks (a realtime record cannot block REAPER's UI for its own duration). Deliberately shares NO interface with the offline backend — the lifecycles genuinely differ (the former `ICaptureBackend` interface was deleted in Q-W3, T4-26).
|
||||
- `capture_realtime_finalize` (`shell/capture`) — the file-side half of the realtime-record shell (Q-W3, T4-08): discovers the file REAPER actually recorded, moves it into the bank, runs the Auto-tail PCM decay-scan trim, and populates the finished `Sample`.
|
||||
- `insert` — placement via `InsertMedia`. **Conform-to-project-tempo is an explicit opt-in flag, never silent stretching.**
|
||||
- `insert` — placement via `InsertMedia`. **Conform-to-project-tempo is an explicit opt-in flag, never silent stretching.** The mono collapse needs no change here: `insert.cpp` passes only a path to `InsertMedia`, and REAPER derives the item's channel count from the file itself — a 1-channel WAV yields a mono item for free.
|
||||
- `provenance_shell` — FX-chain identity queries via `TrackFX_*`/`TakeFX_*` APIs; feeds the pure `provenance` fingerprint builder. Stamps `Sample.provenance` on capture; ambiguous/mixed cases record nothing conservatively.
|
||||
- `track_guid` — shared `MediaTrack*` → canonical GUID-string formatter; single source of truth for membership keys.
|
||||
- `item_read` — the ONE place a `MediaItem*` is read for its canonical GUID string (`itemGuid`) and for the durable `P_LANENAME` of the fixed lane it sits on (`itemLaneName`); extracted from previously-duplicated `itemGuid`/`itemLaneName` pairs in `view.cpp` and `bank_panel.cpp` — the item-read analog of `track_guid`'s single `MediaTrack*`→GUID-key formatter. Callers must already know the track is fixed-lane (`I_FREEMODE==2`) before calling `itemLaneName`; the pure `isOnManualLane` predicate handles the non-fixed-lane case separately.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// REAPER-facing offline-render backend (OfflineRenderBackend) plus the shared
|
||||
// backend helpers (makeUniqueTag / stampCaptureSample).
|
||||
// backend helpers (makeUniqueTag / collapseCapturedFileToMono / stampCaptureSample).
|
||||
//
|
||||
// Includes reaper_plugin_functions.h WITHOUT REAPERAPI_IMPLEMENT — main.cpp is
|
||||
// the one TU that defines the API pointers; here they are extern.
|
||||
@@ -31,7 +31,7 @@
|
||||
#include <vector>
|
||||
|
||||
#include "core/capture/capture_paths.h"
|
||||
#include "core/capture/wav_codec.h" // hashWavContent — the one WAV/RIFF owner
|
||||
#include "core/capture/wav_codec.h" // hashWavContent / collapseToMono — the one WAV/RIFF owner
|
||||
#include "core/util/file_bytes.h"
|
||||
#include "core/capture/render_settings.h"
|
||||
#include "core/capture/render_window.h" // frameCountFor — the exact-bounds number
|
||||
@@ -201,29 +201,54 @@ std::string makeUniqueTag(const std::string& prefix) {
|
||||
std::to_string(++counter);
|
||||
}
|
||||
|
||||
void collapseCapturedFileToMono(const std::string& absolutePath) {
|
||||
bool collapseCapturedFileToMono(const std::string& absolutePath) {
|
||||
const std::vector<std::uint8_t> bytes = util::readFileBytes(absolutePath);
|
||||
if (bytes.empty()) return;
|
||||
if (bytes.empty()) return false;
|
||||
|
||||
const MonoCollapse collapse = collapseToMono(bytes);
|
||||
if (!collapse.collapsed) return;
|
||||
if (!collapse.collapsed) return false;
|
||||
|
||||
// One truncating write — the same shape, and the same accepted mid-write residual,
|
||||
// as the realtime Auto-tail trim (capture_realtime_finalize.cpp).
|
||||
std::ofstream out(absolutePath, std::ios::binary | std::ios::trunc);
|
||||
if (!out) return;
|
||||
// Sibling temp + rename, NOT an in-place truncating write: this runs unconditionally
|
||||
// on the deterministic offline path (which never reopened its render for write before
|
||||
// this step existed), so a mid-write failure here must not land a truncated file that
|
||||
// stampCaptureSample then hashes as a false CaptureStatus::Ok. rename() replaces the
|
||||
// destination in one step, so the original bytes are never destroyed until the
|
||||
// replacement is known-complete; a failed write or rename leaves the original file
|
||||
// untouched and self-cleans the temp rather than littering it.
|
||||
const std::string tempPath = absolutePath + ".moncollapse.tmp";
|
||||
{
|
||||
std::ofstream out(tempPath, std::ios::binary | std::ios::trunc);
|
||||
if (!out) return false;
|
||||
out.write(reinterpret_cast<const char*>(collapse.bytes.data()),
|
||||
static_cast<std::streamsize>(collapse.bytes.size()));
|
||||
const bool wroteOk = static_cast<bool>(out);
|
||||
out.close();
|
||||
if (!wroteOk) {
|
||||
std::error_code ec;
|
||||
std::filesystem::remove(tempPath, ec);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
std::error_code ec;
|
||||
std::filesystem::rename(tempPath, absolutePath, ec);
|
||||
if (ec) {
|
||||
std::filesystem::remove(tempPath, ec); // don't leave litter on a failed rename
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void stampCaptureSample(Sample& s, const CaptureRequest& req,
|
||||
ReaProject* rateProj, ReaProject* timeSigProj,
|
||||
const std::string& absolutePath) {
|
||||
// Track GUIDs echoed from the request (the caller resolved the selection; the
|
||||
// backends stay source-agnostic). channelCount starts at the request value only
|
||||
// as the fallback for an unparseable file — the produced FILE overrides it below.
|
||||
// backends stay source-agnostic). channelCount starts at 0 (unknown, the same
|
||||
// sentinel bank_model already uses for a pre-field entry) rather than the
|
||||
// request's value — the request always asks for 2, so echoing it would claim a
|
||||
// measurement that never happened for the unparseable-file case below. The
|
||||
// produced FILE overrides it below whenever it parses.
|
||||
s.trackGuids = req.trackGuids;
|
||||
s.channelCount = req.channelCount;
|
||||
s.channelCount = 0;
|
||||
|
||||
// PROJECT_SRATE can read 0 on a project that never pinned a rate — stays 0
|
||||
// (honest "unknown") rather than a bogus literal.
|
||||
@@ -473,7 +498,7 @@ CaptureResult OfflineRenderBackend::capture(const CaptureRequest& request) {
|
||||
// renderer's file rather than one this step had already rewritten. The collapse
|
||||
// preserves the frame count, so the two are order-independent in outcome — only
|
||||
// in what each is measuring.
|
||||
collapseCapturedFileToMono(expectedPath);
|
||||
const bool collapsedToMono = collapseCapturedFileToMono(expectedPath);
|
||||
|
||||
// Record the request's own bounds (exact) rather than re-measuring the file.
|
||||
Sample s;
|
||||
@@ -501,7 +526,8 @@ CaptureResult OfflineRenderBackend::capture(const CaptureRequest& request) {
|
||||
result.message = "Captured [" +
|
||||
std::to_string(request.startSeconds) + "s, " +
|
||||
std::to_string(request.endSeconds) + "s] -> " +
|
||||
paths.relativePath;
|
||||
paths.relativePath +
|
||||
(collapsedToMono ? " (collapsed to mono)" : "");
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
@@ -107,11 +107,14 @@ std::string makeUniqueTag(const std::string& prefix);
|
||||
// bit-identical (the pure `collapseToMono` decides). Every other file is left
|
||||
// untouched, byte for byte, so the not-collapsed path is exactly what the backend
|
||||
// produced. Must run BEFORE stampCaptureSample, which measures the landed file.
|
||||
void collapseCapturedFileToMono(const std::string& absolutePath);
|
||||
// Returns whether the file was actually rewritten (collapsed AND the write landed) —
|
||||
// callers use it to make the collapse observable in the reported CaptureResult.
|
||||
bool collapseCapturedFileToMono(const std::string& absolutePath);
|
||||
|
||||
// Stamps the metadata shared by both backends onto `s`: trackGuids (echoed from the
|
||||
// request) + channelCount (measured from the produced file's `fmt`; the request
|
||||
// value only as the fallback for a file that cannot be parsed), resolved
|
||||
// request) + channelCount (measured from the produced file's `fmt`; 0/unknown as the
|
||||
// fallback for a file that cannot be parsed — never the request's value, which is
|
||||
// always 2 and was never actually measured), resolved
|
||||
// sampleRate (request rate, else PROJECT_SRATE
|
||||
// from `rateProj`), captureTempo, the capture-start time signature
|
||||
// (TimeMap_GetTimeSigAtTime against `timeSigProj` — offline passes nullptr for the
|
||||
|
||||
@@ -186,7 +186,7 @@ CaptureResult finalizeRecording(ReaProject* proj, MediaTrack* temp,
|
||||
|
||||
// Channel-domain rewrite, after the frame-domain trim so it acts on the final
|
||||
// frame set; it preserves the frame count, so the trimmed length above still holds.
|
||||
collapseCapturedFileToMono(destPath);
|
||||
const bool collapsedToMono = collapseCapturedFileToMono(destPath);
|
||||
|
||||
// Pure recorded-capture -> Sample mapping (identity, bounds echo, tier).
|
||||
RecordedCapture cap;
|
||||
@@ -225,7 +225,8 @@ CaptureResult finalizeRecording(ReaProject* proj, MediaTrack* temp,
|
||||
std::to_string(request.startSeconds) + "s, " +
|
||||
std::to_string(request.endSeconds) + "s] (recorded " +
|
||||
std::to_string(result.sample.lengthSeconds) + "s) -> " +
|
||||
paths.relativePath;
|
||||
paths.relativePath +
|
||||
(collapsedToMono ? " (collapsed to mono)" : "");
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
@@ -51,6 +51,16 @@ static void putFloat(std::vector<std::uint8_t>& b, float f) {
|
||||
std::memcpy(tmp, &f, 4);
|
||||
for (int i = 0; i < 4; ++i) b.push_back(tmp[i]);
|
||||
}
|
||||
static float floatFromBits(std::uint32_t bits) {
|
||||
float f;
|
||||
std::memcpy(&f, &bits, 4);
|
||||
return f;
|
||||
}
|
||||
static std::uint32_t bitsFromFloat(float f) {
|
||||
std::uint32_t bits;
|
||||
std::memcpy(&bits, &f, 4);
|
||||
return bits;
|
||||
}
|
||||
|
||||
// A canonical 32-bit-float WAV: RIFF/WAVE, fmt (tag 3, 16-byte body), data holding
|
||||
// `frames` interleaved frames of `channels`. `leadingJunk` optionally inserts an
|
||||
@@ -746,6 +756,25 @@ static void testCollapseChangesContentHash() {
|
||||
CHECK(hashWavContent(c.bytes) != hashWavContent(wav));
|
||||
}
|
||||
|
||||
// The float->double->float rebuild's stated hole is a SIGNALING NaN (double promotion
|
||||
// quiets it); a QUIET NaN is not that hole. Both channels carry the identical
|
||||
// quiet-NaN bit pattern, so the predicate collapses; the rebuilt mono channel must
|
||||
// carry that exact bit pattern back, not merely "some NaN".
|
||||
static void testCollapsePreservesQuietNaNBitPattern() {
|
||||
constexpr std::uint32_t kQuietNaNBits = 0x7FC12345u; // exponent all-ones, mantissa MSB set
|
||||
auto wav = buildFloatWav(2, 48000, 1,
|
||||
[kQuietNaNBits](std::size_t, std::uint16_t) {
|
||||
return floatFromBits(kQuietNaNBits);
|
||||
});
|
||||
const MonoCollapse c = collapseToMono(wav);
|
||||
CHECK(c.collapsed);
|
||||
const WavLayout L = parseWavLayout(c.bytes);
|
||||
CHECK(L.valid && L.channelCount == 1 && L.frameCount() == 1);
|
||||
const auto pcm = extractFloatFrames(c.bytes, L, 0, 1);
|
||||
CHECK(pcm.size() == 1);
|
||||
if (!pcm.empty()) CHECK(bitsFromFloat(pcm[0]) == kQuietNaNBits);
|
||||
}
|
||||
|
||||
int main() {
|
||||
testParseCanonicalStereo();
|
||||
testParseMonoAndLeadingChunk();
|
||||
@@ -780,6 +809,7 @@ int main() {
|
||||
testCollapseThroughOddPaddedLeadingChunk();
|
||||
testCollapseDeclinesOnUnparseableBytes();
|
||||
testCollapseChangesContentHash();
|
||||
testCollapsePreservesQuietNaNBitPattern();
|
||||
|
||||
if (g_fail == 0) std::printf("wav_codec: all tests passed\n");
|
||||
else std::printf("wav_codec: %d CHECK(s) FAILED\n", g_fail);
|
||||
|
||||
Reference in New Issue
Block a user