Adding an Orchestra to Sonic Pi Using Samples

#1

Adding an Orchestra to Sonic Pi Using Samples

Where to get some orchestra samples

Orchestra Samples

Download, unzip and move selected samples to a sample directory.
I used sampath = “c:/home/pi/samples/”

Here is the code that makes it work:

#Example1.rb
# Sonic Pi Wav Orchestra
# you will need to download the wav files from
# http://virtualplaying.com
# unzip and copy selected files to a folder defined by
# the samps variable below

# use sampath to point ot the folder where the wav files
# are located
sampath = "c:/home/pi/samples/"
# setup several wav files using the path
samps=[]
samps.push(sampath+"trumpets-sus-e4.wav")
samps.push(sampath+"tuba-sus-e2.wav")
#samps.push(sampath+"tuba-sus-e2-PB-loop.wav")
samps.push(sampath+"cello-c4.wav")
samps.push(sampath+"violin-e4.wav")
samps.push(sampath+"oboe-e4-pb-loop.wav")
#samps.push(sampath+"flute4_b.wav")
#samps.push(sampath+"piccolo-c4-pb-loop.wav")

# define the sample pitch of each instrument
sp=[]
sp.push(:E4) # the sample pitch trumpet
sp.push(:E4) # the sample pitch tuba
sp.push(:C4) # the sample pitch cello
sp.push(:E4) # the sample pitch violin
sp.push(:E4) # the sample pitch oboe
#sp.push(:C4) # the sample pitch flute
#sp.push(:C4) # the sample pitch piccolo

# one way to play wave files
halftone=Math.log(2.0)/12.0
octtone=Math.log(2.0)
centtone=halftone/100.0
tempo=1.0
define :playwavnote do |sampsn,spitch,pitch,time,cents=0.0,amp=1.0,pan=0.0|
  # Determine the number of half tones to shift the pitch
  a=note spitch
  if a !=nil
    b=note pitch
    if b !=nil
      puts a,b, b-a
      rate1=Math.exp((b-a)*halftone+cents*centtone)
      puts "rate1=",rate1
      sample sampsn,
        rate:rate1,
        attack: 0.1,
        sustain: tempo*time*0.5,
        release: tempo*time*2.0/3.0,amp: amp ,pan: pan
    end # if b!=nil
  end # if a!- nil
  sleep tempo*time
end #define :playwavenote

#test cases
playwavnote samps[0],:r,:C4,0.25
playwavnote samps[0],sp[0],:r,0.25
playwavnote samps[0],sp[0],:C4,0.25

#try a scale with different instuments
keyscale=scale(:c4,:major)
j=0
while j<samps.length
  puts samps[j],sp[j]
  i=0
  #try a scale
  while i<keyscale.length
    playwavnote samps[j],sp[j],keyscale[i],0.25
    i+=1
  end #next i
  j+=1
end #next j

Listen

Listen2

One sample per live-loop.
If you have two loops with violins playing in unison you will hear only one.
Use the cents to detune one of the violins slightly.

4 Likes
#2

Nice example. I have used orchestral samples for over 18 months now, and produced code in Sonic Pi to make it relatively easy to use the samples in the Sonatina Symphonic Orchestra There are more up to date sets of samples, but I found this very adequate for playing orchestral pieces with Sonic PI. If you search for Sonatina on my blog you will find examples of use , although some need to be updated, where they have been split into two or more files, and are now better played using the run_file command. Further examples on my three soundcloud sites
one two three with code on my gist site
The most ambitious orchestral project I did with this was to play Mozart’s Requiem with Sonic Pi. There is a video here With my code I used several samples to cover the range of each instrument, automatically switching to the relevant one depending on the pitch of the note, although one sample can work quite well over an instrument range as illustrated in your example, and makes the coding much simpler.

#3

Thanks @Robin.Newman for confirming that using orchestra samples with Sonic Pi are a valid approach.
I’ve seen the Sonatina samples before and likely used a few of them.
The several sample approach seems to be a valid one as some instruments don’t seem to work as well with a one sample, one formula approach. It may be that a data structure for each instrument might produce better results. The structure could contain the various sample file names, the parameters used to decide which one to use and finally the attack, delay, sustain and release factors or formulas.

The Mozart’s Requiem must have taken a while to produce. I am majorly impressed.

#4

@robin.newman,

This is some great code that’s been much help. I was playing with changing the attack and release in addition to the sustain and have run into problems. Since the rate is being changed to account for missing notes, the duration is also changed. Unless the attack and release are the same for the length of the note, the duration of the note varies and is dependent on the rate as well. Is there some way to grab what the rate of the sample will be?

#5

I’m not sure that this is the case. I don’t think the envelope paramaters are affected by rpitch: values
try this

sample :loop_amen,sustain: 1,release: 0
sleep 1
sample :loop_amen,rpitch: 12,sustain: 1,release: 0
sleep 1
sample :loop_amen,rpitch: -12,sustain:1,release:0

They each last exactly the same time, 1 beat

If you are interested, when you have an rpitch: value x, then
the sample duration is altered by f**x #f raised to the power x
where x is 1/2**(1/12.0) #1/(12th root of 2)

eg look at the output of this

(-5).step(5,0.5) do |x|
  #print sample duration for given rpitch: value
  puts x,(sample_duration :loop_garzul,rpitch: x)
  f = 1/2**(1/12.0) #1/(12th root of 2)
  #print sample duration calculated from rpitch value x
  puts x,(sample_duration :loop_garzul) * (f**x)
  puts #blank line
end

gives output

 ├─ -5.0 10.678718833360275
 ├─ -5.0 10.67871883336028
 │
 ├─ -4.5 10.374716437208077
 ├─ -4.5 10.37471643720808
 │
 ├─ -4.0 10.079368399158984
 ├─ -4.0 10.079368399158989
 │
 ├─ -3.5 9.792428346437243
 ├─ -3.5 9.792428346437244
 │
 ├─ -3.0 9.513656920021768
 ├─ -3.0 9.513656920021772
 │
 ├─ -2.5 9.242821574978183
 ├─ -2.5 9.242821574978185
 │
 ├─ -2.0 8.979696386474984
 ├─ -2.0 8.979696386474986
 │
 ├─ -1.5 8.724061861322062
 ├─ -1.5 8.724061861322062
 │
 ├─ -1.0 8.475704754874362
 ├─ -1.0 8.475704754874362
 │
 ├─ -0.5 8.234417893147937
 ├─ -0.5 8.234417893147937
 │
 ├─ 0.0 8.0
 ├─ 0.0 8.0
 │
 ├─ 0.5 7.772255529228847
 ├─ 0.5 7.772255529228847
 │
 ├─ 1.0 7.550994501453547
 ├─ 1.0 7.550994501453547
 │
 ├─ 1.5 7.33603234563737
 ├─ 1.5 7.336032345637369
 │
 ├─ 2.0 7.127189745122714
 ├─ 2.0 7.127189745122713
 │
 ├─ 2.5 6.9242924880491445
 ├─ 2.5 6.924292488049143
 │
 ├─ 3.0 6.727171322029717
 ├─ 3.0 6.727171322029714
 │
 ├─ 3.5 6.535661812964399
 ├─ 3.5 6.535661812964397
 │
 ├─ 4.0 6.3496042078727974
 ├─ 4.0 6.349604207872796
 │
 ├─ 4.5 6.168843301631763
 ├─ 4.5 6.168843301631761
 │
 ├─ 5.0 5.993228307506726
 ├─ 5.0 5.993228307506723
#6

@robin.newman, so the envelope isn’t affected by the rate change at all? I seem to be having a problem with the length of notes not matching what I input to the function. It’ll work for some notes but others it’s held too long, almost as if the release isn’t being counted as part of the length of the note and plays it after.

I just added a couple parameters to change the attack and release of each note and use the length, attack, and release to find the sustain of the note.

Here’s the modified pl function:

define :pl do |np,d,inst,amp,attack,length,release,tp=0,pan=0|
  
  setup(inst,path)
  #check if note in range of supplied samples
  #use lowest/highest sample for out of range
  change=0 #used to give rpitch for coverage outside range
  frac=0
  n=np+tp #note allowing for transposition
  if n.is_a?(Numeric) #allow frac tp or np
    frac=n-n.to_i
    n=n.to_i
  end
  if note(np)+tp<note(low) #calc adjustment for low note
    change=note(np).to_i+tp-note(low)
    n=note(low)
  end
  if note(np).to_i+tp > note(high) #calc adjustment for high note
    change = note(np).to_i+tp-note(high)
    n=note(high)
  end
  if change < -3 or change > 5 #set allowable out of range
    #if outside print messsage
    puts 'inst: '+inst+' note '+np.to_s+' with transpostion '+tp.to_s+' out of sample range'
  else #otherwise calc and play it
    #calculate base note and octave
    base=note(n)%12
    oc = note(n) #do in 2 stages because of alignment bug
    oc=oc/12 -1
    #find first part of sample note
    slookup=['c','c#','d','d#','e','f','f#','g','g#','a','a#','b']
    #lookup sample to use,and rpitch offset, according to offsetclass
    case offsetclass
    when 0
      oc += 1 if base == 11 #adjust if sample needs next octave
      snumber=[0,0,3,3,3,6,6,6,9,9,9,0]
      offset=[ 0,1,-1,0,1,-1,0,1,-1,0,1,-1]
    when 1
      snumber=[1,1,1,4,4,4,7,7,7,10,10,10]
      offset=[-1,0,1,-1,0,1,-1,0,1,-1,0,1]
    when 2
      oc -= 1 if base == 0 #adjust if sample needs previous octave
      snumber=[11,2,2,2,5,5,5,8,8,8,11,11]
      offset=[1,-1,0,1,-1,0,1,-1,0,1,-1,0]
    when 3
      snumber=[0,1,2,3,4,5,6,7,8,9,10,11] #this class has sample for every note
      offset=[0,0,0,0,0,0,0,0,0,0,0,0]
    end

    #generate sample name
    if inst == 'Tuba sus'
      sname=sampleprefix+(slookup[snumber[base]]).to_s+oc.to_s+'-PB-loop'
    elsif inst == '1st Violins piz'
      sname=sampleprefix+(slookup[snumber[base]]).to_s+oc.to_s+'-PB.wav'
    else
      sname=sampleprefix+(slookup[snumber[base]]).to_s+oc.to_s
    end
    
      #play sample with appropriate rpitch value
    sample paths,sname,rpitch: offset[base]+change+frac,sustain: (length-attack-release),amp: amp,attack: attack,release: release,pan: p
  end
end
#7

One thing that may be catching you out. ASDR values for envelopes applied to samples are independent of tempo settings. Thus if you double the tempo, your original settings may not be what you expect.
The example below shows a chord played by a sample from the Sonatina library (substitute the sample you want), which is used to play a chord. You can alter the envelope values by adjusting a,s and r and these are modified to account for your current tempo setting.
As long as the total of a+s+r is less than that of the default sample length this should work as expected.

define :mul do |bpm| #mutlplier to adjust for tempo changes
  return 60.0/bpm
end

live_loop :v do
  bpm=30
  use_bpm bpm
  
  a=0.1;s=0.2;r=0.1
  sample sv,attack: a*mul(bpm),sustain: s*mul(bpm),release: r*mul(bpm)
  sample sv,rpitch: 4,attack: a*mul(bpm),sustain: s*mul(bpm),release: r*mul(bpm)
  sample sv,rpitch: 7,attack: a*mul(bpm),sustain: s*mul(bpm),release: r*mul(bpm)
  sleep 0.5
end

Try altering the bpm to differnt values.
For comparison, redefine mul as

define :mul do |bpm| #mutlplier to adjust for tempo changes
  return 1 #(here mul has no effect. it is set to 1)
end

and then try different values for the bpm tempo setting.

Finally, you must make sure that your original sample length is long enough. If you use a rate multiplier (implicit with rpitch) then the overall length of the sample will get shorter, and it may not last long enough for the adsr envelope you are trying to apply.
For example:
sample :ambi_glass_rub has a duration of 3.1493650793650794 beats
if you try sample :ambi_glass_rub,sustain: 20, release: 1 it will only last 3.1493650793650794 not 21!

1 Like
#8

@robin.newman,

Sonic-Pi can adjust the tempo difference with samples using the bt function, correct?’

Also, do you have any theories on how the function would run if sustain was negative? Somehow Sonic-pi lets pl run even if attack and release added together are greater than the length of the note, making sustain negative.

{rpitch: 0, sustain: -0.3504, amp: 0.9918, attack: 0.4754, release: 0.375, rate: 1.0}

This was output to my log when running some code.

#9

Ooops I’d forgotten about the bt function. Yes you are right you can use bt to adjust the times of the a d s r elements to allow for tempo changes. This is essentially what my example above did, except that I calculated it explicitly.
so you would use something like
sample sname, attack bt(a),sustain:bt(s),release: bt®
where a,s and r are the values specified for 60bpm.
As far as the negative values for sustain are concerned, I think you are in uncharted territory there, and results don’t seem to be very predictable. I tried some experiments, and sometimes (for low negative values) I heard something, but for others, nothing was heard, although no errors were flagged up. Best avoided I think. If you read the adsr section in the tutorial it does go into some detail as to how the various envelope factors come together, and you may be able to work out exactly what happens for negative values, but why would you want to use them anyway?

#10

The negative values weren’t intentional. Even though they were mistakes, I was curious and couldn’t figure out what it meant explicitly.