Skip to main content

Turning Terminal into a Fire-Breathing Synth

·662 words·4 mins·

Weekend outcome: Saw @thorstenball challenge devs to make OpenSimplex noise dance in the terminal in his Joy & Curiosity newsletter. I went further colour palettes, character sets and a tiny audio engine. Here’s what popped out.

Demo
#

Ingredients
#

PieceLibrary / Tool
Noisehttps://deno.land/x/open_simplex_noise
RuntimeDeno 2.x
ColourANSI 256-colour escapes
SoundWAV buffer → afplay (macOS)
Recorderasciinema + ffmpeg

How it works (quick tour)
#

  • NoisemakeNoise4D() gives a 4-D field. Feed (x,y,t,w) where t increments every frame and w drifts slowly ➜ organic motion.
  • Characters – map the value from (-1,1) into an ASCII set (░▒▓█, .-:=+*#, …).
  • Colour – same value selects a colour from the active palette (fire, ocean, ice…).
  • Sound – every third frame take the noise at (0,0,t,w), scale to ~220–440 Hz, write a 100 ms WAV, pipe to afplay, delete file.
  • Controls←/→ speed • ↑/↓ zoom • c/v palette • z/x charset • s/l presets • q quit.

Key Code Snippets
#

The Core Animation Loop
#

function animate() {
  clearScreen();

  for (let y = 0; y < height; y++) {
    let row = "";
    for (let x = 0; x < width; x++) {
      const n = noise(x / zoomX, y / zoomY, t, w);
      row += getColor(n) + getChar(n);
    }
    console.log(row);
  }

  printHUD();

  // Audio generation every 3rd frame
  if (frameCount++ % 3 === 0) {
    const toneNoise = noise(0, 0, t, w);
    const freq = 220 + toneNoise * 220;  // 220-440 Hz range
    playTone(freq).catch(() => {});
  }

  t += speed;
  w += 0.01;
}

WAV Generation Magic
#

function generateWavData(freq: number, durationMs = 100): Uint8Array {
  const sampleRate = 44100;
  const numSamples = (durationMs / 1000) * sampleRate;
  const bytesPerSample = 2;
  const buffer = new ArrayBuffer(44 + numSamples * bytesPerSample);
  const view = new DataView(buffer);

  // WAV Header (44 bytes)
  writeStr(0, "RIFF");
  view.setUint32(4, 36 + numSamples * bytesPerSample, true);
  writeStr(8, "WAVE");
  writeStr(12, "fmt ");
  view.setUint32(16, 16, true); // Subchunk1Size
  view.setUint16(20, 1, true);  // PCM format
  view.setUint16(22, 1, true);  // mono
  view.setUint32(24, sampleRate, true);
  
  // Generate sine wave samples
  for (let i = 0; i < numSamples; i++) {
    const t = i / sampleRate;
    const sample = Math.sin(2 * Math.PI * freq * t);
    const intSample = Math.floor(sample * 32767);
    view.setInt16(44 + i * 2, intSample, true);
  }

  return new Uint8Array(buffer);
}

Color & Character Mapping
#

function getColor(n: number): string {
  const colors = palettes[currentPalette];
  const i = Math.floor((n + 1) / 2 * (colors.length - 1));
  return colors[Math.min(i, colors.length - 1)];
}

function getChar(n: number): string {
  const charset = charsets[currentCharset];
  const i = Math.floor((n + 1) / 2 * (charset.length - 1));
  return charset[Math.min(i, charset.length - 1)];
}

Full repo: https://github.com/rockey5520/open_simplex_noise

Coding approach / file breakdown
#

Goal: keep everything hack-friendly no external build, pure Deno.

open_simplex_noise/
├─ noise_ascii.ts   # main loop: render, input, audio trigger
├─ sound.ts         # minimal WAV writer + platform player
└─ preset.json      # saved via `s` hotkey

Key decisions
#

ConcernChoice & why
RuntimeDeno 2.x easy URL imports, single file run.
Raw inputDeno.stdin.setRaw(true) to capture arrow keys without waiting for Enter.
Animation loopsetInterval(animate, 50) gives ~20 FPS; plenty for ANSI.
Noise sampling(x/zoomX, y/zoomY, t, w) keeps code readable; tweak zoom at runtime.
WAV generationHand-write header (44 bytes) + 16-bit mono samples; no deps.
Temp filesDeno.makeTempFile() ➜ play ➜ delete; avoids lingering files.
Platform audioafplay for macOS, aplay/paplay if Linux; swap one line.

Run it locally
#

# macOS
deno run --allow-run --allow-read --allow-write noise_ascii.ts

(Linux: edit sound.ts, replace afplay with aplay or paplay)

What’s next (maybe)
#

  • Port to Canvas + Web Audio.
  • More palettes: Added 7 total (fire, ocean, sunset, neon, forest, sandstorm, ice)
  • More charsets: Added 6 total (classic, blocks, lines, bars, wide, symbols)

Credits & shout-outs
#

If you remix this, tag me (@RakeshMothukuri) always keen to see odd hacks.