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); } } }