ca44fc8794
Adds ReleaseRepository/ReleaseManager (IReleaseService) for paged medium-filtered release reads and Session/Mix satellite writes, UnifiedReleaseService orchestrating vault+SQL, and ReleaseController (5 endpoints). Refactors WaveformProfileService for configurable bucketCount/vaultName (backward-compatible) and adds the mix-waveforms vault. Promotes brittle error-string literals to named constants (MixHasNoTrackMessage, MixTrackNoAudioMessage) on UnifiedReleaseService.
134 lines
5.2 KiB
C#
134 lines
5.2 KiB
C#
using DeepDrftContent.Constants;
|
|
using DeepDrftContent.FileDatabase.Models;
|
|
using Microsoft.Extensions.Logging;
|
|
using Microsoft.Extensions.Options;
|
|
using FileDb = DeepDrftContent.FileDatabase.Services.FileDatabase;
|
|
|
|
namespace DeepDrftContent.Processors;
|
|
|
|
/// <summary>
|
|
/// Computes a track's waveform loudness profile from its WAV bytes and persists it as a sidecar
|
|
/// in the <see cref="VaultConstants.WaveformProfiles"/> vault, keyed by the track's EntryKey.
|
|
/// The profile is the upload-time, off-the-playback-path representation the frontend fetches to
|
|
/// render the WaveformSeeker. The loudness measure is injected (<see cref="ILoudnessAlgorithm"/>)
|
|
/// so it can be swapped without changing storage or the wire format.
|
|
/// </summary>
|
|
public class WaveformProfileService
|
|
{
|
|
private const string ProfileExtension = ".wfp";
|
|
|
|
private readonly FileDb _fileDatabase;
|
|
private readonly AudioProcessor _audioProcessor;
|
|
private readonly ILoudnessAlgorithm _loudnessAlgorithm;
|
|
private readonly WaveformProfileOptions _options;
|
|
private readonly ILogger<WaveformProfileService> _logger;
|
|
|
|
public WaveformProfileService(
|
|
FileDb fileDatabase,
|
|
AudioProcessor audioProcessor,
|
|
ILoudnessAlgorithm loudnessAlgorithm,
|
|
IOptions<WaveformProfileOptions> options,
|
|
ILogger<WaveformProfileService> logger)
|
|
{
|
|
_fileDatabase = fileDatabase;
|
|
_audioProcessor = audioProcessor;
|
|
_loudnessAlgorithm = loudnessAlgorithm;
|
|
_options = options.Value;
|
|
_logger = logger;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Computes the loudness profile from <paramref name="wavBytes"/> and stores it under
|
|
/// <paramref name="entryKey"/> in <paramref name="vaultName"/> (defaults to
|
|
/// <see cref="VaultConstants.WaveformProfiles"/> when null). Bucket resolution defaults to
|
|
/// <see cref="WaveformProfileOptions.BucketCount"/> (512) when <paramref name="bucketCount"/> is null;
|
|
/// pass a higher value (e.g., 2048) for the Mix high-res datum. Returns false (and logs) on any
|
|
/// failure — a missing profile is handled gracefully downstream, so callers on the upload path
|
|
/// log-and-continue rather than failing the upload. Does not throw for expected failure modes.
|
|
/// </summary>
|
|
public async Task<bool> ComputeAndStoreAsync(
|
|
ReadOnlyMemory<byte> wavBytes,
|
|
string entryKey,
|
|
int? bucketCount = null,
|
|
string? vaultName = null)
|
|
{
|
|
var effectiveBucketCount = bucketCount ?? _options.BucketCount;
|
|
var effectiveVaultName = vaultName ?? VaultConstants.WaveformProfiles;
|
|
|
|
try
|
|
{
|
|
var pcm = _audioProcessor.TryExtractPcm(wavBytes.Span);
|
|
if (pcm is null)
|
|
{
|
|
_logger.LogWarning(
|
|
"Waveform profile not computed for {EntryKey}: WAV PCM could not be extracted.",
|
|
entryKey);
|
|
return false;
|
|
}
|
|
|
|
var value = pcm.Value;
|
|
var profile = _loudnessAlgorithm.Compute(
|
|
value.Pcm.Span,
|
|
value.Channels,
|
|
value.SampleRate,
|
|
value.BitsPerSample,
|
|
effectiveBucketCount);
|
|
|
|
var quantized = Quantize(profile);
|
|
|
|
await EnsureVaultAsync(effectiveVaultName);
|
|
|
|
var binary = new MediaBinary(new MediaBinaryParams(quantized, quantized.Length, ProfileExtension));
|
|
var stored = await _fileDatabase.RegisterResourceAsync(effectiveVaultName, entryKey, binary);
|
|
|
|
if (!stored)
|
|
{
|
|
_logger.LogWarning("Waveform profile vault write failed for {EntryKey}.", entryKey);
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
catch (Exception ex) when (ex is not OperationCanceledException)
|
|
{
|
|
_logger.LogError(ex, "Waveform profile computation failed for {EntryKey}.", entryKey);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the stored quantized profile bytes for a track from <paramref name="vaultName"/>
|
|
/// (defaults to <see cref="VaultConstants.WaveformProfiles"/> when null), or null if no profile
|
|
/// is stored (existing tracks predate profiling, and computation may have failed). Each byte is
|
|
/// a peak-normalized loudness value in [0, 255].
|
|
/// </summary>
|
|
public async Task<byte[]?> GetProfileAsync(string entryKey, string? vaultName = null)
|
|
{
|
|
var binary = await _fileDatabase.LoadResourceAsync<MediaBinary>(
|
|
vaultName ?? VaultConstants.WaveformProfiles, entryKey);
|
|
return binary?.Buffer;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Maps each [0, 1] bucket to a [0, 255] byte. 1.0 maps to 255; the multiply-by-255 with a
|
|
/// truncating cast keeps every in-range value within a byte without a clamp branch.
|
|
/// </summary>
|
|
private static byte[] Quantize(double[] profile)
|
|
{
|
|
var bytes = new byte[profile.Length];
|
|
for (var i = 0; i < profile.Length; i++)
|
|
{
|
|
bytes[i] = (byte)(profile[i] * 255);
|
|
}
|
|
return bytes;
|
|
}
|
|
|
|
private async Task EnsureVaultAsync(string vaultName)
|
|
{
|
|
if (!_fileDatabase.HasVault(vaultName))
|
|
{
|
|
await _fileDatabase.CreateVaultAsync(vaultName, MediaVaultType.Media);
|
|
}
|
|
}
|
|
}
|