# Chiptune song

> A minuet written as notes and played by the four voices of an 8-bit console. Drop a voice, change the tempo or the key without losing the beat.

Example `chiptune-song`, running at https://nacatamalon.com/en/examples/chiptune-song. The whole catalogue: https://nacatamalon.com/llms/examples.md.

`useSong` turns notes written as text into sound inside the game, with the four voices of an 8-bit console: two squares, a triangle and noise. The whole tune is a few hundred bytes of text instead of megabytes of audio.

And it does what a recording cannot: `setVoice` drops or brings back a voice, `setTempo` speeds it up and `setTranspose` changes the key, all three carrying on from the beat it was on. Each voice is one line: a letter, `#` or `b`, `'` or `,` for the octave, and the length. `|` ends a bar, and when one does not add up, `errors` says where.

Space to play, 1 to 4 for the voices, arrows for the tempo and the key.

## Code

`index.ts`:

```ts
import { createGame } from 'nacatamalon';
import * as Settings from './settings';
import { Player } from './scenes/demo';

const game = createGame('#app', {
    width: Settings.WIDTH,
    height: Settings.HEIGHT,
    background: '#0a0a12',
});

// Declare Scenes
game({
    Player,
});
```

`components/musicplayer.ts`:

```ts
import { createSprite, createText, useKeyboard, useLoadBitmapFont, useSong, useUpdate } from 'nacatamalon';

import * as Settings from '../settings';
import { MINUET } from '../data/song';

export const MusicPlayer = () => {
    const font = useLoadBitmapFont(Settings.ARCADE);
    const keys = useKeyboard();
    const song = useSong(MINUET);

    createText({
        text: 'MINUET IN G',
        font,
        style: {
            fontSize: 16
        },
        tint: Settings.WHITE,
        anchor: {
            x: 0.5,
            y: 0
        },
        transform: {
            x: 240,
            y: 30
        }
    });

    // One bar per voice, lit while it is heard.
    const rows = Settings.VOICES.map((voice, i) => {
        createText({
            text: `${i + 1} ${voice.toUpperCase()}`,
            font,
            style: {
                fontSize: 8
            },
            tint: Settings.WHITE,
            transform: {
                x: 80,
                y: 96 + i * 26
            }
        });

        return createSprite({
            tint: Settings.ON,
            width: 120,
            height: 10,
            transform: {
                x: 320,
                y: 100 + i * 26
            }
        });
    });

    createText({
        text: 'SPACE PLAY   1-4 VOICES   ARROWS TEMPO AND KEY',
        font,
        style: {
            fontSize: 8
        },
        tint: Settings.WHITE,
        anchor: {
            x: 0.5,
            y: 0
        },
        transform: {
            x: 240,
            y: 286
        }
    });

    const status = createText({
        text: '',
        font,
        style: {
            fontSize: 8
        },
        tint: Settings.ON,
        anchor: {
            x: 0.5,
            y: 0
        },
        transform: {
            x: 240,
            y: 236
        }
    });

    let key = 0;

    useUpdate(() => {
        if (keys.justPressed(' ')) {
            if (song.playing) {
                song.stop({
                    fade: 0.3
                });
            } else {
                song.play();
            }
        }

        Settings.VOICES.forEach((voice, i) => {
            if (keys.justPressed(String(i + 1))) {
                song.setVoice(voice, song.getVoice(voice) > 0 ? 0 : 1, 0.1);
            }

            rows[i]!.tint = song.getVoice(voice) > 0 ? Settings.ON : Settings.OFF;
        });

        if (keys.justPressed('ArrowRight')) {
            song.setTempo(Math.min(300, song.tempo + 12));
        }

        if (keys.justPressed('ArrowLeft')) {
            song.setTempo(Math.max(48, song.tempo - 12));
        }

        if (keys.justPressed('ArrowUp')) {
            song.setTranspose(++key);
        }

        if (keys.justPressed('ArrowDown')) {
            song.setTranspose(--key);
        }

        status.text = `${song.playing ? 'PLAYING' : 'STOPPED'}   ${song.tempo} BPM   KEY ${key > 0 ? '+' : ''}${key}`;
    });
};
```

`data/song.ts`:

```ts
import { type TSongDoc } from 'nacatamalon';
import { DOCUMENTS } from 'nacatamalon/files';

export const MINUET: TSongDoc = {
    format: DOCUMENTS.SONG.FORMAT,
    tempo: 132,
    meter: [3, 4],
    voices: {
        pulse1: {
            notes: "d'4 g8 a b c' | d'4 g g | e'4 c'8 d' e' f#' | g'4 g g | c'4 d'8 c' b a | b4 c'8 b a g | f#4 g8 a b g | a2. |",
            duty: 0.25
        },
        pulse2: {
            notes: "b2. | b2. | c'2. | b2. | a2. | g2. | d2. | f#2. |",
            volume: 0.6
        },
        triangle: {
            notes: 'g,2. | b,2. | c2. | b,2. | a,2. | g,2. | d2 d,4 | d,2. |'
        },
        noise: {
            notes: 'x4 r r | x4 r r | x4 r r | x4 r r | x4 r r | x4 r r | x4 r r | x4 x8 x x4 |',
            volume: 0.6
        },
    },
};
```

`scenes/demo.ts`:

```ts
import { createScene, useSpawn } from 'nacatamalon';

import { MusicPlayer } from '../components/musicplayer';

export const Player = () => {
    useSpawn(MusicPlayer)();

    return createScene();
};
```

`settings.ts`:

```ts
import { getColor } from 'nacatamalon';
import type { TSongVoiceName } from 'nacatamalon';

export const WIDTH = 480;
export const HEIGHT = 320;
export const ARCADE = {
    json: '/example-assets/fonts/bitmaps/nacatamal-arcade/nacatamal-arcade.json',
    atlas: '/example-assets/fonts/bitmaps/nacatamal-arcade/nacatamal-arcade.png',
};
export const VOICES: TSongVoiceName[] = ['pulse1', 'pulse2', 'triangle', 'noise'];
export const ON = getColor('#f5c518');
export const OFF = getColor('#3a3650');
export const WHITE = getColor('#ffffff');
```
