namespace DeepDrftContent.Processors; /// /// 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 time 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 , which is content-agnostic and parameterized by bucket count. /// public static class WaveformResolution { /// ≈333 samples/sec (≈3 ms/sample): one quarter note at 180 BPM (333 ms) holds ~111 samples. public const int SamplesPerSecond = 333; /// /// 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). /// public const int MaxBucketCount = 2_000_000; /// /// 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. /// public const int MinBucketCount = 2048; /// /// Maps a track's duration (seconds) to a bucket count of ceil(durationSeconds × 333), /// clamped to [, ]. Non-finite or negative /// durations fall to the floor. A 60-minute track → ~1.2M buckets; a 3-minute track → ~60k. /// 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; } }