10 KiB
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, scrollers of five kinds (sine, bounce, zoom, spin, round a
circle), a 3D scroller, and a perspective crawl, flashes and strobes, chrome and glass materials, a camera-filling
quad, a torus knot, and three effects worked out a pixel at a time in Lua every frame and handed to
a sprite with spriteSetPixels: a plasma with a cycling palette, a lookup-table tunnel, and a
tape's snow. Nothing in any of the three demos is a video.
Every demo opens on the Singe logo, faded up from black to white, held, and faded out, before its music starts, and closes the same way on the Kangaroo Punch logo after the music ends and the picture fades to black. The logos are rendered from assets/ into demos/art/ by util/demoLogos.sh. They are on the demo's own clock (the opening card runs from -3.5 s to 0), so util/demoCapture.sh takes them with a negative start or a stretch past the end.
Scroller text can be changed freely. Each scroller is given the bars it runs between, not a speed:
K.scroller(t, font, text, y, amp, startTime, endTime, style) works its speed out from the text so
the first letter enters as the scene starts and the last leaves as it ends. Text too short to fill
the time at a readable speed is repeated until it does; text too long to read at its speed still
ends on time and puts a line in the trace (DEMO scroller too long to read, with about how many
characters would fit). K.scroller3D(t, blocks, text, y, startTime, endTime) is timed the same
way, with solid letters from K.blockFont(glyphs, camera, fov, cell, alpha): a pixel font
extruded into blocks, hung off the camera in the scene's front layer (nodeSetLayer), so they
stand over 2D and 3D alike. Its letters are larger, so it is allowed to go slower before it
repeats. The text round the circle is written in over the first half of its scene,
and the crawl spaces whatever lines it is given over its scene, the same way.
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, fast: two streetlights pass the car on every beat, a 3D city on the horizon; a bouncing scroller |
| 29-30 | bass out | the car flies off the crest in slow motion, the road dropping away below, and lands on bar 31 |
| 31-38 | main section | the chrome knot and glass, a cut on every downbeat; a spinning scroller |
| 39-46 | main section | slaloming round the towers of a neon city, a swerve a bar and a barrel roll every two |
| 47-48 | filtered drop | the tape rewinds the flight |
| 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-64 | loud | plasma, a rotozoomed logo, a ring of palms, text written round a circle |
| 65-68 | loud | low and fast over neon mountains toward the sun, under a perspective crawl of Singe's features |
| 69-76 | breakdown, then the kick | a cloth flag waving on the night grid, gusting with the kicks once they return; a zooming scroller |
| 77-78 | riser, dead stop | into hyperspace, white, and black for the stop |
| 79-94 | finale | road, city, knot, and mountains, cut every two bars, fireworks on the snares; over the road the logotype and the dragon, beating its wings on the beat |
| 95- | ending | the set switches off |
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-28 | full | a mirrored kaleidoscope of plasma under vector bobs |
| 29-32 | a cloud of glowing dots that takes a new shape every bar | |
| 33-40 | choppy again | the club, cut on every beat, black in the gaps, the lasers changing pattern every bar |
| 41-48 | denser hats | the twister, drawn a scanline at a time, over copper bars |
| 49-52 | horns | a checkered ball bouncing on the beat; a ball drops on every snare and piles up |
| 53-56 | an equalizer city, a band of the music to each column and its last moments running back | |
| 57-60 | glenz glass in a tunnel scrubbed by the kick | |
| 61-64 | a turntable, the record scratched on every snare, the camera cutting every two beats | |
| 65-72 | dense | lasers in the haze: 24 beams in six patterns, a new one every two beats |
| 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, cutting faster: club, dots, equalizer, club spinning, turntable, club |
| 113- | the last downbeat | the ball shatters and the lights go down |
Insert Coin (bar n starts at 0.0067 + (n - 1) x 1.77778 s)
From bar 1 to bar 121, a large scroller of translucent 3D block letters (FEATURE_TEXT near the
top of insertCoin.singe, in the demo's own pixel font from art/font.lua) runs over every era,
saying what Singe is and what it can do.
| 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 | analog: 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-48 | full | vector: glowing wireframe asteroids, split by the ship's aimed shot on every snare |
| 49-56 | full | 32-bit: a flat-shaded ship weaving a winding path through gates, one a beat, banking into the turns |
| 57-64 | heavy | light gun: targets slide along three rails, and each snare flashes the screen and drops the one under the crosshair |
| 65-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-88 | melody returns | versus: two fighters trade blows on the kicks and snares, ROUND 1, FIGHT!, K.O. |
| 89-96 | melody | the boss, firing on the kick, the hats, and the snare |
| 97-104 | full | the bonus stage: coins rain down far along the tunnel and ride its floor off the screen |
| 105-112 | full | rhythm: a note highway whose notes are the song's own kicks, snares, and hats |
| 113-120 | melody | the high score table, which is the credits |
| 121-136 | final | every era again, a bar each, then GAME ON |
| 137- | outro | GAME OVER, and the set switched off on the last hit |
Making them again
Everything in art/ and sync.lua is generated:
util/neonCoastArt.py,util/mirrorballArt.py,util/insertCoinArt.pydraw each demo's pictures (PIL and numpy).util/neonCoastCar.shrenders Neon Coast's car with Singe's own 3D renderer (util/neonCoastCar.singebuilds the model): seven steering angles, each drawn over black and over white so every pixel's true alpha comes out of the pair.util/demoSync.pywritessync.luafrommusic.mp3. It needs librosa, which Singe does not:python3 -m venv env && env/bin/pip install librosa.
util/demoRecord.sh neonCoast out.mp4 records a whole demo with its music, using Singe's own
recorder: --deterministic and --record together keep every frame and generate the sound in
step with the picture, and Neon Coast's three minutes take about two and a half on a desktop.
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.