# Layered music

> One minuet recorded in four files, melody, harmony, bass and drums, played together and in time. Bring a layer in or out and it comes in on the beat, without the music starting over.

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

`useMusic` plays one piece of music recorded in several files, its layers: here the melody, the harmony, the bass and the drums of the same minuet, all the same length. They always play together and in time, and the game only changes how loud each one is with `setLayer`. A layer that comes back comes in on the beat it is on, because it never stopped: that is music that follows the game (the drums for a fight, the melody gone in a cave) without starting over.

**Which one to use.** `useSound` plays one file: an effect, an ambience, a song that is a single recording. `useMusic` plays one piece in several files that the game fades in and out, this example. `useSong` plays a tune written as notes in a `.song`, made into sound by the engine's 8-bit synthesizer, which can also change its tempo and key.

Space to play, 1 to 4 to bring a layer in or out. The light blinks on every beat (`onBeat`, which needs the music's `bpm`), yellow on the first of the bar.

## 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/mixer.ts`:

```ts
import { createSprite, createText, useKeyboard, useLoadBitmapFont, useMusic, useUpdate } from 'nacatamalon';
import * as Settings from '../settings';

export const Mixer = () => {
    const font = useLoadBitmapFont(Settings.ARCADE);
    const keys = useKeyboard();

    const music = useMusic({
        layers: {
            melody: {
                src: '/example-assets/audio/minuet_melody.wav'
            },
            harmony: {
                src: '/example-assets/audio/minuet_harmony.wav'
            },
            bass: {
                src: '/example-assets/audio/minuet_bass.wav'
            },
            drums: {
                src: '/example-assets/audio/minuet_drums.wav',
                volume: 0
            },
        },
        // Its beat, for onBeat: 120 a minute, three to a bar.
        bpm: 120,
        beatsPerBar: 3,
    });

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

    // A bar per layer, as long as the layer is loud: it grows and shrinks over the fade.
    const bars = Settings.LAYERS.map((layer, i) => {
        createText({
            text: `${i + 1} ${layer.toUpperCase()}`,
            font,
            style: {
                fontSize: 8
            },
            tint: Settings.WHITE,
            transform: {
                x: 80,
                y: 96 + i * 26
            }
        });
        createSprite({
            tint: Settings.OFF,
            width: Settings.FULL,
            height: 10,
            anchor: {
                x: 0,
                y: 0.5
            },
            transform: {
                x: 250,
                y: 100 + i * 26
            }
        });

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

    // A light that blinks on every beat, yellow on the first of the bar.
    const light = createSprite({
        tint: Settings.OFF,
        width: 12,
        height: 12,
        transform: {
            x: 240,
            y: 222
        }
    });

    let flash = 0;
    let first = false;

    music.onBeat((n) => {
        flash = 1;
        first = n === 1;
    });

    createText({
        text: 'SPACE PLAY   1-4 BRING A LAYER IN OR OUT',
        font,
        style: {
            fontSize: 8
        },
        tint: Settings.WHITE,
        anchor: {
            x: 0.5,
            y: 0
        },
        transform: {
            x: 240,
            y: 286
        }
    });

    useUpdate((dt) => {
        if (keys.justPressed(' ')) {
            if (music.playing) {
                music.stop({
                    fade: 0.5
                });
            } else {
                music.play();
            }
        }

        Settings.LAYERS.forEach((layer, i) => {
            // In or out over half a second, without the others noticing.
            if (keys.justPressed(String(i + 1))) {
                music.setLayer(layer, music.getLayer(layer) > 0 ? 0 : 1, 0.5);
            }

            const goal = Settings.FULL * music.getLayer(layer);
            const bar = bars[i]!;

            bar.width += Math.sign(goal - bar.width) * Math.min(Math.abs(goal - bar.width), (Settings.FULL / 0.5) * dt);
        });
        flash = Math.max(0, flash - dt * 4);
        light.tint = flash > 0 ? {
            ...(first ? Settings.ON : Settings.WHITE),
            a: 0.3 + 0.7 * flash
        } : Settings.OFF;
    });
};
```

`scenes/demo.ts`:

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

import { Mixer } from '../components/mixer';

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

    return createScene();
};
```

`settings.ts`:

```ts
import { getColor } 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 LAYERS = ['melody', 'harmony', 'bass', 'drums'] as const;
export const ON = getColor('#f5c518');
export const OFF = getColor('#3a3650');
export const WHITE = getColor('#ffffff');
export const FULL = 140;
```
