Tweaking Amp Settings
Sonic Pi is software to make sounds and music driven by code. Sonic Pi comes with an IDE of sorts. You can program the composition you'd like to play in the IDE. With one button, you get immediate feedback hearing how your code sounds.
A few years ago I wrote about controlling Sonic Pi with Ruby code without needing to code in the IDE directly. That relied on the sonic-pi-cli gem. For my Anyone Can Play Guitar (With Ruby) talk, I took a different approach.
Updated Wiring
As of this writing, Sonic Pi has released version 4.3.0. It's progressed significantly since version 3.2, which is the version that I used in my original post. One of the consequences is that the sonic-pi-cli gem does not appear to work with later versions of Sonic Pi. I couldn't get earlier versions of Sonic Pi to work on my computer. I didn't want to deal with virtualization to see if I could get an earlier version of Sonic Pi working with the gem. I also didn't have the time to update the gem to work with later version of Sonic Pi. Sorry - I had a presentation to write!
I needed a quick way to achieve the same, or similar, results. So, I'll admit - I cheated.
Input Jack
Sonic Pi reads an init file every time that the application boots. You might use this file for helper methods to recall in the application's editor. Instead, I will store the code to play the song itself. It gets read when the application boots and starts playing the song.
It's not the greatest long-term solution. You don't want to hear the song every time you open the application forevermore. I know I don't. But it is good enough for demonstration purposes. That said, I still needed to construct the code to play the song.
Speaker
Much like in my original version, I built an amplifier to communicate with Sonic Pi. In my original post, my guitar class knew how to generate its sound output to the amp. In this version, the amp knows how to do that.
class SonicPiAmplifier < Amplifier
def sound_output(play_operation, duration: 0.25)
[
"with_synth :pluck do",
" #{play_operation}, release: #{duration})",
"end",
"sleep(#{duration})\n",
].join("\n")
end
end
For each note to play, this is using Sonic Pi's DSL to play a note with a synth patch that sounds a bit like a guitar. It plays the note for the provided duration, and then sleeps for that same duration. That's so Sonic Pi won't immediately play the next note on top of this one.
Power Amp
To simplify, let's assume that amplifiers work by progressing sound through two components. Sound moves through a pre amp to a power amp. The power amp is what's responsible for sending the sound to the speaker. That's what will use the sound_output
method we've built.
class SonicPiAmplifier < Amplifier
def power_amp_stage(sound)
play_operation = "play(:#{sound.to_s}"
output = sound_output(play_operation, duration: sound.duration)
@sounds << output
output
end
end
The play_operation
string is again a command from Sonic Pi's DSL. As you'd expect, it plays a sound. We retrieve the value of the sound to play from the Note
class we constructed in a prior post. We pass this into our sound_output
. We store the result in our list of @sounds
that the amplifier projects, and return it as well.
Audio Loopback
The reason we're keeping track of our @sounds
is that we want to be able to replay them again after the fact. Our amplifier will use this ability to write the song it played to a file.
class SonicPiAmplifier < Amplifier
def write_to_file(location)
File.open(location, "w") do |file|
@sounds.each do |sound|
file.write(sound)
end
end
end
end
We'll write this to Sonic Pi's init file. Then, when we open up the application, it will play the song from the amplifier on boot.
Output
Time to take this for a rip. We'll grab a guitar, decide which song we want to play, plug in our amplifier we just built, and play the song.
def play
guitar = Blues::Guitar.new
guitar.restring(gauge_set: :srv)
guitar.tune(:down_half_step)
song = Blues::Shuffle.new(guitar)
amp = Blues::SonicPiAmplifier.new(volume: 10, on: true)
guitar.plug_in(amplifier: amp)
song.play { |measure| measure.map { |sound| puts sound } }
amp
end
Running this method in isolation won't push any air through our speakers. Instead, we need our cheat. We'll write all the sounds from the song to our init file, and then have our script open Sonic Pi in a subshell.
init = "#{Dir.home}/.sonic-pi/config/init.rb"
play.write_to_file(init)
`open "/Applications/Sonic\ Pi.app"`
Sonic Pi will start, read the init file with all the instructions to play our song, and start playing.
Coda
Sam Aaron very helpfully on Twitter suggested a more reasonable approach that highlights built-in Sonic Pi functionality. With a combination of live loops and OSC messages I could have avoided relying on the init file. Thanks to Sam for the tip - and for Sonic Pi!
One approach would be to implement some live_loop listeners to incoming OSC which you could then send to at your leisure from a separate pure Ruby process.
That way you can show off the best of both worlds!