using DeepDrftContent.Constants;
using DeepDrftContent.FileDatabase.Models;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using FileDb = DeepDrftContent.FileDatabase.Services.FileDatabase;
namespace DeepDrftContent.Processors;
///
/// Computes a track's waveform loudness profile from its WAV bytes and persists it as a sidecar
/// in the 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 ()
/// so it can be swapped without changing storage or the wire format.
///
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 _logger;
public WaveformProfileService(
FileDb fileDatabase,
AudioProcessor audioProcessor,
ILoudnessAlgorithm loudnessAlgorithm,
IOptions options,
ILogger logger)
{
_fileDatabase = fileDatabase;
_audioProcessor = audioProcessor;
_loudnessAlgorithm = loudnessAlgorithm;
_options = options.Value;
_logger = logger;
}
///
/// Computes the loudness profile from and stores it under
/// . 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.
///
public async Task ComputeAndStoreAsync(ReadOnlyMemory wavBytes, string entryKey)
{
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,
_options.BucketCount);
var quantized = Quantize(profile);
await EnsureVaultAsync();
var binary = new MediaBinary(new MediaBinaryParams(quantized, quantized.Length, ProfileExtension));
var stored = await _fileDatabase.RegisterResourceAsync(
VaultConstants.WaveformProfiles, 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;
}
}
///
/// Returns the stored quantized profile bytes for a track, 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].
///
public async Task GetProfileAsync(string entryKey)
{
var binary = await _fileDatabase.LoadResourceAsync(
VaultConstants.WaveformProfiles, entryKey);
return binary?.Buffer;
}
///
/// 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.
///
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()
{
if (!_fileDatabase.HasVault(VaultConstants.WaveformProfiles))
{
await _fileDatabase.CreateVaultAsync(VaultConstants.WaveformProfiles, MediaVaultType.Media);
}
}
}