diff --git a/DeepDrftContent.Services/Audio/WavOffsetService.cs b/DeepDrftContent.Services/Audio/WavOffsetService.cs index c5467c3..e40a59b 100644 --- a/DeepDrftContent.Services/Audio/WavOffsetService.cs +++ b/DeepDrftContent.Services/Audio/WavOffsetService.cs @@ -112,8 +112,11 @@ public class WavOffsetService if (chunkSize < 0) return null; - if (chunkId == "fmt ") + if (chunkId == "fmt " && !foundFmt) { + // Use the first fmt chunk encountered — that is the WAV-spec-authoritative + // chunk. Subsequent fmt chunks in a malformed file are ignored, matching + // AudioProcessor.FindChunk which also returns the first match. if (chunkSize < 16) return null; diff --git a/DeepDrftContent/Controllers/TrackController.cs b/DeepDrftContent/Controllers/TrackController.cs index ee9b3b1..da65828 100644 --- a/DeepDrftContent/Controllers/TrackController.cs +++ b/DeepDrftContent/Controllers/TrackController.cs @@ -125,6 +125,16 @@ public class TrackController : ControllerBase public async Task PutTrack(string trackId, [FromBody] AudioBinaryDto track) { _logger.LogInformation("PutTrack called with trackId: {TrackId}", trackId); + + // Reject unknown MIME types up front rather than silently storing the binary + // with a ".bin" extension. GetExtension returns ".bin" for any unrecognised + // MIME, so treat that as the sentinel for an unsupported type. + if (MimeTypeExtensions.GetExtension(track.Mime) == ".bin") + { + _logger.LogWarning("PutTrack rejected: unsupported MIME type '{Mime}' for track {TrackId}", track.Mime, trackId); + return BadRequest($"Unsupported MIME type: {track.Mime}"); + } + var audioBinary = AudioBinary.From(track); // Direct FileDatabase write: this endpoint receives an already-processed AudioBinaryDto, // not a WAV file, so TrackService.AddTrackFromWavAsync does not apply. See constructor comment. diff --git a/DeepDrftWeb.Client/Services/StreamingErrorHandler.cs b/DeepDrftWeb.Client/Services/StreamingErrorHandler.cs index 84ba02a..a84e862 100644 --- a/DeepDrftWeb.Client/Services/StreamingErrorHandler.cs +++ b/DeepDrftWeb.Client/Services/StreamingErrorHandler.cs @@ -10,15 +10,18 @@ public static class StreamingErrorHandler return lowerError switch { - _ when lowerError.Contains("network") || lowerError.Contains("connection") || lowerError.Contains("timeout") => + _ when lowerError.Contains("network") || lowerError.Contains("connection") || lowerError.Contains("timeout") => "Unable to load audio. Please check your connection and try again.", - - _ when lowerError.Contains("audio") || lowerError.Contains("decode") || lowerError.Contains("format") => + + _ when lowerError.Contains("header") || lowerError.Contains("wav") || lowerError.Contains("invalid wav") => + "This file format is not supported. Only WAV files can be played.", + + _ when lowerError.Contains("audio") || lowerError.Contains("decode") || lowerError.Contains("format") => "This audio file may be corrupted or in an unsupported format.", - - _ when lowerError.Contains("cancel") || lowerError.Contains("abort") => + + _ when lowerError.Contains("cancel") || lowerError.Contains("abort") => "Audio loading was cancelled.", - + _ => "Unable to play audio. Please try again." }; } diff --git a/DeepDrftWeb/Interop/audio/PlaybackScheduler.ts b/DeepDrftWeb/Interop/audio/PlaybackScheduler.ts index c4ff178..577b83d 100644 --- a/DeepDrftWeb/Interop/audio/PlaybackScheduler.ts +++ b/DeepDrftWeb/Interop/audio/PlaybackScheduler.ts @@ -64,7 +64,10 @@ export class PlaybackScheduler { * Get current playback position in seconds (includes playbackOffset for seek-beyond-buffer) */ getCurrentPosition(): number { - if (this.playbackAnchorTime === 0) { + // Use isActive_ as the sentinel for "playback is running", not playbackAnchorTime == 0. + // AudioContext.currentTime can legitimately be 0 at context creation, so comparing + // against 0 would incorrectly treat an active stream started at t=0 as paused. + if (!this.isActive_) { return this.playbackAnchorPosition + this.playbackOffset; } const elapsed = this.contextManager.currentTime - this.playbackAnchorTime; @@ -143,8 +146,11 @@ export class PlaybackScheduler { return; // No new buffers } - if (this.nextScheduleTime === 0) { - this.nextScheduleTime = this.contextManager.currentTime + 0.01; + // Use isActive_ as the sentinel for "playback is running", not nextScheduleTime === 0. + // AudioContext.currentTime can legitimately be 0 at context creation, which would cause + // nextScheduleTime === 0 to incorrectly reset a value already set by playFromPosition. + if (!this.isActive_) { + return; } this.scheduleBuffersFrom(this.nextBufferIndex, 0); diff --git a/DeepDrftWeb/Interop/audio/StreamDecoder.ts b/DeepDrftWeb/Interop/audio/StreamDecoder.ts index 3162ba6..f8d4783 100644 --- a/DeepDrftWeb/Interop/audio/StreamDecoder.ts +++ b/DeepDrftWeb/Interop/audio/StreamDecoder.ts @@ -51,6 +51,9 @@ export class StreamDecoder { private contextManager: AudioContextManager; private wavHeader: WavHeader | null = null; private rawChunks: Uint8Array[] = []; + // totalRawBytes and processedBytes are JS number (IEEE 754 double), which can + // represent integers exactly up to 2^53 bytes (~8 PB). WAV files are bounded + // at 4 GB by the 32-bit RIFF size field, so overflow is not a practical concern. private totalRawBytes: number = 0; private processedBytes: number = 0; private totalStreamLength: number = 0; diff --git a/DeepDrftWeb/Interop/wavutils.ts b/DeepDrftWeb/Interop/wavutils.ts index 0ef0390..b60fd13 100644 --- a/DeepDrftWeb/Interop/wavutils.ts +++ b/DeepDrftWeb/Interop/wavutils.ts @@ -22,11 +22,16 @@ class WavUtils { // Need a DataView that spans the entire buffer for chunk searching const view = new DataView(concatenated.buffer); + // Allocate TextDecoder once for the entire parse pass. Constructing it + // inside the chunk-walk loop would create a new instance per iteration, + // which is non-trivial and unnecessary — a single instance is reusable. + const decoder = new TextDecoder(); + // Check RIFF header - const riff = new TextDecoder().decode(concatenated.slice(0, 4)); + const riff = decoder.decode(concatenated.slice(0, 4)); if (riff !== 'RIFF') return null; - const wave = new TextDecoder().decode(concatenated.slice(8, 12)); + const wave = decoder.decode(concatenated.slice(8, 12)); if (wave !== 'WAVE') return null; // Variables to store parsed header info @@ -43,7 +48,7 @@ class WavUtils { // Find fmt and data chunks let chunkOffset = 12; while (chunkOffset < totalSize - 8) { - const chunkId = new TextDecoder().decode(concatenated.slice(chunkOffset, chunkOffset + 4)); + const chunkId = decoder.decode(concatenated.slice(chunkOffset, chunkOffset + 4)); const chunkSize = view.getUint32(chunkOffset + 4, true); if (chunkId === 'fmt ') {