using System.Text;
namespace DeepDrftContent.Audio;
///
/// Service for creating WAV audio streams starting from a byte offset.
/// Synthesizes a valid WAV header for the remaining audio data.
///
public class WavOffsetService
{
///
/// WAV audio format code for linear PCM. The pipeline (AudioProcessor,
/// WavOffsetService, and wavutils.ts) is PCM-only by design — IEEE Float
/// (format 3) and other formats are rejected at parse time so the
/// synthesized header here can safely assume PCM.
///
public const short PcmFormat = 1;
///
/// Creates a stream containing a synthesized WAV header followed by audio data from the specified offset.
/// The returned stream is composed of a small header buffer and a non-owning slice over the input
/// buffer — no copy of the audio payload is made.
///
/// The complete WAV file buffer
/// Byte offset into the raw audio data (not including original header)
/// Stream with new WAV header + audio data from offset, or null if invalid
public Stream? CreateOffsetStream(byte[] fullAudioBuffer, long byteOffset)
{
var format = ParseWavHeader(fullAudioBuffer);
if (format == null)
return null;
// Validate offset is within bounds and block-aligned
if (byteOffset < 0 || byteOffset >= format.DataSize)
return null;
// Align to block boundary for clean audio
var alignedOffset = (byteOffset / format.BlockAlign) * format.BlockAlign;
// Calculate new data size (long arithmetic — DataSize may be up to ~4 GB)
var newDataSize = format.DataSize - alignedOffset;
if (newDataSize <= 0)
return null;
// MemoryStream does not support offsets or lengths beyond int.MaxValue.
// RF64 (>2 GB audio segments) is not supported; reject before truncating.
var sourcePosition = format.HeaderSize + alignedOffset;
if (sourcePosition > int.MaxValue || newDataSize > int.MaxValue)
throw new NotSupportedException("Audio file segment exceeds 2 GB; RF64 not supported");
var newDataSizeInt = (int)newDataSize;
var sourcePositionInt = (int)sourcePosition;
// Create new WAV header using the format reported by the parsed header.
// PCM is the only format we accept (see PcmFormat / ParseWavHeader), but
// threading format.AudioFormat through keeps the header self-consistent
// and prevents drift if the validation contract is ever relaxed.
var newHeader = CreateWavHeader(format, newDataSizeInt);
// Compose: 44-byte header followed by a non-copying slice of the audio payload.
// Wrapping the original buffer in a MemoryStream window avoids a 100MB+ copy
// that the previous MemoryStream(capacity).Write(...) implementation forced.
var headerStream = new MemoryStream(newHeader, writable: false);
var dataStream = new MemoryStream(
fullAudioBuffer,
sourcePositionInt,
newDataSizeInt,
writable: false,
publiclyVisible: false);
return new ConcatStream(headerStream, dataStream);
}
///
/// Parses the WAV header from a buffer to extract format information.
/// PCM-only — IEEE Float (format 3) and other non-PCM formats are rejected
/// so downstream synthesis can safely assume PCM sample encoding.
///
public WavFormat? ParseWavHeader(byte[] buffer)
{
if (buffer.Length < 44)
return null;
// Check RIFF header
var riff = Encoding.ASCII.GetString(buffer, 0, 4);
if (riff != "RIFF")
return null;
var wave = Encoding.ASCII.GetString(buffer, 8, 4);
if (wave != "WAVE")
return null;
// Variables to store parsed header info
int sampleRate = 0;
int channels = 0;
int bitsPerSample = 0;
int byteRate = 0;
int blockAlign = 0;
long dataSize = 0;
int headerSize = 0;
short audioFormat = 0;
bool foundFmt = false;
bool foundData = false;
// Find fmt and data chunks
int chunkOffset = 12;
while (chunkOffset < buffer.Length - 8)
{
var chunkId = Encoding.ASCII.GetString(buffer, chunkOffset, 4);
var chunkSize = BitConverter.ToInt32(buffer, chunkOffset + 4);
if (chunkSize < 0)
return null;
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;
audioFormat = BitConverter.ToInt16(buffer, chunkOffset + 8);
// PCM only. Float32 WAVs were previously accepted here but the synthesized
// header below is PCM-shaped — accepting Float would produce a corrupt file
// claiming PCM with Float-encoded samples. AudioProcessor also rejects
// non-PCM at upload time so this branch is defense in depth.
if (audioFormat != PcmFormat)
return null;
channels = BitConverter.ToInt16(buffer, chunkOffset + 10);
sampleRate = BitConverter.ToInt32(buffer, chunkOffset + 12);
byteRate = BitConverter.ToInt32(buffer, chunkOffset + 16);
blockAlign = BitConverter.ToInt16(buffer, chunkOffset + 20);
bitsPerSample = BitConverter.ToInt16(buffer, chunkOffset + 22);
// Basic validation
if (channels < 1 || channels > 8)
return null;
foundFmt = true;
}
else if (chunkId == "data")
{
// WAV stores DataSize as a 32-bit unsigned int. Read as uint to preserve
// values above int.MaxValue (files between 2–4 GB), then widen to long.
dataSize = (long)BitConverter.ToUInt32(buffer, chunkOffset + 4);
headerSize = chunkOffset + 8; // Audio data starts after 'data' + size (8 bytes)
foundData = true;
}
// Move to next chunk with proper alignment (chunks are word-aligned)
chunkOffset += 8 + ((chunkSize + 1) & ~1);
// If we found both chunks, we're done
if (foundFmt && foundData)
break;
}
// Must have found both fmt and data chunks
if (!foundFmt || !foundData)
return null;
return new WavFormat(
AudioFormat: audioFormat,
SampleRate: sampleRate,
Channels: channels,
BitsPerSample: bitsPerSample,
ByteRate: byteRate,
BlockAlign: blockAlign,
DataSize: dataSize,
HeaderSize: headerSize
);
}
///
/// Creates a standard 44-byte WAV header. The audio format code is taken from
/// rather than hardcoded so the synthesized header matches
/// what was parsed (today always ; see ParseWavHeader).
///
public byte[] CreateWavHeader(WavFormat format, int dataSize)
{
var header = new byte[44];
var fileSize = 36 + dataSize;
// RIFF header
header[0] = (byte)'R'; header[1] = (byte)'I'; header[2] = (byte)'F'; header[3] = (byte)'F';
BitConverter.GetBytes(fileSize).CopyTo(header, 4);
header[8] = (byte)'W'; header[9] = (byte)'A'; header[10] = (byte)'V'; header[11] = (byte)'E';
// fmt chunk
header[12] = (byte)'f'; header[13] = (byte)'m'; header[14] = (byte)'t'; header[15] = (byte)' ';
BitConverter.GetBytes(16).CopyTo(header, 16); // fmt chunk size
BitConverter.GetBytes(format.AudioFormat).CopyTo(header, 20); // Audio format (from parsed header)
BitConverter.GetBytes((short)format.Channels).CopyTo(header, 22);
BitConverter.GetBytes(format.SampleRate).CopyTo(header, 24);
BitConverter.GetBytes(format.ByteRate).CopyTo(header, 28);
BitConverter.GetBytes((short)format.BlockAlign).CopyTo(header, 32);
BitConverter.GetBytes((short)format.BitsPerSample).CopyTo(header, 34);
// data chunk header
header[36] = (byte)'d'; header[37] = (byte)'a'; header[38] = (byte)'t'; header[39] = (byte)'a';
BitConverter.GetBytes(dataSize).CopyTo(header, 40);
return header;
}
}
///
/// WAV format information extracted from header.
///
/// WAV fmt-chunk audio format code (1 = PCM; the only value accepted today).
public record WavFormat(
short AudioFormat,
int SampleRate,
int Channels,
int BitsPerSample,
int ByteRate,
int BlockAlign,
long DataSize,
int HeaderSize
);
///
/// Forward-only read stream over two underlying streams concatenated end-to-end.
/// Lets us serve "[synthesized header][slice of original buffer]" without
/// allocating a single contiguous buffer for the combined payload.
///
internal sealed class ConcatStream : Stream
{
private readonly Stream _first;
private readonly Stream _second;
private readonly long _length;
private long _position;
public ConcatStream(Stream first, Stream second)
{
_first = first;
_second = second;
_length = first.Length + second.Length;
}
public override bool CanRead => true;
public override bool CanSeek => false;
public override bool CanWrite => false;
public override long Length => _length;
public override long Position
{
get => _position;
set => throw new NotSupportedException();
}
public override int Read(byte[] buffer, int offset, int count)
{
var total = 0;
// Loop over _first until it returns 0 (exhausted) or the caller's buffer
// is full. Stream.Read is not required to fill the buffer in one call even
// when data is available (e.g. a future non-MemoryStream _first), so we must
// keep pulling until we get 0 before advancing to _second.
while (count > 0 && _position < _first.Length)
{
var read = _first.Read(buffer, offset, count);
if (read == 0) break;
total += read;
_position += read;
offset += read;
count -= read;
}
if (count > 0)
{
var read = _second.Read(buffer, offset, count);
total += read;
_position += read;
}
return total;
}
public override async ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default)
{
var total = 0;
// Same loop contract as Read() — exhaust _first before reading _second.
while (!buffer.IsEmpty && _position < _first.Length)
{
var read = await _first.ReadAsync(buffer, cancellationToken);
if (read == 0) break;
total += read;
_position += read;
buffer = buffer[read..];
}
if (!buffer.IsEmpty)
{
var read = await _second.ReadAsync(buffer, cancellationToken);
total += read;
_position += read;
}
return total;
}
public override void Flush() { }
public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException();
public override void SetLength(long value) => throw new NotSupportedException();
public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException();
protected override void Dispose(bool disposing)
{
if (disposing)
{
_first.Dispose();
_second.Dispose();
}
base.Dispose(disposing);
}
}