Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
c61ef13
Add common audio mux and transcode contracts
Hawkynt Aug 29, 2026
3268d1f
Add capability-driven audio conversion pipeline
Hawkynt Aug 29, 2026
5f9586c
Reference WAV reader for AAC creation
Hawkynt Aug 29, 2026
354276e
Make ADTS AAC a mux and transcode participant
Hawkynt Aug 29, 2026
4653fe9
Add audio-only ISO BMFF AAC muxer
Hawkynt Aug 29, 2026
5456cd2
Add AAC mux and encode capabilities to MP4
Hawkynt Aug 29, 2026
6c250bd
Reference WAV reader for FLAC creation
Hawkynt Aug 29, 2026
cb3a602
Add canonical FLAC encode and create path
Hawkynt Aug 29, 2026
dd3066b
Reference WAV reader for Ogg creation
Hawkynt Aug 29, 2026
e001cd4
Add Ogg Vorbis and Opus create/encode paths
Hawkynt Aug 29, 2026
c87904d
Bridge pseudo-archive channels into canonical PCM
Hawkynt Aug 29, 2026
d621759
Test audio remux and cross-format conversion pipeline
Hawkynt Aug 29, 2026
945f9b5
Fix audio pipeline option resolution
Hawkynt Aug 29, 2026
77f74a6
Align format creation options with audio pipeline
Hawkynt Aug 29, 2026
96cf783
Add MP3 and WavPack audio conversion adapters
Hawkynt Aug 29, 2026
8507091
Route MP3 and WavPack through audio adapters
Hawkynt Aug 29, 2026
803119c
Test MP3 and WavPack conversion routes
Hawkynt Aug 29, 2026
c3d55b3
Add AU PCM and G.711 conversion support
Hawkynt Aug 29, 2026
4bbfdea
Test AU PCM and G.711 conversion routes
Hawkynt Aug 29, 2026
4fb6f13
Add AIFF and AIFC audio conversion support
Hawkynt Aug 29, 2026
d53b7be
Test AIFF and AIFC conversion routes
Hawkynt Aug 29, 2026
25b7a27
Add WAV compressed-mode conversion adapter
Hawkynt Aug 29, 2026
c1de194
Fix WAV ADPCM framing and 8-bit PCM conversion
Hawkynt Aug 29, 2026
7384343
Wire WAV adapter into common audio conversion
Hawkynt Aug 29, 2026
210fbcd
Allow explicit same-container audio transcoding
Hawkynt Aug 29, 2026
f432f0c
Honor WAVE fact sample counts during decode
Hawkynt Aug 29, 2026
2d0d140
Test compressed WAVE conversion modes
Hawkynt Aug 29, 2026
24f2b6f
Add capability-driven audio conversion inventory
Hawkynt Aug 29, 2026
1902fa7
Test audio conversion capability inventory
Hawkynt Aug 29, 2026
955c35d
Fix CAF Core Audio endian flag semantics
Hawkynt Aug 29, 2026
dee1ba5
Fix CAF writer endian semantics and float creation
Hawkynt Aug 29, 2026
22e5ca5
Add CAF PCM and G.711 conversion adapter
Hawkynt Aug 29, 2026
6a68953
Centralize audio adapter resolution
Hawkynt Aug 29, 2026
0f901d5
Use central resolver in audio inventory
Hawkynt Aug 29, 2026
4fba2df
Use central audio adapter resolver
Hawkynt Aug 29, 2026
2db1995
Set interoperable CAF LPCM format flags
Hawkynt Aug 29, 2026
4bbce69
Set proper CAF signed/float packed flags
Hawkynt Aug 29, 2026
2acc916
Test CAF PCM and G.711 conversion routes
Hawkynt Aug 29, 2026
dffb409
Add AC-3 and DTS conversion adapters
Hawkynt Aug 29, 2026
391f151
Fix elementary audio adapter helper syntax
Hawkynt Aug 29, 2026
96aa13d
Register AC-3 and DTS conversion adapters
Hawkynt Aug 29, 2026
0ce26cc
Test AC-3 and DTS cross-format conversion
Hawkynt Aug 29, 2026
7fe4e80
Add AMR storage descriptors and CAF ima4
Hawkynt Aug 29, 2026
abc5ca6
# restore AMR project graph references
Hawkynt Aug 30, 2026
96d0ff7
Add packet-preserving MP3 and WavPack audio routing
Hawkynt Aug 31, 2026
41bb4f1
Fix audio packet test WAV argument
Hawkynt Aug 31, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions Compression.Lib/AudioAdapterResolver.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
using Compression.Registry;

namespace Compression.Lib;

/// <summary>Central resolver for native and non-invasive audio conversion capabilities.</summary>
internal static class AudioAdapterResolver {
private static readonly WavAudioAdapter Wav = new();
private static readonly CafAudioAdapter Caf = new();
private static readonly Ac3AudioAdapter Ac3 = new();
private static readonly DtsAudioAdapter Dts = new();

public static IAudioPcmSource? ResolvePcmSource(IFormatDescriptor descriptor)
=> descriptor as IAudioPcmSource ?? descriptor.Id switch {
"Wav" => Wav,
"Caf" => Caf,
"Ac3" => Ac3,
"Dts" => Dts,
_ => AudioFormatAdapters.ResolvePcmSource(descriptor),
};

public static IAudioPcmTarget? ResolvePcmTarget(IFormatDescriptor descriptor)
=> descriptor as IAudioPcmTarget ?? descriptor.Id switch {
"Wav" => Wav,
"Caf" => Caf,
"Ac3" => Ac3,
"Dts" => Dts,
_ => AudioFormatAdapters.ResolvePcmTarget(descriptor),
};

public static IAudioDemuxSource? ResolveDemuxSource(IFormatDescriptor descriptor)
=> descriptor as IAudioDemuxSource ?? descriptor.Id switch {
"Mp3" => Mp3AudioPacketAdapter.Instance,
"WavPack" => WavPackAudioPacketAdapter.Instance,
_ => null,
};

public static IAudioMuxTarget? ResolveMuxTarget(IFormatDescriptor descriptor)
=> descriptor as IAudioMuxTarget ?? descriptor.Id switch {
"Mp3" => Mp3AudioPacketAdapter.Instance,
"WavPack" => WavPackAudioPacketAdapter.Instance,
_ => null,
};

public static IArchiveCreatable? ResolvePseudoArchiveTarget(IFormatDescriptor descriptor)
=> descriptor is IArchiveCreatable creator &&
(descriptor.Category == FormatCategory.Audio || descriptor is IAudioContainerFormat)
? creator
: null;
}
67 changes: 67 additions & 0 deletions Compression.Lib/AudioConversionInventory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
using Compression.Registry;

namespace Compression.Lib;

/// <summary>Describes how one registered format participates in the audio conversion graph.</summary>
public sealed record AudioConversionCapability(
string FormatId,
string DisplayName,
bool CanDecodePcm,
bool CanEncodePcm,
bool CanDemuxEncoded,
bool CanMuxEncoded,
bool CanReadPseudoArchive,
bool CanCreatePseudoArchive,
IReadOnlyList<string> EncodeCodecs,
IReadOnlyList<string> MuxCodecs
) {
public bool CanBeSource => this.CanDecodePcm || this.CanDemuxEncoded || this.CanReadPseudoArchive;
public bool CanBeTarget => this.CanEncodePcm || this.CanMuxEncoded || this.CanCreatePseudoArchive;
}

/// <summary>
/// Enumerates the actual registered audio conversion surface. This is capability-based,
/// not documentation-based: adding an encoder/muxer automatically changes the inventory.
/// </summary>
public static class AudioConversionInventory {

public static IReadOnlyList<AudioConversionCapability> Enumerate() {
FormatRegistry.Initialize();
return FormatRegistry.All
.Where(IsAudioCandidate)
.Select(Describe)
.OrderBy(static item => item.FormatId, StringComparer.OrdinalIgnoreCase)
.ToArray();
}

public static AudioConversionCapability Describe(IFormatDescriptor descriptor) {
ArgumentNullException.ThrowIfNull(descriptor);

var pcmSource = AudioAdapterResolver.ResolvePcmSource(descriptor);
var pcmTarget = AudioAdapterResolver.ResolvePcmTarget(descriptor);
var demux = AudioAdapterResolver.ResolveDemuxSource(descriptor);
var mux = AudioAdapterResolver.ResolveMuxTarget(descriptor);
var archive = descriptor as IArchiveFormatOperations;
var creator = AudioAdapterResolver.ResolvePseudoArchiveTarget(descriptor);

return new AudioConversionCapability(
descriptor.Id,
descriptor.DisplayName,
pcmSource is not null,
pcmTarget is not null,
demux is not null,
mux is not null,
archive is not null,
creator is not null,
pcmTarget?.SupportedEncodeCodecs.ToArray() ?? [],
mux?.SupportedMuxCodecs.ToArray() ?? []);
}

private static bool IsAudioCandidate(IFormatDescriptor descriptor)
=> descriptor.Category == FormatCategory.Audio
|| descriptor is IAudioContainerFormat
|| AudioAdapterResolver.ResolvePcmSource(descriptor) is not null
|| AudioAdapterResolver.ResolvePcmTarget(descriptor) is not null
|| AudioAdapterResolver.ResolveDemuxSource(descriptor) is not null
|| AudioAdapterResolver.ResolveMuxTarget(descriptor) is not null;
}
207 changes: 207 additions & 0 deletions Compression.Lib/AudioConversionOperation.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
using Codec.Pcm;
using Compression.Registry;
using FileFormat.Wav;

namespace Compression.Lib;

/// <summary>
/// Capability-driven audio conversion. Routes the least destructive path first:
/// byte-exact passthrough, encoded packet remux, canonical PCM transcode, then the
/// legacy per-channel WAV pseudo-archive bridge.
/// </summary>
public static class AudioConversionOperation {

public static void Convert(
Stream input,
string sourceFormatId,
Stream output,
string targetFormatId,
FormatCreateOptions? options = null
) {
ArgumentNullException.ThrowIfNull(sourceFormatId);
ArgumentNullException.ThrowIfNull(targetFormatId);

FormatRegistry.Initialize();
var source = FormatRegistry.GetById(sourceFormatId)
?? throw new ArgumentException($"Unknown source format '{sourceFormatId}'.", nameof(sourceFormatId));
var target = FormatRegistry.GetById(targetFormatId)
?? throw new ArgumentException($"Unknown target format '{targetFormatId}'.", nameof(targetFormatId));
Convert(input, source, output, target, options);
}

public static void Convert(
Stream input,
IFormatDescriptor source,
Stream output,
IFormatDescriptor target,
FormatCreateOptions? options = null
) {
ArgumentNullException.ThrowIfNull(input);
ArgumentNullException.ThrowIfNull(source);
ArgumentNullException.ThrowIfNull(output);
ArgumentNullException.ThrowIfNull(target);
options ??= new FormatCreateOptions();

var outputCodecExplicitlyRequested =
!string.IsNullOrWhiteSpace(options.MethodName) || options.HasOption("codec");
if (source.Id.Equals(target.Id, StringComparison.OrdinalIgnoreCase) && !outputCodecExplicitlyRequested) {
Rewind(input);
input.CopyTo(output);
return;
}

var demux = AudioAdapterResolver.ResolveDemuxSource(source);
var mux = AudioAdapterResolver.ResolveMuxTarget(target);
if (demux is not null && mux is not null) {
Rewind(input);
if (demux.TryDemux(input, out var encoded) && encoded is not null &&
mux.SupportedMuxCodecs.Contains(encoded.Format.CodecId, StringComparer.OrdinalIgnoreCase) &&
mux.CanMux(encoded.Format, options, out _)) {
mux.Mux(output, encoded, options);
return;
}
}

var pcmTarget = AudioAdapterResolver.ResolvePcmTarget(target);
if (pcmTarget is not null) {
AudioPcmBuffer? pcm = null;
if (AudioAdapterResolver.ResolvePcmSource(source) is { } pcmSource) {
Rewind(input);
pcm = pcmSource.DecodePcm(input);
} else if (TryDecodePseudoArchivePcm(input, source, out var bridgedPcm)) {
pcm = bridgedPcm;
}

if (pcm is not null) {
var codec = ResolveCodec(pcmTarget, options);
if (!pcmTarget.CanEncode(pcm.Format, codec, options, out var reason))
throw new NotSupportedException(
$"{target.Id} cannot encode {pcm.Format.Channels}ch/{pcm.Format.SampleRate}Hz/" +
$"{pcm.Format.BitsPerSample}-bit PCM as '{codec}': {reason ?? "unsupported combination"}.");
pcmTarget.EncodePcm(output, pcm, codec, options);
return;
}
}

if (TryPseudoArchiveBridge(input, source, output, target, options))
return;

throw new NotSupportedException(
$"No audio conversion route exists from '{source.Id}' to '{target.Id}'. " +
"The source must expose encoded packets or PCM/channels and the target must expose a compatible mux/encode/create capability.");
}

private static string ResolveCodec(IAudioPcmTarget target, FormatCreateOptions options) {
if (!string.IsNullOrWhiteSpace(options.MethodName)) return options.MethodName;
var explicitCodec = options.GetOption("codec", string.Empty);
if (!string.IsNullOrWhiteSpace(explicitCodec)) return explicitCodec;
if (target.SupportedEncodeCodecs.Count == 0)
throw new NotSupportedException("The target advertises no audio encoder codecs.");
return target.SupportedEncodeCodecs[0];
}

private static bool TryDecodePseudoArchivePcm(
Stream input,
IFormatDescriptor source,
out AudioPcmBuffer? pcm
) {
pcm = null;
if (source is not IArchiveFormatOperations sourceArchive) return false;

Rewind(input);
var listed = sourceArchive.List(input, password: null);
var entries = listed
.Where(static entry => entry.Kind.Equals("Channel", StringComparison.OrdinalIgnoreCase) &&
entry.Name.EndsWith(".wav", StringComparison.OrdinalIgnoreCase))
.OrderBy(static entry => entry.Index)
.ToArray();

if (entries.Length == 0 && source.Id.Equals("Wav", StringComparison.OrdinalIgnoreCase)) {
var full = listed.FirstOrDefault(static entry =>
entry.Name.Equals("FULL.wav", StringComparison.OrdinalIgnoreCase));
if (full is not null) entries = [full];
}
if (entries.Length == 0) return false;

var decoded = new List<WavReader.ParsedWav>(entries.Length);
foreach (var entry in entries) {
Rewind(input);
var bytes = sourceArchive.ExtractEntryToMemory(input, entry.Name, password: null);
decoded.Add(new WavReader().Read(bytes));
}

if (decoded.Count == 1 && decoded[0].NumChannels > 1) {
var only = decoded[0];
if (only.FormatCode is not (1 or 3)) return false;
pcm = new AudioPcmBuffer(
new AudioPcmFormat(
only.SampleRate,
only.NumChannels,
only.BitsPerSample,
only.FormatCode == 3 ? AudioPcmEncoding.IeeeFloat
: only.BitsPerSample == 8 ? AudioPcmEncoding.UnsignedInteger
: AudioPcmEncoding.SignedInteger),
only.InterleavedPcm);
return true;
}

var first = decoded[0];
if (first.NumChannels != 1 || first.FormatCode is not (1 or 3)) return false;
if (decoded.Any(channel => channel.NumChannels != 1 || channel.FormatCode != first.FormatCode ||
channel.BitsPerSample != first.BitsPerSample || channel.SampleRate != first.SampleRate ||
channel.InterleavedPcm.Length != first.InterleavedPcm.Length))
return false;

var interleaved = PcmCodec.Interleave(decoded.Select(static channel => channel.InterleavedPcm).ToList(), first.BitsPerSample);
pcm = new AudioPcmBuffer(
new AudioPcmFormat(
first.SampleRate,
decoded.Count,
first.BitsPerSample,
first.FormatCode == 3 ? AudioPcmEncoding.IeeeFloat
: first.BitsPerSample == 8 ? AudioPcmEncoding.UnsignedInteger
: AudioPcmEncoding.SignedInteger),
interleaved);
return true;
}

private static bool TryPseudoArchiveBridge(
Stream input,
IFormatDescriptor source,
Stream output,
IFormatDescriptor target,
FormatCreateOptions options
) {
if (source is not IArchiveFormatOperations sourceArchive ||
AudioAdapterResolver.ResolvePseudoArchiveTarget(target) is not { } targetCreate)
return false;

Rewind(input);
var entries = sourceArchive.List(input, password: null)
.Where(static entry => entry.Kind.Equals("Channel", StringComparison.OrdinalIgnoreCase) &&
entry.Name.EndsWith(".wav", StringComparison.OrdinalIgnoreCase))
.OrderBy(static entry => entry.Index)
.ToArray();
if (entries.Length == 0) return false;

var inputs = new List<ArchiveInputInfo>(entries.Length);
foreach (var entry in entries) {
Rewind(input);
var bytes = sourceArchive.ExtractEntryToMemory(input, entry.Name, password: null);
inputs.Add(ArchiveInputInfo.InMemory(entry.Name, bytes));
}

if (target is IArchiveWriteConstraints constraints)
foreach (var archiveInput in inputs)
if (!constraints.CanAccept(archiveInput, out var reason))
throw new NotSupportedException(
$"{target.Id} rejected converted channel '{archiveInput.ArchiveName}': {reason ?? "unsupported input"}.");

targetCreate.Create(output, inputs, options);
return true;
}

private static void Rewind(Stream stream) {
if (stream.CanSeek) stream.Position = 0;
}
}
Loading
Loading