# Three consoles

> The same four bars on the NES, the SNES and the Genesis, played with the engine's instruments.

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

A song says which console it sounds like with `chip`, and that decides its voices. The NES has two squares, a triangle, noise and `dpcm`, a voice of samples; the SNES (`'snes'`) has eight voices of samples and an `echo`; the Genesis (`'genesis'`) has six FM voices plus the squares and noise of its second chip.

Every voice here plays with one of the engine's instruments, asked for by name (`lead`, `strings`, `fm-lead`, `slap-bass`…), so the song defines none of its own. Each voice also has a `pan`, between left (`-1`) and right (`1`), and `song.pan` can be tweened: P carries the arpeggio across to the other side.

Space to play, 1 to 3 for the console, P to move the arpeggio.

## 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: '#14121c'
});

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

`components/musicplayer.ts`:

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

import * as Settings from '../settings';
import { NES, SNES, GENESIS } from '../data/songs';

export const MusicPlayer = () => {
    const keys = useKeyboard();
    const songs = [useSong(NES), useSong(SNES), useSong(GENESIS)];
    // The voice each one plays its arpeggio on.
    const arpOf = (index: number) => (index === 0 ? 'pulse2' : index === 1 ? 's4' : 'fm4');
    const tween = useTween();
    let chosen = 0;

    createText({
        text: 'THREE CONSOLES',
        style: {
            fontSize: 16
        },
        anchor: {
            x: 0.5,
            y: 0
        },
        transform: {
            x: 240,
            y: 40
        }
    });

    const labels = ['1 NES', '2 SNES', '3 GENESIS'].map((text, i) =>
        createText({
            text,
            anchor: {
                x: 0.5,
                y: 0
            },
            transform: {
                x: 240,
                y: 110 + i * 24
            }
        }));

    const marks = [0, 1, 2].map((i) => createSprite({
        width: 8,
        height: 8,
        tint: Settings.ON,
        transform: {
            x: 170,
            y: 114 + i * 24
        }
    }));

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

    createText({
        text: 'SPACE PLAY   1-3 CONSOLE   P PAN',
        tint: Settings.DIM,
        anchor: {
            x: 0.5,
            y: 0
        },
        transform: {
            x: 240,
            y: 280
        }
    });

    const choose = (index: number) => {
        if (index === chosen) {
            return;
        }

        const wasPlaying = songs[chosen]!.playing;

        songs[chosen]!.stop();
        chosen = index;

        if (wasPlaying) {
songs[chosen]!.play();
        }
    };

    useUpdate(() => {
        if (keys.justPressed(' ')) {
            if (songs[chosen]!.playing) {
            songs[chosen]!.stop({
                fade: 0.2
            });
            } else {
            songs[chosen]!.play();
            }
        }

        if (keys.justPressed('1')) {
            choose(0);
        }

        if (keys.justPressed('2')) {
            choose(1);
        }

        if (keys.justPressed('3')) {
            choose(2);
        }

        // The arpeggio carried across to the other side, in a second and a half.
        if (keys.justPressed('p')) {
            const song = songs[chosen]!;
            const voice = arpOf(chosen);

            tween({
                target: song.pan,
                to: {
                    [voice]: song.pan[voice] > 0 ? -1 : 1
                },
                duration: 1.5
            }).play();
        }

        labels.forEach((label, i) => {
            label.tint = i === chosen ? Settings.ON : Settings.DIM;
        });
        marks.forEach((mark, i) => {
            mark.visible = i === chosen;
        });

        const pan = songs[chosen]!.pan[arpOf(chosen)];

        status.text = (songs[chosen]!.playing ? 'PLAYING' : 'STOPPED') + '   ARPEGGIO ' + (pan < -0.05 ? 'LEFT' : pan > 0.05 ? 'RIGHT' : 'MIDDLE');
    });
};
```

`data/songs.ts`:

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

const LEAD = "g'4 f'8 d#' d'4 c'8 d' | d#'4 d'8 c' a#2 | c'4 d#'8 f' g'4 a#'8 g' | f'2 r2 |";
const BASS = 'c,4 c, c c, | a#,,4 a#,, a#, a#,, | g#,,4 g#,, g#, g#,, | g,,4 g,, g, g,, |';
const ARPS = "c'8 d#' g' d#' c' d#' g' d#' | a#8 d' f' d' a# d' f' d' | g#8 c' d#' c' g# c' d#' c' | g8 b d' b g b d' b |";
const HATS = ['@hat x8 x x x x x x x |', 'x8 x x x x x x x |', 'x8 x x x x x x x |', 'x8 x x x x x x x |'].join(' ');
const BEAT = "@drum-kick c'4 @drum-snare c' @drum-kick c'8 c' @drum-snare c'4 |";
const KIT = [BEAT, BEAT, BEAT, "@orch-hit c'2 @drum-kick c'4 @drum-snare c' |"].join(' ');
const GROOVE = '@kick x8 @hat x @snare x @hat x @kick x x @snare x @hat x |';

export const NES: TSongDoc = {
    format: DOCUMENTS.SONG.FORMAT,
    tempo: 140,
    meter: [4, 4],
    voices: {
        pulse1: {
            notes: LEAD,
            instrument: 'lead',
            pan: -0.3
        },
        pulse2: {
            notes: ARPS,
            instrument: 'pluck',
            volume: 0.6,
            pan: 0.6
        },
        triangle: {
            notes: BASS,
            instrument: 'bass'
        },
        noise: {
            notes: HATS,
            volume: 0.5
        },
        dpcm: {
            notes: KIT
        },
    },
};

export const SNES: TSongDoc = {
    format: DOCUMENTS.SONG.FORMAT,
    chip: 'snes',
    tempo: 140,
    meter: [4, 4],
    echo: {
        delay: 0.16,
        feedback: 0.45,
        volume: 0.3
    },
    voices: {
        s1: {
            notes: LEAD,
            instrument: 'flute'
        },
        s2: {
            notes: "c'1 | a#1 | g#1 | g1 |",
            instrument: 'strings',
            volume: 0.8,
            pan: -0.5
        },
        s3: {
            notes: "d#'1 | d'1 | c'1 | b1 |",
            instrument: 'strings',
            volume: 0.8,
            pan: 0.5
        },
        s4: {
            notes: ARPS,
            instrument: 'piano',
            volume: 0.55,
            pan: 0.6
        },
        s5: {
            notes: BASS,
            instrument: 'synth-bass'
        },
        s6: {
            notes: KIT
        },
    },
};

export const GENESIS: TSongDoc = {
    format: DOCUMENTS.SONG.FORMAT,
    chip: 'genesis',
    tempo: 140,
    meter: [4, 4],
    voices: {
        fm1: {
            notes: LEAD,
            instrument: 'fm-lead'
        },
        // The Genesis put each FM voice left, right or in the middle.
        fm2: {
            notes: "c'1 | a#1 | g#1 | g1 |",
            instrument: 'fm-strings',
            volume: 0.7,
            pan: -1
        },
        fm3: {
            notes: "d#'1 | d'1 | c'1 | b1 |",
            instrument: 'fm-strings',
            volume: 0.7,
            pan: 1
        },
        fm4: {
            notes: ARPS,
            instrument: 'epiano',
            volume: 0.6,
            pan: 1
        },
        fm5: {
            notes: BASS,
            instrument: 'slap-bass'
        },
        psgNoise: {
            notes: [GROOVE, GROOVE, GROOVE, GROOVE].join(' ')
        },
    },
};
```

`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';

export const WIDTH = 480;
export const HEIGHT = 320;
export const ON = getColor('#f5c518');
export const DIM = getColor('#6a6680');
```
