Binary Streams – With a Musical Twist

In this video, I’ll show how to deal with binary data inside Business Central.

https://youtu.be/T6ozwpd3qig

In this video, Erik demonstrates how to work with binary streams in Business Central’s AL language by creating something unexpected — a WAV audio file generated entirely from AL code. While not the most practical application of binary streams, it’s a creative and educational way to understand how binary data works at a low level, including byte ordering, data type limitations, and stream manipulation.

Why a WAV File?

In a previous video, Erik covered text streams in Business Central. Many viewers requested a follow-up on binary streams. Rather than going with the typical example of generating an image file, Erik decided to take a more creative route: generating a WAV audio file directly from Business Central. As he puts it, this “probably qualifies as a hack” since nobody ever imagined Business Central producing sound files.

Understanding the WAV File Format

A WAV file is a binary format with three distinct chunks:

  1. RIFF Chunk (12 bytes) — A header that identifies the file as a WAV file so programs can recognize the format.
  2. Format Chunk — Describes the audio format (sample rate, bit depth, number of channels, etc.).
  3. Data Chunk — Contains the actual audio sample data.

Each chunk contains fields of varying sizes — some are 4 bytes, some are 2 bytes — and the byte order matters.

Big Endian vs. Little Endian

When dealing with binary data, it’s critical to understand byte ordering. Different computer architectures store multi-byte values differently:

  • Big Endian — The most significant byte comes first. The hexadecimal value CAFEBABE is stored as CA FE BA BE.
  • Little Endian — The least significant byte comes first. The same value is stored as BE BA FE CA.

When creating binary files, you must match the byte order specified by the file format. WAV files, living in the Intel x86 world, use little-endian byte ordering for numeric fields (while the chunk identifiers like “RIFF” and “WAVE” are stored as ASCII strings in big-endian order).

Writing Binary Data with OutStream.Write

The key to creating binary files in AL is the distinction between OutStream.Write and OutStream.WriteText:

  • Write — Writes the binary representation of a value. An integer writes exactly 4 bytes.
  • WriteText — Writes human-readable text (used in the previous streams video).

For example, the “RIFF” identifier at the start of a WAV file has a specific integer representation. The decimal value 1179011410 corresponds to the four ASCII bytes R I F F. By writing this integer to the stream, you produce exactly the right four bytes in the file.

The 16-Bit Problem in Business Central

The WAV format requires several 16-bit (2-byte) values — for the audio format type, number of channels, and bits per sample. However, Business Central’s AL language doesn’t have a 16-bit integer data type. You have:

  • Byte — 8 bits (1 byte)
  • Integer — 32 bits (4 bytes)

There’s nothing in between. Erik handles this in two ways:

Hack for the Header: Padding with Null Bytes

For simple header values like the audio format (which is just 1), Erik writes a single byte with the value and then writes a null byte (zero) to pad it out to 16 bits. This works because the value fits in 8 bits, and the second byte is just zero.

The Integer-to-Short Conversion for Audio Data

For the actual audio sample data — where values can use the full 16-bit range — a more sophisticated approach is needed. Erik uses a TempBlob as an intermediary stream to slice an integer into its component bytes:

local procedure IntegerToShort(Frequency: Integer; var B1: Byte; var B2: Byte)
var
    TempBlob: Codeunit "Temp Blob";
    InS: InStream;
    OutS: OutStream;
begin
    TempBlob.CreateOutStream(OutS);
    OutS.Write(Frequency);  // Write 4 bytes into the memory stream

    TempBlob.CreateInStream(InS);
    InS.Read(B1);  // Read first byte out
    InS.Read(B2);  // Read second byte out
    // The remaining two bytes (zeros for small values) are discarded
end;

The technique is elegant: write the 4-byte integer into a TempBlob via an OutStream, then read back only the first 2 bytes via an InStream. The last two bytes are simply abandoned — once the function returns, the TempBlob goes out of scope and the memory is freed.

Building the Musical Scale

Erik creates a dictionary mapping musical note names to their frequencies in hertz:

  • C — 261 Hz
  • C# through B — ascending chromatic scale
  • Up to three octaves (each octave doubles the frequency)

The audio is generated as a simple sine wave. Thanks to the Math codeunit now available in Business Central, the Sin function can be used to calculate sample values at a 44.1 kHz sample rate — the same quality as a CD.

The Catch-22: Data Length Before Data

The WAV header requires you to specify the size of the audio data before you write the actual data. This is a common challenge with binary file formats. Erik solves this with two TempBlobs:

  1. Music TempBlob — First, generate all the audio data into this temporary buffer.
  2. Wave TempBlob — Then, write the header (which now knows the data length from the music TempBlob), and use CopyStream to pump the music data after the header.

Finally, a download is triggered using DownloadFromStream, which has its own built-in pump to send the complete WAV file to the browser.

The Generate Function Flow

The overall flow of the generate function works like this:

  1. Set up constants: samples per beat, sample rate (44,100 Hz).
  2. Build the musical scale dictionary.
  3. Write individual notes into the music TempBlob using the AddTone procedure, specifying note name and duration as a fraction of a beat.
  4. Create the wave TempBlob, write the WAV header (now that the music length is known).
  5. Copy the music data from the music TempBlob into the wave TempBlob using CopyStream.
  6. Create an InStream from the wave TempBlob and call DownloadFromStream.

Source Code Reference

The provided source code in the repository shows a page extension on the Customer List that triggers the generation on OnOpenPage. While the specific WAV generation code wasn’t included in the source files, the project also demonstrates other binary stream techniques, such as converting hexadecimal strings to byte arrays using TempBlob streams:

local procedure GetKeyTextFromHex(KeyHex: Text) KeyText: Text
var
    TempBlob: Codeunit "Temp Blob";
    base64: Codeunit "Base64 Convert";
    OutS: OutStream;
    InS: InStream;
    i: Integer;
    b: Byte;
    l: Integer;
    HexByte: Text[2];
begin
    TempBlob.CreateOutStream(OutS);
    l := StrLen(KeyHex);
    for i := 1 to l - 2 do begin
        HexByte := CopyStr(KeyHex, i, 2);
        b := GetByteFromHexByte(HexByte);
        i += 1;
        OutS.Write(b);
    end;
    TempBlob.CreateInStream(InS);
    exit(base64.ToBase64(InS));
end;

This follows the same pattern: use TempBlob as an intermediary, write bytes in via OutStream, and read them back via InStream in whatever format you need.

Demonstration Results

When running the code, Business Central generates a WAV file of approximately 600 kilobytes and triggers a download. The resulting file plays back the musical notes as sine waves. By changing the duration fractions in the AddTone calls, different rhythms can be produced — Erik demonstrated changing values to create a swing-like rhythm, proving the code was genuinely generating audio on the fly.

Key Takeaways

  • OutStream.Write writes the raw binary representation of AL data types — this is the foundation of binary file creation.
  • TempBlob (backed by a .NET MemoryStream) serves as a versatile in-memory buffer for binary data manipulation.
  • Byte ordering matters — when creating binary files, you must match the endianness specified by the format.
  • AL lacks a 16-bit integer type, but you can work around this by writing a 32-bit integer to a TempBlob and reading back only the bytes you need.
  • CopyStream acts as a pump to transfer data between streams, which is essential when you need to compose a file from multiple parts.
  • When a file format requires a data length in the header, generate the data first into a separate TempBlob so you can calculate the length before writing the header.

While generating WAV files from Business Central is admittedly not an everyday use case, the techniques demonstrated — binary writing, byte slicing, stream composition, and working around data type limitations — are directly applicable to any scenario where you need to create or parse binary file formats in AL.