Repository: naudio/NAudio (2026-06-30T20:15:27Z)

Issue:
SampleProvider AIFF 8-bit PCM audio data signedness
AIFF files with 8-bit PCM audio data are read incorrectly by NAudio. Audio bytes seem to be read as *unsigned* 8-bit integers, but they should be read as *signed* 8-bit integers.

The test file [aiff-samplesize-8.aiff](https://github.com/karip/toisto-aiff-test-suite/blob/main/tests/aiff/aiff-samplesize-8.aiff) contains 8-bit PCM audio data and it's raw audio data starts with these hex values: 0x0A, 0x0A, 0x0A, 0x0A, 0x0A, 0x0A, 0x0A, 0x0A, 0x80, 0x83, 0x85, 0x88, 0x8A, 0x8D, 0x8F, 0x92

Those hex values can be converted to signed decimal values [-128, 127]: 10, 10, 10, 10, 10, 10, 10, 10, -128, -125, -123, -120, -118, -115, -113, -110

Those values can be converted to floating point values [-1.0, 1.0] by dividing by 128: 0.078125, 0.078125, 0.078125, 0.078125, 0.078125, 0.078125, 0.078125, 0.078125, -1, -0.9765625,  -0.9609375, -0.9375, -0.921875, -0.8984375, -0.8828125, -0.859375

However, reading the file with NAudio SampleProvider returns these floating point values: -0.921875, -0.921875, -0.921875, -0.921875, -0.921875, -0.921875, -0.921875, -0.921875, 0, 0.0234375, 0.0390625, 0.0625, 0.078125, 0.1015625, 0.1171875, 0.140625

So, it seems that NAudio isn't reading the values correctly.

I had a look at the NAudio source code. It is using [SampleProviderConverters:22](https://github.com/naudio/NAudio/blob/f8568cd4ad20c0683389cc9bc878beb945528047/NAudio.Core/Wave/SampleProviders/SampleProviderConverters.cs#L22C34-L22C44) to convert raw audio data to samples. [Pcm8BitToSampleProvider:33](https://github.com/naudio/NAudio/blob/f8568cd4ad20c0683389cc9bc878beb945528047/NAudio.Core/Wave/SampleProviders/Pcm8BitToSampleProvider.cs#L33) converts bytes to floating point values: `buffer[outIndex++] = sourceBuffer[n] / 128f - 1.0f;`

It should be converting bytes to floating point values like this: `buffer[outIndex++] = sourceBuffer[n] < 128 ? sourceBuffer[n] / 128f : (sourceBuffer[n] / 128f) - 2.0f;`

Of course, Pcm8BitToSampleProvider.cs can't be changed, because then it would return incorrect values for WAV files, which contain unsigned 8-bit data.



Discussion:
I think this could be fixed by converting 8-bit bytes from signed to unsigned in [AiffFileReader](https://github.com/naudio/NAudio/blob/f8568cd4ad20c0683389cc9bc878beb945528047/NAudio.Core/Wave/WaveStreams/AiffFileReader.cs#L238). It already performs endianness conversion for AIFF files, so it could also perform signedness conversion.
Thanks for reporting, I've finally got round to resolving this. Let me know if there are any further issues



Patch:
diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md
index b3ba69b61..b20c9093b 100644
--- a/RELEASE_NOTES.md
+++ b/RELEASE_NOTES.md
@@ -74,6 +74,7 @@ extensive correctness work during development — see [Docs/Sampler.md](Docs/Sam
  * `WasapiRecorder`, `WasapiCapture` and `WasapiLoopbackCapture`: a capture device removed mid-recording (unplugged, or the default device changed) no longer crashes the process with an unhandled exception on the capture thread — the teardown `Stop`/`Reset` calls that themselves fail on the dead device are now best-effort, so `RecordingStopped` always fires with the originating exception and the recorder cleans up correctly (#672)
  * `AcmInterop`: serialised all `msacm32` P/Invokes process-wide, fixing process-killing access violations under concurrent ACM use
  * `WaveFileReader` / `AiffFileReader`: malformed headers declaring `BlockAlign=0` now throw `InvalidDataException` from the constructor instead of `DivideByZeroException` later (#1254); `WaveFileReader` no longer throws on an oversized `fmt` `cbSize` (#482)
+ * `AiffFileReader` / `AiffFileWriter`: 8-bit PCM is now read and written as signed two's-complement per the AIFF spec, instead of being mishandled as unsigned (WAV-style) — fixing DC-shifted/garbled 8-bit AIFF playback (#1178)
  * `WaveFileReader`: a `data` chunk declaring a bogus/oversized length (e.g. FFmpeg's `0xFFFF1000` placeholder when writing to a non-seekable pipe) is now clamped to the bytes actually present, so reading from a `MemoryStream` no longer throws and the reported length reflects the available audio (#1090)
  * `WaveFormat.Serialize`: PCM formats now write the canonical 16-byte `fmt ` chunk (#934, #1098)
  * `WaveViewer`: fixed rendering upside-down (#801, #818) and now renders any source format correctly via `ToSampleProvider()` (#564)
diff --git a/src/NAudio.Core/Wave/WaveOutputs/AiffFileWriter.cs b/src/NAudio.Core/Wave/WaveOutputs/AiffFileWriter.cs
index c8a9e6325..8a59cdd99 100644
--- a/src/NAudio.Core/Wave/WaveOutputs/AiffFileWriter.cs
+++ b/src/NAudio.Core/Wave/WaveOutputs/AiffFileWriter.cs
@@ -209,7 +209,16 @@ public override void Write(byte[] data, int offset, int count)
 
         if (bytesPerSample <= 1)
         {
-            outStream.Write(data, offset, count);
+            // AIFF 8-bit PCM is signed two's-complement, but the incoming bytes are unsigned
+            // (WAV-style, like every other path through Write, which converts WAV layout to
+            // AIFF layout). Flip the sign bit on the way out so the file is valid signed AIFF.
+            // Copy into a scratch buffer so the caller's array is not mutated. See issue #1178.
+            byte[] signedData = new byte[count];
+            for (int i = 0; i < count; i++)
+            {
+                signedData[i] = (byte)(data[offset + i] ^ 0x80);
+            }
+            outStream.Write(signedData, 0, count);
         }
         else
         {
diff --git a/src/NAudio.Core/Wave/WaveStreams/AiffFileReader.cs b/src/NAudio.Core/Wave/WaveStreams/AiffFileReader.cs
index 0fa542b49..72678f297 100644
--- a/src/NAudio.Core/Wave/WaveStreams/AiffFileReader.cs
+++ b/src/NAudio.Core/Wave/WaveStreams/AiffFileReader.cs
@@ -245,7 +245,14 @@ public override int Read(Span<byte> buffer)
             switch (WaveFormat.BitsPerSample)
             {
                 case 8:
-                    // no swap required
+                    // AIFF 8-bit PCM is signed two's-complement, whereas the shared
+                    // Pcm8BitToSampleProvider (and WAV) treat 8-bit as unsigned. There is no
+                    // endianness to swap, but flipping the sign bit converts the signed source
+                    // byte to the unsigned value the downstream converter expects. See issue #1178.
+                    for (int i = 0; i < read.Length; i++)
+                    {
+                        read[i] ^= 0x80;
+                    }
                     break;
                 case 16:
                     for (int i = 0; i < read.Length; i += bytesPerSample)


Test patch:
diff --git a/tests/NAudio.Core.Tests/Aiff/AiffFileWriterTests.cs b/tests/NAudio.Core.Tests/Aiff/AiffFileWriterTests.cs
index c048bed70..2f7f934d6 100644
--- a/tests/NAudio.Core.Tests/Aiff/AiffFileWriterTests.cs
+++ b/tests/NAudio.Core.Tests/Aiff/AiffFileWriterTests.cs
@@ -145,6 +145,68 @@ public void StreamConstructorLeavesCallerSuppliedStreamOpen()
         Assert.That(buffer, Is.EqualTo(sourceData));
     }
 
+    // AIFF 8-bit PCM is signed; the bytes handed to Write are unsigned (WAV-style), so the
+    // writer must flip the sign bit. Asserting on the raw on-disk SSND bytes (rather than a
+    // round-trip, where the reader's matching flip would cancel the bug out). See #1178.
+    [Test]
+    public void Write8BitStoresSignedSamplesOnDisk()
+    {
+        var ms = new MemoryStream();
+        var format = new WaveFormat(8000, 8, 1);
+        var unsignedInput = new byte[] { 0x80, 0x8A, 0x00, 0xFF }; // 128 (silence), 138, 0, 255
+
+        using (var writer = new AiffFileWriter(new IgnoreDisposeStream(ms), format))
+        {
+            writer.Write(unsignedInput, 0, unsignedInput.Length);
+        }
+
+        var onDisk = ExtractSsndSoundData(ms.ToArray());
+        Assert.That(onDisk, Is.EqualTo(new byte[] { 0x00, 0x0A, 0x80, 0x7F })); // unsigned ^ 0x80
+    }
+
+    [Test]
+    public void Write8BitDoesNotMutateCallerBuffer()
+    {
+        var ms = new MemoryStream();
+        var format = new WaveFormat(8000, 8, 1);
+        var input = new byte[] { 0x80, 0x8A, 0x00, 0xFF };
+        var original = (byte[])input.Clone();
+
+        using (var writer = new AiffFileWriter(new IgnoreDisposeStream(ms), format))
+        {
+            writer.Write(input, 0, input.Length);
+        }
+
+        Assert.That(input, Is.EqualTo(original));
+    }
+
+    [Test]
+    public void EightBitRoundTripsThroughWriterAndReader()
+    {
+        var format = new WaveFormat(8000, 8, 1);
+        var unsigned = new byte[] { 0x00, 0x40, 0x80, 0xC0, 0xFF, 0x8A };
+
+        var roundTripped = WriteAndRead(format, writer => writer.Write(unsigned, 0, unsigned.Length));
+
+        Assert.That(roundTripped, Is.EqualTo(unsigned));
+    }
+
+    // Locates the SSND chunk's sound data within a written AIFF file.
+    private static byte[] ExtractSsndSoundData(byte[] aiff)
+    {
+        for (int i = 0; i + 16 <= aiff.Length; i++)
+        {
+            if (aiff[i] == (byte)'S' && aiff[i + 1] == (byte)'S' &&
+                aiff[i + 2] == (byte)'N' && aiff[i + 3] == (byte)'D')
+            {
+                int ckSize = (aiff[i + 4] << 24) | (aiff[i + 5] << 16) | (aiff[i + 6] << 8) | aiff[i + 7];
+                int dataLength = ckSize - 8; // minus the offset(4) + blockSize(4) fields
+                return aiff.AsSpan(i + 16, dataLength).ToArray();
+            }
+        }
+        throw new InvalidOperationException("SSND chunk not found");
+    }
+
     private static byte[] WriteAndRead(WaveFormat format, Action<AiffFileWriter> writeAction)
     {
         var ms = new MemoryStream();
diff --git a/tests/NAudio.Core.Tests/Aiff/AiffReaderTests.cs b/tests/NAudio.Core.Tests/Aiff/AiffReaderTests.cs
index ab24f0bd0..0866de186 100644
--- a/tests/NAudio.Core.Tests/Aiff/AiffReaderTests.cs
+++ b/tests/NAudio.Core.Tests/Aiff/AiffReaderTests.cs
@@ -1,6 +1,8 @@
 ﻿using System;
 using NUnit.Framework;
 using System.IO;
+using System.Text;
+using NAudio.Utils;
 using NAudio.Wave;
 using System.Diagnostics;
 
@@ -31,14 +33,14 @@ public void ConvertAiffToWav()
 
     private static void ConvertAiffToWav(string aiffFile, string wavFile)
     {
-        using var reader = new AiffFileReader(aiffFile);
-        using var writer = new WaveFileWriter(wavFile, reader.WaveFormat);
-        byte[] buffer = new byte[4096];
-        int bytesRead = 0;
-        do
-        {
-            bytesRead = reader.Read(buffer, 0, buffer.Length);
-            writer.Write(buffer, 0, bytesRead);
+        using var reader = new AiffFileReader(aiffFile);
+        using var writer = new WaveFileWriter(wavFile, reader.WaveFormat);
+        byte[] buffer = new byte[4096];
+        int bytesRead = 0;
+        do
+        {
+            bytesRead = reader.Read(buffer, 0, buffer.Length);
+            writer.Write(buffer, 0, bytesRead);
         } while (bytesRead > 0);
     }
 
@@ -89,4 +91,66 @@ public void MalformedAiffWithTruncatedSsndDoesNotThrowOnRead()
             while (reader.Read(buffer, 0, buffer.Length) > 0) { }
         });
     }
+
+    // Regression: AIFF stores 8-bit PCM as signed two's-complement (unlike WAV's unsigned
+    // 8-bit), so the reader must convert it for the shared unsigned 8-bit sample converter.
+    // The on-disk bytes and expected float values below are from the issue report. See #1178.
+    [Test]
+    [Category("UnitTest")]
+    public void Read8BitPcmTreatsSamplesAsSigned()
+    {
+        byte[] signedSoundData =
+        {
+            0x0A, 0x0A, 0x0A, 0x0A, 0x0A, 0x0A, 0x0A, 0x0A,
+            0x80, 0x83, 0x85, 0x88, 0x8A, 0x8D, 0x8F, 0x92
+        };
+        float[] expected =
+        {
+            0.078125f, 0.078125f, 0.078125f, 0.078125f, 0.078125f, 0.078125f, 0.078125f, 0.078125f,
+            -1f, -0.9765625f, -0.9609375f, -0.9375f, -0.921875f, -0.8984375f, -0.8828125f, -0.859375f
+        };
+
+        var aiff = BuildAiff(44100, channels: 1, bitsPerSample: 8, soundData: signedSoundData);
+
+        using var reader = new AiffFileReader(new MemoryStream(aiff));
+        var samples = reader.ToSampleProvider();
+        var actual = new float[signedSoundData.Length];
+        int read = samples.Read(actual);
+
+        Assert.That(read, Is.EqualTo(expected.Length));
+        for (int i = 0; i < expected.Length; i++)
+        {
+            Assert.That(actual[i], Is.EqualTo(expected[i]).Within(1e-6f), $"sample {i}");
+        }
+    }
+
+    // Builds a minimal uncompressed AIFF (FORM/COMM/SSND) with the supplied sound data
+    // placed verbatim, so a test can control the exact on-disk bytes.
+    private static byte[] BuildAiff(int sampleRate, short channels, short bitsPerSample, byte[] soundData)
+    {
+        int numSampleFrames = soundData.Length / (channels * (bitsPerSample / 8));
+        using var ms = new MemoryStream();
+        var bw = new BinaryWriter(ms);
+
+        void Tag(string s) => bw.Write(Encoding.ASCII.GetBytes(s));
+        void WriteBE16(short v) { bw.Write((byte)(v >> 8)); bw.Write((byte)v); }
+        void WriteBE32(int v)
+        {
+            bw.Write((byte)(v >> 24)); bw.Write((byte)(v >> 16)); bw.Write((byte)(v >> 8)); bw.Write((byte)v);
+        }
+
+        const int commSize = 18;                 // channels(2) + frames(4) + sampleSize(2) + rate(10)
+        int ssndSize = 8 + soundData.Length;     // offset(4) + blockSize(4) + data
+        int formSize = 4 + (8 + commSize) + (8 + ssndSize);
+
+        Tag("FORM"); WriteBE32(formSize); Tag("AIFF");
+        Tag("COMM"); WriteBE32(commSize);
+        WriteBE16(channels); WriteBE32(numSampleFrames); WriteBE16(bitsPerSample);
+        bw.Write(IEEE.ConvertToIeeeExtended(sampleRate));   // 10-byte 80-bit extended
+        Tag("SSND"); WriteBE32(ssndSize); WriteBE32(0); WriteBE32(0);
+        bw.Write(soundData);
+        bw.Flush();
+
+        return ms.ToArray();
+    }
 }