Skip to content

Music: draw a wave and hear it

Music: draw a wave and hear it

Run it here, in your browser

Every sound is a wave, and the shape of the wave decides how it sounds. Drag the mouse inside the left box to draw one repeat of a wave, and let go to hear it. The right box is the spectrum: a bar for each pitch. A smooth wave has one tall bar, and a jagged wave has many. Four buttons give you a sine, a square, a saw and a soft wave to start from. (This one uses real time, so the picture changes from run to run.)

How it works

  • shape is a list of N=48 heights, from -1 to 1. mouse_pressed(), mouse_dragged() and mouse_released() change them. paint() moves the point nearest the mouse. The drag code fills the points the mouse jumped over, so a fast drag leaves no gaps.
  • rebuild() makes one second of sound from the shape. For each of the 44,100 numbers, it works out where that moment falls in the shape and blends two neighbouring points. PITCH = 220 repeats a second is the note A3.
  • f.create_sound(numbers) turns the list into a sound, and sound.loop() plays it. The sketch removes the average first, so the wave does not sit above zero.
  • f.draw_spectrum(sound, ...) draws the bars on the right.
  • f.create_button() makes the four buttons. button.clicked() is true on the frame it is pressed, and preset() builds the shape.
  • f.begin_shape(), f.vertex() and f.end_shape() draw the shape, and the same shape again four times at the bottom. That repeated wave is what the speaker follows.

Make it yours

  • Change PITCH to 330 or 440 for a higher note.
  • Change set_volume(0.8) in rebuild() to play it quieter.
  • Add a triangle wave. In preset(), before the else, add elif name == "triangle": and out.append(4 * abs(p - 0.5) - 1). Then add "triangle" to the list of names in setup().
  • Change the colours: "deepskyblue" and "orange".
  • Draw a wave with sharp corners and compare its spectrum with a smooth one.

The code

Copy it into a file and run it.

"""Music: draw a wave and hear it

Every sound is a wave, and the shape of the wave decides how it sounds. Drag the mouse inside the
left box to draw one repeat of a wave, and let go to hear it. The right box is the spectrum: a bar
for each pitch. A smooth wave has one tall bar, and a jagged wave has many. Four buttons give you
a sine, a square, a saw and a soft wave to start from.

How it works:
- shape is a list of N=48 heights, from -1 to 1. mouse_pressed(), mouse_dragged() and
  mouse_released() change them. paint() moves the point nearest the mouse. The drag code fills the
  points the mouse jumped over, so a fast drag leaves no gaps.
- rebuild() makes one second of sound from the shape. For each of the 44,100 numbers, it works out
  where that moment falls in the shape and blends two neighbouring points. PITCH = 220 repeats a
  second is the note A3.
- f.create_sound(numbers) turns the list into a sound, and sound.loop() plays it. The sketch removes
  the average first, so the wave does not sit above zero.
- f.draw_spectrum(sound, ...) draws the bars on the right.
- f.create_button() makes the four buttons. button.clicked() is true on the frame it is pressed,
  and preset() builds the shape.
- f.begin_shape(), f.vertex() and f.end_shape() draw the shape, and the same shape again four times
  at the bottom. That repeated wave is what the speaker follows.

Make it yours:
- Change PITCH to 330 or 440 for a higher note.
- Change set_volume(0.8) in rebuild() to play it quieter.
- Add a triangle wave. In preset(), before the else, add elif name == "triangle": and
  out.append(4 * abs(p - 0.5) - 1). Then add "triangle" to the list of names in setup().
- Change the colours: "deepskyblue" and "orange".
- Draw a wave with sharp corners and compare its spectrum with a smooth one.
"""
# gallery: time-dependent
import math

import funground as f

N = 48                       # points across one period
RATE = 44100
PITCH = 220                  # the note is A3: this many repeats of the shape every second
BX, BY, BW, BH = 20, 66, 300, 160          # the drawing box
SX, SW = 340, 280                          # the spectrum box (same top and height)

shape = [math.sin(2 * math.pi * i / N) for i in range(N)]     # heights from -1 (bottom) to 1 (top)
sound = None
dirty = False                # the shape changed and the sound is out of date
last = None                  # where the mouse was a moment ago, while drawing
buttons = {}


def preset(name):
    """One period of a classic wave, as N heights."""
    out = []
    for i in range(N):
        p = i / N
        if name == "sine":
            out.append(math.sin(2 * math.pi * p))
        elif name == "square":
            out.append(1.0 if p < 0.5 else -1.0)
        elif name == "saw":
            out.append(2 * p - 1)
        else:       # soft: a sine with a little of its second and third harmonics
            out.append(math.sin(2 * math.pi * p) + 0.4 * math.sin(4 * math.pi * p)
                       + 0.15 * math.sin(6 * math.pi * p))
    peak = max(abs(v) for v in out)
    return [v / peak for v in out]


def rebuild():
    """Make one second of sound by repeating the shape, and play it on a loop."""
    global sound, dirty
    dirty = False
    mean = sum(shape) / N                       # a wave that sits above zero would only push the speaker
    period = [v - mean for v in shape]
    peak = max(abs(v) for v in period)
    if peak < 1e-6:
        period = [0.0] * N                      # a flat line is silence
    else:
        period = [0.5 * v / peak for v in period]       # leave plenty of room below the loudest
    numbers = []
    for t in range(RATE):
        place = (t * PITCH / RATE) % 1 * N      # where in the period this moment falls
        i = int(place)
        a, b = period[i], period[(i + 1) % N]
        numbers.append(a + (b - a) * (place - i))       # a line between two points
    if sound:
        sound.stop()
    sound = f.create_sound(numbers)
    sound.set_volume(0.8)
    sound.loop()


def paint(x, y):
    """Move the point nearest to x to the height of y."""
    i = min(max(round((x - BX) / (BW / N)), 0), N - 1)
    shape[i] = min(1.0, max(-1.0, 1 - 2 * (y - BY) / BH))


def in_box(x, y):
    return BX - 6 <= x <= BX + BW + 6 and BY - 6 <= y <= BY + BH + 6


def mouse_pressed():
    global last, dirty
    if in_box(f.mouse_x, f.mouse_y):
        last = (f.mouse_x, f.mouse_y)
        paint(*last)
        dirty = True


def mouse_dragged():
    global last, dirty
    if last is None:
        return
    # fill in the points the mouse jumped over, so a quick drag leaves no gaps
    x0, y0 = last
    steps = max(1, int(abs(f.mouse_x - x0) / (BW / N)) + 1)
    for k in range(1, steps + 1):
        paint(x0 + (f.mouse_x - x0) * k / steps, y0 + (f.mouse_y - y0) * k / steps)
    last = (f.mouse_x, f.mouse_y)
    dirty = True


def mouse_released():
    global last
    if last is not None:
        last = None
        if dirty:
            rebuild()              # hear it when you let go


def setup():
    f.size(640, 420)
    for name in ("sine", "square", "saw", "soft"):
        buttons[name] = f.create_button(name)
    rebuild()


def draw():
    global shape
    for name, button in buttons.items():
        if button.clicked():
            shape = preset(name)
            rebuild()
    f.background("#121a2b")
    f.text_align("left", "top")
    f.text_size(22)
    f.no_stroke()
    f.fill("white")
    f.text("The shape of a wave is its sound", 20, 16)
    f.text_size(13)
    f.fill("#9fb4d8")
    f.text("drag in the left box to draw the wave, then let go to hear it", 20, 44)

    # the drawing box, with the middle line
    f.fill("#1b2742")
    f.rect(BX, BY, BW, BH, 6)
    f.stroke("#3b4d78")
    f.stroke_width(1)
    f.line(BX, BY + BH / 2, BX + BW, BY + BH / 2)
    f.no_fill()
    f.stroke("deepskyblue")
    f.stroke_width(3)
    f.begin_shape()
    for i, v in enumerate(shape):
        f.vertex(BX + (i + 0.5) * BW / N, BY + BH / 2 - v * BH / 2)
    f.end_shape()
    f.no_stroke()
    f.fill("white")
    for i, v in enumerate(shape):
        f.circle(BX + (i + 0.5) * BW / N, BY + BH / 2 - v * BH / 2, 5)

    # the spectrum of what is playing
    f.fill("#1b2742")
    f.rect(SX, BY, SW, BH, 6)
    f.fill("orange")
    f.draw_spectrum(sound, SX + 6, BY + 8, SW - 12, BH - 16, bands=48)

    f.fill("#9fb4d8")
    f.text("one period (drag here)", BX, BY + BH + 6)
    f.text("spectrum: low pitch left, high right", SX, BY + BH + 6)

    # the shape repeated, which is what the speaker follows
    f.fill("#1b2742")
    f.rect(20, 270, 600, 110, 6)
    f.no_fill()
    f.stroke("#7be0a8")
    f.stroke_width(2)
    f.begin_shape()
    for i in range(N * 4 + 1):
        v = shape[i % N]
        f.vertex(20 + i * 600 / (N * 4), 325 - v * 46)
    f.end_shape()
    f.no_stroke()
    f.fill("#9fb4d8")
    f.text("the same shape, over and over: the speaker follows this", 20, 388)
    f.text(f"{PITCH} repeats a second is the note A3", 20, 404)


f.run()

View on GitHub ยท Back to the gallery