From 706edd91dc8ee7658784ed4ea7c50aaf15fa657c Mon Sep 17 00:00:00 2001 From: MrRafael-dev <94740791+MrRafael-dev@users.noreply.github.com> Date: Tue, 28 May 2024 10:56:05 -0300 Subject: [PATCH] Added a new tutorial: making music with IWAS Added a new tutorial in the category that explains how to make music using IWAS. Includes explanation in how to import/export disk files, the file format used by the editor, how to write a simple parser, and how to write a simple sequencer using it. --- .../tutorials/iwas/finishing-the-sequencer.md | 274 +++++++ .../tutorials/iwas/implementing-the-driver.md | 264 +++++++ site/docs/tutorials/iwas/introduction.md | 17 + .../tutorials/iwas/the-iwas-file-format.md | 679 ++++++++++++++++++ site/sidebars.js | 18 + 5 files changed, 1252 insertions(+) create mode 100644 site/docs/tutorials/iwas/finishing-the-sequencer.md create mode 100644 site/docs/tutorials/iwas/implementing-the-driver.md create mode 100644 site/docs/tutorials/iwas/introduction.md create mode 100644 site/docs/tutorials/iwas/the-iwas-file-format.md diff --git a/site/docs/tutorials/iwas/finishing-the-sequencer.md b/site/docs/tutorials/iwas/finishing-the-sequencer.md new file mode 100644 index 00000000..a3afeeb9 --- /dev/null +++ b/site/docs/tutorials/iwas/finishing-the-sequencer.md @@ -0,0 +1,274 @@ +# Finishing the sequencer + +Here's the full code explained above, with a small song file included: + +```typescript +import * as w4 from "./wasm4"; + +/** Music data. */ +export const data: usize = memory.data([ + 0x49, 0x57, 0x41, 0x53, 0x00, 0x01, 0x03, 0x02, 0x00, 0x00, 0x00, 0x01, + 0x01, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x05, 0x05, 0x00, 0x64, 0x01, 0x00, 0x00, 0x00, 0x00, 0x05, 0x0F, 0x00, + 0x64, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x01, 0x01, 0x00, 0x2B, 0x2B, 0x2B, 0x00, 0x00, 0x2B, 0x2B, 0x2B, + 0x00, 0x2B, 0x2B, 0x2E, 0x2E, 0x29, 0x29, 0x00, 0x00, 0x00, 0x30, 0x30, + 0x30, 0x30, 0x00, 0x30, 0x30, 0x00, 0x30, 0x30, 0x00, 0x30, 0x30, 0x00, + 0x30, 0x30, 0x00, 0x30, 0x30, 0x00, 0x2E, 0x2E, 0x00, 0x2B, 0x2B, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x0F, 0x00, 0x64, + 0x02, 0x00, 0x00, 0x00, 0x00, 0x05, 0x0F, 0x00, 0x64, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFB, 0x00, 0x01, 0x01, 0x00, + 0x26, 0x26, 0x26, 0x00, 0x00, 0x26, 0x26, 0x26, 0x00, 0x26, 0x26, 0x29, + 0x29, 0x24, 0x24, 0x00, 0x00, 0x00, 0x2B, 0x2B, 0x2B, 0x2B, 0x00, 0x2B, + 0x2B, 0x00, 0x2B, 0x2B, 0x00, 0x2B, 0x2B, 0x00, 0x2B, 0x2B, 0x00, 0x2B, + 0x2B, 0x00, 0x29, 0x29, 0x00, 0x26, 0x26, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x05, 0x0F, 0x00, 0x64, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x05, 0x0F, 0x00, 0x64, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x01, 0x00, 0x21, 0x21, 0x21, 0x21, + 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x24, 0x24, 0x24, 0x24, 0x24, + 0x24, 0x24, 0x24, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, + 0x1F, 0x00, 0x00, 0x00, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x21, 0x21, 0x21, + 0x21, 0x21, 0x21, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, + 0x05, 0x00, 0x00, 0x64, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x0F, 0x00, + 0x64, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x01, 0x01, 0x00, 0x24, 0x00, 0x00, 0x24, 0x00, 0x00, 0x2E, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x24, 0x00, 0x00, 0x24, 0x00, 0x2E, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x24, 0x00, 0x00, 0x24, 0x00, 0x00, 0x2E, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x24, 0x00, 0x00, 0x24, 0x00, 0x00, 0x2E, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00]); + +/** Note lookup table. */ +const IWAS_NOTE_LOOKUP: usize = memory.data([ + 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. */ +const IWAS_SPEED_LOOKUP: usize = memory.data([ + 0, 2, 4, 6, 8, 10, 20, 30, 40, 60, 80 +]); + +/** Speed counter. */ +let counter: u8 = 0; + +/** Note cursor. */ +let cursor: u8 = 0; + +/** + * Load `u16` value (big-endian). + * + * @param offset Address offset + */ +function loadUint16BE(offset: usize): u16 { + /** High byte. */ + const hi: u16 = u16(load(offset)); + + /** Low byte. */ + const lo: u16 = u16(load(offset + 1)); + + return (hi * 0x100) + lo; +} + +/** + * Play music. + */ +function play(): void { + // Countdown delay... + counter = counter > 0? counter - 1: 0; + + // Return if it's not ready to play yet... + if(counter > 0) { + return; + } + + // When it reaches zero, it will iterate through each channel to play + // every note... + for(let ichannel: u8 = 0; ichannel < 4; ichannel += 1) { + /** Channel data offset. */ + const offset: usize = data + 32 + (224 * ichannel); + + /** Note value. */ + const note: i8 = load(offset + 32 + (cursor % 192)); + + /** Check if channel is enabled. */ + const isEnabled: bool = load(offset + 30); + + // Ignore if is not enabled... + if(!isEnabled) { + return; + } + + // Notes with a value of -128 represent a note break. + // + // Note breaks will mute a specific channel if it's still + // playing a tone. + if(note === -128) { + w4.tone(0, 0, 0, ichannel); + continue; + } + + // Notes ranging from -96 and 96 are valid tones. + // + // Positive notes will use the main channel, while + // negative notes will use the shadow channel. + // + // Anything else, or 0, can be ignored. + else if(note !== 0 && note >= -96 && note <= 96) { + /** Instrument offset: + * - Main channel if positive. + * - Shadow channel if negative. + */ + const instrument: usize = offset + (note > 0? 0: 9); + + /** + * Note index for the note lookup table. + * + * If negative, it's value will be mirrored. + * For instance, `-1` will become `1`. + */ + const inote: u8 = u8(Math.abs(note)) - 1; + + // Get frequency from note lookup table... + const frq1: u32 = (load(IWAS_NOTE_LOOKUP + (inote * 2))); + + + + // Get instrument data. + // + // A positive note index should point to the main channel, and + // a negative note index should point to the shadow channel. + // + // `frq2` is a big-endian value, so it must be converted. + // + // Values are converted to `u32` in order to better fit into the + // bitwise operations needed for the `tone` function to work. + const frq2: u32 = u32(loadUint16BE(instrument)); + const atk: u32= u32(load(instrument + 2)); + const dec: u32 = u32(load(instrument + 3)); + const sus: u32 = u32(load(instrument + 4)); + const rel: u32 = u32(load(instrument + 5)); + const peak: u32 = u32(load(instrument + 6)); + const vol: u32 = u32(load(instrument + 7)); + const mode: u32 = u32(load(instrument + 8)); + const pan: u32 = 0; + + // Play tone: + w4.tone( + frq1 | (frq2 << 16), + (atk << 24) | (dec << 16) | sus | (rel << 8), + (peak << 8) | vol, + ichannel | (mode << 2) | (pan << 4) + ); + } + } + + /** Music length. + * + * To get the actual note count, the page number + * can be multiplied by 16. + */ + const length: u8 = load(data + 6) * 16; + + /** Speed index for the speed lookup table. */ + const ispeed: u8 = load(data + 7); + + /** Speed value. */ + const speed: u8 = load(IWAS_SPEED_LOOKUP + ispeed); + + // Advance counter and cursor... + counter = speed; + cursor = cursor < length? cursor + 1: 0; +} + +/** + * WASM-4 update event. + */ +export function update(): void { + play(); + w4.text(`counter: ${counter}\nCursor: ${cursor}`, 0, 0); +} +``` + +And with that, we have a way of making music for WASM-4 using nothing but a small sound tool! + +## Optional challenges + +Here's some optional challenges you can take in order to make this a little more efficient: + - Parse and export it to your own music formats. + - Use a compression/decompression algorithm to reduce size. + - Make a longer music by chaining multiple files together. + - Break a song into patterns and tracks to reduce size. + +If you're already familiar with a Digital Audio Workstation (DAW), you can also look for +a more advanced tool, like (w4on2)[https://github.com/JerwuQu/w4on2]. + +Keep in mind, however, that no matter which tool you use, there might not be any driver +available for the language you want to use. So consider understanding the format first, +then learn how to implement your own driver if necessary (use a pre-existing driver as a reference). \ No newline at end of file diff --git a/site/docs/tutorials/iwas/implementing-the-driver.md b/site/docs/tutorials/iwas/implementing-the-driver.md new file mode 100644 index 00000000..7d8fd5c0 --- /dev/null +++ b/site/docs/tutorials/iwas/implementing-the-driver.md @@ -0,0 +1,264 @@ +# Implementing a driver + +**For this guide, we will use the 1KB binary file directly.** + +To get a byte array, you can either use an exporter, a command line tool, or (a web tool)[https://notisrac.github.io/FileToCArray/]. + +## Getting started + +Once we have our file converted into a byte array, we should have it added on our code: + +## Lookup tables and variables + +```typescript +/** Music data. */ +const data: usize = memory.data([ + // 1024 bytes... +]); +``` + +IWAS uses lookup tables for notes and speed values, so we can start including those, too: + +```typescript +/** Note lookup table. */ +const IWAS_NOTE_LOOKUP: usize = memory.data([ + 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. */ +const IWAS_SPEED_LOOKUP: usize = memory.data([ + 0, 2, 4, 6, 8, 10, 20, 30, 40, 60, 80 +]); +``` + +We will also require at least 2 variables to control the music: + - **A speed counter** to handle the speed. + - **A cursor** which will point to the next note that should be played. + +```typescript +/** Speed counter. */ +let counter: u8 = 0; + +/** Note cursor. */ +let cursor: u8 = 0; +``` + +And we also know that IWAS stores `u16` and `u32` values as big-endian. +We won't be using the header for anything in this guide, so converting `u32` +to the right order can be ignored. + +The `frq2` property used by the instruments, however, is important, so we're adding +one small function to correct it to the right order later on: + +```typescript +/** + * Load `u16` value (big-endian). + * + * @param offset Address offset + */ +function loadUint16BE(offset: usize): u16 { + /** High byte. */ + const hi: u16 = u16(load(offset)); + + /** Low byte. */ + const lo: u16 = u16(load(offset + 1)); + + return (hi * 0x100) + lo; +} +``` + +## Declaring the function + +Let's make a `play` function, which will be responsible for playing our music. + +We can also figure what we need at the moment: + - Make the countdown timer. + - Check if it's `0`. + - When it's ready to play a note, it will apply for all channels. + +And this is what we have: + +```typescript +/** + * Play music. + */ +function play(): void { + // Countdown delay... + counter = counter > 0? counter - 1: 0; + + // Return if it's not ready to play yet... + if(counter > 0) { + return; + } + + // When it reaches zero, it will iterate through each channel to play + // every note... + for(let ichannel: u8 = 0; ichannel < 4; ichannel += 1) { + // ... + } +} +``` + +## Offsets and notes + +Next, we'll need to figure out a few things: + - The offset for each channel. + - The note value. + +The header is 32 bytes, and each channel has 224 bytes, which means we can get their offsets on a loop simply using `32 + (224 * ichannel)`. The same thing applies with the note offset, except it will also add to the 32 bytes of the channel header. + +We can also cap the cursor at 192, the maximum amount of notes a channel can have, in order to avoid any out-of-bounds shenanigans. + +```typescript +/** Channel data offset. */ +const offset: usize = data + 32 + (224 * ichannel); + +/** Note value. */ +const note: i8 = load(offset + 32 + (cursor % 192)); +``` + +For the sake of consistency, we could also check if the `is_enabled` property is set: + +```typescript +/** Check if channel is enabled. */ +const isEnabled: bool = load(offset + 30); + +// Ignore if is not enabled... +if(!isEnabled) { + return; +} +``` + +## Reading notes + +With an offset value and the note value, we can start to implement how each note should be interpreted. + +The **note break** is a very specific value, so we can check for that first. It will interrupt any sounds +being played on a given channel. To do that, we can simply reset the channel with an empty `tone`. + +Apart from that, any valid note must be within `-96` and `96`, and it can't be `0`. + +```typescript +// Notes with a value of -128 represent a note break. +// +// Note breaks will mute a specific channel if it's still +// playing a tone. +if(note === -128) { + w4.tone(0, 0, 0, ichannel); + continue; +} +// Notes ranging from -96 and 96 are valid tones. +// +// Positive notes will use the main channel, while +// negative notes will use the shadow channel. +// +// Anything else, or 0, can be ignored. +else if(note !== 0 && note >= -96 && note <= 96) { + // ... +} +``` + +## Getting the instrument + +Each channel has 2 instruments available for use. For simplicity, IWAS calls each one a "main channel" and a "shadow channel". + +To check if a note should use the main or the shadow channel, we can look if the value is either positive or negative: + - **Positive values** will use the **main channel.** + - **Negative values** will use the **shadow channel.** + +Both instruments will have the same byte length, which is 9 bytes, and if we look at the data structure, we can see +they're pretty close from one to another. + +We can calculate the offset of the instrument we need to fetch simply comparing if the value is positive, and if it is, +then we just need to add 9 to the result. That way we can point to the main channel or the shadow channel. + +```typescript +/** Instrument offset (main channel if positive, shadow channel if negative). */ +const instrument: usize = offset + (note > 0? 0: 9); +``` + +The `tone` function uses 2 frequencies, but instruments only have `frq2`. The `frq1` is taken from the **note lookup table**, +and we can calculate an offset for it, too: + +```typescript +/** Note index for the note lookup table. If negative, it's value will be mirrored (e.g. `-1` becomes `1`). */ +const inote: u8 = u8(Math.abs(note)) - 1; + +// Get frequency from note lookup table... +const frq1: u16 = load(IWAS_NOTE_LOOKUP + (inote * 2)); +``` + +Now we just need to fetch the instrument data and call `tone`, mixing all the properties together with bitwise operations. We can also see +our little-endian to big-endian function being used by `frq2`: + +```typescript +// Get instrument data. +// +// A positive note index should point to the main channel, and +// a negative note index should point to the shadow channel. +// +// `frq2` is a big-endian value, so it must be converted. +// +// Values are converted to `u32` in order to better fit into the +// bitwise operations needed for the `tone` function to work. +const frq2: u32 = u32(loadUint16BE(instrument)); +const atk: u32= u32(load(instrument + 2)); +const dec: u32 = u32(load(instrument + 3)); +const sus: u32 = u32(load(instrument + 4)); +const rel: u32 = u32(load(instrument + 5)); +const peak: u32 = u32(load(instrument + 6)); +const vol: u32 = u32(load(instrument + 7)); +const mode: u32 = u32(load(instrument + 8)); +const pan: u32 = 0; + +// Play tone: +w4.tone( + frq1 | (frq2 << 16), + (atk << 24) | (dec << 16) | sus | (rel << 8), + (peak << 8) | vol, + ichannel | (mode << 2) | (pan << 4) +); +``` + +Also, notice how we're casting all values to `u32`, despite being a single byte. + +The `tone` function expects all arguments to be `u32`, with all ADSR envelope settings +being grouped together using bitwise operations. However, if we apply those on the values +as `u8`, they will occasionally wrap from 0 to 255 and thus potentially not giving the +intended value. + +This effect can be specially seen on `frq2`, but it might affect other values, too, so we +convert all the values, just to be safe. + +## Music speed and length + +Due to screen size limitations, IWAS splits a song into 12 pages, each one containing 16 notes. +Getting the note count should be as simple as multiplying the page number by 16. + +To reset the counter, we use the values provided by the speed lookup table. + +```typescript +/** Music length. To get the actual note count, the page number can be multiplied by 16. */ +const length: u8 = load(data + 6) * 16; + +/** Speed index for the speed lookup table. */ +const ispeed: u8 = load(data + 7); + +/** Speed value. */ +const speed: u8 = load(IWAS_SPEED_LOOKUP + ispeed); + +// Advance counter and cursor... +counter = speed; +cursor = cursor < length? cursor + 1: 0; +``` \ No newline at end of file diff --git a/site/docs/tutorials/iwas/introduction.md b/site/docs/tutorials/iwas/introduction.md new file mode 100644 index 00000000..2def9128 --- /dev/null +++ b/site/docs/tutorials/iwas/introduction.md @@ -0,0 +1,17 @@ +# Introduction + +It's possible to use IWAS as a minimal sound editor. To export your song: + 1. Press **Enter** to get to the main menu. + 2. Select the option **Disk Options**. + 3. Select the option **Export Disk**. + +A file `iwas.disk` will be downloaded by the browser and saved. + +To load a different song: + 1. Press **Enter** to get to the main menu. + 2. Select the option **Disk Options**. + 3. Select the option **Export Disk**. + 4. Select and load a `.disk` file to load it. + 5. On IWAS, click on **Load/Reload Song** button, then confirm it by clicking on the **Load** button when a message appears. + +Now we can use the IWAS save data to export our song. \ No newline at end of file diff --git a/site/docs/tutorials/iwas/the-iwas-file-format.md b/site/docs/tutorials/iwas/the-iwas-file-format.md new file mode 100644 index 00000000..1c9ea70b --- /dev/null +++ b/site/docs/tutorials/iwas/the-iwas-file-format.md @@ -0,0 +1,679 @@ +# 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: + +```typescript +/** + * Load `u16` value (big-endian). + * + * @param offset Address offset + */ +function loadUint16BE(offset: usize): u16 { + /** High byte. */ + const hi: u16 = u16(load(offset)); + + /** Low byte. */ + const lo: u16 = u16(load(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. + +| Note | Frequency (Hz) | +|:-----:|:--------------:| +| 1 | 16 | +| 2 | 17 | +| 3 | 18 | +| 4 | 19 | +| 5 | 20 | +| 6 | 21 | +| 7 | 23 | +| 8 | 24 | +| 9 | 25 | +| 10 | 27 | +| 11 | 29 | +| 12 | 30 | +| 13 | 32 | +| 14 | 34 | +| 15 | 36 | +| 16 | 38 | +| 17 | 41 | +| 18 | 43 | +| 19 | 46 | +| 20 | 49 | +| 21 | 51 | +| 22 | 55 | +| 23 | 58 | +| 24 | 61 | +| 25 | 65 | +| 26 | 69 | +| 27 | 73 | +| 28 | 77 | +| 29 | 82 | +| 30 | 87 | +| 31 | 92 | +| 32 | 98 | +| 33 | 103 | +| 34 | 110 | +| 35 | 116 | +| 36 | 123 | +| 37 | 130 | +| 38 | 138 | +| 39 | 146 | +| 40 | 155 | +| 41 | 164 | +| 42 | 174 | +| 43 | 185 | +| 44 | 196 | +| 45 | 207 | +| 46 | 220 | +| 47 | 233 | +| 48 | 246 | +| 49 | 261 | +| 50 | 277 | +| 51 | 293 | +| 52 | 311 | +| 53 | 329 | +| 54 | 349 | +| 55 | 369 | +| 56 | 392 | +| 57 | 415 | +| 58 | 440 | +| 59 | 466 | +| 60 | 493 | +| 61 | 523 | +| 62 | 554 | +| 63 | 587 | +| 64 | 622 | +| 65 | 659 | +| 66 | 698 | +| 67 | 739 | +| 68 | 783 | +| 69 | 830 | +| 70 | 880 | +| 71 | 932 | +| 72 | 987 | +| 73 | 1046 | +| 74 | 1108 | +| 75 | 1174 | +| 76 | 1244 | +| 77 | 1318 | +| 78 | 1396 | +| 79 | 1479 | +| 80 | 1567 | +| 81 | 1661 | +| 82 | 1760 | +| 83 | 1864 | +| 84 | 1975 | +| 85 | 2093 | +| 86 | 2217 | +| 87 | 2349 | +| 88 | 2489 | +| 89 | 2637 | +| 90 | 2793 | +| 91 | 2959 | +| 92 | 3135 | +| 93 | 3322 | +| 94 | 3520 | +| 95 | 3729 | +| 96 | 3951 | + +This lookup table is also available below as an JSON array. + +```json +[ + 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. + +| Speed | Value | +|:-----:|:-----:| +| 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. + +```javascript +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. + +```json +[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) | Property | Type | Category | Description | +| -------------- | ---------------- | ----------- |------------------------- | -------------------------------------------------- | +| `0000` | `magic` | `u32` | Header | The "IWAS" magic number: `0x_49_57_41_53` | +| `0004` | `version` | `u16` | Header | Version | +| `0006` | `length` | `u8` | Editor setting | Music length, in pages (up to 12 pages) | +| `0007` | `speed` | `u8` | Editor setting | Music speed | +| `0008` | `del_mode` | `bool` | Editor setting | Switch between add/remove mode | +| `0009` | `auto_loop` | `bool` | Editor setting | Auto loop music | +| `000A` | `auto_scroll` | `bool` | Editor setting | Auto scroll when playing music | +| `000B` | `preview_notes` | `bool` | Editor setting | Play a "preview" of a note after adding it | +| `000C` | `grid_y` | `u16` | Application state | Vertical grid scroll | +| `000E` | `grid_hpage` | `u8` | Application state | Current horizontal page | +| `000F` | `unused` | `[u8; 17]` | Reserved | Unused 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 +| -------------- | -------------------- | +| `0020` | Channel 0 (pulse) | +| `0100` | Channel 1 (pulse) | +| `01E0` | Channel 2 (triangle) | +| `02C0` | Channel 3 (noise) | + +For quick reference, each channel will be listed below. + +### Channel 0 (pulse) + +| Location (hex) | Property | Type | Category | Description | +| -------------- | ---------------- | ----------- |------------------------- | -------------------------------------------------- | +| `0020` | `frq2` | `u16` | Main channel | Frequency #2 (frequency #1 uses note lookup table) | +| `0022` | `atk` | `u8` | Main channel | ADSR attack | +| `0023` | `dec` | `u8` | Main channel | ADSR decay | +| `0024` | `sus` | `u8` | Main channel | ADSR sustain | +| `0025` | `rel` | `u8` | Main channel | ADSR release | +| `0026` | `peak` | `u8` | Main channel | Peak | +| `0027` | `vol` | `u8` | Main channel | Volume | +| `0028` | `mode` | `u8` | Main channel | Duty cycle mode | +| `0029` | `shadow_frq2` | `u16` | Shadow channel | Frequency #2 (frequency #1 uses note lookup table) | +| `002B` | `shadow_atk` | `u8` | Shadow channel | ADSR attack | +| `002C` | `shadow_dec` | `u8` | Shadow channel | ADSR decay | +| `002D` | `shadow_sus` | `u8` | Shadow channel | ADSR sustain | +|`,002E` | `shadow_rel` | `u8` | Shadow channel | ADSR release | +| `002F` | `shadow_peak` | `u8` | Shadow channel | Peak | +| `0030` | `shadow_vol` | `u8` | Shadow channel | Volume | +| `0031` | `shadow_mode` | `u8` | Shadow channel | Duty cycle mode | +| `0032` | `unused` | `[u8; 8]` | Reserved | Unused space (reserved for future use) | +| `0033` | `offset_x` | `i8` | Editor-specific settings | Horizontal offset misplacement | +| `003B` | `offset_y` | `i8` | Editor-specific settings | Vertical offset misplacement | +| `003C` | `shadow_enabled` | `bool` | Editor-specific settings | Shadow channel editing | +| `003D` | `show_lines` | `bool` | Editor-specific settings | Connect added notes with lines | +| `003E` | `is_enabled` | `bool` | Editor-specific settings | Enable/disable the channel | +| `003F` | `edit_anywhere` | `bool` | Editor-specific settings | Add/remove notes from anywhere | +| `0040` | `notes` | `[i8; 192]` | Music | Note data (192 notes) | + +### Channel 1 (pulse) + +| Location (hex) | Property | Type | Category | Description | +| -------------- | ---------------- | ----------- |------------------------- | -------------------------------------------------- | +| `0100` | `frq2` | `u16` | Main channel | Frequency #2 (frequency #1 uses note lookup table) | +| `0102` | `atk` | `u8` | Main channel | ADSR attack | +| `0103` | `dec` | `u8` | Main channel | ADSR decay | +| `0104` | `sus` | `u8` | Main channel | ADSR sustain | +| `0105` | `rel` | `u8` | Main channel | ADSR release | +| `0106` | `peak` | `u8` | Main channel | Peak | +| `0107` | `vol` | `u8` | Main channel | Volume | +| `0108` | `mode` | `u8` | Main channel | Duty cycle mode | +| `0109` | `shadow_frq2` | `u16` | Shadow channel | Frequency #2 (frequency #1 uses note lookup table) | +| `010B` | `shadow_atk` | `u8` | Shadow channel | ADSR attack | +| `010C` | `shadow_dec` | `u8` | Shadow channel | ADSR decay | +| `010D` | `shadow_sus` | `u8` | Shadow channel | ADSR sustain | +|`,010E` | `shadow_rel` | `u8` | Shadow channel | ADSR release | +| `010F` | `shadow_peak` | `u8` | Shadow channel | Peak | +| `0110` | `shadow_vol` | `u8` | Shadow channel | Volume | +| `0111` | `shadow_mode` | `u8` | Shadow channel | Duty cycle mode | +| `0112` | `unused` | `[u8; 8]` | Reserved | Unused space (reserved for future use) | +| `0113` | `offset_x` | `i8` | Editor-specific settings | Horizontal offset misplacement | +| `011B` | `offset_y` | `i8` | Editor-specific settings | Vertical offset misplacement | +| `011C` | `shadow_enabled` | `bool` | Editor-specific settings | Shadow channel editing | +| `011D` | `show_lines` | `bool` | Editor-specific settings | Connect added notes with lines | +| `011E` | `is_enabled` | `bool` | Editor-specific settings | Enable/disable the channel | +| `011F` | `edit_anywhere` | `bool` | Editor-specific settings | Add/remove notes from anywhere | +| `0120` | `notes` | `[i8; 192]` | Music | Note data (192 notes) | + +### Channel 2 (triangle) + +| Location (hex) | Property | Type | Category | Description | +| -------------- | ---------------- | ----------- |------------------------- | -------------------------------------------------- | +| `01E0` | `frq2` | `u16` | Main channel | Frequency #2 (frequency #1 uses note lookup table) | +| `01E2` | `atk` | `u8` | Main channel | ADSR attack | +| `01E3` | `dec` | `u8` | Main channel | ADSR decay | +| `01E4` | `sus` | `u8` | Main channel | ADSR sustain | +| `01E5` | `rel` | `u8` | Main channel | ADSR release | +| `01E6` | `peak` | `u8` | Main channel | Peak | +| `01E7` | `vol` | `u8` | Main channel | Volume | +| `01E8` | `mode` | `u8` | Main channel | Duty cycle mode | +| `01E9` | `shadow_frq2` | `u16` | Shadow channel | Frequency #2 (frequency #1 uses note lookup table) | +| `01EB` | `shadow_atk` | `u8` | Shadow channel | ADSR attack | +| `01EC` | `shadow_dec` | `u8` | Shadow channel | ADSR decay | +| `01ED` | `shadow_sus` | `u8` | Shadow channel | ADSR sustain | +|`,01EE` | `shadow_rel` | `u8` | Shadow channel | ADSR release | +| `01EF` | `shadow_peak` | `u8` | Shadow channel | Peak | +| `01F0` | `shadow_vol` | `u8` | Shadow channel | Volume | +| `01F1` | `shadow_mode` | `u8` | Shadow channel | Duty cycle mode | +| `01F2` | `unused` | `[u8; 8]` | Reserved | Unused space (reserved for future use) | +| `01F3` | `offset_x` | `i8` | Editor-specific settings | Horizontal offset misplacement | +| `01FB` | `offset_y` | `i8` | Editor-specific settings | Vertical offset misplacement | +| `01FC` | `shadow_enabled` | `bool` | Editor-specific settings | Shadow channel editing | +| `01FD` | `show_lines` | `bool` | Editor-specific settings | Connect added notes with lines | +| `01FE` | `is_enabled` | `bool` | Editor-specific settings | Enable/disable the channel | +| `01FF` | `edit_anywhere` | `bool` | Editor-specific settings | Add/remove notes from anywhere | +| `0200` | `notes` | `[i8; 192]` | Music | Note data (192 notes) | + +### Channel 3 (noise) + +| Location (hex) | Property | Type | Category | Description | +| -------------- | ---------------- | ----------- |------------------------- | -------------------------------------------------- | +| `02C0` | `frq2` | `u16` | Main channel | Frequency #2 (frequency #1 uses note lookup table) | +| `02C2` | `atk` | `u8` | Main channel | ADSR attack | +| `02C3` | `dec` | `u8` | Main channel | ADSR decay | +| `02C4` | `sus` | `u8` | Main channel | ADSR sustain | +| `02C5` | `rel` | `u8` | Main channel | ADSR release | +| `02C6` | `peak` | `u8` | Main channel | Peak | +| `02C7` | `vol` | `u8` | Main channel | Volume | +| `02C8` | `mode` | `u8` | Main channel | Duty cycle mode | +| `02C9` | `shadow_frq2` | `u16` | Shadow channel | Frequency #2 (frequency #1 uses note lookup table) | +| `02CB` | `shadow_atk` | `u8` | Shadow channel | ADSR attack | +| `02CC` | `shadow_dec` | `u8` | Shadow channel | ADSR decay | +| `02CD` | `shadow_sus` | `u8` | Shadow channel | ADSR sustain | +|`,02CE` | `shadow_rel` | `u8` | Shadow channel | ADSR release | +| `02CF` | `shadow_peak` | `u8` | Shadow channel | Peak | +| `02D0` | `shadow_vol` | `u8` | Shadow channel | Volume | +| `02D1` | `shadow_mode` | `u8` | Shadow channel | Duty cycle mode | +| `02D2` | `unused` | `[u8; 8]` | Reserved | Unused space (reserved for future use) | +| `02D3` | `offset_x` | `i8` | Editor-specific settings | Horizontal offset misplacement | +| `02DB` | `offset_y` | `i8` | Editor-specific settings | Vertical offset misplacement | +| `02DC` | `shadow_enabled` | `bool` | Editor-specific settings | Shadow channel editing | +| `02DD` | `show_lines` | `bool` | Editor-specific settings | Connect added notes with lines | +| `02DE` | `is_enabled` | `bool` | Editor-specific settings | Enable/disable the channel | +| `02DF` | `edit_anywhere` | `bool` | Editor-specific settings | Add/remove notes from anywhere | +| `02E0` | `notes` | `[i8; 192]` | Music | Note 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. + +```html + + + + + +IWAS disk exporter + + + +
+
+
+ + clear +
+
+
+ +
+
+ + + +``` diff --git a/site/sidebars.js b/site/sidebars.js index f4548c5c..d5712e78 100644 --- a/site/sidebars.js +++ b/site/sidebars.js @@ -76,6 +76,24 @@ module.exports = { ] } ] + }, + { + type: "category", + label: "Tutorials", + collapsible: false, + items: [ + { + type: "category", + label: "Making music with IWAS", + collapsible: true, + items: [ + "tutorials/iwas/introduction", + "tutorials/iwas/the-iwas-file-format", + "tutorials/iwas/implementing-the-driver", + "tutorials/iwas/finishing-the-sequencer", + ] + } + ] } ], };