mz2synth: make sounds from images

E. Lamprecht’s MZ2SYNTH is a delightfully weird piece of code. It is an advanced wavetable synthesizer programmed only by an input image. Here’s an example:

Documentation is pretty sparse, so I’ve had to work it out as best I can:

  1. input data must be a 720 px high NetPBM PPM or PGM image with a black background
  2. waveforms are specified by pixel colour: sine, square, sawtooth and triangle are red, green, blue and luminance
  3. dynamics are manipulated by changing the pixel brightness
  4. the input plays at a constant rate along the horizontal pixels, defaulting to 10 pixels/second
  5. The pitch is specified by the Y coordinate. To convert from MIDI note number n to an input coordinate for mz2synth, use this formula:
    y=6×(140 – n)
    So for Middle C (MIDI note 60), the Y coordinate would be 480.

I’ve created a very simple example that plays a C major scale with simple sine waves with no dynamics.

The input image:

a black vertical strip with a red staircase pattern across the middle

The resulting audio:

And the python code that produced the image:

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# mz2-draw - draw a Cmaj scale in the right input format for mz2synth
# scruss, 2025-11
# mz2synth - https://github.com/frankenbeans/MZ2SYNTH
# command line:
#   mz2 -v -o mz2-cmaj.au mz2-cmaj.ppm

from PIL import Image, ImageDraw


# convert midi not number (20..127) to vertical offset
# for mz2 input
# notes < 20 (G#0) can't be played by mz2
def midi_to_y(n):
    return 6 * (140 - n)


middle_c = 60
maj_scale = (0, 2, 4, 5, 7, 9, 11, 12)
# maj_chord = (0, 4, 7)

# mz2 input must be 720 px high, preferably black bg
im = Image.new("RGB", (10 * len(maj_scale), 720), "black")
draw = ImageDraw.Draw(im)

for i, d in enumerate(maj_scale):
    # bright red lines mean full volume sine waves
    draw.line(
        [
            10 * i,
            midi_to_y(middle_c + d),
            10 * i + 8,
            midi_to_y(middle_c + d),
        ],
        "red",
        1,
    )

# mz2 can only read NetPBM PPM format
im.save("mz2-cmaj.ppm")

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *