fa57861dbf
ILoudnessAlgorithm strategy (RmsLoudnessAlgorithm first impl), WaveformProfileService stores quantized byte[] sidecar in new MediaFileVault (profiles vault), wired into UnifiedTrackService.UploadAsync; failure is logged and swallowed. WaveformProfileDto and WaveformProfileOptions in shared projects.
124 lines
4.6 KiB
C#
124 lines
4.6 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"/>. 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)
|
|
{
|
|
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;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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].
|
|
/// </summary>
|
|
public async Task<byte[]?> GetProfileAsync(string entryKey)
|
|
{
|
|
var binary = await _fileDatabase.LoadResourceAsync<MediaBinary>(
|
|
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()
|
|
{
|
|
if (!_fileDatabase.HasVault(VaultConstants.WaveformProfiles))
|
|
{
|
|
await _fileDatabase.CreateVaultAsync(VaultConstants.WaveformProfiles, MediaVaultType.Media);
|
|
}
|
|
}
|
|
}
|