# FamiTracker songs

> Songs made in FamiTracker, by game, each made once into a .song and played on the engine's NES.

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

FamiTracker is where NES music is written today. Each of these songs was exported from it as text (File, Export text) and made once into a `.song` with its samples, which is what the editor does when an export is dropped on it. A `.song` loads at once with `useLoadSong`; the export would have to be read every time.

From there they play on the engine's NES like any other song, and their tempo and key change while they play. Only the song chosen is loaded, and one loaded before is not loaded again.

Left and right for the game, up and down for the song, space to play, `[` and `]` for the tempo, `-` and `=` for 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: '#14121c'
});

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

`components/trackerplayer.ts`:

```ts
import { createText, useKeyboard, useLoadSong, useScene, useSong, useUpdate } from 'nacatamalon';

import * as Settings from '../settings';
import { time } from '../helpers/time';
import { GAMES } from '../data/catalog';

export const TrackerPlayer = ({ game = 0, chosen = 0, playing = false }: { game?: number; chosen?: number; playing?: boolean; }) => {
    const keys = useKeyboard();
    const scene = useScene();
    const { name, songs } = GAMES[game]!;

    const file = useLoadSong({
        src: '/example-assets/tracker/' + songs[chosen]![1] + '.song'
    });

    const song = useSong(file, {
        autoplay: playing
    });

    let key = 0;

    createText({
        text: '< ' + name.toUpperCase().slice(0, 46) + ' >',
        tint: Settings.ON,
        anchor: {
            x: 0.5,
            y: 0
        },
        transform: {
            x: 240,
            y: 14
        }
    });
    createText({
        text: 'GAME ' + (game + 1) + ' OF ' + GAMES.length + '  -  ' + songs.length + (songs.length === 1 ? ' SONG' : ' SONGS'),
        tint: Settings.DIM,
        anchor: {
            x: 0.5,
            y: 0
        },
        transform: {
            x: 240,
            y: 30
        }
    });

    const first = Math.min(Math.max(0, chosen - Math.floor(Settings.SHOWN / 2)), Math.max(0, songs.length - Settings.SHOWN));

    for (let i = 0; i < Math.min(Settings.SHOWN, songs.length); i += 1) {
        createText({
            text: (first + i === chosen ? '> ' : '  ') + songs[first + i]![0].toUpperCase().slice(0, 50),
            tint: first + i === chosen ? Settings.ON : Settings.DIM,
            transform: {
                x: 24,
                y: 60 + i * 18
            },
        });
    }

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

    createText({
        text: 'LEFT/RIGHT GAME   UP/DOWN SONG   SPACE PLAY',
        tint: Settings.DIM,
        anchor: {
            x: 0.5,
            y: 0
        },
        transform: {
            x: 240,
            y: 272
        }
    });
    createText({
        text: '[ ] TEMPO   - = KEY',
        tint: Settings.DIM,
        anchor: {
            x: 0.5,
            y: 0
        },
        transform: {
            x: 240,
            y: 290
        }
    });

    useUpdate(() => {
        const nextGame = (keys.justPressed('ArrowRight') ? 1 : 0) - (keys.justPressed('ArrowLeft') ? 1 : 0);
        const nextSong = (keys.justPressed('ArrowDown') ? 1 : 0) - (keys.justPressed('ArrowUp') ? 1 : 0);

        if (nextGame !== 0) {
            scene.change('Player', {
                props: {
                    game: (game + nextGame + GAMES.length) % GAMES.length,
                    chosen: 0,
                    playing: song.playing
                }
            });

            return;
        }

        if (nextSong !== 0) {
            scene.change('Player', {
                props: {
                    game,
                    chosen: (chosen + nextSong + songs.length) % songs.length,
                    playing: song.playing
                }
            });

            return;
        }

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

        if (keys.justPressed(']')) {
            song.setTempo(Math.round(song.tempo * 1.1));
        }

        if (keys.justPressed('[')) {
            song.setTempo(Math.round(song.tempo / 1.1));
        }

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

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

        status.text = file.status === 'loading'
            ? 'LOADING...'
            : file.status === 'error'
                ? 'COULD NOT BE LOADED'
                : (song.playing ? 'PLAYING  ' + time(song.position) : 'STOPPED') + '   KEY ' + (key > 0 ? '+' : '') + key;
    });
};
```

`data/catalog.ts`:

```ts
// Every song of the page, by game: its name and where its .song is under /tracker/.
export const GAMES: ReadonlyArray<{ name: string; songs: ReadonlyArray<readonly [string, string]>; }> = [
    {
        name: 'Astyanax',
        songs: [['Remlia Castle', 'astyanax/remlia-castle'],]
    },
    {
        name: 'Batman',
        songs: [['Stage 1', 'batman/stage-1'],]
    },
    {
        name: 'Bionic Commando',
        songs: [['Area 1', 'bionic-commando/area-1'],]
    },
    {
        name: 'Castlevania',
        songs: [['Heart of Fire', 'castlevania/heart-of-fire'], ['Wicked Child', 'castlevania/wicked-child'],]
    },
    {
        name: "Castlevania 2 - Simon's Quest",
        songs: [['Bloody Tears', 'castlevania-2-simons-quest/bloody-tears'],]
    },
    {
        name: "Castlevania 3 - Dracula's Curse",
        songs: [['Mad Forest', 'castlevania-3-draculas-curse/mad-forest'], ['Vampire Killer', 'castlevania-3-draculas-curse/vampire-killer'],]
    },
    {
        name: "Chip 'n Dale Rescue Rangers",
        songs: [['Zone J', 'chip-n-dale-rescue-rangers/zone-j'],]
    },
    {
        name: 'Crystalis',
        songs: [['Mt. Sabre', 'crystalis/mt-sabre'],]
    },
    {
        name: "Disney's DuckTales 2",
        songs: [['Track 01', 'disneys-ducktales-2/us-pista-01'], ['Track 02', 'disneys-ducktales-2/us-pista-02'], ['Track 03', 'disneys-ducktales-2/us-pista-03'], ['Track 04', 'disneys-ducktales-2/us-pista-04'], ['Track 07', 'disneys-ducktales-2/us-pista-07'], ['Track 09', 'disneys-ducktales-2/us-pista-09'], ['Track 10', 'disneys-ducktales-2/us-pista-10'], ['Track 11', 'disneys-ducktales-2/us-pista-11'], ['Track 12', 'disneys-ducktales-2/us-pista-12'], ['Track 13', 'disneys-ducktales-2/us-pista-13'], ['Track 14', 'disneys-ducktales-2/us-pista-14'], ['Track 15', 'disneys-ducktales-2/us-pista-15'], ['Track 16', 'disneys-ducktales-2/us-pista-16'], ['Track 17', 'disneys-ducktales-2/us-pista-17'],]
    },
    {
        name: 'Final Fantasy',
        songs: [["Matoya's Cave", 'final-fantasy/matoyas-cave'], ['Prologue Theme', 'final-fantasy/prologue-theme'],]
    },
    {
        name: 'Final Fantasy III',
        songs: [['Title Theme', 'final-fantasy-iii/title-theme'],]
    },
    {
        name: 'Gauntlet',
        songs: [['Track 01', 'gauntlet/track-01'], ['Track 02', 'gauntlet/track-02'], ['Track 03', 'gauntlet/track-03'], ['Track 04', 'gauntlet/track-04'], ['Track 05', 'gauntlet/track-05'], ['Track 06', 'gauntlet/track-06'], ['Track 07', 'gauntlet/track-07'], ['Track 09', 'gauntlet/track-09'], ['Track 10', 'gauntlet/track-10'],]
    },
    {
        name: 'Gun.Smoke',
        songs: [['Track 01', 'gunsmoke/track-01'], ['Track 02', 'gunsmoke/track-02'], ['Track 03', 'gunsmoke/track-03'], ['Track 04', 'gunsmoke/track-04'], ['Track 05', 'gunsmoke/track-05'], ['Track 06', 'gunsmoke/track-06'], ['Track 07', 'gunsmoke/track-07'], ['Track 08', 'gunsmoke/track-08'], ['Track 09', 'gunsmoke/track-09'], ['Track 10', 'gunsmoke/track-10'], ['Track 11', 'gunsmoke/track-11'], ['Track 12', 'gunsmoke/track-12'], ['Track 13', 'gunsmoke/track-13'], ['Track 14', 'gunsmoke/track-14'], ['Track 15', 'gunsmoke/track-15'],]
    },
    {
        name: 'Journey to Silius',
        songs: [['Main Theme', 'journey-to-silius/main-theme'],]
    },
    {
        name: 'Kick Master',
        songs: [['Sector 4 Bottomless Crevasse', 'kick-master/sector-4-bottomless-crevasse'],]
    },
    {
        name: "Kirby's Adventure",
        songs: [['Butter Building', 'kirbys-adventure/butter-building'], ['Grape Garden', 'kirbys-adventure/grape-garden'], ['Green Greens Intro', 'kirbys-adventure/green-greens-intro'], ['Green Greens', 'kirbys-adventure/green-greens'], ['Orange Ocean', 'kirbys-adventure/orange-ocean'],]
    },
    {
        name: 'The Legend of Zelda',
        songs: [['Track 01', 'the-legend-of-zelda/track-01'], ['Track 02', 'the-legend-of-zelda/track-02'], ['Track 03', 'the-legend-of-zelda/track-03'], ['Track 04', 'the-legend-of-zelda/track-04'], ['Track 07', 'the-legend-of-zelda/track-07'], ['Track 08', 'the-legend-of-zelda/track-08'], ['Track 13', 'the-legend-of-zelda/track-13'], ['Track 14', 'the-legend-of-zelda/track-14'], ['Track 15', 'the-legend-of-zelda/track-15'],]
    },
    {
        name: 'Mega Man',
        songs: [['Cut Man Stage', 'mega-man/cut-man-stage'], ['Wily Fortress 1', 'mega-man/wily-fortress-1'],]
    },
    {
        name: 'Mega Man 2',
        songs: [['Bubble Man Stage', 'mega-man-2/bubble-man-stage'], ['Wily Fortress 1', 'mega-man-2/wily-fortress-1'],]
    },
    {
        name: 'Mega Man 3',
        songs: [['Proto Man Theme', 'mega-man-3/proto-man-theme'], ['Spark Man Stage', 'mega-man-3/spark-man-stage'],]
    },
    {
        name: 'Mega Man 4',
        songs: [['Opening 1', 'mega-man-4/opening-1'], ['Opening 2', 'mega-man-4/opening-2'], ['Ring Man Stage', 'mega-man-4/ring-man-stage'], ['Skull Man Stage', 'mega-man-4/skull-man-stage'],]
    },
    {
        name: 'Mega Man 6',
        songs: [['Blizzard Man Stage', 'mega-man-6/blizzard-man-stage'], ['Flame Man Stage', 'mega-man-6/flame-man-stage'], ['Knight Man Stage', 'mega-man-6/knight-man-stage'],]
    },
    {
        name: "Mike Tyson's Punch-Out!!",
        songs: [['Bicycle Theme', 'mike-tysons-punch-out/bicycle-theme'],]
    },
    {
        name: 'Ninja Gaiden',
        songs: [['Act 4 Part 2', 'ninja-gaiden/act-4-part-2'],]
    },
    {
        name: 'Nintendo World Cup',
        songs: [['Track 01', 'nintendo-world-cup/track-01'], ['Track 02', 'nintendo-world-cup/track-02'], ['Track 15', 'nintendo-world-cup/track-15'], ['Track 16', 'nintendo-world-cup/track-16'], ['Track 17', 'nintendo-world-cup/track-17'], ['Track 18', 'nintendo-world-cup/track-18'], ['Track 19', 'nintendo-world-cup/track-19'], ['Track 20', 'nintendo-world-cup/track-20'], ['Track 25', 'nintendo-world-cup/track-25'],]
    },
    {
        name: 'Princess Tomato in Salad Kingdom',
        songs: [['Track 01', 'princess-tomato-in-salad-kingdom/track-01'], ['Track 02', 'princess-tomato-in-salad-kingdom/track-02'], ['Track 03', 'princess-tomato-in-salad-kingdom/track-03'], ['Track 04', 'princess-tomato-in-salad-kingdom/track-04'], ['Track 05', 'princess-tomato-in-salad-kingdom/track-05'], ['Track 06', 'princess-tomato-in-salad-kingdom/track-06'], ['Track 07', 'princess-tomato-in-salad-kingdom/track-07'], ['Track 08', 'princess-tomato-in-salad-kingdom/track-08'], ['Track 09', 'princess-tomato-in-salad-kingdom/track-09'], ['Track 10', 'princess-tomato-in-salad-kingdom/track-10'], ['Track 11', 'princess-tomato-in-salad-kingdom/track-11'], ['Track 12', 'princess-tomato-in-salad-kingdom/track-12'], ['Track 13', 'princess-tomato-in-salad-kingdom/track-13'], ['Track 15', 'princess-tomato-in-salad-kingdom/track-15'],]
    },
    {
        name: 'Rygar',
        songs: [['Stage 2 Theme', 'rygar/stage-2-theme'],]
    },
    {
        name: 'Section Z',
        songs: [['Title Theme', 'section-z/title-theme'],]
    },
    {
        name: 'Shatterhand',
        songs: [['Area E-F', 'shatterhand/area-e-f'],]
    },
    {
        name: 'Spy vs Spy',
        songs: [['Track 01', 'spy-vs-spy/track-01'], ['Track 02', 'spy-vs-spy/track-02'],]
    },
    {
        name: 'Super Mario Bros. 2',
        songs: [['Track 01', 'super-mario-bros-2/track-01'], ['Track 02', 'super-mario-bros-2/track-02'], ['Track 03', 'super-mario-bros-2/track-03'], ['Track 04', 'super-mario-bros-2/track-04'], ['Track 05', 'super-mario-bros-2/track-05'], ['Track 06', 'super-mario-bros-2/track-06'], ['Track 07', 'super-mario-bros-2/track-07'], ['Track 08', 'super-mario-bros-2/track-08'], ['Track 10', 'super-mario-bros-2/track-10'],]
    },
    {
        name: 'Teenage Mutant Ninja Turtles II - The Arcade Game',
        songs: [['The Arcade Game - pista 01', 'teenage-mutant-ninja-turtles-ii-the-arcade-game/the-arcade-game-pista-01'], ['The Arcade Game - pista 02', 'teenage-mutant-ninja-turtles-ii-the-arcade-game/the-arcade-game-pista-02'], ['The Arcade Game - pista 03', 'teenage-mutant-ninja-turtles-ii-the-arcade-game/the-arcade-game-pista-03'], ['The Arcade Game - pista 12', 'teenage-mutant-ninja-turtles-ii-the-arcade-game/the-arcade-game-pista-12'], ['The Arcade Game - pista 14', 'teenage-mutant-ninja-turtles-ii-the-arcade-game/the-arcade-game-pista-14'], ['The Arcade Game - pista 19', 'teenage-mutant-ninja-turtles-ii-the-arcade-game/the-arcade-game-pista-19'], ['The Arcade Game - pista 20', 'teenage-mutant-ninja-turtles-ii-the-arcade-game/the-arcade-game-pista-20'], ['The Arcade Game - pista 21', 'teenage-mutant-ninja-turtles-ii-the-arcade-game/the-arcade-game-pista-21'], ['The Arcade Game - pista 22', 'teenage-mutant-ninja-turtles-ii-the-arcade-game/the-arcade-game-pista-22'], ['The Arcade Game - pista 27', 'teenage-mutant-ninja-turtles-ii-the-arcade-game/the-arcade-game-pista-27'], ['The Arcade Game - pista 30', 'teenage-mutant-ninja-turtles-ii-the-arcade-game/the-arcade-game-pista-30'], ['The Arcade Game - pista 31', 'teenage-mutant-ninja-turtles-ii-the-arcade-game/the-arcade-game-pista-31'], ['The Arcade Game - pista 32', 'teenage-mutant-ninja-turtles-ii-the-arcade-game/the-arcade-game-pista-32'], ['The Arcade Game - pista 33', 'teenage-mutant-ninja-turtles-ii-the-arcade-game/the-arcade-game-pista-33'], ['The Arcade Game - pista 34', 'teenage-mutant-ninja-turtles-ii-the-arcade-game/the-arcade-game-pista-34'], ['The Arcade Game - pista 35', 'teenage-mutant-ninja-turtles-ii-the-arcade-game/the-arcade-game-pista-35'], ['The Arcade Game - pista 36', 'teenage-mutant-ninja-turtles-ii-the-arcade-game/the-arcade-game-pista-36'], ['The Arcade Game - pista 37', 'teenage-mutant-ninja-turtles-ii-the-arcade-game/the-arcade-game-pista-37'], ['The Arcade Game - pista 38', 'teenage-mutant-ninja-turtles-ii-the-arcade-game/the-arcade-game-pista-38'],]
    },
    {
        name: 'Wizards & Warriors',
        songs: [['Intro', 'wizards-and-warriors/intro'],]
    },
];
```

`helpers/time.ts`:

```ts
export const time = (seconds: number | null): string => {
    const s = Math.floor(seconds ?? 0);

    return Math.floor(s / 60) + ':' + String(s % 60).padStart(2, '0');
};
```

`scenes/demo.ts`:

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

import { TrackerPlayer } from '../components/trackerplayer';

export const Player: TSceneFn<{ game?: number; chosen?: number; playing?: boolean; }> = (props) => {
    useSpawn(TrackerPlayer)(props);

    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('#8a86a0');
export const SHOWN = 8;
```
