Rhythm notation for tuplets with amplitude

Hi everyone! :blush:

I’m starting a generative EDM project, and I was looking for the simplest way to represent complex rhythms. This is what I found:

  1. My starting point was something I observed in @Nechoj’s work: x---x---x---x---, where each x represents a hit.
  2. I wanted to represent amplitude information, so I replaced the xs with digits 0-9: 8---6---8---6---. (A similar approach can be found in @mlange’s work — see the discussion.)
  3. My “aha” moment was when I realised that simply by factoring the length of the string into the calculation of duration, I could always represent a rhythm in its simplest form: e.g. 8686 = 8---6---8---6---. A simple calculation can determine the spacing of a pattern based on its number of characters.

At last, I have a simple way to represent arbitrary tuplets! No genre is beyond reach!

Here is a simple cross-genre demo. The most complex rhythm is in the closed hi-hats. It involves a triplet. I think the best way to understand the implementation is to take a look.

use_bpm 116

live_loop :kick do
  pattern = "8----8--".ring
  3.times do
    pattern.length.times do
      sample :bd_haus, amp: (pattern[look].to_f / 9) if (pattern[tick] != "-")
      sleep 4/pattern.length.to_f
    end
  end
  tick_reset
  pattern = "8".ring
  pattern.length.times do
    sample :bd_haus, amp: (pattern[look].to_f / 9) if (pattern[tick] != "-")
    sleep 4/pattern.length.to_f
  end
  tick_reset
end

live_loop :snare do
  pattern = "-6-6".ring
  pattern.length.times do
    sample :sn_zome, amp: (pattern[look].to_f / 9), sustain: 0.2 if (pattern[tick] != "-")
    sleep 4/pattern.length.to_f
  end
  tick_reset
  pattern = "---16--4----6---".ring
  pattern.length.times do
    sample :sn_zome, amp: (pattern[look].to_f / 9), sustain: 0.2 if (pattern[tick] != "-")
    sleep 4/pattern.length.to_f
  end
  tick_reset
end

live_loop :closedHiHat do
  pattern = "8--6--6--2468--6--6--6--".ring
  pattern.length.times do
    sample :drum_cymbal_closed, amp: (pattern[look].to_f / 9), sustain: 0.2 if (pattern[tick] != "-")
    sleep 4/pattern.length.to_f
  end
  tick_reset
  pattern = "86668666".ring
  pattern.length.times do
    sample :drum_cymbal_closed, amp: (pattern[look].to_f / 9), sustain: 0.2 if (pattern[tick] != "-")
    sleep 4/pattern.length.to_f
  end
  tick_reset
  pattern = "8--6--6--2468--6--6--6--".ring
  pattern.length.times do
    sample :drum_cymbal_closed, amp: (pattern[look].to_f / 9), sustain: 0.2 if (pattern[tick] != "-")
    sleep 4/pattern.length.to_f
  end
  tick_reset
  pattern = "8666866-".ring
  pattern.length.times do
    sample :drum_cymbal_closed, amp: (pattern[look].to_f / 9), sustain: 0.2 if (pattern[tick] != "-")
    sleep 4/pattern.length.to_f
  end
  tick_reset
end

It’s so simple that someone may have already (cough) beat me to it.

EDIT: According to @Eli, this intuitive approach may have been around for at least a decade. It was discussed in the Google Group that preceded our present forum. I hope this more recent post may save some people the trouble of having to search.

Best,
d0lfyn

5 Likes

To clarify, the closed hi-hat is resolved to triplets because the pattern has a length of 24, which is divisible by 3. I hope that makes sense? Each pattern is performed in one measure.

1 Like

Just to clarify this point: I studied many examples provided by other users here and in some of those a string like x---x---x---x--- was used to code the drum patterns. Finally, I adopted it, but I cannot say who actually invented it or used it for the first time.

Coding further information into it like amplitudes is a great idea. As long as the pattern is preserved and contains the dashes - there is still the benefit that it is a visualization of the pattern being played. This is a main benefit. You can see what the drum is playing when looking at the code.

1 Like

I used a similar approach for the drums here, but without amplitude. In another unpublished project in progress, I also included parsing to amplitudes. There, I use x/X for 10, and -, 1, …, 9 for 0 to 9.

Further, in the project mentioned first, I used a similar string notation to control amplitudes and other “instrument” parameters over the entire track (see here). You can find the respective parsing functions at the end of the document (str_scale and str_select). Note that parsing into arrays is done before starting any loops for the sake of performance.

Also note that spaces and the pipe character | are ignored to allow for a more readible representation. The same technique can be applied to drum patterns to allow for nicely readible rhythms that span multiple measures.

EDIT: See also this thread.

2 Likes

yeah, that’s common for percussion practice with kick-snare-hat patterns, etc. When it looks like that, it’s easy to follow along with your eye and to make quick changes for practicing purposes. More than four bars, though, and I think standard notation becomes much easier to follow along. I have a text file full of little bits to practice. For example:

c--- c--- c-c- --c- --c- c---
s--- s--- s--- s--- s--- s---
k--- --k- k--- --k- k--- --k-

“k” is kick, “s” is snare, and “c” is cymbal (to represent rides or hi hats, etc.). For hats, I use “h” and “o” to represent open and closed. So basically, it’s using letters to represent timbre, but using the numbers for dynamics is more musically useful beyond these little drum practice exercises.

Oh, yeah, my sequencer represents rhythms in a similar way: an integer always lasts one beat, so the tuplet is built into to the integer’s bit length, its pattern of 1’s and 0’s representing the rhythm. I use velocity vectors as a separate component, so then I’m able to modulate rhythms and dynamics in ways that are easy for me to track visually, which is important to me for live coding.

1 Like

@mlange I added a mention to you in the post! :smiley:

That could be handy! It’ll take another step because the proposed notation is irregular, but your approach is one option. The multi-measure notation would be split into an array of patterns, for additional parsing or direct usage. This could even be useful for a less deterministic approach, such as Markov chaining or simple randomness, since some patterns span multiple measures.

Things are falling into place. Next, I’m looking for a representation of multi-measure melodies that can be built from motifs.

1 Like

@d0lfyn

Just a little my friend… :slight_smile:

People were already using this kind of notation back in Sept 2017, when in_thread went live…

I can distinctly rememeber it being around on the previous google group we used to use, back to about 2016.

And even then, I wouldn’t like to say ‘we’ (as in the Sonic Pi gang) first created it. It’s so intuitive I’d guess it’s been around since the early 2010’s…

Eli…

2 Likes

Hi @Eli!

Ah, my “literature review” consisted of googling “sonic pi drum notation” and looking through the first 2 pages of results :sweat_smile: I’ll revise the post to mention its origins. Hopefully with a more recent post, fewer people will find themselves reinventing the wheel.

Thanks!
d0lfyn

Some additions to the notation which are intended for use with melodies, chords, bass lines, &c.:

  1. Multi-measure patterns can be represented with |s separating each measure (thanks to @mlange for bringing up his work on this).
  2. Sustained notes can be represented with +s, e.g. 8++++8++|+|-. (The extra |- at the end tells the interpreter to sustain the last + for the entire measure.)

Here’s a simple demo to illustrate the notation. Note that I kill the synths rather than calculate their durations, as I intend to use this notation with MIDI. Durations would be a little more difficult to calculate, especially when spanning multiple measures.

use_bpm(156)

live_loop :beat do
  in_thread do
    notes = [
      :A4, :G4, :A4, :C5, :B4, :G4,
      :E4, :G4, :E4, :G4, :A4,
      :A4, :C5, :B4, :G4,
    ].freeze
    playing = nil
    
    pattern = -"8+++++448++6++6+|++4-6-4-8+++6+++|8+++++++8++6++6+|+|-"
    subpatterns = pattern.split("|")
    i = 0
    
    subpatterns.each do |sp|
      i += 1
      unit = 4/sp.length.to_f
      sp.length.times do
        tick
        playing.kill if (!playing.nil? && ((sp[look] != "+") || (((look + 1) == sp.length)) && (i == subpatterns.length)))
        with_synth :pulse do
          playing = play notes[tick(:noteTick)], amp: (sp[look].to_f / 10), sustain: 16 if (sp[look].match(/[\d]/))
        end
        wait(unit)
      end
      tick_reset
    end
  end
  
  in_thread do
    notes = [
      :A4, :G4, :A4, :C5, :B4, :G4,
      :E4, :G4, :E4, :G4, :A4,
      :A4, :C5, :B4, :G4,
    ].freeze
    playing = nil
    
    pattern = -"8+++++448++6++6+|++4-6-4-8+++6+++|8+++++++8++6++6+|+|-"
    subpatterns = pattern.split("|")
    i = 0
    
    subpatterns.each do |sp|
      i += 1
      unit = 4/sp.length.to_f
      sp.length.times do
        tick
        playing.kill if (!playing.nil? && ((sp[look] != "+") || (((look + 1) == sp.length)) && (i == subpatterns.length)))
        with_synth :saw do
          playing = play (notes[tick(:noteTick)] + 12), amp: (sp[look].to_f / 10) / 2, sustain: 16 if (sp[look].match(/[\d]/))
        end
        wait(unit)
      end
      tick_reset
    end
  end
  
  in_thread do
    playing = nil
    pattern = -"--8+|++8+|++8+|++82"
    i = 0
    subpatterns = pattern.split("|")
    subpatterns.each do |sp|
      i += 1
      unit = 4/sp.length.to_f
      sp.length.times do
        tick
        playing.kill if (!playing.nil? && ((sp[look] != "+") || (((look + 1) == sp.length)) && (i == subpatterns.length)))
        playing = sample :elec_mid_snare, amp: (sp[look].to_f / 10), sustain: 16 if (sp[look].match(/[\d]/))
        wait(unit)
      end
      tick_reset
    end
  end
  
  in_thread do
    playing = nil
    pattern = -"8686|8686|8686|868-"
    subpatterns = pattern.split("|")
    i = 0
    subpatterns.each do |sp|
      i += 1
      unit = 4/sp.length.to_f
      sp.length.times do
        tick
        playing.kill if (!playing.nil? && ((sp[look] != "+") || (((look + 1) == sp.length)) && (i == subpatterns.length)))
        playing = sample :bd_haus, amp: (sp[look].to_f / 10), sustain: 16 if (sp[look].match(/[\d]/))
        wait(unit)
      end
      tick_reset
    end
  end
  
  wait(16)
  
  in_thread do
    notes = [
      :A4, :G4, :A4, :C5, :B4, :G4,
      :E4, :G4, :E4, :G4, :A4,
      :A4, :C5, :B4, :G4,
    ].freeze
    playing = nil
    
    pattern = -"8+++++448++6++6+|++4-6-4-8+++6+++|8+++++++8++6++6+|+|-"
    subpatterns = pattern.split("|")
    i = 0
    
    subpatterns.each do |sp|
      i += 1
      unit = 4/sp.length.to_f
      sp.length.times do
        tick
        playing.kill if (!playing.nil? && ((sp[look] != "+") || (((look + 1) == sp.length)) && (i == subpatterns.length)))
        with_synth :pulse do
          playing = play notes[tick(:noteTick)], amp: (sp[look].to_f / 10), sustain: 16 if (sp[look].match(/[\d]/))
        end
        wait(unit)
      end
      tick_reset
    end
  end
  
  in_thread do
    notes = [
      :A4, :G4, :A4, :C5, :B4, :G4,
      :E4, :G4, :E4, :G4, :A4,
      :A4, :C5, :B4, :G4,
    ].freeze
    playing = nil
    
    pattern = -"8+++++448++6++6+|++4-6-4-8+++6+++|8+++++++8++6++6+|+|-"
    subpatterns = pattern.split("|")
    i = 0
    
    subpatterns.each do |sp|
      i += 1
      unit = 4/sp.length.to_f
      sp.length.times do
        tick
        playing.kill if (!playing.nil? && ((sp[look] != "+") || (((look + 1) == sp.length)) && (i == subpatterns.length)))
        with_synth :saw do
          playing = play (notes[tick(:noteTick)] + 12), amp: (sp[look].to_f / 10) / 2, sustain: 16 if (sp[look].match(/[\d]/))
        end
        wait(unit)
      end
      tick_reset
    end
  end
  
  in_thread do
    playing = nil
    pattern = -"--8+|++8+|++8+|++9+"
    i = 0
    subpatterns = pattern.split("|")
    subpatterns.each do |sp|
      i += 1
      unit = 4/sp.length.to_f
      sp.length.times do
        tick
        playing.kill if (!playing.nil? && ((sp[look] != "+") || (((look + 1) == sp.length)) && (i == subpatterns.length)))
        playing = sample :elec_mid_snare, amp: (sp[look].to_f / 10), sustain: 16 if (sp[look].match(/[\d]/))
        wait(unit)
      end
      tick_reset
    end
  end
  
  in_thread do
    playing = nil
    pattern = -"8686|8686|84648464|824262428-------"
    subpatterns = pattern.split("|")
    i = 0
    subpatterns.each do |sp|
      i += 1
      unit = 4/sp.length.to_f
      sp.length.times do
        tick
        playing.kill if (!playing.nil? && ((sp[look] != "+") || (((look + 1) == sp.length)) && (i == subpatterns.length)))
        playing = sample :bd_haus, amp: (sp[look].to_f / 10), sustain: 16 if (sp[look].match(/[\d]/))
        wait(unit)
      end
      tick_reset
    end
  end
  
  wait(16)
  
end
1 Like

Oh yeah, there are lot of these around … but it is always fun to reinvent :slight_smile: I also did one years ago to ziffers. I ended up creating a subgroup syntax like in tidal:

(B H) (B (H H)) (BK H) ((BK B) H)

or using note legth characters:

q HB H

where capital letters represented some samples and those characters grouped together like HB are played simultaniously.

2 Likes

Yep! Different use cases result in different approaches. Someone should catalogue all these notations!

Your approach is very concise. It kind of reminds me of abc music notation. I wonder if I could develop a variant on your work for my purposes :thought_balloon:

Is there any known “parser” that would allow to convert ABC notation into SP-usable melodies? This would probably be a useful tool as it seems kind of the standard notation for presenting scores in digital media (e.g. with the great abcjs library; I use it in the project presented in this thread).

Ahh, so you were using ABC! Your illustrations led me to look for parsers, and that’s how I found out about abcjs.

It looks like @xavierriley has done some work in this domain. You might want to reach out to him! :slight_smile:

I was thinking of building a simple web app with abcjs to facilitate custom motif entry. I personally don’t think I have a use for ABC in my programs.

And there is also one that @samaaron made that is inspired by ixilang. Its quess its still experimental and undocumented, but works quite well.

2 Likes