Skip to content

vsariola/tubiform

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

78 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

brainlez Coders! : tubiform

A 256 byte DOS intro written by pestis / bC!, submitted to the Lovebyte 2022 256b intro competition. tubiform.com is the official COVOX version submitted to the competition. spkrtubi.com is a fallback PC speaker version, in case you don't have COVOX and still want to enjoy at least some sound. Note that the DOSBox does not emulate the speaker that well, but it sounds more reasonable on real machines.

Source code: https://github.com/vsariola/tubiform

Capture: https://youtu.be/FYwOUeJrgLo

Tested to be working on DOSBox, FreeDOS (on a modern laptop) and MS-DOS 6.22 (on a Pentium MMX 233). One effect is glitching when the CPU type is other than dynamic in DOSBox. Maybe it's not happy with the way the intro messes with the FPU stack.

Based on DOSBox cycle estimates, I would've estimated it to be watchable on a 66 MHz 486, but it was already kinda blurry on the 233 MHz Pentium. If you are experiencing issues running this on actual hardware, please get in touch (pestis @ Sizecoding & Demoscene discords).

There is a dosbox.conf file included in the repo; not sure if this the best configuration to watch the intro, but that's what it was developed on.

Massive love to everyone answering my clueless questions on the sizecoding discord #x86 channel: superogue, HellMood, TomCat, baze, Ped7g, byteobserver, ttg, DevEd/AYCE, Dresdenboy, Kuemmel, Harekiet, unlord, DrClaw. Seriously guys, you are awesome! I tried my best to put your generous advice into good use.

Building

You need nasm. Then:

nasm tubiform.asm -fbin -o tubiform.com
nasm spkrtubi.asm -fbin -o spkrtubi.com

or just run the build.bat.

Statistics

  • Init & deinit: 45 bytes
  • Visuals: 71 bytes
  • Music player & sync / script engine: 85 bytes
  • Song data: 39 bytes
  • Script data: 16 bytes
  • Total: 256 bytes

Technical

  • I started with the tracked music. The idea of the player routine is similar to the one I used in the TIC-80 intro cracklebass, but heavily simplified. There are three channels. Each channel is one octave apart, and plays notes at different speeds: bass channel is 2x faster than mid channel, which in turn is 2x faster than the treble channel. Each channel has it's own order list, which advances at the same time for all the channels. This means that, before the order list advances, the bass channel has repeated a pattern more times than the treble channel. This trick allows the bass and the treble play exactly the same pattern, but it does not quite sound the same, and the loop length is the treble channels loop length.
  • Each note has a linearly decaying envelope and the waveform is a square wave. Instead of a square wave that has positive and negative amplitude, it goes between zero and positive value, so depending on the oscillator phase, we either add the channel envelope value to the total sample value or we don't.
  • For each pattern, the order list contains two nibbles: one for chord, and one for index in the pattern table. All note frequencies in the pattern are multiplied by the chord nibble. The I-IV-V chord progression was not done because I particularly like it, but because they have ratios that can represented with integers that fit a nibble: I = 6, IV = 8 and V = 9. This gives the classic Pythagorean tuning 4/3 ratios for the fourths and 3/2 ratios for the fifths.
  • Similarly, the pattern notes are stored with integer frequency values, with the ratios chosen according to the Pythagorean tuning. The patterns were packed so that they slightly overlap: the last note of one pattern is the first note of another pattern. The patterns are stored backwards, because running time backwards was an easier way to make linearly decaying envelopes, instead of linearly attacking envelopes.
  • The envelopes of the three channels are stored and added to a few places, to make visuals sync to music.
  • There is also a "script": when the order list advances, the script contains two bytes: one byte indicating the address of the byte to change, and another byte indicating the new value. Mostly, this script is used to mutate the code for the visuals. In particular, we mutate the x87 FPU instructions, which are two bytes long. Typically, the first byte is D9 and we change the second byte. Basically, all the visuals are some glitched out variation of the classic tunnel effect. The visuals were prototyped with Shadertoy, where I tested what would happen if I replace tan with sin etc. Some of the variations totally mess up FPU stack: some of the instructions just modify st0, while others push new values on the stack. We don't care if the FPU stack overflows regularly as long as it gives nice visuals.
  • We also change where one particular constant is pulled in the visuals. In the beginning, it points to a location with static data, but after the first pattern, we change it to point to the location containing the time counter, so the visuals start to move with time.
  • We also use the script to change how much is added to the pixel color, to make the intro go from black and white to a more colorful variation in the last moments of the intro.
  • The last thing the script does is to mutate the exit condition: instead of the classic esc check
in al,60h
dec ax
jnz main

we mutate the jnz label target to point to the dec ax i.e.

in al,60h
decrease:
dec ax
jnz decrease

So ax immediately loops to 0 regardless of the button pressed, and the intro exits. As an added bonus, in both situations (esc pressed or song ends), we can now be sure that ax is 0.

  • The music player is hooked to the interrupt 08h. Using interrupt 1Ch could save us from issuing End-of-Interrupt ourselves. However, this would mean that also the native DOS 08h interrupt gets called at a very high rate, so the system time would run forward a lot. Hooking to 08h meant that the system time of day only only misses the time the intro is running.
  • Overall, I tried to make the exit as clean as possible: restore the original interrupt handler and return back to text mode. This is regardless of how the intro ends: if the song ends or if the user presses ESC. I was told you get extra cool points for clean exits.

License: MIT

Screenshot of the intro