Recursive AI

Convert AIFF to WAV in C#

All onemedia recipes

Media.Remux("in.aiff", "out.wav") writes the PCM audio of an AIFF file as a WAV file. AIFF stores samples big-endian and WAV little-endian, so each sample’s bytes are swapped; nothing else changes: no decoding, no new bit depth or sample rate. The same call takes the PCM, A-law or µ-law track of a CAF, MOV, MP4, MKV or W64 file. WAV writing is new in 0.6.0-preview.1, and past 4 GiB the file becomes RF64 by itself.

Terminal
dotnet new console -o AiffToWav && cd AiffToWav
dotnet add package RecursiveAI.OneMedia --version 0.6.0-preview.1
ffmpeg -v error -f lavfi -i sine=f=440:d=3:r=48000 -ac 2 -c:a pcm_s24be -metadata title="Test tone" tone.aiff

tone.aiff is three seconds of a 440 Hz tone, 24-bit stereo at 48 kHz, with a title. Put this in Program.cs and run dotnet run:

C#
using RecursiveAI.OneMedia;
using RecursiveAI.OneMedia.Remuxing;
 
string input = args.Length > 0 ? args[0] : "tone.aiff";
string output = args.Length > 1 ? args[1] : "tone.wav";
RemuxReport report = Media.Remux(input, output);   // .wav: WAVE, RF64 past 4 GiB
 
foreach (RemuxTrackReport track in report.Tracks)
    if (track.OutputTrack >= 0)
        Console.WriteLine($"track {track.InputTrack} -> {track.OutputTrack}: {track.Packets} packets copied");
foreach (RemuxLoss loss in report.Losses)
    Console.WriteLine($"not carried: {loss.Kind} {loss.Subject}: {loss.Reason}");
foreach (string change in report.Changes)
    Console.WriteLine($"changed: {change}");
Console.WriteLine($"{report.Mux.BytesWritten:N0} bytes written");
 
using MediaFile wav = Media.Open(output);
Console.WriteLine($"{wav.Format}: {wav.Audio?.Codec}, {wav.Audio?.Audio?.SampleRate} Hz, {wav.Audio?.Audio?.Channels} channels, {wav.Duration}, title \"{wav.Tags.Title}\"");
Text
track 0 -> 0: 212 packets copied
changed: tags carried through the common view (1 edits, 0 pictures)
880,488 bytes written
wav: pcm, 48000 Hz, 2 channels, 00:00:03, title "Test tone"

The title travels through the common tag view into the WAV’s LIST INFO chunk. 24-bit stereo is written as WAVE_FORMAT_EXTENSIBLE, the record WAV uses for samples wider than 16 bits.

Check it with ffmpeg

The samples decoded from both files are the same:

Terminal
ffprobe -v error -show_entries stream=codec_name,sample_rate,channels,bits_per_sample:format=format_name,duration:format_tags=title -of compact tone.wav
for f in tone.aiff tone.wav; do echo "$f $(ffmpeg -v error -i $f -f s32le - | md5sum)"; done
Text
stream|codec_name=pcm_s24le|sample_rate=48000|channels=2|bits_per_sample=24
format|format_name=wav|duration=3.000000|tag:title=Test tone
tone.aiff e2631706b9a3b2520948316282c4988b  -
tone.wav e2631706b9a3b2520948316282c4988b  -

A browser can play the result where it couldn’t play the input: headless Chrome 149 refused tone.aiff and played tone.wav.

Limits

  • PCM in, PCM out. In the words of the WAV writing page: “Nothing is decoded, converted between widths or rates, or encoded.” A track in another codec with no WAV format record is refused. To get WAV from MP3, AAC or Opus, decode it: Extract the audio from a video does that with OneAudio.
  • One track, seekable output. From the README: “one track, and the output must be able to seek”. The sizes in the header are written at the end, so a pipe can’t be the output.
  • Broadcast Wave. bext, cart, iXML and LIST INFO chunks are written by WavMuxer with WavMuxOptions, and the chunks of a WAV or RF64 input travel unchanged; to edit the tags of an existing WAV, see Edit MP4, MKV and MP3 tags.
  • Big files. Past 4 GiB the header is rewritten as RF64 in place, without moving a sample byte; set the layout in WavMuxOptions to refuse that or to write RF64 or BW64 from the start.