Sonic Pi: beware where it can lead you!

binary

I have added a video of this program driving iTunes visualiser to my youtube channel. See it at https://youtu.be/6V3wqwxYimA

Recently I wrote some code in Sonic Pi which investigated the production of a rhythmic pattern based on the binary numbers 0 to 15. This was inspired by an article I found at http://bernhardwagner.net/musings/RPABN.html where the writer use hex names to represent rhythmic patterns: thus E3 was 11100011 and A7 was 10100111. He also explored generating new patterns by shifting the bits in the original pattern 1 bit left or right. Thus E3 becomes 11000111 or C7 when you rotate it. In the program I developed I started with 4 strings, representing the number 0 to 15

0000000100100011 0100010101100111 1000100110101011 1100110111101111
These were assigned to 4 ring variables r1 to r4, and then 4 live_loops were set up, each one using a different sequence of these 4 rings r1r2r3r4 r2r3r4r1 r3r4r1r2 and r4r1r2r3

Each was used to play either a sequence of notes (chosen at random) from a pentatonic scale, with the tonic note changing using the sequence :c3,:g4,:g2,:c3. The mechanism used was to use the “look” function to test in turn each digit in the sequence and to play the note (two ocataves higher) if a 1 was detected. In each live loop a second note was played (at the base pitch) using the inverse rhythm, ie when a 0 was detected.
The base note altered every 64 ticks, and every 16 ticks the individual rings in each sequence were rotated either clockwise or anti-clockwise). As a rhythmic pattern, the resulting sounds continued ad infinitum, and to give a more presentable end result I decided to play the pattern for a given number of cycles, and to sandwich it between a winding up and winding down start and finish note generated by the fm synth with varying note pitch. I also added a level control so that the sound could gradually fade away before stopping.

The end result was quite pleasing, and I placed a recording on soundcloud.com and published to code on my gist site. By now I had become quite hooked on experimenting with the system, and lots of questions along the line of “I wonder what would happen if?…” sprang to mind. The result of a further couple of days of playing and experimenting resulted in version 2 being born, which I present here. I do this partly for my own benefit, as there are so many bits added that it is quite difficult to follow all the ins and outs of the final program. But here goes, and I hope that you will find it interesting and informative.

The first thing I decided to do was to emphasise the rotating nature of the patterns, by adding an accent to the first pulse of each 16. Here I must say that the one thing that makes the whole program possible is the use of tick and look functions. These can be very powerful in enabling to make selections and changes depending on the current values returned by these functions. In its simplest form tick is a counter that increments by 1 every time you invoke it, whereas look returns the current value that tick holds without altering it. I find it best to use one isolated tick at the start of a loop and then to use multiple look functions to extract its value at other position in the loop. You can use this value to iterate around the values in a ring variable. Thus if a=(ring 2,4,6,8) then a.look will return 2,4,6,8,2,4,6,8… as look changes from 0,1,2,3,4,5,6,7….
However, as well as a basic tick, you can create other tick counters with statements like
tick_set :foo,look/4  In this case as look increases you will get this:
look value:            0,1,2,3,4,5,6,7,8,9,10,11,12,13,14…
look(:foo) value    0,0,0,0,1,1,1,1,2,2,2,2,3,3….
That is look(:foo) increases at quarter of the rate.
Another useful trick I use to give a trigger say every 16 increments is:
puts”triggered” if look%16==0
which will print triggered every 16 times round the loop.

Secondly, in order to hear the rhythm “make up” of the four parts more clearly I decided to utilise the use_bpm_mul function to slow down the tempo during the first half of the “performance” and to speed it back up to its starting value during the second half. The user specifies the percentage drop in tempo required. (figures 0 to 30 are probably appropriate) and a factor is calculated to progressively change the tempo every 64 pulses after the first 32. Halfway through the factor is inverted e.g. 0.8 becomes 1.0/0.8 = 1.25 and the tempo speeds up again. By setting the percentage change to 0 the tempo will remain constant throughout.

The third change was to allow different scale structures rather than the original minor_pentatonic. To do this, I choose a scale type at random, and generated a list of the note offsets for the scale using scale(0,scale_name)
{for octave 0} and I then shuffled this to put the offsets in random order and generated the note by adding in the note value for the base note sequence :c3,:g3,:g2,:c3. To give further variation I also shuffled this sequence. I used the same shuffled sequence for each of the four live_loops x1 to x4, but for two of them I reversed the order of the shuffled sequence. Further flexibility came from having an alternative base note sequence :c3,:f3,:g3,:g2 which could be selected.

With quite a complex range of notes being played, it was sometimes difficult to get the full sense of the rhythms, so I added an alternative note selection, where instead of playing a sequence based on the notes in a given scale type, I played just the base note for each thread, with an offset for threads 2,3 and 4 to give the notes in a major chord: +4,+7 and +12. I also simplified things a bit for both the scale and chord options by setting the main rhythm part an octave higher that the inverted rhythm part.

The next change to the sound output was to set the pan values for the main rhythm parts to 0.8 and for the inverted parts to -0.8 and then to flip them to the opposite side every 32 ticks.

In the original program as discussed I played both the main rhythm and the inverse rhythm together. This gave a very full texture, but made it difficult to hear the rhythms generated.  In this version I have added options to play just the normal or inverted rhythms or both together as previously. I also added options to select which ones of the four generated parts should play. All of the above may sound very confusing but you can see the end result in the two play commands inside each of the four live_loops x1 to x4

play n+12,amp: rvol+krv,attack: y*t,release: (1-y)*t,pan: 0.8*flip if (r.look == 1) and rhythm.include?"n"#main rhythm
play n,amp: lvol+klv,attack: y*t,release: (1-y)*t,pan: -0.8*flip if (r.look != 1) and rhythm.include?"i" #inverse rhythm

r holds the current rhythm eg (ring 1, 1, 1, 0, 0, 1, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0)
The first note plays when a 1 is encoutered, the second when a 0 is found as look iterates through the ring. A second condition for the note to play is given by rhythm.include?”n” and rhythm.include?”i” If the variable rhythm includes an “n” then the main rhythm note is played and if it contains an “i” then the inverse rhythm note is played. Either or both can be selected.
The accents are given by krv and klv which are set non-zero on each 16th tick
flip is either 1 or -1 and is switched every 32 ticks reversing the pan values.
t {between 0 and 1} is set by by user and adjusts the timbre of the note by altering the attack/release proportions. rvol and lvol enable the user to adjust the relative amp settings for the right and left channels, bearing in mind that they cover different frequency ranges.
n is set to the current base note (plus chord offset) which alters every 64 when using the chord play option, otherwise, if using the scale based option, it is set to  note(base+bs.look) where bs holds the shuffled list of scale note offsets, which is added to the base note, or to note(base+bs.reverse.look) if it is to play the sequence backwards.

I now present sections of the program each with some comments. Unfortunately it is not possible to incorporate full comments in the program itself, or it exceeds the length limit to be able to play on a Mac. (It is a few character short of this limit!)

#Rotating binary rhythms by Robin Newman (v2), January 2016
#see rbnrpi.wordpress.com for details

use_debug false

define :bpm do
  return 60.0/Thread.current.thread_variable_get(:sonic_pi_spider_sleep_mul)
end

#function rotates ring entries
define :rv do |r,dir=1| #if dir = -1 then the rotation is backwards
  return ring( r[0].rotate(dir),r[1].rotate(dir),r[2].rotate(dir),r[3].rotate(dir))
end

loop do
  rs=Random.new_seed
  puts "random seed "+rs.to_s+"\n"
  use_random_seed(rs)

  sname=scale_names.choose
  ######### user settings below ###########
  rvol=0.4 #vol sets for left and right
  lvol=0.6
  kr=0.4 #accent inc right
  kl=0.4 #accent inc left
  numpasses=2 #should be even
  t=0.1
  tempochange=[0,10,16,20].choose
  plscale=[TRUE,TRUE,FALSE,TRUE].choose #TRUE scale based: FALSE chord notes
  entryexit=[TRUE,FALSE].choose #control entry/exit notes
  y=0.25 #adjust attack/release ratio
  startlevel=0.7
  loops=["1234","13","24","12","34"].choose#set loops to play x1...x4
  rhythm=["ni","ni","n","i"].choose  #n normal i=inverse
  ##############################################
  puts "USER DATA SETTINGS"
  puts "Number of 256 'tick' passes set to "+numpasses.to_s
  puts "playing major chords" if plscale==FALSE
  puts"playing notes from "+sname.to_s+" scale" if plscale==TRUE
  puts"playing loop numbers "+loops
  puts "playing rhythms "+rhythm+" (n normal, i inverse)"
  puts "entryexit note selected " if entryexit
  use_bpm 60

In the first section, a function bpm is defined. This uses an internal function in Sonic Pi to return the current bpm setting for the context when the function is called. I use this in calculating the current reduction in tempo as the program progresses.

The program utilises “selection rings” each of which contains 4 rings of 16 binary digits. A second function rv is defined which rotates the elements of each of these four rings either one place clockwise or anticlockwise depending on the dir variable.
Following this, the remainder of the program is placed inside a loop which repeats indefinitely (until you press the stop button), on each iteration playing a rotating binary rhythm set up with randomly selected parameters to control it.
Normally Sonic Pi generates a deterministic range of random numbers when it is running. This means that the same sequence of numbers is generated on each run, and that values returned by .choose or .shuffle will be repeatable. By changing the random number seed using the use_random_seed() function the sequence can be changed to a different one. To save manually changing this you can use the Ruby function Random.new_seed to generate an arbitrary value to place in the use_random_seed function. This means that a different setup will be generated EVERY time the loop starts. You could move the three lines after the loop do line outside this loop, and everything would work fine, but I have deliberately placed the lines inside the loop so that the (very long) seed number is printed for EVERY cycle played, and you can see the number required to recreate a particular rendition. But be warned, you will NOT be able to recreate a “nice” result without retaining the value of this very long integer. You can of course, just put in your own (known) value instead of using this, although in this case you should place it outside the loop, otherwise you will get exactly the same version on each iteration of the loop.
The scale_name is selected by sname=scale_names.choose and then there is a section where the user can preset various program settings. In this version choices are made for you for a list associated with each entry with a .choose command. Many of these settings have already been discussed: others are self evident. startlevel adjusts the initial level set in the with_fx :level command.
When the program is run, the values associated with many of these parameters are printed on the screen. If you want to make manual adjustments, you can remove the outside loop doend and set individual values for the parameters on each run.

bn=[ring(:c3,:f3,:g3,:g2 ),ring(:c3,:g3,:g2,:c3)].choose.shuffle #base notes
  puts"base notes "+bn.to_s
  bs=scale(0,sname).shuffle #get sequence of notes in scale shuffled
  #tempo changes
  fac1= (1-tempochange.to_f/100)**(1.0/(numpasses*4/2)) #calculate bmp_mul factor for slow down
  #puts fac1
  puts "tempo reduction set to "+((1-fac1**(numpasses*4/2))*100).round.to_s+"%" #check
  fac5=fac4=fac3=fac2=fac1 #set individual starting fac values for each loop

  #start with 4 16 bit rhythm patterns based on the binary numbers 0-15
  #set up binary rhythms r1 0-3,r2 4-7,r3 8-11,r4 12-15
  br=["0000000100100011","0100010101100111","0100010101100111","1100110111101111"].shuffle
  r1=[br[0],br[0].reverse].choose
  r2=[br[1],br[1].reverse].choose
  r3=[br[2],br[2].reverse].choose
  r4=[br[3],br[3].reverse].choose
  puts "Starting Rhythms "+r1+" "+r2+" "+r3+" "+r4 #currently selected rhythms

  #now convert to rings
  r1= r1.split('').map(&:to_i).ring
  r2= r2.split('').map(&:to_i).ring
  r3= r3.split('').map(&:to_i).ring
  r4= r4.split('').map(&:to_i).ring

  #make 4 selection rings
  rl1=ring(r1,r2,r3,r4)
  rl2=rl1.rotate
  rl3=rl2.rotate
  rl4=rl3.rotate

In the second section of the program, first the bass notes are setup in the ring bn and shuffled, and the shuffled note offsets for the chosen scale are setup in the ring bs.
fac1, the factor to use with each application of the use_bpm_mul command is calculated using the formula
fac1= (1-tempochange.to_f/100)**(1.0/(numpasses*4/2)) Basically with a tempo change every 64 ticks and a complete pass of the rhythm changes every 256 ticks there 4 changes each complete pass. If the program runs for numpasses there are (numpasses*4/2) steps to reduce the tempo by the required percentage, HALF the duration of the piece. (1-tempochange.to_f/100) is the complete tempo change required e.g. 0.8 for 20%  **(1.0/number of stages) works out the number of stages root of this number which gives the required factor. eg for 20% reduction with numpasses=4 we get 1/8th root of 0.8 = 0.9724924724660731 If we multiply this number by itself 8 times we get 0.8 which is the required factor. fac1 to fac5 are all set to this value to give one separate variable for each of the live_loops that need it.
br holds the initial 4 binary strings for number 0-3,4-7,8-11,12-15 which are initially shuffled in their order. The shuffled order is then split into four strings r1,r2,r3 and r4 and these are randomly reversed in their order. eg
r1=[br[0],br[0].reverse].choose
Then four selection rings rl1 to rl4 are created, using the four elements r1..r4 arranged in different orders r1r2r3r4, r2r3r4r1, r3r4r1r2 and r4r1r2r3
Later in the program they will be rotated using the rv function previously described.

  if entryexit then #optional fm "wind up" note to start
    use_synth :fm
    p=play 36,sustain: 5,divisor: 12,amp: 0.3
    control p,note_slide: 3,note: 56,amp_slide: 4,amp: 1
    sleep 3
    control p,amp_slide: 2,amp:0
  end

The fourth section generates the starting note (if selected by the value of entryexit.
It uses the fm synth, and starts a low note playing sustained for 5 beats with a starting pitch 36. As this note plays, its pitch is controlled and raised over 3 seconds up to 56 whilst at the same time the amp: is increased to 1, and then faded out to 0.

  with_fx :level do |v| #used to fade out rhythms at the end
    control v,amp: startlevel #set initial level
    with_fx :reverb do
      live_loop :audio do
        tick
        if look >= (numpasses-1)*256+128 #start fading during last pass
        then
          limit=1 #set level endpoint
          limit=0.7 if entryexit #leave 30% if exit note
          control v,amp: startlevel*( 1.0-limit*(look-128-(numpasses-1)*256).to_f/128) #fade to 30% or 0%
        end
        fac5=1.0/fac5 if look==numpasses*256/2 #change fac to speed up at half way point
        use_bpm_mul fac5 if (32-look%64)==0
        sleep t
        stop if look==numpasses*256 #stop after numpasses completed
      end

The main execution section starts with the fifth section. First with_fx wrappers for :level and :reverb are set up. The level setting control is v, which is used to set the starting value to startlevel. Thereafter it is controlled by the live_loop :audio
This live_loop uses its tick counter to wait until the program is halfway through the final complete loop pass when it triggers a reduction in the level setting either to zero or to 30% depending upon whether the exit note is to be played or not as selected in the user settings. The bpm setting for this loop is adjusted according to the tempochange selected by the user, so that the timings remain synchronised to the four main live_loops x1 to x4 which generate the notes played. The factor fac5 is applied every 64 ticks after the first 32 to accomplish this. Halfway through fac5 is changed to 1.0/fac5 so that the tempo starts increasing again. A stop command kills the loop when look equals the number of passes selected *256, 256 being the number of ticks in one rhythmic cycle.

      live_loop :x1 do
        use_synth :blade
        tick
        tick_set :rc1,look/256 #used to select current ring from selection ring
        tick_set :bs1,look/64 #used to select base note pitch
        tick_set :flipper1,look/32 #used to flip pan settings

        base=bn.look(:bs1) #get base note
        r=rl1.look(:rc1) #get current ring

        if look%16==0 then #set emphasis for first beat of 16 (added to amp: setting)
          krv=kr;klv=kl
        else
          krv=klv=0
        end

        if look(:flipper1)%2==0 then #set pan flip
          flip=1
        else
          flip=-1
        end

        fac1=1.0/fac1 if look==numpasses*256/2 #change fac to speed up at half way point

        use_bpm_mul fac1 if (32-look%64)==0#apply bpm_mul every 64 beats
        puts "initial tempo=100%" if look == 0
        puts "tempo now reduced to "+(((bpm.to_f/60*10000).round).to_f/100).to_s+"% will increase back to 100%" if look == numpasses/2*256 - 32 and (tempochange !=0)
        if plscale then #select note according to whether scale or chord selected
          n=note(base+bs.look)
        else
          n=base
        end
        if loops.include?"1" then
          play n+12,amp: rvol+krv,attack: y*t,release: (1-y)*t,pan: 0.8*flip if (r.look == 1) and rhythm.include?"n"#main rhythm
          play n,amp: lvol+klv,attack: y*t,release: (1-y)*t,pan: -0.8*flip if (r.look != 1) and rhythm.include?"i" #inverse rhythm
        end
        sleep t

        rl1=rv(rl1) if look%16==0 #rotate all the rings in the selection ring

        if look(:rc1)==numpasses then
          cue :go
          stop
        end
        puts "Current pass "+(look(:rc1)+1).to_s if look%256==0 #print current cycle on the screen
      end

The four live_loops x1 to x4 are essentially the same, with minor differences. Each one uses a different synth. I will describe live_loop :x1. To avoid accidental “double ticking” ONE tick call is used in the loop in the third line. Three other named ticks are started.
:rc1 is set to increment every 256 ticks and is used to select the current ring from the selection ring.
:bs1 is set to increment every 64 ticks and is used to select the bass note pitch.
:flipper is set to increment every 32 ticks and is used to flip the pan settings.
The comments in the live_loop together with the previous description of other parts of the program should make the remaining operation of the loop reasonably clear.
Unlike the other three x2..x4 loops this loop also has a couple of puts statements to put information about the current tempo setting and the current pass number on the screen.
The application of the rv rotate function alters the rhythmic pattern as the loop plays.
Live_loop :x4 has one important addition. It includes the line:

      cue :fin if look==(numpasses*256)-20 #trigger finish loop

This is used to trigger final “wind-down” fm note if it has been enabled according to the user flag entryexit. This trigger occurs 20 ticks or 2 seconds at full tempo before the the live_loops :x1 rto :x4 terminate.
live_loop :x1 also contains an extra command to send a cue :go to allow the loop doend to complete when live_loop :x1 terminates in the case that the entryexit  is NOT to be generated. Otherwise the loop exit follows when the wind-down note has completed.

   end #reverb

    #end piece.....fm "wind down" finishes.
    if entryexit then
      sync :fin
      use_synth :fm
      p=play 56,attack: 0.1,sustain_level: 0.9,sustain: 6,divisor: 12,amp: 1
      sleep 1 #sustain for 1 sec before changing
      control p,note_slide: 6,note: 36
      sleep 3
      puts "finished!"
      sleep 1
    end
    sync :go if !entryexit #wait for sync from loop x1
    sleep 1
  end#end level
end#loop

The final section first ends the :reverb fx after the end of live_loop :x4, then triggers the exit note assuming entryexit is TRUE when it receives its sync :fin Again this is produced using the fm synth. This starts with the pitch with which the entry note finished 56 and uses a control function to reduce the pitch to 36 over 6 beats, producing a winding down sound to finish the piece.

For me this turned into a large project, with much experimentation and things to try out. As I said at the beginning be warned. Beware where Sonic Pi can lead you. This is why I think it is such a great program. it lets you explore both musical, mathematical and programming ideas all at the same time. It gives -usually :-)  immediate aural feedback to your ideas, is very powerful and flexible and basically great fun to use. Sadly it does use up an inordinate amount of time…but it is all worth it!

The program has been tested on Sonic Pi v2.9 on both MAcOSX, Win PC and Raspbian Jessie running on a Pi2. It is unlikely to perform very well on a Model B+ or earlier. I haven’t tested it on Ubuntu, but I see no reason why it wont; work on that platform too.

You can download the complete version 2 program from my gist here

There is a soundcloud item containing two audio specimens from this new version here

Advertisements

A third visit to Mozart’s Dice generated minuet using Sonic Pi

Screen Shot 2016-01-20 at 18.11.59

I have previously written two articles  on this topic, the first in August 2013, which used python and lilypond together with timidity to play the midi files produced. Imagemagick was used to facilitate the production a web pages to display the music while it played. Whilst this worked it was quite complex to set up, and fairly slow to generate each minuet.

In the second article in January 2015 I used Sonic Pi to play the minuets, but still used lilypond to produce the music display, and was quite complex to setup.

Now in this third visit, the whole process is controlled by Sonic Pi, and I utilise 176 images of the different bars to build a web page displaying the current minuet being played.
Sonic Pi is helped by an external script, written in Ruby which interacts with the Sonic Pi workspace to produce the webpages which are displayed in the Epiphany browser on Raspbian Jesse, using a Pi2 to run the whole process.

I have taken the opportunity to rework the code in Sonic Pi which produces the sound from the version used in 2015. The present version is much quicker in producing the webpages and the whole process works in a more streamlined manner than previously.

Two additions needs to be installed. The first is the sonic-pi-cli gem. In order to do this, without installing and using a system like rvm, we have to do a bit of preliminary work so that the correct permissions will be in place during the install process. First, if you have switched to booting into a command line environment, start the graphical environment using startx. You may also like to switch back to this environment on boot by using Raspberry Pi Configuration utility in Menu -> Preferences. The graphics environment is needed for Sonic Pi to work.
Start a terminal window and type the following:

sudo mkdir /var/lib/gems
sudo chown pi /var/lib/gems
sudo chown pi /usr/local/bin
gem install sonic-pi-cli

Then revert the ownership of the /usr/local/bin folder

sudo chown root /usr/local/bin

Note if you a have a pre-existing /var/lib/gems folder you will get an error when you try and create it. Ignore the error, but alter the second line above from

sudo chown pi /var/lib/gems

to

sudo chown pi -R /var/lib/gems

Now test the cli by starting Sonic Pi, loading in any piece of reasonable duration, eg one of the example files, and running it. While it is running from the terminal window type:

sonic_pi stop

The piece should stop playing, showing that the cli command is working.

The second addition is to add the package xautomation which is used to switch the browser to full screen view.

sudo apt-get update
sudo apt-get install xautomation

Other than that, you only need to download the Sonic Pi file MozartDiceMinuet.rb and the helper Ruby script minuetcontrol.rb together with the image folder cards which contains the 176 images of the separate bars. Download them all to the pi home folder.

Open MozartDiceMinuets.rb with leafpad and copy the text into an empty Sonic Pi buffer.
start a terminal window, and start the control script running by typing

ruby minuetcontrol.rb

and pressing return

(You will not see anything happening, as the script awaits the creation of a text file which is done by Sonic Pi)
Now switch to Sonic Pi and run the workspace containing MozartDiceMinuet.

As supplied the program should play three waltzes one after the other, displaying the music on the Epiphany web-browser.

To re-run, you just need to start the minuetcontrol.rb script again, before pressing run on Sonic Pi.

In the sonic-pi program there are three user adjustments you can make.

numtunes is a variable that holds the number of minutes to generate. (default value 3)
use_random_seed(123456) is used to adjust the random number generator seed, and hence the sequence of minutes that will be generated. If you leave it unalterd you will always get the SAME three minutes generated. Alter it to produce a different sequence.
testbars=0 is a variable which is normally set to 0. If you set it to 1 then the program will generate 11 sequences, each containing 16 bars in numerical order from the 176 that are available. This is used for testing, to check each bar sequence is correct.

In the accompanying minuetcontrol.rb script there is one user adjustment
The variable bpix=1200 holds the width of the table (in pixels) which will display the bar images in the html page displayed on the Epiphany browser. This should be adjusted to suit the resolution of the screen you are using.  It should be approx 80-100 pixels LESS than the screen width, but should also be a multiple of 8. (I used 1200 for my HDMPPi monitor, 720 when running on a Raspberry Pi official touch screen monitor, and 1840 when running on my 19.5″ BENQ monitor).

The listings for the Sonic-Pi MozartDiceProgram and the minuetcontrol.rb script are shown below.

#Mozart's Musikalisches Wurkfelspiel KV 516f coded for Sonic Pi
#by Robin Newman January 2016
#This version uses a helper ruby script minuetcontrol.rb which should be launched
#prior to starting the Sonic Pi script using: ruby minuetcontrol.rb
#It also requires the folder cards to be located in the pi home directory.
#The music is displayed in the epiphany browser.

###### user adjustable parameters ####################
#set the number of minuets (numtunes) to play and the random number seed
numtunes=3
use_random_seed(123456)
testbars=0 #set to 1 to test all bars in sequence
###### end of user adjustable parameters #############

numtunes=11 if testbars==1
set_sched_ahead_time! 5 #give SP plenty of time to setup notes
use_debug false
use_synth :tri
#define arrays to holds note info for each bar lh and rh
rn=[]
ln=[]
rd=[]
ld=[]
#duration info: s is tempo scale factor
s=0.2
dsq=0.5*s
sq=1*s
q=2*s
qd=3*s
c=4*s
cd=6*s

p1=[sq]*6 #rhythm patterns
p2=[sq,sq,sq,sq,q]
p3=[q,sq,sq,sq,sq]
p4=[q,q,q]
p5=[c,q]
p6=[q,c]
p7=[q,sq,sq,q]
p8=[sq,sq,q,q]
p9=[sq,sq,q,sq,sq]
#plays a given bar, rh and lh, flip is used for 2nd time bar change
define :pb do |n,flip=0|
in_thread do
rn[n].zip(rd[n]).each do |p,d|
play p,attack:0.08*d,sustain: 0.82*d,release: 0.1*d,amp: 0.8
sleep d
end
end
nl=n
#change lh for repeat bars
if (flip==1) and [5,24,30,33,81,91,94,100,107,123,127].include? n then
nl=177 #alternative lh for second time bar
end
ln[nl].zip(ld[nl]).each do |p,d|
play p,attack:0.08*d,sustain: 0.82*d,release: 0.1*d,amp: 0.8
sleep d
end
end
define :trn do |n,num,offset=2| #produces trill sequence using n and tone (or semitone) above: n notes total
n = note_info(n).midi_note
return [n,n+offset]*(num / 2) #trill set to start on lower note. Can be swapped over
end
define :trd do |d,num| #produces trill note durations. d is total duration, num number of notes
return [d/num]*num
end
#define lookup table which selects bar numbers to play
tbl=[[96, 22, 141, 41, 105, 122, 11, 30, 70, 121, 26, 9, 112, 49, 109, 14],
[32, 6, 128, 63, 146, 46, 134, 81, 117, 39, 126, 56, 174, 18, 116, 83],
[69, 95, 158, 13, 153, 55, 110, 24, 66, 139, 15, 132, 73, 58, 145, 79],
[40, 17, 113, 85, 161, 2, 159, 100, 90, 176, 7, 34, 67, 160, 52, 170],
[148, 74, 163, 45, 80, 97, 36, 107, 25, 143, 64, 125, 76, 136, 1, 93],
[104, 157, 27, 167, 154, 68, 118, 91, 138, 71, 150, 29, 101, 162, 23, 151],
[152, 60, 171, 53, 99, 133, 21, 127, 16, 155, 57, 175, 43, 168, 89, 172],
[119, 84, 114, 50, 140, 86, 169, 94, 120, 88, 48, 166, 51, 115, 72, 111],
[98, 142, 42, 156, 75, 129, 62, 123, 65, 77, 19, 82, 137, 38, 149, 8],
[3, 87, 165, 61, 135, 47, 147, 33, 102, 4, 31, 164, 144, 59, 173, 78],
[54, 130, 10, 103, 28, 37, 106, 5, 35, 20, 108, 92, 12, 124, 44, 131]]

define :throw do #throws two dice and returns total (throws offset 0-5 to match arrays)

return rrand_i(0,5)+rrand_i(0,5)
end
#pick the bars for a minuet
define :minuet do
w=[]
8.times do |i|
w.concat [tbl[throw][i]]
end
8.times do |i|
w.concat [tbl[throw][i+8]]
end
return w
end
rn[1]=[:f5,:d5,:g5];rd[1]=p4
ln[1]=[:f3,:d3,:g3];ld[1]=p4
rn[2]=[:a4,:fs4,:g4,:b4,:g5];rd[2]=p3
ln[2] = ln[86] = ln[121] = ln[133] = [[:b2,:g3],:r];ld[2] = ld[86] = ld[121] = ld[133] =p5
rn[3]=rn[59]=rn[87]=rn[144]=[:g5,:c5,:e5];rd[3]=rd[59]=rd[87]=rd[144]=p4
ln[3] = ln[6] = ln[32] = ln[40] = ln[41] = ln[43] = ln[51] = ln[60] = ln[69] = ln[74] = ln[84] = \
ln[95] = ln[115] = ln[119] = ln[136] = ln[142] = ln[148] = ln[152] = ln[167] = [[:c3,:e3],:r];ld[3] = ld[6] = ld[32] = ld[40] = ld[41] = \
ld[43] = ld[51] = ld[60] = ld[69] = ld[74] = ld[84] = ld[95] = ld[115] = ld[119] = ld[136] = ld[142] = ld[148] = ld[152] = ld[167] = p5
rn[4]=[:g5]+trn(:d5,8);rd[4]=[q]+trd(c,8)
ln[4]=[:g2,:b2,:g3,:b2];ld[4]=p8
rn[5]=rn[24]=rn[30]=rn[33]=rn[81]=rn[91]=rn[94]=rn[100]=rn[107]=rn[123]=rn[127]=[[:g4,:b4,:d5,:g5],:r]; \
rd[5]=rd[24]=rd[30]=rd[33]=rd[81]=rd[91]=rd[94]=rd[100]=rd[107]=rd[123]=rd[127]=p5
ln[5] = ln[24] = ln[30] = ln[33] = ln[81] = ln[91] = ln[94] = ln[100] = ln[107] = ln[123] = ln[127] = [:g2,:g3,:f3,:e3,:d3]; \
ld[5] = ld[24] = ld[30] = ld[33] = ld[81] = ld[91] = ld[94] = ld[100] = ld[107] = ld[123] = ld[127] = p3
rn[6]=rn[32]=rn[174]=[:g4,:c5,:e5];rd[6]=rd[32]=rd[174]=p4
rn[7]=[:e5,:c5,:e5,:g5,:c6,:g5];rd[7]=p1
ln[7]=[[:c3,:g3],:r];ld[7]=p5
rn[8] = rn[14] = rn[79] = rn[83] = rn[93] = rn[111] = rn[131] = rn[151] = rn[170] = rn[172] = [:c5,:r];\
rd[8] = rd[14] = rd[79] = rd[83] = rd[93] = rd[111] = rd[131] = rd[151] = rd[170] = rd[172] = p5
ln[8] = ln[14] = ln[79] = ln[83] = ln[93] = ln[111] = ln[131] = ln[151] = ln[170] = ln[172] = [:c3,:g2,:c2];\
ld[8] = ld[14] = ld[79] = ld[83] = ld[93] = ld[111] = ld[131] = ld[151] = ld[170] = ld[172] = p4
rn[9]=[[:c5,:e5],[:b4,:d5],:r];rd[9]=p4
ln[9]=[:g3,:g2];ld[9]=p5
rn[10]=[:b4,:a4,:b4,:c5,:d5,:b4];rd[10]=[sq]*8
ln[10]= ln[34]= [:g3,:r];ld[10]= ld[34]= p5
rn[11]=[:e5,:c5,:b4,:a4,:g4,:fs4];rd[11]=[sq]*8
ln[11] = ln[21] = ln[36] = ln[62] = ln[106] = ln[110] = ln[118] = ln[134] = ln[147] = ln[159] = ln[169] = [:c3,:d3,:d2];\
ld[11] = ld[21] = ld[36] = ld[62] = ld[106] = ld[110] = ld[118] = ld[134] = ld[147] = ld[159] = ld[169] = p4
rn[12] = rn[54] = rn[124] = rn[130] = [[:e4,:c5]]*3;rd[12] = rd[54] = rd[124] = rd[130] =p4
ln[12] = ln[54] = ln[75] = ln[124] = ln[130] = ln[146] = ln[161]= [:c3,:c3,:c3];\
ld[12] = ld[54] = ld[75] = ld[124] = ld[130] = ld[146] = ld[161]= p4
rn[13]=[:c5,:g4,:e4];rd[13]=p4
ln[13] = ln[17] = ln[45] = ln[50] = ln[61] = ln[85] = ln[103] = ln[156] = [[:e3,:g3],:r];\
ld[13] = ld[17] = ld[45] = ld[50] = ld[61] = ld[85] = ld[103] = ld[156] = p5
rn[15]=[:e5,:g5,:e5,:c5];rd[15]=p7
ln[15] = ln[19] = ln[48] = ln[101] = ln[108] = ln[162] = [[:c3,:g3],[:c3,:e3]];\
ld[15] = ld[19] = ld[48] = ld[101] = ld[108] = ld[162] = p5
rn[16]=[:a5,:fs5,:d5];rd[16]=p4
ln[16]=ln[120]=[[:d3,:fs3],[:c3,:fs3]];ld[16]=ld[120]=p5
rn[17]=[:c5,:g4,:c5,:e5,:g4,:c5];rd[17]=p1
rn[18]=[:g4,:c5,:e5];rd[18]=p4
ln[18] = ln[76] = ln[87] = [[:c3,:e3],[:c3,:g3]];ld[18] = ld[76] = ld[87] = p5
rn[19]=[:e5,:c5,:e5,:g5];rd[19]=p8
rn[20]=[:g5,:b5,:d6,:d5];rd[20]=p7
ln[20]=ln[139]=[:b2,:r];ld[20]=ld[139]=p5
rn[21]=[:c5,:e5,:g5,:d5,:a4,:fs5];rd[21]=p1
rn[22]=rn[57]=rn[96]=rn[112]=[:e5,:c5,:g4];rd[22]=rd[57]=rd[96]=rd[112]=p4
ln[22] = ln[53] = ln[63] = ln[80] = ln[96] = ln[104] = ln[105] = ln[153] = ln[154] = ln[157] = [:c3,:r];\
ld[22] = ld[53] = ld[63] = ld[80] = ld[96] = ld[104] = ld[105] = ld[153] = ld[154] = ld[157] = p5
rn[23]=[:f5,:e5,:d5,:e5,:f5,:g5];rd[23]=p1
ln[23]=[:f3,:e3,:d3,:e3,:f3,:g3];ld[23]=p1
rn[25]=[:d4,:fs4,:a4,:d5,:fs5,:a5];rd[25]=p1
ln[25]=ln[70]=[:d3,:c3];ld[25]=ld[70]=p5
rn[26]=[[:c5,:e5]]*3;rd[26]=p4
ln[26]=[:c3,:e3,:g3,:e3,:c4,:c3];ld[26]=p1
rn[27]=[:f5,:e5,:f5,:d5,:c5,:b4];rd[27]=p1
ln[27] = ln[113]=ln[166]=[[:g3,:b3],:r];ld[27]=ld[113]=ld[166]=p5
rn[28]=[:fs5,:d5,:a4,:a5,:fs5,:d5];rd[28]=p1
ln[28]=[[:c3,:a3],:r];ld[28]=p5
rn[29]=[:b4,:d5,:g5,:d5,:b4];rd[29]=p2
ln[29]=[:g3,:g2];ld[29]=p5
rn[31]=[:e5,:c5,:g4,:e5];rd[31]=p8
ln[31]=ln[64]=[[:c3,:g3],[:c3,:g3]];ld[31]=ld[64]=p5
rn[34]=[:e5,:c5,:d5,:b4,:g4];rd[34]=p2
rn[35]=[:c5,:d5,:fs5];rd[35]=p4
ln[35]=[[:d3,:fs3],[:c3,:a3]];ld[35]=p5
rn[36]=[:a4,:e5,:d5,:g5,:fs5,:a5];rd[36]=p1
rn[37]=[:g5,:b5,:g5,:d5,:b4];rd[37]=p2
ln[37] = ln[46] = ln[47] = ln[55] = ln[155] = ln[163] =[[:b2,:d3],:r];\
ld[37] = ld[46] = ld[47] = ld[55] = ld[155] = ld[163] =p5
rn[38]=rn[98]=rn[137]=rn[142]=[:c5,:g4,:e5];rd[38]=rd[98]=rd[137]=rd[142]=p4
ln[38] = ln[49] = ln[57] = ln[58] = ln[59] = ln[73] = ln[98] = ln[112] = ln[137] = ln[144] = ln[174] =[[:c3,:e3],:g3]*3;\
ld[38] = ld[49] = ld[57] = ld[58] = ld[59] = ld[73] = ld[98] = ld[112] = ld[137] = ld[144] = ld[174] = p1
rn[39]=[:g5,:g4,:g4];rd[39]=p4
ln[39]=[:b2,:d3,:g3,:d3,:b2,:g2];ld[39]=p1
rn[40]=rn[67]=rn[160]=[:c5,:b4,:c5,:e5,:g4,:c5];\
rd[40]=rd[67]=rd[160]=p1
rn[41]=[:c5,:b4,:c5,:e5,:g4];rd[41]=p2
rn[42]=[:b4,:c5,:d5,:b4,:a4,:g4];rd[42]=p1
ln[42] = ln[128] = ln[158] =[:g2,:r];ld[42] = ld[128] = ld[158] =p5
rn[43]=rn[60]=rn[152]=rn[168]=[:g5,:f5,:e5,:d5,:c5];\
rd[43]=rd[60]=rd[152]=rd[168]=p3
rn[44]=[:a4,:f5,:d5,:a4,:b4];rd[44]=p3
ln[44] = ln[52] = ln[72] = ln[116] = ln[145] = ln[149] = ln[173] =[:f3,:g3];\
ld[44] = ld[52] = ld[72] = ld[116] = ld[145] = ld[149] = ld[173] =p5
rn[45]=[:c5,:b4,:c5,:g4,:e4,:c4];rd[45]=p1
rn[46]=[:g5,:b5,:g5,:d5,:b4];rd[46]=p3
rn[47]=[:g5,:g5,:d5,:b5];rd[47]=p7
rn[48]=[:e5,:c5,:e5,:g5,:c6];rd[48]=p3
rn[49]=[:e5,:c5,:g4];rd[49]=p4
rn[50]=[:c5,:e5,:c5,:g4];rd[50]=p7
rn[51]=rn[84]=rn[115]=[:c5,:g4,:e5,:c5,:g5,:e5];rd[51]=rd[84]=rd[115]=p1
rn[52]=[:d5,:cs5,:d5,:f5,:g4,:b4];rd[52]=p1
rn[53]=[[:c5,:e5],[:c5,:e5],[:d5,:f5],[:e5,:g5]];rd[53]=p7
rn[54]=[[:e4,:c5]]*3;rd[54]=p4
rn[55]=[:g5,:b5,:d5];rd[55]=p4
ln[55]=[[:b2,:d3],:r];ld[55]=p5
rn[56]=[:d5,:b4,:g4,:r];rd[56]=p8
ln[56] = ln[92] = [[:g2,:g3],:g3];ld[56] = ld[92] = p5
rn[58]=[:g5,:e5,:c5];rd[58]=p4
rn[61]=[:c5,:e5,:c5,:g5];rd[61]=p7
rn[62]=[:e5,:c5,:b4,:g4,:a4,:fs4];rd[62]=p1
rn[63]=[:e5,:c5,:b4,:c5,:g4];rd[63]=p2
rn[64]=[:e5,:g5,:c6,:g5,:e5,:c5];rd[64]=p1
rn[65]=[:d5,:a4,:d5,:fs5];rd[65]=p8
ln[65]=ln[117]=[[:d3,:fs3],:r];ld[65]=ld[117]=p5
rn[66]=[:fs5,:a5,:fs5];rd[66]=p4
ln[66]=[[:d3,:a3],[:d3,:fs3],[:c3,:d3]];ld[66]=p4
ln[67] = ln[168] =[[:c3,:e3],[:e3,:g3]];ld[67] = ld[168] =p5
rn[68]=[:g5,:b5,:g5,:d5,:g5];rd[68]=p3
ln[68] = ln[165] = [:b2,:r];ld[68] = ld[165] = p5
rn[69]=[:g5,:e5,:c5];rd[69]=p4
rn[70]=rn[105]=[:fs5,:a5,:fs5,:d5,:fs5];rd[70]=rd[105]=p3
rn[71]=[:g5,:b5,:d6,:b5,:g5];rd[71]=p2
ln[71] = ln[88] = ln[143] = ln[176] = [[:b2,:d3]]*2;ld[71] = ld[88] = ld[143] = ld[176] =p5
rn[72]=[:f5,:e5,:d5,:c5,:b4,:d5];rd[72]=p1
rn[73]=[:g5,:e5,:c5];rd[73]=p4
rn[74] = rn[76] = rn[136] = rn[148] = [:c6,:b5,:c6,:g5,:e5,:c5];rd[74] = rd[76] = rd[136] = rd[148] = p1
rn[75]=[[:d5,:fs5]]*3;rd[75]=p4
rn[77]=[:g5,:b5,:g5,:d5];rd[77]=p8
ln[77]=[[:b2,:d3],[:b2,:g3]];ld[77]=p5
rn[78]=[:c5,:c4,:r];rd[78]=p4
ln[78]=[:c3,:c2];ld[78]=p5
rn[80]=[:d5,:b4,:a4,:g4,:a4,:fs5];rd[80]=[q,dsq,dsq,dsq,dsq,q]
rn[82]=[:d5,:b4,:g4,:g5];rd[82]=p8
ln[82]=[[:b2,:g3],[:b2,:d3]];ld[82]=p5
rn[85]=[:c5,:e5,:g4];rd[85]=p4
rn[86]=[:d5,:d5,:g5,:b5];rd[86]=p7
rn[88]=[:g5,:d5,:g5,:b5,:g5,:d5];rd[88]=p1
rn[89]=[:f5,:e5,:d5,:g5];rd[89]=p8
ln[89]=[:f3,:e3,:d3,:g3];ld[89]=p8
rn[90]=[:fs5,:a5,:d6,:a5,:fs5,:a5];rd[90]=p1
ln[90]=[[:c3,:a3]]*2;ld[90]=p5
rn[92]=[[:b4,:d5],:g5,:b5,:d5];rd[92]=p7
rn[95]=[:g5,:e5,:c5];rd[95]=p4
rn[97]=rn[163]=[:g5,:fs5,:g5,:d5,:b4,:g4];rd[97]=rd[163]=p1
ln[97]=[[:b2,:d3],[:b2,:g3]];ld[97]=p5
rn[99]=[:fs5,:a5,:d5];rd[99]=p4
ln[99] = ln[102] =[[:c3,:a3]]*2;ld[99] = ld[102]=p5
rn[101]=rn[104]=rn[157]=rn[162]=[:e5,:d5,:e5,:g5,:c6,:g5];\
rd[101]=rd[104]=rd[157]=rd[162]=p1
rn[102]=[:fs5,:d5,:a4,:fs5];rd[102]=p8
rn[103]=[:c5,:e5,:c5,:g4,:e4];rd[103]=p2
rn[106]=[:a4,:d5,:c5,:b4,:a4];rd[106]=p3
rn[108]=[:e5,:g5,:c6];rd[108]=p4
rn[109]=[:d5,:f5,:d5,:f5,:b4,:d5];rd[109]=p1
ln[109]=[[:f3,:a3],[:g3,:d4]];ld[109]=p5
rn[110]=[[:b4,:d5],[:a4,:c5],[:a4,:c5],[:g4,:b4],[:g4,:b4],[:fs4,:a4]];rd[110]=p1
rn[113]=[:f5,:d5,:b4];rd[113]=p4
rn[114]=[[:b4,:d5]]*3;rd[114]=p4
ln[114]=[:g3,:g3,:g3];ld[114]=p4
rn[116]=[:d5,:f5,:a5,:f5,:d5,:b4];rd[116]=p1
rn[117]=[:d5,:a4,:d5,:fs5,:a5,:fs5];rd[117]=p1
rn[118]=[:e5,:a5,:g5,:b5,:fs5,:a5];rd[118]=p1
rn[119]=[:e5,:c5,:g5,:e5,:c6,:g5];rd[119]=p1
rn[120]=[:d6,:a5,:fs5,:d5,:a4];rd[120]=p1
rn[121]=[:g5,:b5,:g5,:d5];rd[121]=p7
rn[122]=[:g5,:fs5,:g5,:b5,:d5];rd[122]=p2
ln[122]=[[:b2,:d3]]*3;ld[122]=p4
rn[125]=[:g5,:e5,:d5,:b4,:g4];rd[125]=p2
ln[125]=ln[132]=ln[175]=[:g3,:g2,:r];ld[125]=ld[132]=ld[175]=p4
rn[126]=[:c5,:g4,:c5,:e5,:g5,[:c5,:e5]];rd[126]=p1
ln[126]=[:e3,:e3,:c3];ld[126]=[c,sq,sq]
rn[128]=[:b4,:d5,:g5];rd[128]=p4
rn[129]=[:a5,:g5,:fs5,:g5,:d5];rd[129]=p2
ln[129]=[[:b2,:d3],[:b2,:d3],[:b2,:g3]];ld[129]=p4
rn[132]=[[:c5,:e5],[:b4,:d5],[:g4,:b4],:g4];rd[132]=p7
rn[133]=[:d5,:g5,:d5,:b4,:d5];rd[133]=p3
rn[134]=[:a4,:e5,[:b4,:d5],[:a4,:c5],[:g4,:b4],[:fs4,:a4]];rd[134]=p1
rn[135]=[:fs5,:fs5,:d5,:a5];rd[135]=p7
ln[135]=[[:c3,:d3]]*3;ld[135]=p4
rn[138]=[[:a4,:d5,:fs5]]+trn(:g5,8,-1);rd[138]=[q]+trd(c,8)
ln[138]=[:d2,:d3,:cs3,:d3,:c3,:d3];ld[138]=p1
rn[139]=[:g5,:b5,:g5,:b5,:d5];rd[139]=p2
rn[140]=[:a4,:a4,:d5,:fs5];rd[140]=p7
ln[140]=[[:c3,:fs3],[:c3,:fs3],[:c3,:a3]];ld[140]=p4
rn[141]=[:d5,:e5,:f5,:d5,:c5,:b4];rd[141]=p1
ln[141]=[[:b2,:g3],:g2];ld[141]=p5
rn[143]=[:g5,:d5,:b4,:g4];rd[143]=p7
rn[145]=[:d5,:f5,:a4,:d5,:b4,:d5];rd[145]=p1
rn[146]=[[:fs4,:d5],[:d5,:fs5],[:fs5,:a5]];rd[146]=p4
rn[147]=[:e5,:c6,:b5,:g5,:a5,:fs5];rd[147]=p1
rn[149]=[:f5,:d5,:a4,:b4];rd[149]=p8
rn[150]=[[:g4,:c5,:e5]]+trn(:f5,8,-1);rd[150]=[q]+trd(c,8)
ln[150]=[:c3,:b2,:c3,:d3,:e3,:fs3];ld[150]=p1
rn[153]=[:d5,:a4,:fs5,:d5,:a5,:fs5];rd[153]=p1
rn[154]=[:d5,:cs5,:d5,:fs5,:a5,:fs5];rd[154]=p1
rn[155]=[:g5,:b5,:g5,:d5,:b4,:g4];rd[155]=p1
rn[156]=[:c5,:g4,:e5,:c5,:g5];rd[156]=p1
rn[158]=[:b4,:d5,:b4,:a4,:g4];rd[158]=p3
rn[159]=[:e5,:g5,:d5,:c5,:b4,:a4];rd[159]=p1
ln[160]=[[:c3,:e3]]*2;ld[160]=p5
rn[161]=[[:fs4,:d5]]*3;rd[161]=p4
rn[164]=[:d5,:g4];rd[164]=p6
ln[164]=[:g3,:fs3,:g3,:d3,:b2,:g2];ld[164]=p1
rn[165]=[:d5,:b4,:g4];rd[165]=p4
rn[166]=[:d5,:b5,:g5,:d5,:b4];rd[166]=p2
rn[167]=[:c5,:c5,:d5,:e5];rd[167]=p7
rn[169]=[:e5,:g5,:d5,:g5,:a4,:fs5];rd[169]=p1
rn[171]=[:b4,:c5,:d5,:e5,:f5,:d5];rd[171]=p1
ln[171]=[[:g2,:g3],[:b2,:g3]];ld[171]=p5
rn[173]=[:f5,:a5,:a4,:b4,:d5];rd[173]=p9
rn[175]=[:e5,:c5,:b4,:d5,:g5];rd[175]=p2
rn[176]=[:a5,:g5,:b5,:g5,:d5,:g5];rd[176]=p1
#second time bars
ln[177]=[:g2,:b3,:g3,:fs3,:e3];ld[177]=p3

define :playminuets do |numtimes,test=0| #test=1 when testing all bars
numtunes.times do |i|
if test==0 then
perform=minuet #for normal use test = 0 setup minuet
else
perform =range(16*i+1,16*i+17) #otherwise set up bar linear list
end
puts perform
performplus=[numtunes]+perform
file=File.open("/tmp/perform.txt","w") #write current bar numbers to a file
file.puts performplus.to_s
file.close
puts"wait for sync via sonic-pi-cli"
sync :go
if test==0 then #if not testing then do repeats: y=2
y=2
else #if testing y=1
y=1
end
y.times do |k|
8.times do |i|
pb(perform[i],k)
end
end
y.times do
8.times do |i|
pb(perform[i+8])
end
end
sleep 3*c
end
end

playminuets(numtunes,testbars) #play the minuets
#setup epiphany kill
file=File.open("/tmp/kill.txt","w")
file.puts "kill"
file.close
sleep 5 #allow for sched ahead
set_sched_ahead_time! 1 #reset
#Ruby control program for Sonic Pi Mozart Dice Minuet by Robin Newman, January 2016
#use in conjunction with SP MozartDiceMinuet.rb
#requires sonic-pi-cli gem to be installed
#requires a Pi2. Will NOT work on a Mac or PC
#requires folder "cards" containing images of the 176 bars
#This script reads the bar numbers for each minuet from the perform.txt file
#and creates an html page of the relevant bar number images
#which it then displays in the Epiphany browser.
#It polls for the existance of the perform.txt file
#reads its data, which consists of the number of minuets to be played,
#followed by the bar numbers for the current minuet.
#Having read the data, it creates a web page and displays it, then sends a cue to Sonic Pi
#to start playing the minuet, before deleting the perform.txt file.
#A "while" loop is used to repeat this process till all the minuets have been played.
#After the loop ends it waits for a file kill.txt to be generated by Sonic Pi,
#then after a pause to allow SP to finish playing the final minuet, because of the set_sched_ahead! 5s value,
#it quits the epiphany browser, and deletes temp files to clean up and exits.

bpix=1200 #browser width in pixels: adjust as desired 1200 for HDMIPi, 1840 for my 1920x1080 monitor, 740 for RP touch screen monitor

#function todelete any leftover files
def cleanup()
system("rm /tmp/kill.txt >/dev/null 2>&1") #ignore errors if file not found
  system("rm /tmp/minuet.html >/dev/null 2>&1")
  system("rm /tmp/go.txt >/dev/null 2>&1")
system("rm /tmp/perform.txt >/dev/null 2>&1")
end

cleanup()

imgwidth=bpix/8 #image width
imgheight=imgwidth*1.58
cnt=0 #variable for current minuet number
flag=false #flag set to true when final minuet is played.
file=File.open("/tmp/go.txt","w") #create cue :go file used to send cue to Sonic Pi
file.puts "cue :go"
file.close

#start main loop
while flag==false do
    while not File.file?("/tmp/perform.txt") #wait for next perform.txt file to be generated by Sonic Pi
      sleep 0.25 #pause before next poll
    end
    sleep 5 #wait to make sure file is fully written and closed by Sonic Pi
    file=File.open("/tmp/perform.txt","r") #read the data
    l=file.gets
    file.close
    l=l[1..-3] #tidy up l
    l=l.split(",").map(&:to_i) #convert to integer array
    numtunes=l[0] #first entry is number of minuets to play (set by Sonic Pi)
    cnt +=1 #bump for next minuet number
    l=l[1..-1] #strip numtunes leaving bar number data list
    file=File.open("/tmp/minuet.html","w") #write html file for displaying selected bars in a table
    file.puts "<html><head><title>minuet "+cnt.to_s+" of "+numtunes.to_s+"</title></head>"
    file.puts"
<h1 align='center'>Sonic Pi plays Mozart Minuet "+cnt.to_s+" of "+numtunes.to_s+"</h1>
"
    file.puts"
<h2 align='center'>Generated from Musikalisches W&uuml;rfelspiel FV 516 f</h2>
"
    file.puts "
<table align='center' width='"+bpix.to_s+"' cellspacing='0' cellpadding='0'>
<tr>"
    8.times do |i|
      file.puts "
<td><img src='file:///home/pi/cards/"+l[i].to_s.rjust(4,"0")+".jpg' style='width:"+imgwidth.to_s+";height:"+imgheight.to_s+"'>
<td>"
    end
    file.puts"</tr>
<tr>"
    8.times do |i|
      file.puts "
<td><img src='file:///home/pi/cards/"+l[i+8].to_s.rjust(4,"0")+".jpg' style='width:"+imgwidth.to_s+";height:"+imgheight.to_s+"'>
<td>"
    end
    file.puts"</tr>
</table>
"
    file.close
   system("epiphany-browser file:///tmp/minuet.html &") #open epiphany-browser with next web page
    if cnt==1 then
        system("xte 'sleep 6' 'key F11'&")
    end
     sleep 1 #allow to open
    system('cat /tmp/go.txt | sonic_pi&') #send cue: go to SP using sonic-pi-cli to start playing
    system("rm /tmp/perform.txt&") #delete perform.txt file ready for next minuet (if any) to be created
    sleep 10 #pause while minuet plays. Makes sure don't poll for next perform.txt too early
    flag=true if (cnt == numtunes)
  end
  while not File.file?("/tmp/kill.txt") #last time, wait for SP to create kill file
    sleep 0.25 #pause between polls
  end
  sleep 7 #pause to make sure SP finshed playing, allowng for 5s sched_ahead_time
  system("killall epiphany-browser&") #shut down browser
  sleep 2
cleanup()

The file spmozart.tar.gz  can be downloaded as described below.
This contains the two files above, plus the cards folder containing the 176 images for the different bars. It also has a README.txt file with installation details.
To install the system on your Pi2 use the following procedure.

open a terminal window on the Pi2

cd ~/
wget http://r.newman.ch/rpi/spmozart.tar.gz
tar zxvf spmozart.tar.gz
mv spmozart/cards ~/
mv spmozart/minuetcontrol.rb ~/

Open a FileManager Window and navigate to spmozart
Right click MozartDiceMinuet.rb and select Text Editor to open it with the leafpad text editor.
Select all (ctrl+A) and copy (ctrl+C) (leave the window open)
Switch to Sonic Pi and select a blank buffer space.
Paste the file data in. (Alt+V)

Switch back to the terminal window, and start the minuetcontrol script by typing
ruby minuetcontrol.rb
(then press return)
You will not see anything happening, as the script is waiting for Sonic Pi to generate a text file.
Switch back to Sonic Pi and click RUN to run the workspace containing the MozartDiceMinuet program.

 

 

 

 

 

A touchscreen driven jukebox player for Sonic Pi

TouchScreen Sonic Pi Jukebox

TouchScreen Sonic Pi Jukebox

This is a project which grew out of a previous jukebox program I had written for Sonic Pi, which enabled it to be controlled either with a python program running on the Pi, or via my mobile phone using the Telegram App. With the arrival of the Raspberry Pi official touch screen display, I saw that this might be used as the front end to such a program. I produced a first working version a couple of months ago, but it had some shortcomings, and recently I revisited and the result is this article.

There is a video of the project here

Prerequisites
You need a Raspberry Pi offical touch screen, with a Raspberry Pi attached. Preferrably this should be a Pi2. I haven’t tested with a Pi Model B+ but it should work, although performance may not be as good.
Also required, is a suitable means of listening to the sound via the 3.5mm output jack on the Pi.
You should have an up to date copy of Raspian (Jessie) on the SD micro card in the Pi.

A touch screen is not a magic panacea. You have to do a little bit of work to be able to use it to control a program. One of the environments which lends itself to producing touch screen operated front ends is Kivy. I had not heard of this before, but came across it in the superb article on using the Raspberry Pi touch screen to control GPIO connected devices by Matt Richardson.

The project falls into Four parts.
1 Installing Kivy
2 Adding the sonic_pi_cli gem which enables text files to be sent to Sonic Pi for execution.
3. Creating the kivy main.py program which which is the basis of the Jukebox. This will also involve choosing the (up to 12) pieces that you wish to play. For convenience I will supply some.
4. Configuring the Pi to autostart Sonic Pi and the Kivy program on startup.

Installing Kivy
I cannot better the excellent description in Matt Richardson’s article. Follow this up to and including step 17.

Adding sonic-pi-cli gem
In order to do this, without installing and using a system like rvm, we have to do a bit of preliminary work so that the correct permissions will be in place during the install process. First, if you have switched to booting into a command line environment, start the graphical environment using startx. You may also like to switch back to this environment on boot by using Raspberry Pi Configuration utility in Menu -> Preferences. The graphics environment is needed for Sonic Pi to work.
Start a terminal window and type the following:

sudo mkdir /var/lib/gems
sudo chown pi /var/lib/gems
sudo chown pi /usr/local/bin
gem install sonic-pi-cli

When the install has completed, reset the ownership of /usr/local/bin

sudo chown root /usr/local/bin

Now test the cli by starting Sonic Pi, loading in any piece of reasonable duration, eg one of the example files, and running it. While it is running from the terminal window type:

sonic_pi stop

The piece should stop playing, showing that the cli command is working.

Creating the kivy main.py program
Kivy can be quite confusing to start with. Partly this is because there are two ways to use it. You can have a .kv file which contains details of the screen layout, and a separate python script which contains the logic of how these items are used, or everything can be contained just in a python script. After some playing around, and googling for kivy scripts I elected to use the former, and the program I came up with has elements of Matt Richardson’s script to control GPIO pins, together with bits from a snippet I found at http://stackoverflow.com/questions/18958520
The program listing is below. Also required are two small graphics files which can be downloaded in the resources file at the end of the post.

#!/usr/bin/python
#Sonic Pi touch screen kivy program by Robin Newman
#with acknowlegements: https://github.com/mrichardson23/rpi-kivy-screen
# and http://stackoverflow.com/questions/18958520
# written January 2016

from kivy.app import App
from kivy.lang import Builder
from kivy.uix.floatlayout import FloatLayout
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.button import Button
from kivy.uix.label import Label
from kivy.uix.slider import Slider
from kivy.uix.image import Image
import os

b={} #holds button ids for binding
s=[] #holds text for buttons
c=[] #holds linked commands for buttons

#build text lists
#path to Sonic Pi files to play (include trailing /)
sf='/home/pi/SP-files/'
#used in building command line for sonic_pi gem
pre='cat '+sf
post=' | sonic_pi' 

#description (d**) (2 lines max separated by \n), and filename  (n**) entries
#for the 12 files in the jukebox selection
d1='Ambient\nExperience'
n1='ambient_experiment.rb'
d2='Blimp\nZones'
n2='blimp_zones.rb'
d3='Blip\nRhythm'
n3='blip_rhythm.rb'
d4='Chord\nInversions'
n4='chord_inversions.rb'
d5='Filtered\ndnb'
n5='filtered_dnb.rb'
d6='FM\nNoise'
n6='fm_noise.rb'
d7='Jungle'
n7='jungle.rb'
d8='Ocean'
n8='ocean.rb'
d9='Reich\nPhase'
n9='reich_phase.rb'
d10='Shufflit'
n10='shufflit.rb'
d11='Tilburg\nv2'
n11='tilburg_2.rb'
d12='Time\nMachine'
n12='time_machine.rb'

####### You shouldn't need to alter anything after this line
#setup strings for description and filenames
s.append('Tune 1\n'+d1)
c.append(pre+n1+post)
s.append('Tune 2\n'+d2)
c.append(pre+n2+post)
s.append('Tune 3\n'+d3)
c.append(pre+n3+post)
s.append('Tune 4\n'+d4)
c.append(pre+n4+post)
s.append('Tune 5\n'+d5)
c.append(pre+n5+post)
s.append('Tune 6\n'+d6)
c.append(pre+n6+post)
s.append('Tune 7\n'+d7)
c.append(pre+n7+post)
s.append('Tune 8\n'+d8)
c.append(pre+n8+post)
s.append('Tune 9\n'+d9)
c.append(pre+n9+post)
s.append('Tune 10\n'+d10)
c.append(pre+n10+post)
s.append('Tune 11\n'+d11)
c.append(pre+n11+post)
s.append('Tune 12\n'+d12)
c.append(pre+n12+post)
s.append('Stop\nPlaying')
c.append('sonic_pi stop')
s.append('Quit\nProgram')
c.append('')

def press_callback(obj):
    global s,c #make lists global
    if obj.text == str(s[13]):
        os.system('sonic_pi stop')
        os.system('killall sonic-pi')
        os.system('killall ruby')
        App.get_running_app().stop()
    for i in range(0,13):
        if obj.text == s[i]:
            os.system('sonic_pi stop') #double stop sent for stop command but doesn't matter
            os.system(str(c[i]))

def update_vol(obj, value):  #upadte volume via alsamixer
    global vol
    #print ("Updating volume to:" + str(obj.value))
    vol=obj.value
    os.system("amixer sset 'Master' "+str(vol)+"% >/dev/null")

    

Builder.load_string("""
:	
    boxes: _boxes 
    AnchorLayout:
        anchor_x: 'center'
        anchor_y: 'top'
        ScreenManager:  #maybe can simplify as only 1 screen used
            size_hint: 1, 1
            id: _screen_manager
            Screen:
                name: 'screen1'
                BoxLayout: 
                    orientation: 'vertical'
                    padding: 50
                    id: _boxes
    AnchorLayout:
        anchor_x: 'center'
        anchor_y: 'bottom'
        BoxLayout:
            orientation: 'horizontal'
            size_hint: 1,.12
            Label:
                #valign: 'top'
                font_size: '20sp'
                text: 'Sonic Pi 12-Tunes Touch Screen Jukebox'""")


class Boxes(FloatLayout):
    global s,c
    def __init__(self, **kwargs):
        super(Boxes, self).__init__(**kwargs)
        bx1 = BoxLayout(orientation='horizontal')
        bx2 = BoxLayout(orientation='horizontal')
        bx3 = BoxLayout(orientation='horizontal')
        bx4 = BoxLayout(orientation='horizontal')
        bx5 = BoxLayout(orientation='horizontal')
        bx6 = BoxLayout(orientation='horizontal')

        bx1.add_widget(Image(source='sonic-pi-web-logo.png'))
        bstop=Button(text=s[12])
        bstop.bind(on_press=press_callback)
        bx1.add_widget(bstop)
        bquit=Button(text=s[13])
        bquit.bind(on_press=press_callback)
        bx1.add_widget(bquit)
        bx1.add_widget(Image(source='logo.png'))

        for i in range(0,4):
            b[i]=Button(text=str(s[i]))
            b[i].bind(on_press=press_callback) 
            bx2.add_widget(b[i])
        for i in range(4,8):
            b[i]=Button(text=str(s[i]))
            b[i].bind(on_press=press_callback)
            bx3.add_widget(b[i])
        for i in range(8,12):
            b[i]=Button(text=str(s[i]))
            b[i].bind(on_press=press_callback)
            bx4.add_widget(b[i])       
        bx5.add_widget(Label(text='Adjust volume -->',font_size='20sp'))
        vol=80 #initial value
        b[12]= Slider(orientation='horizontal', min=0,max=100,value=vol)
        b[12].bind(on_touch_down=update_vol,on_touch_move=update_vol)
        bx5.add_widget(b[12])

        self.boxes.add_widget(bx1)
        self.boxes.add_widget(bx2)
        self.boxes.add_widget(bx3)
        self.boxes.add_widget(bx4)
        self.boxes.add_widget(bx5)

class TestApp(App):
    def build(self):
        return Boxes()

if __name__ == '__main__':
    TestApp().run()

I do not intend to give a detailed explanation of the program, but a few pointers may help. There are one or two user configurable bits near the start of the program. The variable sf holds the absolute path to the location of the sonic pi files that will be played by the jukebox program.

n1 to n12 are variables which hold the filenames of the 12 sonic pi files to be played. I have them ending in .rb but they could equally well be .txt files. In this example version I have used 12 of the example files included in Sonic Pi. These were accessed from /opt/sonic-pi/etc/examples and I used entries in the wizard and iillusionist folders stored there, copying them to the folder specified by sf /home/pi/SP-files.
In my own version I have 12 of my own sonic pi files used instead.

d1 to d12 are corresponding variables which hold a 1 or two line description of these files, which appear as the text on the selector buttons on the screen. If you want a second line it is separated from the first by a \n eg d4=’Chord\nInversions’.

From the variables n1-12 and d1-12 two string arrays s and c are generated, the first containing the complete text for each button incluidng the Tune 1…Tune 12 prefix, and teh second the complete command needed to be sent to the cli gem to tell Sonic Pi what to play.

The program defines two functions which are activated by the touch elements in the screen. First there is a callback routing which is called whenever one of the 14 touch buttons is touched. (The 12 program select buttons, plus buttons for Stop Playing and Quit Program.) This compares the text of the calling button with the values in the s array and when it finds a match  stops Sonic Pi playing the current tune (if any) and then sends the command for the selected tune. It deals separately with the commands to stop playing or to quit the program, in the latter case quitting Sonic Pi as well.
The second function deals with the graphical slider which is used as a volume control. This extracts the value (0-100) selected by the user, and sends it via an os.system command to utility amixer where it is used to adjust the ‘Master’ control. Here it is perhaps worth saying that I also installed pulseaudio on the system, which you should also do before using this script. Normally amixer only exposes a ‘PCM’ control which can be used to adjust the volume. However experiment showed that this was very non-linear, and did not give a very satisfactory performance. By installing pulseaudio the ‘Master’ volume control is exposed which gives a much more satisfactory adjustment to the volume. If you DONT want to install pulseaudio, you can amend the program by altering the line

os.system("amixer sset 'Master' "+str(vol)+"% >/dev/null")

to read

os.system("amixer sset 'PCM' "+str(vol)+"% >/dev/null")

To install pulseaudio use

sudo apt-get install pulseaudio

The program should be saved with name main.py and stored in a folder named SP-touch-screen created in the user pi home director together with the two logo files sonic-pi-web-logo.png and logo.png (download from the resources file at the end of the post).

Interim testing
At this stage you do some interim testing to see whether things are working ok so far. One word of warning. Once you start a kivy program ctrl-C will NOT work to stop the program. You can only use the built in Quit Program button to do this. It there are any problems you can end up with no means ot stopping the program other than removing the power lead.  As this is undesirable, you are strongly advised to have a remote ssh login available to the Raspberry Pi, so that you can log onto it from an external PC or Mac, and from there use killall python to stop the program running. If you are unfamiliar how to set this up, then the guide https://www.raspberrypi.org/documentation/remote-access/ssh/ should help.
To try out the program, make sure you have set up the folder SP-files to contain the 12 examples files used in the program, then start up Sonic Pi from the Menu->Programming link. When it has started, use a terminal window and type:

cd ~/SP-touch-screen
python main.py

which should startup the touch screen kivy front end. Try out some of the buttons to see if you can play a tune. If a tune is running pressing another tune selector button will stop the first tune and start the second. Stop Playing should sto the currently playing tune. You should also be able to change the volume by tapping on the horizontal selector at different points, or sliding the circle left or right. Finally pressing Quit Program should quit both the kivy screen AND Sonic Pi.

Configuring the Raspberry Pi to autostart into the touchscreen jukebox
The final stage is to add a small script and a .desktop file in order to get the Raspberry Pi to startup the program automatically on boot. To do this I utilised the fact that the Pi will autostart any application whose desktop file entry is copied to the folder /home/pi/.config/autostart
In this case we have to generate a small python script because we want TWO programs to start one after the other. First we must ensure that Sonic Pi is running, then we must start the kivy screen program. The startup script which I called startupkivy is shown below.

#!/usr/bin/python

import os
import time

os.system('/usr/bin/sonic-pi &')
time.sleep(8)
os.system("amixer sset 'Master' 90%") #starting volume setting
os.system('python /home/pi/SP-touch-screen/main.py &')

This needs to be placed in the folder /usr/local/bin
which you can do using

sudo nano /usr/local/bin/startupkivy

type in the text and then use ctrl+x followed by Y and return key to exit.
Set the file executable using

sudo chmod 755 /usr/local/bin/startupkivy

Now set up the desktop file:

cd ~/.config
mkdir autostart
cd autostart
nano kivy.desktop

type in the text:

[Desktop Entry]
Encoding=UTF-8
Version=1.0
Type=Application
Exec="startupkivy"
Icon=/usr/share/pixmaps/sonic-pi.png
Terminal=false
Name=startupkivy
Categories=Application;Development;

and quit with ctrl+x, followed by Y, and return key
If you wish, you can copy this file to the desktop as well using:

cp kivy.desktop ~/Desktop

That completes the setup. Now you can test it out.
First make sure that the Pi is set too boot to the GUI interface. You can check with the Raspberry Pi Configuration utility in Preferences. Make sure that the Boot: To Desktop button is marked.
Now reboot your Pi and it should startup in the GUI and immediately start Sonic Pi, followed 8 seconds later by the touchscreen jukebox program.
It should be possible to startup, play a selection of tunes, quit the program, select Shutdown from the Menu and shutdown the Pi, all with the use of your finger, and with no mouse or keyboard at all.
If you have copied the .desktop file to the Desktop you can double click it to restart Sonic Pi and the kivy main.py program if you wish.

At the time of writing Sonic Pi 2.7 is still the version of Sonic Pi bundled in Jessie. IF you want a later version you can download it from sonic-pi.net and install it on your desktop. For example version 2.9 installs in sonic-pi-v2.9.0 YOu can alter the startupkivy file to use this instead of the default version by changing the line:

os.system('/usr/bin/sonic-pi &')

to

os.system('/home/pi/sonic-pi-v2.9.0/bin/sonic-pi &')

Resources
You can download a file containing :
the SP-touch-screen folder containing the two logo files and the main.py script
the startupkivy program which should be copied (using sudo) to /usr/local/bin
the kivy.desktop file  which should be placed in ~/.config/autostart

To download open a terminal window and type

wget http://r.newman.ch/rpi/touchscreen/resources.tar.gz
tar zxvf resources.tar.gz
cd resources

All you have to do is to install the other requirements, kivy, sonic-pi-cli, pulseaudio as detailed above. Have fun!