2026年時点の4kintroの作り方の一例。

今日AkibaExecutableParty行ってきて、Sointuすげえなってなったのもあり、
現状で作ることになる4kb introの開発手順を思いついて作ってみた。

できるだけコンパイル周りを明確にしておくのが目的。

目的

準備が簡単なデモの作り方を確認する。
色々お試しできる環境が必要。かつ簡単な単位で生成していき、自動化ができるようにする。

IDE依存で作る場合、コンパイラオプションが山のように付くためどれが必要かが分かりづらくなる。最近だと最適化やセキュリティチェック用のオプションも多くなってきており、気軽にコンパイルができないため、そういうのをなくした単純コンパイルが可能な環境を用意しておく。

音楽

Sointuを使う。
vsariola/sointu: Fork of 4klang that can target 386, amd64 and WebAssembly. Tools run on Windows, Mac & Linux

これでソングをソースコードとして吐き出す。
名前.asm、名前.h、名前.incが出力されていたら成功。

ヘッダのディレクティブは32bit環境の記述を前提としているので、基本的には32bit環境でコンパイルするのが想定の模様。

sointu-compile -arch 386 -os windows song.yml

ウィンドウ周り

適当に書く。
今回のコードは、終了してもプロセス自体は死なないようで、タスクマネージャから終了してください。
本来だとこういうの作っちゃあかんけど、テストなのでご勘弁を。

ちなみにstdcallにしている理由は、Sointuの生成されたヘッダ部分に「__stdcall」と書いてあるため。
つまりSointuに合わせて環境調整したともいえる。

main.c

#include <windows.h>
#include <mmsystem.h>
#include <mmreg.h>

#ifdef __cplusplus
extern "C" {
#endif
    // Sointuが1回で全楽曲をレンダリングする関数
    void __stdcall su_render_song(void* buffer);
#ifdef __cplusplus
}
#endif

#define SAMPLE_RATE 44100
#define CHANNELS 2

// 🎵 楽曲の長さに応じてここを設定(例: 44100Hz * 2ch * 180秒 ≒ 15,876,000)
#define SU_LENGTH_IN_SAMPLES (SAMPLE_RATE * CHANNELS * 180)

// 巨大なバッファですが、グローバル変数(BSS領域)に置くことでexeサイズは肥大化しません
float audio_buffer[SU_LENGTH_IN_SAMPLES];

HWAVEOUT hWaveOut;
WAVEHDR waveHdr;

LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) {
    if (msg == WM_DESTROY) { ExitProcess(0); }
    return DefWindowProc(hwnd, msg, wp, lp);
}

void __cdecl EntryPoint() {
    // 1. 先にSointuで「楽曲全体の波形」をすべて計算する
    su_render_song(audio_buffer);

    // 2. ウィンドウ生成
    HWND hwnd = CreateWindowA("edit", "Sointu Demo", WS_POPUP | WS_VISIBLE, 
                              0, 0, 640, 480, NULL, NULL, NULL, NULL);
    HDC hdc = GetDC(hwnd);

    // 3. オーディオデバイスの初期化
    WAVEFORMATEX wfx = {
        .wFormatTag = WAVE_FORMAT_IEEE_FLOAT,
        .nChannels = CHANNELS,
        .nSamplesPerSec = SAMPLE_RATE,
        .wBitsPerSample = 32,
        .nBlockAlign = (CHANNELS * 32) / 8,
        .nAvgBytesPerSec = SAMPLE_RATE * ((CHANNELS * 32) / 8),
        .cbSize = 0
    };

    waveOutOpen(&hWaveOut, WAVE_MAPPER, &wfx, 0, 0, CALLBACK_NULL);

    waveHdr.lpData = (LPSTR)audio_buffer;
    waveHdr.dwBufferLength = sizeof(audio_buffer);
    waveHdr.dwFlags = 0;
    
    waveOutPrepareHeader(hWaveOut, &waveHdr, sizeof(WAVEHDR));
    waveOutWrite(hWaveOut, &waveHdr, sizeof(WAVEHDR)); // これでバックグラウンドで最後まで自動再生されます

    // 5. グラフィック&メインループ
    int frame = 0;
    while (1) {
        MSG msg;
        if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) {
            DispatchMessage(&msg);
        }

        HBRUSH brush = CreateSolidBrush(RGB((frame) % 255, 50, 100));
        RECT rect = {0, 0, 640, 480};
        FillRect(hdc, &rect, brush);
        DeleteObject(brush);

        frame++;
        Sleep(16); // 約60fps
    }
}

コンパイル・オブジェクトコード生成

まずソングからオブジェクトコードを生成する。
ツールはNASMを使う。32bit環境を想定してコマンドを入力しておく。

64bit環境はヘッダにも書いてないので、基本的に考えない。
必要があれば自分で追加して

netwide-assembler/nasm: A cross-platform x86 assembler with an Intel-like syntax

nasm -f win32 song.asm -o song.obj

次にmain.cのコンパイルを行う。これはVisualStudioのコンパイラを使う。
VisualStudioをインストールしたあとで、フォルダ等を読み込んだコマンドプロンプト経由で行うのが簡単。
今回は32bit環境、x86 NativeTools Command Promptという方を利用した。

cl.exe /c /O1 /GS- main.c /Fomain.obj

最後にCrinklerでリンクする。
Crinklerも32bit/64bitが存在するので確認しておくこと。
(リリースにはどっちのバージョンも封入されてる)

runestubbe/Crinkler: Crinkler is an executable file compressor (or rather, a compressing linker) for compressing small 32-bit Windows demoscene executables. As of 2026, it is the most widely used tool for compressing 1k/4k/8k intros.

crinkler.exe /ENTRY:EntryPoint /SUBSYSTEM:WINDOWS main.obj winmm.lib user32.lib gdi32.lib kernel32.lib /OUT:demo.exe

これで4kbの実行ファイルが出力できる。
ちなみに音楽いれると1.6kbだった。音楽入れないと600byteを切る。

自分が4kb intro作った頃と比べて、だいぶとんでもない事になってる。

うまく動かないときは、Sointu側のアセンブリコード、ないしはmain関数を修正することになるので注意ね。