Skip to content

Instantly share code, notes, and snippets.

@rbnpi
Last active Dec 28, 2020
Embed
What would you like to do?
Experimental replacement for scsynthexternal.rb in Sonic Pi3.2.2 running on New RPi OS with pulseaudio use READMEFIRST!

This hack is a temporary fix to get Sonic Pi 3.2.2 working with the new pulse-audio environment in the latest Raspberry Pi O released on 2020-12-02. Do not install it on earlier versions of the OS (unless you have done a apt upgrade)

You also need to install one other package sudo apt install pulseaudio-module-jack for it to work

The file scsynthexternal.rb is a drop in replacement for the file of the same name installed by the sonic-pi_3.2.2_f_armhf.deb file downloaded from sonic-pi-net The location of the installed file is /opt/sonic-pi/app/server/ruby/lib/scsynthexternal.rb

Download the replacement file into the Downloads folder of your Raspberry Pi. then execute the following commands make a backup of the original and install the replacement.

cd ~/Downloads
sudo mv /opt/sonic-pi/app/server/ruby/lib/sonicpi/scsynthexternal.rb /opt/sonic-pi/app/server/ruby/lib/sonicpi/scsynthexternal.rb.bak
sudo cp scsynthexternal.rb /opt/sonic-pi/app/server/ruby/lib/sonicpi/scsynthexternal.rb

to restore the original you can type:

sudo mv /opt/sonic-pi/app/server/ruby/lib/sonicpi/scsynthexternal.rb.bak /opt/sonic-pi/app/server/ruby/lib/sonicpi/scsynthexternal.rb

I would appreciate feedback on your experience in using this at robin dot newman at gmail dot com The ideas for this solution came from a post here https://github.com/SebiderSushi/sonic-pi-via-pulseaudio/blob/main/sonic-pi-via-pulseaudio I have merely incorporated them into Sonic Pi.

#--
# This file is part of Sonic Pi: http://sonic-pi.net
# Full project source: https://github.com/samaaron/sonic-pi
# License: https://github.com/samaaron/sonic-pi/blob/master/LICENSE.md
#
# Copyright 2013, 2014, 2015, 2016 by Sam Aaron (http://sam.aaron.name).
# All rights reserved.
#
# Permission is granted for use, copying, modification, and
# distribution of modified versions of this work as long as this
# notice is included.
#++
require_relative "util"
require_relative "promise"
require_relative "osc/osc"
require_relative "thread_id"
require 'fileutils'
module SonicPi
class SCSynthExternal
include Util
attr_reader :version
def initialize(events, opts={})
@events = events
@hostname = opts[:hostname] || "127.0.0.1"
@port = opts[:scsynth_port] || 4556
@send_port = opts[:scsynth_send_port] || 4556
@register_cue_event_lambda = opts[:register_cue_event_lambda]
raise "No cue event lambda!" unless @register_cue_event_lambda
@out_queue = SizedQueue.new(20)
@scsynth_thread_id = ThreadId.new(-5)
@version = request_version.freeze
boot
end
def sys(cmd)
log "System: #{cmd}"
system cmd
end
def send(*all_args)
address, *args = *all_args
log "OSC ~ #{address} #{args.inspect}" if osc_debug_mode
@osc_server.send(@hostname, @send_port, address, *args)
end
def send_at(ts, *all_args)
address, *args = *all_args
if osc_debug_mode
if (a = __system_thread_locals.get(:sonic_pi_spider_time)) && (b = __system_thread_locals.get(:sonic_pi_spider_start_time))
vt = a - b
elsif st = __system_thread_locals.get(:sonic_pi_spider_start_time)
vt = ts - st
else
vt = -1
end
log "BDL #{'%11.5f' % vt} ~ [#{vt}:#{ts.to_f}] #{address} #{args.inspect}"
end
@osc_server.send_ts(ts, @hostname, @send_port, address, *args)
end
def reboot
shutdown
boot
end
def booted?
!!@scsynth_pid
end
def shutdown
puts "Sending /quit command to scsynth"
begin
@osc_server.send(@hostname, @send_port, "/quit")
rescue Exception => e
puts "Error during scsynth shutdown when attempting to send /quit OSC message to server #{@hostname} on port #{@send_port}"
puts " --> #{e.message}"
puts " --> #{e.backtrace.inspect}\n\n"
end
puts "Stopping OSC server..."
@osc_server.stop
puts "Stopped OSC server..."
t1, t2 = nil, nil
t1.join if t1
t2.join if t2
#close log file if it's open
@scsynth_log_file.close if @scsynth_log_file
@scsynth_log_file = nil
end
private
def request_version
version_string = `"#{scsynth_path}" -v`
m = version_string.match(/\A\s*scsynth\s+([0-9.a-zA-Z-]+)\s.*/)
if m && m[1] && !m[1].empty?
"v#{m[1]}"
else
""
end
end
def boot
if booted?
server_log "Server already booted..."
return false
end
puts "Booting server..."
@osc_server = OSC::UDPServer.new(0, use_decoder_cache: true, use_encoder_cache: true)
@osc_server.add_global_method do |address, args, info|
case address
when '/n_end'
id = args[0].to_i
@events.async_event ['/n_end/', id], args
when '/n_off'
id = args[0].to_i
@events.async_event ['/n_off/', id], args
when '/n_on'
id = args[0].to_i
@events.async_event ['/n_on/', id], args
when '/n_go'
id = args[0].to_i
@events.async_event ['/n_go/', id], args
when '/n_move'
id = args[0].to_i
@events.async_event ['/n_move/', id], args
else
@events.async_event address, args
end
p = 0
d = 0
b = 0
m = 60
@register_cue_event_lambda.call(Time.now, p, @scsynth_thread_id, d, b, m, address, args) if address.start_with? "/scsynth/"
end
case os
when :raspberry
boot_server_raspberry_pi
when :linux
boot_server_linux
when :osx
boot_server_osx
when :windows
boot_server_windows
end
true
end
def raspberry?
os == :raspberry
end
def log_boot_msg
puts ""
puts ""
puts "Booting Sonic Pi"
puts "----------------"
puts ""
log "\n\n\n"
end
def scsynth_path
case os
when :raspberry
"scsynth"
when :linux
"scsynth"
when :osx
path = "#{native_path}/scsynth"
raise "Unable to find SuperCollider. Is it installed? I looked here: #{path.inspect}" unless File.exist?(path)
path
when :windows
path = "#{native_path}/scsynth.exe"
raise "Unable to find SuperCollider. Is it installed? I looked here: #{path.inspect}" unless File.exist?(path)
path
end
end
def boot_and_wait(*args, &on_complete_or_error)
puts "Boot - Starting the SuperCollider server..."
puts "Boot - #{args.join(' ')}"
p = Promise.new
p2 = Promise.new
booted = false
connected = false
begin
FileUtils.rm scsynth_log_path if File.exist?(scsynth_log_path)
@scsynth_log_file = File.open(scsynth_log_path, 'w')
rescue
@scsynth_log_file = nil
end
@scsynth_log_file.puts "# Starting SuperCollider #{Time.now.strftime("%Y-%m-%d %H:%M:%S")}" if @scsynth_log_file
at_exit { @scsynth_log_file.close if @scsynth_log_file}
scsynth_pipe = IO.popen(args)
@scsynth_pid = scsynth_pipe.pid
register_process(@scsynth_pid)
t1 = Thread.new do
__system_thread_locals.set_local(:sonic_pi_local_thread_group, :scsynth_log_tracker)
scsynth_pipe.each_line do |l|
@scsynth_log_file.puts l if @scsynth_log_file
@scsynth_log_file.flush if @scsynth_log_file
if !booted && l =~ /SuperCollider 3 server ready/
p.deliver! true
booted = true
end
end
end
begin
p.get(60)
rescue PromiseTimeoutError => e
kill_and_deregister_process(@scsynth_id)
t1.kill
msg = "Boot - Unable to boot SuperCollider - boot server log did not report server ready"
puts msg
on_complete_or_error.call if on_complete_or_error
raise msg
end
puts "Boot - SuperCollider booted successfully."
puts "Boot - Connecting to the SuperCollider server..."
boot_s = OSC::UDPServer.new(0) do |a, b, info|
puts "Boot - Receiving ack from scsynth"
p2.deliver! true unless connected
connected = true
end
t2 = Thread.new do
__system_thread_locals.set_local(:sonic_pi_local_thread_group, :scsynth_external_boot_ack)
Kernel.loop do
begin
puts "Boot - Sending /status to server: #{@hostname}:#{@send_port}"
boot_s.send(@hostname, @send_port, "/status")
rescue Exception => e
puts "Boot - Error sending /status to server: #{e.message}"
end
sleep 2
end
end
begin
p2.get(30)
rescue Exception => e
Process.kill(9, @scsynth_pid)
ensure
t2.kill
boot_s.stop
end
on_complete_or_error.call if on_complete_or_error
unless connected
puts "Boot - Unable to connect to SuperCollider"
raise "Boot - Unable to connect to SuperCollider"
end
puts "Boot - Server connection established"
end
def boot_server_osx
temp_input_rate = nil
current_input = nil
current_input_rate = nil
temp_switch = false
disable_input = true
log_boot_msg
puts "Boot - Booting on OS X"
puts "Boot - Checkout audio rates on OSX:"
# Force sample rate for both input and output to 44k
# If these are not identical, then scsynth will refuse
# to boot.
begin
audio_in_rate = :unknown_in_rate
audio_out_rate = :unknown_out_rate
require 'coreaudio'
audio_in_rate = CoreAudio.default_input_device.nominal_rate
audio_out_rate = CoreAudio.default_output_device.nominal_rate
puts "Boot - Input audio rate: #{audio_in_rate}"
puts "Boot - Output audio rate: #{audio_out_rate}"
if (audio_in_rate == audio_out_rate)
# yey we're good to go
puts "Boot - Audio rates match. Enabling input"
disable_input = false
else
puts "Boot - Audio rates don't match. Let's see if we can find a temporary audio input with the correct rate to switch to"
current_input = CoreAudio.default_input_device
current_input_rate = current_input.nominal_rate
current_output = CoreAudio.default_output_device
current_output_rate = current_output.nominal_rate
valid_inputs = CoreAudio.devices.filter {|x| x.input_stream.channels > 0 && x.available_sample_rate.find_index {|y| y.first == current_output_rate}}
temp_input = valid_inputs[0]
if temp_input
puts "Boot - Found suitable temporary input #{temp_input.name}"
temp_input_rate = temp_input.nominal_rate
puts "Boot - switching to temporary input #{temp_input.name}"
CoreAudio.set_default_input_device(temp_input)
CoreAudio.default_input_device(nominal_rate: current_output_rate)
temp_switch = true
end
end
rescue Exception
# Something went wrong whilst attempting to determine and modify the audio
# rates. For safety do not enable inputs
puts "Boot - Unable to detect audio rates. Disabling input"
end
if disable_input
num_inputs = "0"
puts "Boot - Booting with no audio inputs"
else
num_inputs = "16"
puts "Boot - Booting with max 16 inputs"
end
boot_and_wait(scsynth_path,
"-u", @port.to_s,
"-a", num_audio_busses_for_current_os.to_s,
"-m", "131072",
"-D", "0",
"-R", "0",
"-l", "1",
"-i", num_inputs,
"-o", "16",
"-U", "#{native_path}/supercollider/plugins/",
"-b", num_buffers_for_current_os.to_s,
"-B", "127.0.0.1") do
if temp_switch
puts "Boot - switching rate of temp input back to original: #{temp_input_rate}"
CoreAudio.default_input_device(nominal_rate: temp_input_rate)
puts "Boot - switching default input device back to original: #{current_input.name}"
CoreAudio.set_default_input_device(current_input)
puts "Boot - switching default input rate back to original: #{current_input_rate}"
CoreAudio.default_input_device(nominal_rate: current_input_rate)
end
end
end
def boot_server_windows
log_boot_msg
puts "Booting on Windows"
boot_and_wait(scsynth_path,
"-u", @port.to_s,
"-m", "131072",
"-a", num_audio_busses_for_current_os.to_s,
"-D", "0",
"-R", "0",
"-l", "1",
"-i", "16",
"-o", "16",
"-U", "#{native_path}/plugins/",
"-b", num_buffers_for_current_os.to_s,
"-B", "127.0.0.1")
end
def boot_server_raspberry_pi
log_boot_msg
puts "Booting on Raspberry Pi"
#Start Jack if not already running
if `ps cax | grep jackd`.split(" ").first.nil?
#Jack not running - start a new instance
puts "Jackd not running on system. Starting..."
jackCmd="jackd -T -ddummy -r48000 -p1024"
jack_pid = spawn "exec #{jackCmd}"
register_process jack_pid
else
puts "Jackd already running. Not starting another server..."
end
#register_process jack_pid
block_size = raspberry_pi_1? ? 512 : 128
boot_and_wait("scsynth",
"-u", @port.to_s,
"-m", "131072",
"-a", num_audio_busses_for_current_os.to_s,
"-D", "0",
"-R", "0",
"-l", "1",
"-i", "2",
"-o", "2",
"-z", block_size.to_s,
"-c", "128",
"-U", "/usr/lib/SuperCollider/plugins",
"-b", num_buffers_for_current_os.to_s,
"-B", "127.0.0.1")
`jack_connect SuperCollider:out_1 system:playback_1`
`jack_connect SuperCollider:out_2 system:playback_2`
`jack_connect SuperCollider:in_1 system:capture_1`
`jack_connect SuperCollider:in_2 system:capture_2`
`pactl load-module module-jack-source connect=0 client_name=JACK_to_PulseAudio`
`pactl load-module module-loopback source=jack_in`
`jack_connect SuperCollider:out_1 JACK_to_PulseAudio:front-left`
`jack_connect SuperCollider:out_2 JACK_to_PulseAudio:front-right`
sleep 3
end
def boot_server_linux
log_boot_msg
puts "Booting on Linux"
#Start Jack if not already running
if `ps cax | grep jackd`.split(" ").first.nil?
#Jack not running - start a new instance
puts "Jackd not running on system. Starting..."
jackCmd = "jackd -R -T -p 32 -d alsa -n 3 -p 2048 -r 44100"
jack_pid = spawn "exec #{jackCmd}"
register_process jack_pid
else
puts "Jackd already running. Not starting another server..."
end
boot_and_wait("scsynth",
"-u", @port.to_s,
"-m", "131072",
"-a", num_audio_busses_for_current_os.to_s,
"-D", "0",
"-R", "0",
"-l", "1",
"-i", "16",
"-o", "16",
"-b", num_buffers_for_current_os.to_s,
"-B", "127.0.0.1")
`jack_connect SuperCollider:out_1 system:playback_1`
`jack_connect SuperCollider:out_2 system:playback_2`
`jack_connect SuperCollider:in_1 system:capture_1`
`jack_connect SuperCollider:in_2 system:capture_2`
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment