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.
This commit is contained in:
daniel-c-harvey
2026-06-17 10:18:44 -04:00
parent ad94354632
commit accf20ba57
16 changed files with 612 additions and 155 deletions
+3 -2
View File
@@ -68,7 +68,8 @@ public class ReleaseController : ControllerBase
// GET api/release/{entryKey}/mix/waveform (unauthenticated)
// Serves the high-res waveform datum for a Mix release as base64. Mirrors GET api/track/{id}/waveform
// but reads from the mix-waveforms vault. 404 when the release is not a Mix, carries no waveform key,
// but reads the Mix's track datum from the track-waveforms vault. 404 when the release is not a Mix,
// carries no waveform key,
// or no datum is stored. Public read — addresses by the opaque EntryKey, not the int PK (§3e). The
// {entryKey} string segment cannot collide with the ApiKey-gated POST {id:long}/mix/waveform (different
// verb + constraint). Declared before the shorter "{entryKey}" route for clarity.
@@ -91,7 +92,7 @@ public class ReleaseController : ControllerBase
return NotFound();
}
var bytes = await _waveformProfileService.GetProfileAsync(waveformEntryKey, VaultConstants.MixWaveforms);
var bytes = await _waveformProfileService.GetProfileAsync(waveformEntryKey, VaultConstants.TrackWaveforms);
if (bytes is null)
{
_logger.LogInformation("Mix waveform key set but no datum stored for release: {EntryKey}", entryKey);
+34 -2
View File
@@ -138,8 +138,9 @@ public class TrackController : ControllerBase
}
// GET api/track/waveform-status ([ApiKeyAuthorize])
// Admin backfill view: returns every track with a flag for whether a waveform profile is
// stored in the WaveformProfiles vault. The catalogue is small enough that the CMS panel reads
// Admin backfill view: returns every track with flags for whether each waveform datum is stored —
// the 512-bucket player-bar profile (WaveformProfiles vault) and the per-track high-res visualizer
// datum (TrackWaveforms vault, phase-12 §5). The catalogue is small enough that the CMS panel reads
// the whole list unpaged. Declared before the parameterized "{trackId}" route so the literal
// segment is never treated as a trackId.
[ApiKeyAuthorize]
@@ -158,12 +159,14 @@ public class TrackController : ControllerBase
foreach (var track in tracks.Value)
{
var profile = await _waveformProfileService.GetProfileAsync(track.EntryKey);
var highRes = await _waveformProfileService.GetProfileAsync(track.EntryKey, VaultConstants.TrackWaveforms);
status.Add(new WaveformStatusDto
{
TrackId = track.Id,
EntryKey = track.EntryKey,
TrackName = track.TrackName,
HasProfile = profile is not null,
HasHighRes = highRes is not null,
});
}
@@ -600,6 +603,35 @@ public class TrackController : ControllerBase
return Ok();
}
// POST api/track/{trackId}/waveform/high-res ([ApiKeyAuthorize])
// Track-cardinal generalization of the Mix-only waveform trigger (phase-12 §5): compute and store
// the per-track high-res datum for ANY track from its vault audio, keyed by EntryKey in the
// track-waveforms vault. Drives the CMS per-row "Generate high-res" action and the batch backfill.
// Re-runnable: a second call recomputes and overwrites. trackId is the EntryKey. 404 when no audio
// is stored under that key; 500 when the WAV cannot be decoded or the vault write fails. Declared
// before the parameterized PUT "{trackId}" route so the literal "waveform/high-res" segment wins.
[ApiKeyAuthorize]
[HttpPost("{trackId}/waveform/high-res")]
public async Task<ActionResult> GenerateHighResWaveform(string trackId)
{
var audio = await _trackContentService.GetAudioBinaryAsync(trackId);
if (audio is null)
{
_logger.LogWarning("GenerateHighResWaveform: no audio in vault for {TrackId}", trackId);
return NotFound();
}
var stored = await _waveformProfileService.ComputeAndStoreHighResAsync(
audio.Buffer, trackId, audio.Duration);
if (!stored)
{
_logger.LogError("GenerateHighResWaveform: computation/storage failed for {TrackId}", trackId);
return StatusCode(500, "Failed to generate high-res waveform datum.");
}
return Ok();
}
[ApiKeyAuthorize]
[HttpPut("{trackId}")]
public async Task<ActionResult> PutTrack(string trackId, [FromBody] AudioBinaryDto track)