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 /// in (defaults to /// when null). Bucket resolution defaults to /// (512) when 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. /// public async Task ComputeAndStoreAsync( ReadOnlyMemory 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; } } /// /// Returns the stored quantized profile bytes for a track from /// (defaults to 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]. /// public async Task GetProfileAsync(string entryKey, string? vaultName = null) { var binary = await _fileDatabase.LoadResourceAsync( vaultName ?? 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(string vaultName) { if (!_fileDatabase.HasVault(vaultName)) { await _fileDatabase.CreateVaultAsync(vaultName, MediaVaultType.Media); } } }