Files
daniel-c-harvey accf20ba57 feat(waveform): generalize high-res compute to every track (Direction B)
Per-track high-res datum keyed by EntryKey in the renamed track-waveforms vault; computed at upload for all tracks, regenerable per-track via CMS, with a re-runnable backfill. Mix read path repointed so it keeps working.
2026-06-17 10:18:44 -04:00

54 lines
2.8 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
namespace DeepDrftContent.Processors;
/// <summary>
/// Derives the bucket count for a track's high-resolution loudness datum from the audio's duration, so
/// the stored profile captures at a constant <em>time</em> resolution instead of a fixed bucket count.
/// Applies to every track (Mix, Session, Cut) — the release is just the host (phase-12 §5).
///
/// Rationale (phase-9 Mix Visualizer redesign spec §F): the max-zoom window shows one quarter note
/// at 180 BPM = 333 ms of audio, and a smooth glassy curve wants ~100+ sample points across that
/// window. A fixed 2048-bucket datum gives fractions of a sample per 333 ms window on any real-length
/// audio (a 30-minute mix gets ~0.38 buckets), so long content is badly under-sampled. Capturing at a
/// constant ≈333 samples/sec (≈3 ms/sample) makes a 333 ms window hold ~111 samples regardless of
/// length — the direct expression of "high enough resolution regardless of content length."
///
/// This is the orchestration-side derivation (duration → bucket count); the actual compute/store stays
/// in <see cref="WaveformProfileService"/>, which is content-agnostic and parameterized by bucket count.
/// </summary>
public static class WaveformResolution
{
/// <summary>≈333 samples/sec (≈3 ms/sample): one quarter note at 180 BPM (333 ms) holds ~111 samples.</summary>
public const int SamplesPerSecond = 333;
/// <summary>
/// Upper cap on bucket count (~2,000,000 samples ≈ a 100-minute track at 333/s). Past this length we
/// accept slightly-below-target density rather than an unbounded datum (spec §F mitigation #1).
/// </summary>
public const int MaxBucketCount = 2_000_000;
/// <summary>
/// Floor on bucket count. Keeps the historical 2048-bucket density as the minimum so a degenerate
/// near-zero or very-short track still yields a usable profile rather than zero/handful of buckets.
/// </summary>
public const int MinBucketCount = 2048;
/// <summary>
/// Maps a track's duration (seconds) to a bucket count of <c>ceil(durationSeconds × 333)</c>,
/// clamped to [<see cref="MinBucketCount"/>, <see cref="MaxBucketCount"/>]. Non-finite or negative
/// durations fall to the floor. A 60-minute track → ~1.2M buckets; a 3-minute track → ~60k.
/// </summary>
public static int BucketCountForDuration(double durationSeconds)
{
if (double.IsNaN(durationSeconds) || durationSeconds <= 0)
return MinBucketCount;
// Guard against overflow before the cast: anything at/above the cap clamps anyway.
var raw = Math.Ceiling(durationSeconds * SamplesPerSecond);
if (raw >= MaxBucketCount)
return MaxBucketCount;
var buckets = (int)raw;
return buckets < MinBucketCount ? MinBucketCount : buckets;
}
}