Stream the waveform compute so large uploads no longer buffer the whole file (Wave 2 OOM)

This commit is contained in:
daniel-c-harvey
2026-06-25 21:49:11 -04:00
parent aa0b64329f
commit 9347f11ff0
10 changed files with 594 additions and 120 deletions
+4 -4
View File
@@ -112,10 +112,10 @@ public class ReleaseController : ControllerBase
}
// POST api/release/{id}/mix/waveform ([ApiKeyAuthorize], no body)
// Server-side trigger: fetch the Mix's track audio from the vault, compute a duration-derived high-res
// waveform via ComputeAndStoreHighResAsync, store it in the track-waveforms vault, and set
// MixMetadata.WaveformEntryKey. 404 when the release is missing or has no stored audio; 500 on
// compute/storage failure. Declared before "{id:long}".
// Server-side trigger: stream the Mix's track audio from the vault, compute a duration-derived
// high-res waveform, store it in the track-waveforms vault, and set MixMetadata.WaveformEntryKey.
// 404 when the release is missing or has no stored audio; 500 on compute/storage failure. Declared
// before "{id:long}".
[ApiKeyAuthorize]
[HttpPost("{id:long}/mix/waveform")]
public async Task<ActionResult> GenerateMixWaveform(long id, CancellationToken ct = default)
+23 -9
View File
@@ -756,15 +756,18 @@ public class TrackController : ControllerBase
[HttpPost("{trackId}/waveform")]
public async Task<ActionResult> GenerateWaveform(string trackId)
{
var audio = await _trackContentService.GetAudioBinaryAsync(trackId);
if (audio is null)
// Streaming compute (Wave 2): the WAV is read from the vault in bounded chunks, never buffered
// whole. Tri-state: null = no vault audio (404), false = present but uncomputable / write failed
// (500), true = stored.
var stored = await _waveformProfileService.ComputeAndStoreProfileStreamingAsync(
_ => _trackContentService.OpenAudioStreamAsync(trackId), trackId, HttpContext.RequestAborted);
if (stored is null)
{
_logger.LogWarning("GenerateWaveform: no audio in vault for {TrackId}", trackId);
return NotFound();
}
var stored = await _waveformProfileService.ComputeAndStoreAsync(audio.Buffer, trackId);
if (!stored)
if (stored is false)
{
_logger.LogError("GenerateWaveform: profile computation/storage failed for {TrackId}", trackId);
return StatusCode(500, "Failed to generate waveform profile.");
@@ -784,16 +787,27 @@ public class TrackController : ControllerBase
[HttpPost("{trackId}/waveform/high-res")]
public async Task<ActionResult> GenerateHighResWaveform(string trackId)
{
var audio = await _trackContentService.GetAudioBinaryAsync(trackId);
if (audio is null)
// The high-res bucket count is duration-derived. Read the duration from the vault index metadata
// (no body load); its absence means the track has no vault audio → 404.
var duration = await _trackContentService.GetAudioDurationAsync(trackId);
if (duration 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)
// Streaming compute (Wave 2): bounded read of the vault WAV. Tri-state mapping as in
// GenerateWaveform — null (entry vanished between the metadata read and the compute) → 404.
var stored = await _waveformProfileService.ComputeAndStoreHighResStreamingAsync(
_ => _trackContentService.OpenAudioStreamAsync(trackId), trackId, duration.Value,
HttpContext.RequestAborted);
if (stored is null)
{
_logger.LogWarning("GenerateHighResWaveform: no audio in vault for {TrackId}", trackId);
return NotFound();
}
if (stored is false)
{
_logger.LogError("GenerateHighResWaveform: computation/storage failed for {TrackId}", trackId);
return StatusCode(500, "Failed to generate high-res waveform datum.");