Combining oscillators with trigger_synth

I’ve been learning music theory and got to a section on harmonics. I wanted to play around with this in sonic pi. My first solution was based on some codes by martin:

def play_harmonic(fundamental, amps, *args)
  use_synth :sine
  partials = [fundamental,
              fundamental * 2,
              fundamental * 3,
              fundamental * 4,
              fundamental * 5,
              fundamental * 6,
              fundamental * 7,
              fundamental * 8].map{|hz| hz_to_midi(hz) }
  
  partials.each_with_index do |note, index|
    play note, *args
  end
end

sine = [1,0,0,0,0,0,0,0]
violin = [1,0.8,0.5,0.25,0.35,0.35,0.5,0.5]
flute = [1,0.5,0.8,0.35,0.25,0.1525,0.125,0.255]
trumpet = [0.96,0.94,1,0.75,0.73,0.75,0.48,0.5]
guitar = [1,0.75,0.65,0.65,0.7,0.75,0.6,0.8]

play_harmonic 100, flute, release: 3

However after hearing some delay with this implementation, and because I fancied playing around with some code, I decided to try to implement it in a similar way to how sonic-pi implements play_chord (you can find this here). Here’s what I’ve ended up with:

def play_harmonic(fundamental, amps, *args)
  sn = :sine.to_sym
  info = Synths::SynthInfo.get_info(sn)
  args_h = resolve_synth_opts_hash_or_array(args)
  args_h = normalise_and_resolve_synth_args(args_h, info, true)
  
  partials = [fundamental,
              fundamental * 2,
              fundamental * 3,
              fundamental * 4,
              fundamental * 5,
              fundamental * 6,
              fundamental * 7,
              fundamental * 8].map{|hz| hz_to_midi(hz) }
  
  unless __system_thread_locals.get(:sonic_pi_spider_real_time_mode)
    if __thread_locals.get(:sonic_pi_mod_sound_timing_guarantees)
      unless in_good_time?
        __delayed_message "!! Out of time, skipping: synth #{sn.inspect}, #{arg_h_pp({note: notes}.merge(args_h))}"
        return BlankNode.new(args_h)
      end
    end
  end
  
  unless __thread_locals.get(:sonic_pi_mod_sound_synth_silent)
    __delayed_message "synth #{sn.inspect}, #{arg_h_pp({note: partials}.merge(args_h))}"
  end
  
  chord_group = @mod_sound_studio.new_group(:tail, current_group, "CHORD")
  cg = ChordGroup.new(chord_group, partials, info)
  
  nodes = []
  amp = args_h[:amp] || 1.0
  partials.each_with_index do |note, index|
    if note
      args_h[:note] = note
      args_h[:amp] = amp * amps[index]
      nodes << trigger_synth(:sine, args_h.dup, cg, info)
    end
  end
  cg.sub_nodes = nodes
  __thread_locals.set(:sonic_pi_local_last_triggered_node, cg)
  cg
end

sine = [1,0,0,0,0,0,0,0]
violin = [1,0.8,0.5,0.25,0.35,0.35,0.5,0.5]
flute = [1,0.5,0.8,0.35,0.25,0.1525,0.125,0.255]
trumpet = [0.96,0.94,1,0.75,0.73,0.75,0.48,0.5]
guitar = [1,0.75,0.65,0.65,0.7,0.75,0.6,0.8]

play_harmonic 100, flute, release: 3

There is a potential here for a bit of further abstraction here too. Maybe a trigger_amped_chords that allows the playing of midi/notes simultaneously but at different volumes.

Figured I’d share, could be interesting for others :slight_smile:

Interesting that you mention you hear some delay. How so? Does it happen when using the first example above exactly?

(Btw, it doesn’t seem to be using amps - in order to do so I needed to change the implementation of play_harmonic slightly) -

def play_harmonic(fundamental, amps, args)
  use_synth :sine
  partials = [fundamental,
              fundamental * 2,
              fundamental * 3,
              fundamental * 4,
              fundamental * 5,
              fundamental * 6,
              fundamental * 7,
              fundamental * 8].map{|hz| hz_to_midi(hz) }
  
  partials.each_with_index do |note, index|
    play note, args.merge(amp: amps[index])
  end
end
2 Likes

Hi there @theibbster, welcome to our forums!

I’m also like @ethancrawford and am curious as to what delays you were hearing. Everything is scheduled ahead of time with the same timestamp so I’d be very surprised to hear that there were delays. Could you explain a little more about the differences between what you expected and what you observed?

Also, whilst playing with and copying the internal code is definitely to be encouraged for learning, it’s not code that we would normally want to write directly in Sonic Pi for compositions or performances.

The main issue is that it’s not supported, and all those internal calls may (and likely will) completely change without warning. This includes calls to def - instead prefer define.

Glad you’re experimenting and having fun though :slight_smile:

I must’ve made a mistake with the code, when I went through now and made sure everything was right they both sounded the same.

@samaaron Thanks for the welcome! I wouldn’t use it for compositions or anything was just enjoying messing around. I was happily surprised it didn’t all go over my head! Maybe after some time I’d even be able to contribute to the code :smiley:

@ethancrawford yeah, I made a mistake with the code I posted , however it seems your merge doesn’t work either, as args is an array with a hash inside like this [{:sustain=>1, :release=>3}].

Edit: my bad, code has been updated

So keeping that in mind, and in case anyone wants to use similar code for additive synthesis or playing chords with each note having it’s own amplitude, here’s what I ended up with:

define :play_harmonic do |fundamental, amps, args|
  use_synth :sine
  partials = [fundamental,
              fundamental * 2,
              fundamental * 3,
              fundamental * 4,
              fundamental * 5,
              fundamental * 6,
              fundamental * 7,
              fundamental * 8].map{|hz| hz_to_midi(hz) }
  
  partials.each_with_index do |note, index|
    play note, args.merge(amp: amps[index])
  end
end

I switched to define as well in keeping with what sam said so it could be used for composition or performance.

2 Likes

Sure :slight_smile: that works too.
(I’d gone for not using splats for args in mine).

1 Like