Skip to content

Making sound: a sargam phrase over a drone

Making sound: a sargam phrase over a drone

Run it here, in your browser

A short phrase in sargam (note names used in Indian music) plays over a drone. A ladder has one row for each swara, with Sa at the bottom. The row for the swara you hear lights up, and a pink line follows the voice up and down. (This one uses real time, so the picture changes from run to run.)

How it works

  • Sa is the home note, the one the music is built on. Here it is D4. A swara is a note named from Sa: S r R g G m M P d D n N. A small letter is komal (flat), and a capital M is tivra (sharp).
  • f.melody(PHRASE, sa=SA, tuning="just") reads swaras instead of note names. A ' after a swara is the octave above, and a comma is the octave below. "just" tuning uses pure ratios from Sa.
  • A drone is a steady background that holds Sa and Pa. This one imitates a tanpura, the stringed instrument that plays it. f.pluck() makes plucked strings, f.sequence() joins them one after another, and sound.reverb() gives them a room to ring in.
  • The hum is made from numbers: f.create_sound() turns a list of three added sine waves into a sound.
  • phrase.pitch() gives the pitch now. swara_height() uses a % 12 to fold it into one octave, so the voice always lands on a row of the ladder.
  • The pink line takes the middle of the last five readings, so it is calm, and it breaks at rests.

Make it yours

  • Change SA to "C4" or "A3". The whole phrase and the drone move to that key.
  • Change PHRASE. S is Sa, and R, G, m, P, D, N are the other swaras. Add :2 to make one last longer.
  • Change TEMPO to sing it faster or slower.
  • Change tuning="just" to tuning="equal" and listen for the small differences.
  • Change the first string of the drone: use "m," in place of "P," in strings. A drone for some ragas has Ma there.

The code

Copy it into a file and run it.

"""Making sound: a sargam phrase over a drone

A short phrase in sargam (note names used in Indian music) plays over a drone. A ladder has one
row for each swara, with Sa at the bottom. The row for the swara you hear lights up, and a pink
line follows the voice up and down.

How it works:
- Sa is the home note, the one the music is built on. Here it is D4. A swara is a note named
  from Sa: S r R g G m M P d D n N. A small letter is komal (flat), and a capital M is tivra
  (sharp).
- f.melody(PHRASE, sa=SA, tuning="just") reads swaras instead of note names. A ' after a swara is
  the octave above, and a comma is the octave below. "just" tuning uses pure ratios from Sa.
- A drone is a steady background that holds Sa and Pa. This one imitates a tanpura, the stringed
  instrument that plays it. f.pluck() makes plucked strings, f.sequence() joins them one after
  another, and sound.reverb() gives them a room to ring in.
- The hum is made from numbers: f.create_sound() turns a list of three added sine waves into a
  sound.
- phrase.pitch() gives the pitch now. swara_height() uses a % 12 to fold it into one octave, so the
  voice always lands on a row of the ladder.
- The pink line takes the middle of the last five readings, so it is calm, and it breaks at rests.

Make it yours:
- Change SA to "C4" or "A3". The whole phrase and the drone move to that key.
- Change PHRASE. S is Sa, and R, G, m, P, D, N are the other swaras. Add :2 to make one last
  longer.
- Change TEMPO to sing it faster or slower.
- Change tuning="just" to tuning="equal" and listen for the small differences.
- Change the first string of the drone: use "m," in place of "P," in strings. A drone for some
  ragas has Ma there.
"""
# gallery: time-dependent
import math

import funground as f

SA = "D4"
TEMPO = 80
PHRASE = "S R G:2 R S,:2 N,:1 D,:1 P,:2 - S:1 R:1 G:1 P:2 G:1 R:1 S:3"
SWARAS = "SrRgGmMPdDnN"

# The soft voice (melody's own) suits a sung line; reverb adds a room around it.
phrase = f.melody(PHRASE, tempo=TEMPO, sa=SA, tuning="just").reverb(0.3)
phrase.set_volume(0.7)

# The drone: the four strings of a tanpura, plucked in turn, to last as long as the phrase.
# Each pluck gets its own reverb, which adds 0.45 s of ringing, so the pluck is made that much
# shorter to keep the drone the same length as the phrase.
strings = ["P,", "S", "S", "S,"]
string_seconds = phrase.duration() / (2 * len(strings))
plucks = [f.pluck(f.note_to_frequency(s, sa=SA), string_seconds - 0.45).reverb(0.3) for s in strings]
cycle = f.sequence(*plucks)
drone = f.sequence(cycle, cycle)
drone.set_volume(0.6)

# A quiet hum under it, made from numbers: three sine waves added up.
hum_hz = f.note_to_frequency("S,", sa=SA)
sa_hz = f.note_to_frequency("S", sa=SA)
hum = f.create_sound([0.2 * (math.sin(2 * math.pi * hum_hz * t / 44100)
                             + 0.5 * math.sin(4 * math.pi * hum_hz * t / 44100)
                             + 0.25 * math.sin(6 * math.pi * hum_hz * t / 44100))
                      for t in range(int(phrase.duration() * 44100))])
hum.set_volume(0.4)
trail = []
recent = []

# When each note of the phrase sounds, in seconds: (start, end) for notes, not rests.
beat = 60 / TEMPO
notes = []
clock = 0.0
for token in PHRASE.split():
    name, _, beats = token.partition(":")
    length = float(beats or 1) * beat
    if name != "-":
        notes.append((clock + 0.1, clock + length - 0.02))    # skip the soft start, where notes overlap
    clock += length

STEP = 25                                  # height of one row
BASE = 335                                 # text baseline of the bottom row, S


def in_a_note():
    t = phrase.current_time()
    return phrase.is_playing() and any(a <= t <= b for a, b in notes)


def swara_height(hz):
    """Where the voice is on the ladder, in rows above S, in one octave: 0 is S and 11 is N."""
    steps = 12 * math.log2(hz / sa_hz)
    return (steps + 0.5) % 12 - 0.5        # an octave slip lands on the right row, not off the ladder


def trace():
    """The next point of the line: a height in rows, or None for a gap."""
    hz = phrase.pitch() if in_a_note() else None
    if hz is None:
        del recent[:]
        return None
    here = swara_height(hz)
    if recent and abs(here - sorted(recent)[len(recent) // 2]) > 2:
        recent[:] = [here]                 # a jump: break the line and start again
        return None
    recent.append(here)
    del recent[:-5]
    return sorted(recent)[len(recent) // 2]


def setup():
    f.size(640, 400)
    for s in (phrase, drone, hum):
        s.loop()


def draw():
    f.background("#1d0f24")
    f.text_size(16)
    f.text_align("left", "center")
    height = trace()
    trail.append(height)
    del trail[:-120]
    heard = None
    if height is not None:
        heard = SWARAS[round(height) % 12]

    # the ladder: one row for each swara, S at the bottom
    f.no_stroke()
    for i, swara in enumerate(SWARAS):
        y = BASE - i * STEP
        f.fill("orange" if swara == heard else "#3a2147")
        f.rect(20, y - 18, 600, 22, 6)
        f.fill("white" if swara == heard else "#c9a7d9")
        f.text(swara, 30, y - 7)

    # the path of the voice, newest on the right; a gap wherever the line breaks
    f.stroke("hotpink")
    f.stroke_width(3)
    f.no_fill()
    last = None
    for k, step in enumerate(trail):
        if step is None:
            last = None
            continue
        point = (60 + k * 4.5, BASE - 7 - step * STEP)
        if last:
            f.line(*last, *point)
        last = point

    f.no_stroke()
    f.fill("white")
    f.text(f"Sa is {SA}   (now: {heard if heard else 'rest'})", 20, 380)


f.run()

View on GitHub ยท Back to the gallery