# Tempo og musik

Klip en film til et musiknummer: Angiv et tempo, placer sekvenser i takter og slag, og udled pulsslag og underdelinger af ét musikalsk ur.

Canonical: https://zxn.studio/da/motion/tempo-and-music

En film, der klippes til musik, skrives i takter og slag, men alt derefter — sekvenser, afspillerens tidslinjeskyder og eksporten — tæller billeder. Et deklareret tempo giver filmen et musikalsk ur, så den kan skrives, som den høres, og lader én funktion håndtere alle konverteringer.

@video(beat-pulse)

## Angiv et tempo

`tempo` står i filmdefinitionen ved siden af størrelse og billedfrekvens.

```ts
const tempo = { bpm: 120, beatsPerBar: 4, offsetSeconds: 0 };
```

`bpm` er påkrævet og skal ligge mellem 1 og 1000. `beatsPerBar` er som standard 4 og skal være et positivt heltal. `offsetSeconds` er som standard 0 og angiver, hvor takt nul ligger i filmen, så et musiknummer med optakt, indtælling eller et øjebliks stilhed før nedslaget stadig passer. Både takter og slag tælles fra nul: Takt 0, slag 0 er første nedslag.

Et ugyldigt tempo kaster en fejl, når filmen defineres, ikke halvvejs gennem en rendering.

## Uret

`useTempo()` returnerer filmens ur. Den kaster en fejl, hvis filmen ikke angiver et tempo, fordi en sekvens placeret på en takt i en film uden takter er en fejl, der bør vises, frem for lydløst at give nul.

| Medlem | Returnerer |
| --- | --- |
| `tempo` | Det `{ bpm, beatsPerBar?, offsetSeconds? }`, filmen har angivet. |
| `beatsPerBar` | Den afklarede værdi, 4 hvis filmen ikke angav noget. |
| `bar(n)` | Det billede, takt `n` starter på. |
| `beat(n)` | Det billede, slag `n` starter på. Brøkdele af slag er tilladt, så `beat(0.5)` er ottendedelen efter nedslaget. |
| `barAt(frame)` | Den takt inklusive brøkdel, som et billede falder på. Negativ før `offsetSeconds`. |
| `beatAt(frame)` | Det slag inklusive brøkdel, som et billede falder på. Negativ før `offsetSeconds`. |
| `onBeat(frame, division?)` | Om et slag eller en underdeling af et slag lander på præcis dette billede. `division` er 1 for hvert slag, 2 for ottendedele, 4 for sekstendedele; værdien skal være et positivt heltal. |

Samme matematik eksporteres som almindelige funktioner til kode uden for en komponent: `barFrame`, `beatFrame`, `barAtFrame`, `beatAtFrame`, `isOnBeat`, `secondsPerBeat`, `resolveMusicalTiming` og `createTempoClock`.

```ts
import { beatFrame, createTempoClock, secondsPerBeat } from "@zxn/motion-core";

const clock = createTempoClock({ bpm: 120 }, 30);
clock.bar(2);                              // 120
clock.beat(0.5);                           // 8, the first eighth note
beatFrame({ bpm: 120 }, 30, 4);            // 60
secondsPerBeat({ bpm: 96 });               // 0.625
```

## Sekvenser på gitteret

`<Cue>` tager både musikalsk tidsstyring og billeder: `startBar`, `startBeat`, `lengthBars` og `lengthBeats`. Takter og slag lægges sammen, så `startBar={2} startBeat={1}` er andet slag i takt to.

```tsx
<Cue startBar={2} startBeat={1} lengthBeats={2}>
  <Flash />
</Cue>
```

En længde angivet i takter eller slag måles fra den afklarede start i stedet for at blive lagt til som et fast antal billeder, så en sekvens på to takter dækker præcis to virkelige takter, uanset hvor den starter. Ved en billedfrekvens, der ikke deler et slag jævnt, er det ikke samme antal billeder, og et fast antal ville forskyde sig i forhold til musikken undervejs i filmen. Inde i en sekvens genstarter `frame` ved nul, præcis som i en sekvens placeret med billeder.

Musikalsk tidsstyring i en film uden tempo kaster en fejl med en forklaring. Det samme gælder en sekvens, hvis musikalske længde bliver under ét billede.

## Én konvertering, én afrunding

Hver takt og hvert slag i systemet omsættes gennem `beatFrame`. Det er det eneste sted, hvor musikalsk tid bliver til et heltalligt billednummer, og derfor lander et klip og et blink, der er skrevet til samme nedslag, på samme billede frem for med ét billedes mellemrum. Konvertering hos hver kalder er vejen til en film, hvor titlen rammer slaget, og understregningen kommer lige efter — den slags fejl, der er usynlig i et stillbillede og tydelig i bevægelse.

Hvert slag beregnes ud fra sit eget indeks i stedet for ved at lægge et slags antal billeder til det foregående. Akkumulering afrunder én gang pr. slag, og fejlen vokser: Ved 29,97 fps varer et slag på 120 bpm 14,985 billeder, så afrunding til 15 hver gang tilføjer en hundrededel billede pr. slag, og ved takt 200 er billedet hele billeder foran musikken. Beregning fra indekset afrunder én gang, og testene holder hvert slag gennem 200 takter inden for et halvt billede ved 30, 29,97 og 24 fps.

`onBeat` er sand på præcis ét billede pr. underdeling: Den finder den underdeling, billednummeret ligger nærmest, konverterer tilbage gennem `beatFrame` og sammenligner. Et blink, den udløser, varer ét billede i stedet for at flimre over to, og intet slag overses, fordi det ønskede billede faldt mellem to underdelinger. Før tempoets forskydning er værdien altid falsk — der findes ikke et slag minus én.

## Gennemgang af opskriften på en slagpuls

[`examples/recipes/beat-pulse.tsx`](https://github.com/zinxan/motion-graphics/blob/main/examples/recipes/beat-pulse.tsx) er den mindste film, der bruger alt dette: Fire ord, der skifter på takter, en ring, der pulserer på hvert slag, og fire prikker, der viser, hvor i takten du er.

Ordene er fire sekvenser på én takt hver. Intet i dem er et billednummer, så ændring af `bpm` flytter alle klip.

```tsx
      {words.map((word, index) => (
        <Cue key={word} startBar={index} lengthBars={1}>
          <FullFrame style={{ alignItems: "center", justifyContent: "center" }}>
            <div style={{ font: "900 260px Inter, system-ui, sans-serif", color: "#fff", letterSpacing: "-0.05em", transform: `scale(${1 + 0.1 * kick})` }}>{word}</div>
          </FullFrame>
        </Cue>
      ))}
```

Pulsen er ikke `onBeat`. En boolesk værdi på ét billede passer til et blink, men en puls skal aftage over flere billeder, så filmen spørger, hvor langt den er forbi det seneste slag, og omsætter det til en faldende værdi.

```tsx
  // How far through the current beat we are: 0 on the beat, 1 just before the next.
  const beat = tempo.beatAt(frame);
  const sinceBeat = frame - tempo.beat(Math.floor(beat));
  const kick = mapRange(sinceBeat, [0, 7], [1, 0], { clamp: true, ease: easing.easeOut });
```

`beatAt` giver slaget inklusive brøkdel; afrunding nedad angiver det senest påbegyndte slag; `beat()` omsætter indekset tilbage til det billede, det landede på. Subtraktionen er derfor i faktiske billeder, og pulsen aftager over de samme syv billeder ved ethvert tempo. Den ene `kick`-værdi styrer både den voksende ring og ordenes skalering, så de ånder sammen.

Prikkerne er positionen i takten aflæst direkte fra uret:

```tsx
        {Array.from({ length: 4 }, (_, dot) => (
          <div key={dot} style={{ width: 30, height: 30, borderRadius: "50%", background: "#fff", opacity: Math.floor(beat) % 4 === dot ? 1 : 0.28 }} />
        ))}
```

Og selve tempoet, med filmens længde skrevet i takter frem for gættet:

```tsx
  // Four bars of four beats at 120 bpm is eight seconds.
  frames: 30 * 8,
  tempo: { bpm: 120, beatsPerBar: 4 },
```

## En sequencer på ottendedele

`onBeat` med en `division` styrer noget hurtigere end pulsslaget. Denne film kører et gitter med otte trin og en equalizer med tolv bånd på ottendedele: Felterne lyser på skift, båndene hopper til en ny højde med fast startværdi på hver ottendedel og falder tilbage, og hele billedet lysnes i ét billede på hvert nedslag.

```tsx
import { FullFrame, defineFilm, easing, mapRange, seededRandom, useTempo, useTimeline } from "@zxn/motion-core";

type Props = {
  readonly label: string;
  readonly accent: string;
  readonly background: string;
};

const steps = 8;
const bands = 12;
const barHeight = 360;

function Sequencer({ label, accent, background }: Props) {
  const { frame, film } = useTimeline();
  const tempo = useTempo();

  // Which eighth note this frame belongs to, and how far past its start it is.
  const eighth = Math.max(0, Math.floor(tempo.beatAt(frame) * 2));
  const step = eighth % steps;
  const sinceEighth = frame - tempo.beat(eighth / 2);
  const attack = mapRange(sinceEighth, [0, 8], [1, 0], { clamp: true, ease: easing.easeOut });

  // A one-frame lift on the beat, and a brighter one on the downbeat.
  const pulse = tempo.onBeat(frame) ? 1 : 0;
  const downbeat = tempo.onBeat(frame) && step === 0 ? 1 : 0;
  const out = mapRange(frame, [film.frames - 14, film.frames - 1], [1, 0], { clamp: true, ease: easing.easeIn });

  return (
    <FullFrame style={{ background, flexDirection: "column", alignItems: "center", justifyContent: "center", gap: 70, opacity: out }}>
      <div style={{ position: "absolute", inset: 0, background: accent, opacity: 0.05 * pulse + 0.1 * downbeat }} />
      <div style={{ font: "800 40px Inter, system-ui, sans-serif", color: "#f3efe7", letterSpacing: "0.36em", opacity: 0.55 + 0.35 * attack }}>
        {label}
      </div>

      <div style={{ display: "flex", alignItems: "flex-end", gap: 16, height: barHeight }}>
        {Array.from({ length: bands }, (_value, band) => {
          // A new height per band per eighth, seeded so it is the same every render.
          const target = 0.2 + seededRandom(`band-${String(band)}-${String(eighth)}`) * 0.8;
          const height = barHeight * (0.12 + (target - 0.12) * attack);
          return (
            <div
              key={band}
              style={{ width: 56, height, borderRadius: 12, background: accent, opacity: 0.45 + 0.55 * (height / barHeight) }}
            />
          );
        })}
      </div>

      <div style={{ display: "flex", gap: 20 }}>
        {Array.from({ length: steps }, (_value, pad) => {
          const live = pad === step;
          return (
            <div
              key={pad}
              style={{
                width: 86,
                height: 86,
                borderRadius: 20,
                border: `3px solid ${accent}`,
                background: live ? accent : "transparent",
                opacity: live ? 0.4 + 0.6 * attack : 0.3,
                transform: `scale(${String(live ? 1 + 0.08 * attack : 1)})`,
              }}
            />
          );
        })}
      </div>
    </FullFrame>
  );
}

export const films = [defineFilm({
  id: "sequencer",
  title: "Sequencer",
  width: 1920,
  height: 1080,
  frameRate: 30,
  // Eight bars of four beats at 128 bpm, rounded up to a whole second.
  frames: 450,
  tempo: { bpm: 128, beatsPerBar: 4 },
  component: Sequencer,
  defaultProps: { label: "EIGHT ON THE FLOOR", accent: "#7a6cff", background: "#0b0c0e" },
  controls: {
    label: { type: "text", label: "Label" },
    accent: { type: "color", label: "Accent" },
    background: { type: "color", label: "Background" },
  },
})];
```

@video(sequencer)

Tre ting er værd at fremhæve. Trinindekset kommer fra `beatAt(frame) * 2` — slaget inklusive brøkdel skaleret med underdelingen — så gitteret og uret ikke kan være uenige. Aftagningen kommer fra `frame - tempo.beat(eighth / 2)` og bruger et slagindeks med brøkdel, fordi `beat()` accepterer det. Og højderne kommer fra `seededRandom` med både båndet og ottendedelen som nøgle, hvilket gør dem vilkårlige, men faste: Træk tilbage til billede 120, og søjlerne står præcis, hvor de stod før.

## Arbejd med rigtig musik

**En film indeholder ingen repræsentation af lyden.** Lyd ligger uden for rammen her; lyd hører hjemme på værtens tidslinje ved siden af det klip, filmen renderes til. Filmens tempo aflæses derfor ikke fra musiknummeret; det angives om det, og de to stemmer kun overens, fordi du har sørget for det.

Match to tal. `bpm` er musiknummerets tempo: Find det i filens metadata, din DAW, butikssiden, hvor du købte det, eller tæl slag i tredive sekunder og gang med to. `offsetSeconds` er der, hvor første nedslag i takt nul falder i den færdige redigering — stilheden før musiknummeret starter plus en eventuel optakt før første nedslag. Er `bpm` lidt forkert, glider filmen ud af takt over sin længde; er `offsetSeconds` forkert, er afvigelsen konstant, hvilket er lettere at høre og rette.

Vælg om muligt en billedfrekvens, hvor et slag er et helt antal billeder. Ved 120 bpm er et slag et halvt sekund: 15 billeder ved 30 fps og 12 ved 24. Så lander hvert slag på et præcist billede, og intet skal afrundes.

Når det ikke går op — 128 bpm ved 30 fps giver 14,0625 billeder pr. slag — går intet i stykker. Slagene lander på nærmeste billede, op til et halvt billede, cirka 17 millisekunder, fra musikkens placering, og afstanden mellem på hinanden følgende slag skifter mellem 14 og 15 billeder. Det er under grænsen for, hvornår et klip opleves som forsinket, og fordi hvert slag beregnes ud fra sit eget indeks, akkumuleres fejlen aldrig. Du bør ikke selv afrunde og derefter tælle i billeder.

Arbejder du med billedfrekvenser til tv, så bemærk, at 29,97 fps er `30000 / 1001`, ikke 30. Angiv den virkelige værdi som filmens `frameRate`, så følger uret den.

## Næste skridt

- [Kompositions-API](/da/motion/composition-api) for den fulde reference.
- [Tænk i billeder](/da/motion/thinking-in-frames) om, hvorfor intet her aflæser et ur.
- [Opskrifter](/da/motion/recipes) for resten af eksemplerne.
- [Film om brugergrænseflader](/da/motion/interface-films), hvis det, du klipper til musik, er en produktgennemgang.

