Skip to main content

The IWAS file format

Before we can export an IWAS song into our project, first we need to understand it's file format.

The IWAS file format was made under the following goals:

  • Size: it must fit into WASM-4's limited 1KB (1024 bytes) of disk space normally used for save games.
  • Simplicity: it must be reasonably easy to understand it's structure for anyone to be able to manipulate the data.
  • Implementor-friendly: it must be easy to anyone to be able to implement a driver for it.

Influence of these goals can be seen on various aspects of it's format:

  • Songs are limited to a maximum of 192 notes (12 pages), equally divided to all channels.
  • Each channel has it's own track, each one indepedent from each other, and stored sequentially.
  • All structures are fixed-size, so they can always be found on the same offsets.
  • All data is uncompressed.

It's important to point out, however, that it was not designed with efficiency in mind. For that, things like compression, trimming out unused channels, or splitting into tracks will have to be included later.

That said, the file structure for IWAS can be seen below.

Endianess#

IWAS makes heavy use of AssemblyScript's DataView, which, just like it's JavaScript counterpart, defaults to big-endian values.

Therefore, with the exception of the lookup tables, all u16 and u32 properties are stored in the big-endian order:

  • A single u32 value is dedicated to the "IWAS" magic header.
  • Some editor-specific settings (e.g. note offsets, scroll) and the frequency for channel's instruments (property frq2) are stored as u16.

Adjusting frq2 to the correct endianess is important to make it sound right. For this, a simple function can be made for conversion:

/** * Load `u16` value (big-endian). *  * @param offset Address offset */function loadUint16BE(offset: usize): u16 {    /** High byte. */    const hi: u16 = u16(load<u8>(offset));
    /** Low byte. */    const lo: u16 = u16(load<u8>(offset + 1));
    return (hi * 0x100) + lo;}

Since lookup tables aren't stored in the file, they can be ordered in any way. There's no requirement for that.

Lookup tables#

IWAS uses 2 lookup tables for it's editor:

  • The note lookup table, which includes all the 96 possible tones it can be played.
  • The speed lookup table, which is used for it's internal delay counter when playing a song.

Note Lookup Table#

Includes all the 96 possible values used in the editor. A more detailed (and complete) list can be seen (here)[https://www.liutaiomottola.com/formulae/freqtab.htm].

Since 0 is used to represent a empty value, the note lookup table is one-indexed.

NoteFrequency (Hz)
116
217
318
419
520
621
723
824
925
1027
1129
1230
1332
1434
1536
1638
1741
1843
1946
2049
2151
2255
2358
2461
2565
2669
2773
2877
2982
3087
3192
3298
33103
34110
35116
36123
37130
38138
39146
40155
41164
42174
43185
44196
45207
46220
47233
48246
49261
50277
51293
52311
53329
54349
55369
56392
57415
58440
59466
60493
61523
62554
63587
64622
65659
66698
67739
68783
69830
70880
71932
72987
731046
741108
751174
761244
771318
781396
791479
801567
811661
821760
831864
841975
852093
862217
872349
882489
892637
902793
912959
923135
933322
943520
953729
963951

This lookup table is also available below as an JSON array.

[      16,   17,   18,   19,   20,   21,   23,   24,      25,   27,   29,   30,   32,   34,   36,   38,      41,   43,   46,   49,   51,   55,   58,   61,      65,   69,   73,   77,   82,   87,   92,   98,     103,  110,  116,  123,  130,  138,  146,  155,     164,  174,  185,  196,  207,  220,  233,  246,     261,  277,  293,  311,  329,  349,  369,  392,     415,  440,  466,  493,  523,  554,  587,  622,     659,  698,  739,  783,  830,  880,  932,  987,    1046, 1108, 1174, 1244, 1318, 1396, 1479, 1567,    1661, 1760, 1864, 1975, 2093, 2217, 2349, 2489,    2637, 2793, 2959, 3135, 3322, 3520, 3729, 3951]

Speed lookup table#

When playing a music, IWAS uses a hardcoded preset of 11 values to determine it's speed.

The speed lookup table is zero-indexed.

SpeedValue
0.1%0
10%2
20%4
30%6
40%8
50%10
60%20
70%30
80%40
90%60
100%80

The delay is controlled by a simple counter, which will count down each frame. Once it reaches zero, the next note will be played, and the counter is restarted with one of the values from the table shown above.

An example of how IWAS speed cycles are controlled can be seen in the code below.

const lookup = [0, 2, 4, 6, 8, 10, 20, 30, 40, 60, 80];let speed = 0;let counter = 0;
function update() {    counter = counter > 0? (counter - 1): 0;
    if(counter === 0) {        play();        counter = lookup[speed];    }}

This lookup table is also available below as an JSON array.

[0, 2, 4, 6, 8, 10, 20, 30, 40, 60, 80]

Header#

Size: 32 bytes total.

  • The length property is defined by pages instead of notes. To get the actual note number, multiply it by 16.

Below is the structure of the header.

Location (hex)PropertyTypeCategoryDescription
0000magicu32HeaderThe "IWAS" magic number: 0x_49_57_41_53
0004versionu16HeaderVersion
0006lengthu8Editor settingMusic length, in pages (up to 12 pages)
0007speedu8Editor settingMusic speed
0008del_modeboolEditor settingSwitch between add/remove mode
0009auto_loopboolEditor settingAuto loop music
000Aauto_scrollboolEditor settingAuto scroll when playing music
000Bpreview_notesboolEditor settingPlay a "preview" of a note after adding it
000Cgrid_yu16Application stateVertical grid scroll
000Egrid_hpageu8Application stateCurrent horizontal page
000Funused[u8; 17]ReservedUnused space (reserved for future use)

Channels#

Size: 224 bytes total:

  • 32 bytes for header.
  • 192 bytes for music.

Since all channels have the same size, they can always be found in their respective offsets.

Location (hex)Channel
0020Channel 0 (pulse)
0100Channel 1 (pulse)
01E0Channel 2 (triangle)
02C0Channel 3 (noise)

For quick reference, each channel will be listed below.

Channel 0 (pulse)#

Location (hex)PropertyTypeCategoryDescription
0020frq2u16Main channelFrequency #2 (frequency #1 uses note lookup table)
0022atku8Main channelADSR attack
0023decu8Main channelADSR decay
0024susu8Main channelADSR sustain
0025relu8Main channelADSR release
0026peaku8Main channelPeak
0027volu8Main channelVolume
0028modeu8Main channelDuty cycle mode
0029shadow_frq2u16Shadow channelFrequency #2 (frequency #1 uses note lookup table)
002Bshadow_atku8Shadow channelADSR attack
002Cshadow_decu8Shadow channelADSR decay
002Dshadow_susu8Shadow channelADSR sustain
,002Eshadow_relu8Shadow channelADSR release
002Fshadow_peaku8Shadow channelPeak
0030shadow_volu8Shadow channelVolume
0031shadow_modeu8Shadow channelDuty cycle mode
0032unused[u8; 8]ReservedUnused space (reserved for future use)
0033offset_xi8Editor-specific settingsHorizontal offset misplacement
003Boffset_yi8Editor-specific settingsVertical offset misplacement
003Cshadow_enabledboolEditor-specific settingsShadow channel editing
003Dshow_linesboolEditor-specific settingsConnect added notes with lines
003Eis_enabledboolEditor-specific settingsEnable/disable the channel
003Fedit_anywhereboolEditor-specific settingsAdd/remove notes from anywhere
0040notes[i8; 192]MusicNote data (192 notes)

Channel 1 (pulse)#

Location (hex)PropertyTypeCategoryDescription
0100frq2u16Main channelFrequency #2 (frequency #1 uses note lookup table)
0102atku8Main channelADSR attack
0103decu8Main channelADSR decay
0104susu8Main channelADSR sustain
0105relu8Main channelADSR release
0106peaku8Main channelPeak
0107volu8Main channelVolume
0108modeu8Main channelDuty cycle mode
0109shadow_frq2u16Shadow channelFrequency #2 (frequency #1 uses note lookup table)
010Bshadow_atku8Shadow channelADSR attack
010Cshadow_decu8Shadow channelADSR decay
010Dshadow_susu8Shadow channelADSR sustain
,010Eshadow_relu8Shadow channelADSR release
010Fshadow_peaku8Shadow channelPeak
0110shadow_volu8Shadow channelVolume
0111shadow_modeu8Shadow channelDuty cycle mode
0112unused[u8; 8]ReservedUnused space (reserved for future use)
0113offset_xi8Editor-specific settingsHorizontal offset misplacement
011Boffset_yi8Editor-specific settingsVertical offset misplacement
011Cshadow_enabledboolEditor-specific settingsShadow channel editing
011Dshow_linesboolEditor-specific settingsConnect added notes with lines
011Eis_enabledboolEditor-specific settingsEnable/disable the channel
011Fedit_anywhereboolEditor-specific settingsAdd/remove notes from anywhere
0120notes[i8; 192]MusicNote data (192 notes)

Channel 2 (triangle)#

Location (hex)PropertyTypeCategoryDescription
01E0frq2u16Main channelFrequency #2 (frequency #1 uses note lookup table)
01E2atku8Main channelADSR attack
01E3decu8Main channelADSR decay
01E4susu8Main channelADSR sustain
01E5relu8Main channelADSR release
01E6peaku8Main channelPeak
01E7volu8Main channelVolume
01E8modeu8Main channelDuty cycle mode
01E9shadow_frq2u16Shadow channelFrequency #2 (frequency #1 uses note lookup table)
01EBshadow_atku8Shadow channelADSR attack
01ECshadow_decu8Shadow channelADSR decay
01EDshadow_susu8Shadow channelADSR sustain
,01EEshadow_relu8Shadow channelADSR release
01EFshadow_peaku8Shadow channelPeak
01F0shadow_volu8Shadow channelVolume
01F1shadow_modeu8Shadow channelDuty cycle mode
01F2unused[u8; 8]ReservedUnused space (reserved for future use)
01F3offset_xi8Editor-specific settingsHorizontal offset misplacement
01FBoffset_yi8Editor-specific settingsVertical offset misplacement
01FCshadow_enabledboolEditor-specific settingsShadow channel editing
01FDshow_linesboolEditor-specific settingsConnect added notes with lines
01FEis_enabledboolEditor-specific settingsEnable/disable the channel
01FFedit_anywhereboolEditor-specific settingsAdd/remove notes from anywhere
0200notes[i8; 192]MusicNote data (192 notes)

Channel 3 (noise)#

Location (hex)PropertyTypeCategoryDescription
02C0frq2u16Main channelFrequency #2 (frequency #1 uses note lookup table)
02C2atku8Main channelADSR attack
02C3decu8Main channelADSR decay
02C4susu8Main channelADSR sustain
02C5relu8Main channelADSR release
02C6peaku8Main channelPeak
02C7volu8Main channelVolume
02C8modeu8Main channelDuty cycle mode
02C9shadow_frq2u16Shadow channelFrequency #2 (frequency #1 uses note lookup table)
02CBshadow_atku8Shadow channelADSR attack
02CCshadow_decu8Shadow channelADSR decay
02CDshadow_susu8Shadow channelADSR sustain
,02CEshadow_relu8Shadow channelADSR release
02CFshadow_peaku8Shadow channelPeak
02D0shadow_volu8Shadow channelVolume
02D1shadow_modeu8Shadow channelDuty cycle mode
02D2unused[u8; 8]ReservedUnused space (reserved for future use)
02D3offset_xi8Editor-specific settingsHorizontal offset misplacement
02DBoffset_yi8Editor-specific settingsVertical offset misplacement
02DCshadow_enabledboolEditor-specific settingsShadow channel editing
02DDshow_linesboolEditor-specific settingsConnect added notes with lines
02DEis_enabledboolEditor-specific settingsEnable/disable the channel
02DFedit_anywhereboolEditor-specific settingsAdd/remove notes from anywhere
02E0notes[i8; 192]MusicNote data (192 notes)

Instruments#

Each channel has 2 instruments.

For simplicity, IWAS refers to them as if they were separate "channels", with each one having their own counterparts:

  • The main channel: primary setting. Their notes are displayed as light in the editor.
  • The shadow channel: secondary setting. Their notes are displayed as dark/outlined in the editor.

These channels can be "mixed" together on the same channel. And because they use the same channel, they can't occupy the same space, and therefore one will interrupt the other.

The channel structure includes all but 2 properties for the tone:

  • Frequency #1: must use the one in the note lookup table, with an index given by the notes.
  • Pan: must be assigned to 0.

Notes#

All 4 channels will have the same note count (up to 192).

Each note is a signed 8-bit value, and it will range from -96 to 96, or -128 if it's a note break. When a music player iterates through each note, the following conditions are expected to happen:

  • If equals -128, then it will mute the channel using a note break.
  • If greater than zero, then it will play a tone using the main channel.
  • If lower than zero, then it will play a tone using the shadow channel.
  • If equals zero, it will do nothing.

Each index corresponds to a frequency stored in the note lookup table. Negative and positive indexes should all point to the same frequency. For instance: indexes -1 and 1 would both point to note index 1 (16Hz).

Note breaks#

A note break is a special mark: when playing a music, it will cut off any previously sound being played on a given channel. Each channel has their own independent note breaks and must not interfere with each other.

It's important to note that, although they are displayed as light in the editor, note breaks are neutral and have no preference for either channel. that is, whenever a cursor hits a note from a main channel or a shadow channel, it must mute it regardless.

On IWAS, note breaks are not technically considered a note, and thus won't move up/down on selection mode.

Parsing the file#

Given how it works, we should now be able to write a parser for it. This can be useful in case we want to improve or manipulate the data directly, or convert it to other formats.

Below is a parser written in vanilla HTML/CSS/JS. It can take the contents of a .disk file and convert it to JSON.

<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /><title>IWAS disk exporter</title><style>:root {    --color-primary: #04080c;    --color-secondary: #fafafa;    --color-link: #0095e9;}
html, body {    margin: 0;    padding: 0;    width: 100%;    height: 100%;    background-color: var(--color-primary);    color: var(--color-secondary);    font-family: Verdana, Geneva, Tahoma, sans-serif;}
a {    color: var(--color-link);}
.container {    display: flex;    justify-content: center;    align-items: center;    width: 100%;    height: 100%;}
.section {    display: flex;    justify-content: center;    align-items: center;    width: 320px;    height: 320px;}
.disk-import {    border: 2px solid;    border-color: var(--color-secondary);    border-style: dashed;    user-select: none;    margin: 2em;}
.disk-import-form {    text-align: center;}
.disk-export {    margin: 1em;}
.export-result {    width: 100%;    height: 100%;    color: var(--color-secondary);    background-color: transparent;}</style></head><body><div class="container" id="disk-container">    <div class="section disk-import">        <div class="disk-import-form">            <label>                Select disk file(s) to import                <input id="disk-file" type="file" />            </label>            <a id="disk-file-clear" href="javascript:void(0)">clear</a>        </div>    </div>    <div class="section disk-export">        <textarea            id="disk-json"            class="export-result"            autocomplete="off"            autocorrect="off"            autocapitalize="off"            spellcheck="false"            readonly="readonly"        ></textarea>    </div></div><script>/** The "IWAS" magic number. */const IWAS_MAGIC = 0x49574153;
/** IWAS version number. */const IWAS_VERSION = 0x0001;
/** * Note lookup table. * Not needed for conversion, but added here for reference. */const IWAS_NOTE_LOOKUP = [    16,   17,   18,   19,   20,   21,   23,   24,    25,   27,   29,   30,   32,   34,   36,   38,    41,   43,   46,   49,   51,   55,   58,   61,    65,   69,   73,   77,   82,   87,   92,   98,   103,  110,  116,  123,  130,  138,  146,  155,   164,  174,  185,  196,  207,  220,  233,  246,   261,  277,  293,  311,  329,  349,  369,  392,   415,  440,  466,  493,  523,  554,  587,  622,   659,  698,  739,  783,  830,  880,  932,  987,  1046, 1108, 1174, 1244, 1318, 1396, 1479, 1567,  1661, 1760, 1864, 1975, 2093, 2217, 2349, 2489,  2637, 2793, 2959, 3135, 3322, 3520, 3729, 3951];
/** * Speed lookup table * Not needed for conversion, but added here for reference. */const IWAS_SPEED_LOOKUP = [0, 2, 4, 6, 8, 10, 20, 30, 40, 60, 80];
/** * Convert IWAS disk to an object. *  * @param {ArrayBuffer} data Data. * @returns  */function iwas2object(data) {    /** Data view. */    const view = new DataView(data);
    /** The "IWAS" magic number. */    const magic = view.getUint32(0);
    /** IWAS version number. */    const version = view.getUint16(4);
    // Reject invalid headers:    if(magic !== IWAS_MAGIC && version !== IWAS_VERSION) {        throw new Error("Invalid IWAS header and/or unsupported version.");    }
    /** IWAS header. */    const header = {        magic,        version,        length: view.getUint8(6),        speed: view.getUint8(7),        del_mode: Boolean(view.getUint8(8)),        auto_loop: Boolean(view.getUint8(9)),        auto_scroll: Boolean(view.getUint8(10)),        preview_notes: Boolean(view.getUint8(11)),        grid_y: view.getUint16(12),        grid_hpage: view.getUint16(14)    };
    /** Channel data array. */    const channels = [];
    // Iterate through all four channels...    for(let i = 0; i < 4; i += 1) {        const unused = 8;        const offset = 32 + (i * 224);
        /** Channel data. */        const channel = {            frq2: view.getUint16(offset),            atk: view.getUint8(offset + 1),            dec: view.getUint8(offset + 2),            sus: view.getUint8(offset + 3),            rel: view.getUint8(offset + 4),            peak: view.getUint8(offset + 5),            vol: view.getUint8(offset + 6),            mode: view.getUint8(offset + 7),            shadow_frq2: view.getUint16(offset),            shadow_atk: view.getUint8(offset + 1),            shadow_dec: view.getUint8(offset + 2),            shadow_sus: view.getUint8(offset + 3),            shadow_rel: view.getUint8(offset + 4),            shadow_peak: view.getUint8(offset + 5),            shadow_vol: view.getUint8(offset + 6),            shadow_mode: view.getUint8(offset + 7),            offset_x: view.getUint8(offset + unused),            offset_y: view.getUint8(offset + unused + 1),            shadow_enabled: Boolean(view.getUint8(offset + unused + 2)),            show_lines: Boolean(view.getUint8(offset + unused + 3)),            is_enabled: Boolean(view.getUint8(offset + unused + 4)),            edit_anywhere: Boolean(view.getUint8(offset + unused + 5)),            notes: []        };
        /** Channel note data. */        const notes = (new Int8Array(view.buffer)).slice(offset + 32, offset + 32 + 192);
        channel.notes.push(...notes);        channels.push(channel);    }
    return {        header,        channels    };}
/** Disk import section. */const disk = {    container: document.querySelector("#disk-container"),    input: document.querySelector("#disk-file"),    clear: document.querySelector("#disk-file-clear"),    output: document.querySelector("#disk-json")};
disk.clear.addEventListener("click", () => {    disk.input.value = null;    disk.output.value = "";});
// Listen for files...disk.input.addEventListener("change", async (event) => {        /** @type {FileList} User selected files. */    const fileList = event.target.files;
    // Return if there are no files:    if(fileList.length < 1) {        return;    }
    /** Result list. */    const result = [];        // Convert files into array buffers...    try {        for(let i = 0; i < fileList.length; i += 1) {            const file = fileList[i];            const buffer = await file.arrayBuffer();            const data = iwas2object(buffer);
            result.push(data);        }
        disk.output.value = JSON.stringify(result, null, 4);    }
    // Display errors on text box...    catch(error) {        disk.output.value = error;        throw error;    }});</script></body></html>