# Tracker loader

> FamiTracker songs loaded straight from their text export with useLoadTrackerSong, no .song made first, and played on the engine's NES.

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

`useLoadTrackerSong` reads a FamiTracker text export (File, Export text) as it lands, with its DPCM samples, and gives back a song that plays on the engine's NES like any other. Nothing has to be made first. What it cannot bring over is said once in the console.

It is the quick way to try a song in the game. For a finished one, a `.song` made from the export once loads faster, as the FamiTracker songs example shows.

Up and down to choose, space to play, left and right 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, useLoadTrackerSong, useScene, useSong, useUpdate } from 'nacatamalon';

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

export const TrackerPlayer = ({ chosen = 0, playing = false }: { chosen?: number; playing?: boolean; }) => {
    const keys = useKeyboard();
    const scene = useScene();

    const file = useLoadTrackerSong({
        src: '/example-assets/tracker-text/' + Settings.SONGS[chosen]![1] + '.txt'
    });

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

    let key = 0;

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

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

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

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

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

    useUpdate(() => {
        const move = (keys.justPressed('ArrowDown') ? 1 : 0) - (keys.justPressed('ArrowUp') ? 1 : 0);

        if (move !== 0) {
            scene.change('Player', {
                props: {
                    chosen: (chosen + move + Settings.SONGS.length) % Settings.SONGS.length,
                    playing: song.playing
                }
            });

            return;
        }

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

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

        if (keys.justPressed('ArrowLeft')) {
            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'
            ? 'READING THE EXPORT...'
            : file.status === 'error'
                ? 'COULD NOT BE READ'
                : (song.playing ? 'PLAYING  ' + time(song.position) : 'STOPPED') + '   KEY ' + (key > 0 ? '+' : '') + key;
    });
};
```

`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<{ 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 SONGS = [['Mega Man 3 - Spark Man Stage', 'mega-man-3/spark-man-stage'], ['Mega Man 3 - Proto Man Theme', 'mega-man-3/proto-man-theme'], ['Super Mario Bros. 2 - Track 01', 'super-mario-bros-2/track-01'], ['Super Mario Bros. 2 - Track 02', 'super-mario-bros-2/track-02'], ['Super Mario Bros. 2 - Track 03', 'super-mario-bros-2/track-03'], ['Super Mario Bros. 2 - Track 04', 'super-mario-bros-2/track-04'], ['Super Mario Bros. 2 - Track 05', 'super-mario-bros-2/track-05'], ['Super Mario Bros. 2 - Track 06', 'super-mario-bros-2/track-06'], ['Super Mario Bros. 2 - Track 07', 'super-mario-bros-2/track-07'], ['Super Mario Bros. 2 - Track 08', 'super-mario-bros-2/track-08'], ['Super Mario Bros. 2 - Track 10', 'super-mario-bros-2/track-10'],] as const;

export const ON = getColor('#f5c518');
export const DIM = getColor('#8a86a0');
export const SHOWN = 9;
```
