singe/demos/README.md

114 lines
6.7 KiB
Markdown

# Singe demos
Three demos in the old demoscene way: a song each, and everything on screen timed to it. Every
one runs live, nothing on screen is a recording of the demo itself, and every motion takes its time
from the music's clock (`musicGetPosition`), so a demo stays with its song on any machine at any
frame rate.
| Demo | Song | Length |
|---|---|---|
| Neon Coast | "Neons in Napoli" by eliza_music | 3:08 |
| Mirrorball | "Vintage Funky Groove" by miiira | 4:05 |
| Insert Coin | "Game Is On" by poorartistt | 4:10 |
Each folder has its own `games.dat`, so Singe runs a demo like any game: from this folder,
`Singe neonCoast/neonCoast.singe`. They need the 3D scene. `kit.lua` is the code all three share:
the music clock, beats and bars, the envelopes and drum hits, the scene runner, and the effects
more than one demo uses (vector bobs, the sine scroller, flashes and strobes, chrome and glass
materials, a camera-filling quad, a torus knot).
## How the timing works
`util/demoSync.py` listens to a song once, offline, and writes `sync.lua` beside the demo:
- **Beats.** A song on a fixed tempo gets a grid, found by folding the onsets of the whole track at
every candidate period and keeping the sharpest; Neon Coast's is 122.000 beats a minute and
Insert Coin's 135.000, with the tracked beats within 4 ms of them. Mirrorball's song speeds up
from about 111 to 117, so it gets the tracker's beats, each moved to the strongest onset near it
and smoothed through its neighbours, and carried on past where the tracker gave up.
- **Bars.** Beat one is the beat where the loudness and the bass jump most often, since songs
change section on a downbeat.
- **Envelopes.** Kick, snare, and hat (the change in each drum's frequency band), bass, loudness,
and brightness, sixty samples a second, each 0 to 1, packed one character a sample.
- **Hits.** The times of the kicks, snares, and hats, for effects that fire once per hit.
A scene in a demo says which bars it spans, so cuts land on downbeats, and inside it effects read
the envelopes and hits at the current time: a flash that decays from every kick, a formation that
steps on every beat, a tunnel whose speed the kick pushes.
## The songs, and what happens in them
**Neon Coast** (bar n starts at 0.1008 + (n - 1) x 1.96721 s)
| Bars | Song | Demo |
|---|---|---|
| 1-3 | pads only | a tape starts: snow, the deck's lettering, the title typed out |
| 4-16 | bass in | the sunset grid; the chrome logotype rises at 9, the title slams in at 13 |
| 17-18 | bass out, snare build | the logotype turns, the fog closes in, strobes on the snare roll |
| 19-28 | full groove | the night drive: a streetlight passes the car on every beat |
| 29-30 | bass out | the car takes a crest and flies, in slow motion |
| 31-46 | main section | the chrome knot and glass, a cut every two bars; plasma billboards from 39 |
| 47-48 | filtered drop | the tape rewinds |
| 49-58 | busy hats | the tunnel, scrubbed by the kick; vector bobs and the scroller |
| 59-60 | bass out | the tunnel stops, the bobs fall in, ARE YOU READY |
| 61-68 | loud | plasma, a rotozoomed logo, a ring of palms |
| 69-76 | breakdown, then the kick | a flag on the night grid, blown by the kicks once they return |
| 77-78 | riser, dead stop | into hyperspace, white, and black for the stop |
| 79-94 | finale | the road and the chrome, cut every two bars, fireworks on the snares |
| 95- | ending | the set switches off; credits |
**Mirrorball** (eight-bar phrases from bar 1 at 0.79 s; the bars shorten as the song speeds up)
| Bars | Song | Demo |
|---|---|---|
| 1-8 | choppy drums and bass | copper bars gated by the groove, the title dropping a letter at a time |
| 9-16 | chords | the club: the ball comes down, spots sweep, the floor swaps on every beat |
| 17-24 | lead | a rotozoomer, copper bars coloured by the note being played, the scroller |
| 25-32 | full | a mirrored kaleidoscope of plasma under vector bobs |
| 33-40 | choppy again | the club, cut on every beat, black in the gaps |
| 41-48 | denser hats | the twister, drawn a scanline at a time, over copper bars |
| 49-56 | horns | a checkered ball bouncing on the beat; a ball drops on every snare and piles up |
| 57-64 | | glenz glass in a tunnel scrubbed by the kick |
| 65-72 | dense | lasers in the haze |
| 73-80 | | plasma and a rotozoomed logo |
| 81-88 | breakdown | the dark club; the floor relights a tile a beat when the bass returns |
| 89-96 | build | everything back on, strobing |
| 97-112 | full, then final | the party, then everything at once |
| 113- | the last downbeat | the ball shatters; credits |
**Insert Coin** (bar n starts at 0.0067 + (n - 1) x 1.77778 s)
| Bars | Song | Demo |
|---|---|---|
| 1-4 | intro | a tube warms up; INSERT COIN, and a coin drops on the downbeat of 5 |
| 5-8 | drums in | 1-bit: bat and ball in phosphor, the ball meeting a bat on every beat |
| 9-16 | groove | 8-bit: a formation that steps on the beat; the cannon fires on the snare |
| 17-24 | lead melody | 16-bit: a runner over parallax hills, jumping on the backbeat |
| 25-32 | lead melody | 16-bit: a kart round a track seen in perspective |
| 33-40 | breakdown, riser | a tape loading |
| 41-56 | full | 32-bit: a flat-shaded ship through rings, one a beat, snapped to a grid |
| 57-72 | heavy | now: chrome balls into a wall of glossy bricks, real physics, on the kick |
| 73-80 | breakdown | GAME OVER, and a countdown to continue, a number a bar |
| 81-96 | melody returns | the boss, firing on the kick, the hats, and the snare |
| 97-112 | full | the bonus stage: coins pour in on the beat |
| 113-120 | melody | the high score table, which is the credits |
| 121-136 | final | every era again, two bars each, then GAME ON |
| 137- | outro | GAME OVER, and the set switched off on the last hit |
## Making them again
Everything in `art/`, `video/`, and `sync.lua` is generated:
- `util/neonCoastArt.py`, `util/mirrorballArt.py`, `util/insertCoinArt.py` draw each demo's
pictures (PIL and numpy).
- `util/demoVideo.py` precalculates the per-pixel effects as videos: the tunnel (every frame a
keyframe, so a demo can seek to any frame and run the tunnel at the music's speed), the plasma,
and the tape's snow.
- `util/demoSync.py` writes `sync.lua` from `music.mp3`. It needs librosa, which Singe does not:
`python3 -m venv env && env/bin/pip install librosa`.
`util/demoCapture.sh neonCoast 0 190 [every] [out.mp4]` runs a stretch of a demo headless on a
clock that moves exactly one 20 ms step a frame, saves the frames, and with `every` = 1 puts the
same stretch of the music under them in an MP4. The engine plays the music on real time, so a
capture is silent and the song is added afterwards; a demo run normally plays it itself. Set
`DEMO_STATS = true` before loading a demo to have it print its frame rate every second.