accf20ba57
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.
54 lines
2.8 KiB
C#
54 lines
2.8 KiB
C#
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;
|
||
}
|
||
}
|