0
0

Hi everyone,

I used readData to read audio files and generate peak files for wave form display. It works great with 16-bit files, but I’m having some trouble reading 24-bit files PCM values.

First, what is the block size for 24-bit?

I know 16-bit signed values ranges from -32768 to +32768 and 24-bit ranges from -8388607 to +8388607.

The block size is 4096 bytes for 16-bit files (65536 / 16 = 4096). If I do the same calculation with 24-bit, 16777215 / 24 = 699050.625 bytes. From what I understand, I have to use 32-bit variables to store the 24-bit value. But what block size should I use when reading the file? 699051? How do I adjust the conversion to float arrays?

Here is the full C# code I’m using to generate peak files for 16-bit PCM data. I’ve left the 24-bit code empty on purpose since it doesn’t work. Some code references my own FMOD wrapper but it should be simple to understand.

[code:3o0npy05] // Declare variables
FMOD.RESULT result = FMOD.RESULT.OK;
FileStream fileStream = null;
BinaryWriter binaryWriter = null;
GZipStream gzipStream = null;
bool generatePeakFile = false;
int CHUNKSIZE = 0;
uint length = 0;
uint read = 0;
uint bytesread = 0;
Int16[] left16BitArray = null;
Int16[] right16BitArray = null;
Int32[] left32BitArray = null;
Int32[] right32BitArray = null;
float[] floatLeft = null;
float[] floatRight = null;
byte[] buffer = null;
IntPtr data = new IntPtr(); // initialized properly later
WaveDataMinMax minMax = null;

        try
        {
            // Set current file directory
            m_peakFileDirectory = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location) + "\\Peak Files\\";

            // Get file name from argument
            string fileName = (string)e.Argument;

            // Create sound system with NOSOUND
            MPfm.Sound.System soundSystem = new MPfm.Sound.System(FMOD.OUTPUTTYPE.NOSOUND, string.Empty);

            // Create sound
            MPfm.Sound.Sound sound = soundSystem.CreateSound(fileName, false);

            // Get sound format; specifically bits per sample (changes the calculations later)
            SoundFormat soundFormat = sound.GetSoundFormat();

            // Get the length of the file in PCM bytes               
            sound.BaseSound.getLength(ref length, FMOD.TIMEUNIT.PCMBYTES);

            // Check if the folder for peak files exists
            if (!Directory.Exists(PeakFileDirectory))
            {
                // Create directory
                Directory.CreateDirectory(PeakFileDirectory);
            }

            // Generate the file name for the peak file by using the full path without special characters
            string peakFilePath = PeakFileDirectory + fileName.Replace(@"\", "_").Replace(":", "_").Replace(".", "_") + ".mpfmPeak";

            // Check if peak file exists                
            if(!File.Exists(peakFilePath))
            {
                // Set flag
                generatePeakFile = true;

                // Create peak file
                fileStream = new FileStream(peakFilePath, FileMode.Create, FileAccess.Write);
                binaryWriter = new BinaryWriter(fileStream);
                gzipStream = new GZipStream(fileStream, CompressionMode.Compress);                   
            }

            // Check the bits per sample to determine what chunk size to get                
            if (soundFormat.BitsPerSample == 16)
            {
                // 4096 bytes for 16-bit PCM data
                CHUNKSIZE = 4096;
            }
            else if (soundFormat.BitsPerSample == 24)
            {
                // 699050.625 bytes for 24-bit PCM data (???)   
                CHUNKSIZE = 699051;
            }

            // Create buffer
            data = Marshal.AllocHGlobal(CHUNKSIZE);
            buffer = new byte[CHUNKSIZE];

            // Loop through file using chunk size
            do
            {
                // Check for cancel
                if (m_workerWaveForm.CancellationPending)
                {
                    return;
                }

                // Check the bits per sample
                if (soundFormat.BitsPerSample == 16)
                {
                    // Read data chunk (4096 bytes for 16-bit PCM data)
                    result = sound.BaseSound.readData(data, (uint)CHUNKSIZE, ref read);
                    Marshal.Copy(data, buffer, 0, CHUNKSIZE);
                    bytesread += read;

                    // Is freehglobal needed? it crashes after one use.
                    //Marshal.FreeHGlobal(data);

                    // Convert the byte (8-bit) arrays into a short (16-bit) arrays (signed values)
                    left16BitArray = new Int16[buffer.Length / 4];
                    right16BitArray = new Int16[buffer.Length / 4];

                    // Loop through byte (8-bit) array buffer; increment by 4 (i.e. 4 times more data in 16-bit than 8-bit)
                    for (int i = 0; i < buffer.Length; i = i + 4)
                    {
                        // Convert values to 16-bit
                        left16BitArray[i / 4] = BitConverter.ToInt16(buffer, i);
                        right16BitArray[i / 4] = BitConverter.ToInt16(buffer, i + 2); // alternate between left and right channel
                    }

                    // Convert the short arrays to float arrays (signed values)
                    // This will convert the -32768 to 32768 value range to -1 to 1 (useful for wave display) 
                    floatLeft = new float[left16BitArray.Length];
                    floatRight = new float[left16BitArray.Length];
                    for (int i = 0; i < left16BitArray.Length; i++)
                    {
                        // 16-bit data for unsigned values range from 0 to 65536.
                        floatLeft[i] = left16BitArray[i] / 65536.0f;
                        floatRight[i] = right16BitArray[i] / 65536.0f;                            
                    }
                }
                else if (soundFormat.BitsPerSample == 24)
                {
                   // (non-working code removed)

                    // (I have no idea if this works) Convert the short arrays to float arrays (signed values)
                    // This will convert the -8388608 to 8388608value range to -1 to 1 (useful for wave display) 
                    floatLeft = new float[left32BitArray.Length];
                    floatRight = new float[left32BitArray.Length];
                    for (int i = 0; i < left32BitArray.Length; i++)
                    {
                        // 16-bit data for unsigned values range from 0 to 16777215.
                        floatLeft[i] = left32BitArray[i] / 16777215.0f;
                        floatRight[i] = right32BitArray[i] / 16777215.0f;                            
                    }
                }

                // Calculate min/max
                minMax = AudioTools.GetMinMaxFromWaveData(floatLeft, floatRight, false);
                WaveDataHistory.Add(minMax);

                // Report progress
                m_bytesRead = bytesread;
                m_totalBytes = length;
                m_percentageDone = ((float)bytesread / (float)length) * 100;

                // Write peak information to hard disk
                if (generatePeakFile)
                {
                    // Write peak information
                    binaryWriter.Write((double)minMax.leftMin);
                    binaryWriter.Write((double)minMax.leftMax);
                    binaryWriter.Write((double)minMax.rightMin);
                    binaryWriter.Write((double)minMax.rightMax);
                    binaryWriter.Write((double)minMax.mixMin);
                    binaryWriter.Write((double)minMax.mixMax);
                }                  
            }
            while (result == FMOD.RESULT.OK && read == CHUNKSIZE);

            // Release sound from memory
            sound.Release();

            // Close sound system and release from memory
            soundSystem.Close();
            soundSystem.Release();

            // Set nulls for garbage collection               
            sound = null;
            soundSystem = null;
            left16BitArray = null;
            right16BitArray = null;
            left32BitArray = null;
            right32BitArray = null;
            floatLeft = null;
            floatRight = null;                
            buffer = null;
            minMax = null;
        }
        catch (Exception ex)
        {
            throw ex;
        }
        finally
        {
            // Did we have to generate a peak file?
            if (generatePeakFile)
            {
                // Close writer and stream
                gzipStream.Close();
                binaryWriter.Close();                   
                fileStream.Close();

                // Set nulls
                gzipStream = null;
                binaryWriter = null;
                fileStream = null;
            }
        }

        // Call garbage collector
        GC.Collect();[/code:3o0npy05]

Here is the method that extracts the min/max values from float arrays:
[code:3o0npy05] /// <summary>
/// This method takes the left channel and right channel wave raw data and analyses it to get
/// the maximum and minimum values in the float structure. It returns a data structure named
/// WaveDataMinMax (see class description for more information). Negative values can be converted to
/// positive values before min and max comparaison. Set this parameter to true for output meters and
/// false for wave form display controls.
/// </summary>
/// <param name="waveDataLeft">Raw wave data (left channel)</param>
/// <param name="waveDataRight">Raw wave data (right channel)</param>
/// <param name="convertNegativeToPositive">Convert negative values to positive values (ex: true when used for output meters,
/// false when used with wave form display controls (since the negative value is used to draw the bottom end of the waveform).<</param>
/// <returns>WaveDataMinMax data structure</returns>
public static WaveDataMinMax GetMinMaxFromWaveData(float[] waveDataLeft, float[] waveDataRight, bool convertNegativeToPositive)
{
// Create default data
WaveDataMinMax data = new WaveDataMinMax();

        // Loop through values to get min/max
        for (int i = 0; i &lt; waveDataLeft.Length; i++)
        {
            // Set values to compare
            float left = waveDataLeft[i];
            float right = waveDataRight[i];

            // Do we have to convert values before comparaison?
            if (convertNegativeToPositive)
            {
                // Compare values, if negative then remove negative sign
                if (left &lt; 0)
                {
                    left = -left;
                }
                if (right &lt; 0)
                {
                    right = -right;
                }
            }

            // Calculate min/max for left channel
            if (left &lt; data.leftMin)
            {
                data.leftMin = left;
            }
            if (left &gt; data.leftMax)
            {
                data.leftMax = left;
            }

            // Calculate min/max for right channel
            if (right &lt; data.rightMin)
            {
                data.rightMin = right;
            }
            if (right &gt; data.rightMax)
            {
                data.rightMax = right;
            }

            // Calculate min/max mixing both channels
            if (left &lt; data.mixMin)
            {
                data.mixMin = left;
            }
            if (right &lt; data.mixMin)
            {
                data.mixMin = right;
            }
            if (left &gt; data.mixMax)
            {
                data.mixMax = left;
            }
            if (right &gt; data.mixMax)
            {
                data.mixMax = right;
            }
        }

        return data;
    }[/code:3o0npy05]

Thanks a lot to anyone who can help me, and I also hope other forum users can learn from my sample.

  • You must to post comments
0
0

Here is the code to take the buffer to 32-bit integer arrays:

[code:1lt3zulb] left32BitArray = new Int32[buffer.Length / 6];
right32BitArray = new Int32[buffer.Length / 6];
for (int i = 0; i < buffer.Length; i = i + 6)
{
// Create smaller array in order to add the 4th 8-bit value
byte[] byteArrayLeft = new byte[4] {buffer[i], buffer[i + 1], buffer[i + 2], 0 };
byte[] byteArrayRight = new byte[4] { buffer[i + 3], buffer[i + 4], buffer[i + 5], 0 };

                            // Convert values to 32-bit variables
                            left32BitArray[i / 6] = BitConverter.ToInt32(byteArrayLeft, 0);
                            right32BitArray[i / 6] = BitConverter.ToInt32(byteArrayRight, 0);
                    }[/code:1lt3zulb]
  • You must to post comments
0
0

I have made progress on this, if anyone cares. My comments were very wrong when describing the buffer conversion to 16-bit integer values.

In fact, if I understood it well, the 16-bit PCM data is stored using two bytes per channel (i.e. 2 x 8-bit words) and alternates left and right channels. That makes 2×8-bit for left channel, 2×8-bit for right channel, for every sample.

24-bit PCM data is stored using three bytes per channel (i.e. 3 x 8-bit words), and also alternates channels. Since there are no native 24-bit integer variables, a 32-bit integer must be used, by adding the 0 value for the fourth and last 8-bit word.

This logic works perfectly. Someone on StackOverflow gave me a formula to calculate the block size:
[code:1deal6ih]int blockSize = 3 * channels * (sampleRate / 10);[/code:1deal6ih]
The 3 constant represents the 3 bytes data per channel/sample. Thus 2 must be used for 16-bit data.

[b:1deal6ih]Here is the extreme values problem:[/b:1deal6ih]

Now that I understand how it works, I have another problem with readData and 24-bit FLAC files. When the peak file is generated, it gives a flat wave form instead of something that makes sense. When I look at the buffer coming out of readData at 24-bit, it seems to give extreme values around 0 or 16777215. That would explain why the waveform is flat.

Anyone has an idea why? Thanks, and I hope this also helps other people. I’ll post the final code once it works perfectly and the comments are accurate :)

  • You must to post comments
Showing 2 results
Your Answer

Please first to submit.