Navigation Menu

Skip to content

Instantly share code, notes, and snippets.

@rbnpi
Last active December 28, 2020 09:29
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save rbnpi/8812203f5c9a995620bed9ce3a3c6a20 to your computer and use it in GitHub Desktop.
Save rbnpi/8812203f5c9a995620bed9ce3a3c6a20 to your computer and use it in GitHub Desktop.
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