Bpm ramp over time + minor sonic pi issue

hey! new to this forum & relatively new to coding with sonic pi.

i want to create a piece that sounds human-like, with humanisation and bpm variation. i managed to write a humanisation code i am happy with, however am stuck at the bpm variation part. my idea so far is to create a seperate thread for the bpm to ramp, and sync all other threads to this one. is something like this possible? i asked chatgpt (i only do this when i am desperate), but the results are not very usable, because chatgpt says i should put use_bpm before every musical phrase, which is clunky and not very practical, i would like the bpm variation to interfere as little as possible with my melody code.

hope what i want to get is clear.

extra notes to this post:

  • kind of just a fun thing to code but i would also like to be able to calculate how many bars the piece is for the bar counter so i don’t have to put it in manually, but i think it could also be unnecessarily complicated

  • there is this weird thing that keeps happening, where the bars counted are displaying earlier than wanted in the log (times of the bars in the log are correct, display order not correct for some reason). i managed to catch it on video once by chance, it’s pretty rare but still keeps happening. the code in the video is slightly different than my version now, but i also had the same issue with my current code lol. i uploaded the video on wetransfer: Unique Download Link | WeTransfer (editing this when i get a permanent link). when it shows the counted bar earlier than it should, it shows it again at the right time when it should be displayed. also, sometimes, the wrong bar counting also starts at 21 instead of 11. i use 4.5.1. for windows as seen in the video

current spi code:

# recode ver
# minecraft type of game  music idk
# 4/4
# c major but an fs added idk what mode that is

# being able to divide music simply by function and how i want to is such an advantage when coding

# to do (no order):
# () bpm ramp over time
# (x) code a play_pattern_timed_amp function
# () send the midi output to labs
# (x) humanise amp and timing with rrand
# (x) set a humanisation parameter to be used
# () automatic calculation of how many bars the piece is
# () and of course write the whole thing

use_random_seed 896767 # for humanisation
h = 1.5 # how strong the humanisation in the sleeps is, 0<h
use_bpm 82

# humanised_play_pattern_timed_amp legit freestyle
define :hppta do |notes, times, amps, sleepdev, ampdev|
  comment do # !!!!!!!!!! uncomment if "nil cannot be coerced into integer" type error
    puts "Control: Notes amount: #{notes.length}"
    puts "Control: Times amount: #{times.length}"
    puts "Control: Amps amount: #{amps.length}"
  end
  notes.zip(times, amps).each do |note, time, amp|
    play note, amp: rrand(amp-ampdev, amp+ampdev)
    sleep rrand(time-sleepdev, time+sleepdev)
  end
end

# bar counter, works as long as this stays in 4/4
in_thread do
  b = 0
  50.times do
    b = b+1
    puts "Bar #{b}"
    sleep 4
  end
end

# piano
da = 1 # default amp
dsd = h*0.01 # default sleep deviation
dad = 0.15 # default amp deviation
s = h*0.01 # "simulatenous" notes sleep
use_synth :piano
use_synth_defaults amp: da
with_fx :lpf, cutoff: 70 do
  with_fx :hpf, cutoff: 30 do
    with_fx :reverb, mix: 0.6 do
      in_thread do
        hppta([:g2, :b3, :c3, :e3, :b4, :a4, :g4], [s, 0.5-s, 0.5, 0.5, 1.5, 0.5, 0.5], [da,da,da,da,da,da-0.2,da-0.3], dsd, dad)
        hppta([:fs4, :g2, :b2, :d3, :fs3], [s, 0.5-s, 0.5, 0.5, 1.5], [da, da, da, da, da], dsd, dad)
        hppta([:c4, :a3, :a2, :c3, :e3, :fs3], [1, s, 0.5-s, 0.5, 0.5, 1.5], [da, da, da, da, da, da], dsd, dad)
        hppta([:a3, :b3, :g2, :a3, :b2, :d3, :a3], [0.5, 0.5, s, 0.5-s, 0.5, 0.5, 2.5], [da, da, da, da, da, da, da], dsd, dad)
        # variation repeat
        hppta([:g2, :b3, :c3, :e3, :b4, :a4, :c5], [s, 0.5-s, 0.5, 0.5, 1.5, 0.5, 0.5], [da,da,da,da,da,da-0.2,da-0.3], dsd, dad)
        hppta([:g3, :fs3, :b4, :b2, :d3, :fs4, :c4], [s, s, 0.5-s-s, 0.5, 0.5, 1.5, 1], [da, da, da, da, da, da], dsd, dad)
        hppta([:a3, :a2, :c3, :e3, :fs3], [s, 0.5-s, 0.5, 0.5, 1.5], [da, da, da, da, da], dsd, dad)
        hppta([:a3, :b3, :g2, :g3, :b2, :d3, :g3, :c4, :fs4, :b4, :d5], [0.5, 0.5, s, 0.5-s, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5], [da, da, da, da, da, da, da, da, da, da, da], dsd, dad)
      end
    end
  end
end

#bass kinda strings something
use_synth :hollow
use_synth_defaults amp: 1.25, attack: 0.5, sustain: 3.5, release: 1

with_fx :lpf do
  in_thread do
    sleep 15.75
    play_pattern_timed [[:c3, :g3], [:g2, :d3], [:a2, :e3], [:d2, :b2]], [4]
  end
end

use_synth :dark_ambience
use_synth_defaults amp: 1.5, attack: 0.5, sustain: 3.5, release: 1

with_fx :hpf do
  in_thread do
    sleep 15.75
    play_pattern_timed [[:c3, :g3], [:g2, :d3], [:a2, :e3], [:d2, :b2]], [4]
  end
end

Hey Ava,
Someone else may chip in as well, but here’s my thoughts:

Re live bpm variation, the following thread may be of interest:

I see what you mean with the bar numbers being printed out in the video as bar 1, 13, 2, 14, 3… etc.
Something to note about this is that you are mostly using un-named threads to do things simultaneously. This means every time ‘Run’ is pressed, a new thread is created and run with that code, even if an instance of that thread from the previous run is still executing. In the case of the logs, this causes multiple separate counters to count up at the same time.
The first change you could make to avoid this is to give un-named threads names, if you do not want them to potentially overlap between runs. (Then, whenever Run is pressed, a new thread is only created if the previous instance with the same name has finished executing. See the threads documentation for further details). Hope that makes sense!

I’ll paste some code below which should give an example of how you could address what you want, including bpm variation, correct thread/log behaviour, and bar counting. I’ve tried to only change the code enough to get it to do what you might want it to do.
Happy to clarify anything that might still be unclear about it if needed :slight_smile:

# recode ver
# minecraft type of game  music idk
# 4/4
# c major but an fs added idk what mode that is

# being able to divide music simply by function and how i want to is such an advantage when coding

# to do (no order):
# () bpm ramp over time
# (x) code a play_pattern_timed_amp function
# () send the midi output to labs
# (x) humanise amp and timing with rrand
# (x) set a humanisation parameter to be used
# () automatic calculation of how many bars the piece is
# () and of course write the whole thing

use_random_seed 896767 # for humanisation
h = 1.5 # how strong the humanisation in the sleeps is, 0<h

# Use link as the bpm source, as this allows it to be easily globally modified
use_bpm :link
set_link_bpm! 82

# humanised_play_pattern_timed_amp legit freestyle
define :hppta do |notes, times, amps, sleepdev, ampdev|
  comment do # !!!!!!!!!! uncomment if "nil cannot be coerced into integer" type error
    puts "Control: Notes amount: #{notes.length}"
    puts "Control: Times amount: #{times.length}"
    puts "Control: Amps amount: #{amps.length}"
  end
  notes.zip(times, amps).each do |note, time, amp|
    play note, amp: rrand(amp-ampdev, amp+ampdev)
    sleep rrand(time-sleepdev, time+sleepdev)
  end
end

# piano
da = 1 # default amp
dsd = h*0.01 # default sleep deviation
dad = 0.15 # default amp deviation
s = h*0.01 # "simulatenous" notes sleep

# Extract the melody patterns out of the hppta method so they can be examined once in the beginning
# to calculate the total number of bars

patterns = [
  [[:g2, :b3, :c3, :e3, :b4, :a4, :g4], [s, 0.5-s, 0.5, 0.5, 1.5, 0.5, 0.5], [da,da,da,da,da,da-0.2,da-0.3], dsd, dad],
  [[:fs4, :g2, :b2, :d3, :fs3], [s, 0.5-s, 0.5, 0.5, 1.5], [da, da, da, da, da], dsd, dad],
  [[:c4, :a3, :a2, :c3, :e3, :fs3], [1, s, 0.5-s, 0.5, 0.5, 1.5], [da, da, da, da, da, da], dsd, dad],
  [[:a3, :b3, :g2, :a3, :b2, :d3, :a3], [0.5, 0.5, s, 0.5-s, 0.5, 0.5, 2.5], [da, da, da, da, da, da, da], dsd, dad],
  [[:g2, :b3, :c3, :e3, :b4, :a4, :c5], [s, 0.5-s, 0.5, 0.5, 1.5, 0.5, 0.5], [da,da,da,da,da,da-0.2,da-0.3], dsd, dad],
  [[:g3, :fs3, :b4, :b2, :d3, :fs4, :c4], [s, s, 0.5-s-s, 0.5, 0.5, 1.5, 1], [da, da, da, da, da, da], dsd, dad],
  [[:a3, :a2, :c3, :e3, :fs3], [s, 0.5-s, 0.5, 0.5, 1.5], [da, da, da, da, da], dsd, dad],
  [[:a3, :b3, :g2, :g3, :b2, :d3, :g3, :c4, :fs4, :b4, :d5], [0.5, 0.5, s, 0.5-s, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5], [da, da, da, da, da, da, da, da, da, da, da], dsd, dad]
]

# Apply the 'reduce' method,
# (which calculates a single value of some kind from a _collection_ of things),
# to each item in the patterns array,
# by keeping count of a total sum (starting at 0) of all the note lengths in the composition,
# and adding the sum of each pattern's times (reduce(:+)) to the total sum

# As mentioned below this assumes that the total sum of note durations in the composition is evenly divisible by 4
total_duration = patterns.reduce(0) {|sum, pattern| sum + pattern[1].reduce(:+) }
total_bars = total_duration / 4

# bar counter, works as long as this stays in 4/4
# named threads do not create new thread instances that overlap when running
in_thread(name: :counter) do
  total_bars.times do |i|
    puts "Bar #{i + 1}"
    # change the global bpm via link at a certain point (in this case, when the 5th bar starts)
    set_link_bpm! 120 if i == 4
    
    sleep 4
  end
end

use_synth :piano
use_synth_defaults amp: da
with_fx :lpf, cutoff: 70 do
  with_fx :hpf, cutoff: 30 do
    with_fx :reverb, mix: 0.6 do
      in_thread(name: :piano) do
        patterns.each do |(notes, times, amps, sleepdev, ampdev)|
          hppta(notes, times, amps, sleepdev, ampdev)
        end
      end
    end
  end
end


#bass kinda strings something
use_synth :hollow
use_synth_defaults amp: 1.25, attack: 0.5, sustain: 3.5, release: 1

with_fx :lpf do
  in_thread(name: :hollow) do
    sleep 15.75
    play_pattern_timed [[:c3, :g3], [:g2, :d3], [:a2, :e3], [:d2, :b2]], [4]
  end
end

use_synth :dark_ambience
use_synth_defaults amp: 1.5, attack: 0.5, sustain: 3.5, release: 1

with_fx :hpf do
  in_thread(name: :dark_ambience) do
    sleep 15.75
    play_pattern_timed [[:c3, :g3], [:g2, :d3], [:a2, :e3], [:d2, :b2]], [4]
  end
end
1 Like

I already have methods written to ramp bpms and sync them across loops in my YummyFillings.rb:

The relevant functions are slidebpm, synctoslidingbpm and killslidingbpm.
There’s lots of other goodies in there as well – lfos, envelopes, strum, arpeggiate, whistle, slidemelody, stuttersample, tremolo, vibrato, humanize, swing, tuples and lots more.
Enjoy!

2 Likes

hey, thx for replying!

i think i’ll leave the bar calc code as it is, your method wouldn’t work if i used other instruments too, that would play after the piano part has ended, i guess there should be a comparison to evaluate the longest of the instrument patterns added at the beginning of the bar calc code. also, this makes the bpm jump instead of changing slowly, right? well but yeah thanks for explaining and i definitely have learned something new & have more of a starting point. when i write that i am “legit freestyling” i definitely mean it… have no ruby and only highschool python programming experience so yeahhhhhh

good stuff !!! i definitely want to learn to “think from scratch” so i will try to learn from this !! do you also have a way to write not only linear, but other ways of bpm growth and shrinks? would like to be able to do that too, in the future