Running code via OSC via Python doesn't seem to work

I’ve installed psonic which I expected to send code to my running sonic pi server, but nothing happens.

Very simply, my code does this:

#!/usr/bin/env python3
from psonic import *
set_server_parameter('127.0.0.1',4560)
play(72)

I do see the cues coming through, e.g.
/osc:127.0.0.1:4560/run-code ["play 72"]

But nothing happens. Is the functionality no longer supported?

I’m running Sonic Pi 4.2 on MacOS 12.6

You are correctly sending an OSC message to Sonic Pi, but 4560 is not the port to use for an executable command. It is the port which handles external OSC cues sent to Sonic Pi. It merely sends the data which can be detected and then acted upon in a program. In your case, you could detect the cue using

#here I send the OSC command from Sonic Pi to itself..Same effect as sending it from psonic
use_osc "127.0.0.1",4560
osc "/run-code","play 72"


live_loop :test do
  n = sync "/osc*/run-code"
  puts n[0]  #=> "play 72" as a string
end

What it does is to match the address string /osc*/run-code where the * is a wild card
and returns a list [“play 72”] which extracts “play 72” in n[0] Howver it doesn’t play it.

In fact Sonic Pi uses a different dynamically allocated port to receive commands (usually from one of the editor buffers). It also needs a randomly allocated Token to authenticate the source.
The port and Token are generated by the program daemon.rb which controls the starting and stopping of the various parts of Sonic Pi and checks that all the parts are playing happily together. You can also see the port and the Token in the spider.log file. This means that it is not simple to communicate with the spider server port to which commands are sent and dealt with.
For earlier version, the tool sonic-pi-cli was developed by Nick Johnstone. This could send commands to the original fixed server port in very early versions of Sonic Pi, and in version 3.3.1 to the dynamic port generated by sonic-pi. However it has not been updated to deal with versions from 4.0 onwards. I have a working version which I have called sonic-pi-cli4 which does work with the latest versions, but extracting data from the log file is definitely not encouraged nor very robust, as it will only work while the relevant data and it format in the log file remains unaltered, and it could be changed at any time as the program develops, so at present I just use it privately.
You can read the current server port from the command
puts @port[:server_port]
in a Sonic Pi buffer, but remember it may change when you do a fresh run. I don’t think there is an easy way to read the Token

Below is some code which will work out the server_port and the Token and send the run-command
play [72,76,79] to sonic pi to play a c major chord.

define :token do #this functions reads from spider.log
  #the dynamically allocated token value used in internal comms
  value='' #initialise variable
  #read spider.log
  File.open(ENV['HOME']+'/.sonic-pi/log/spider.log','r') do |f1|
    while l = f1.gets
      if l.include?"Token:" #find line containing Listen port:
        value = l.split(" ").last.to_i #extract port
        break
      end
    end
    f1.close #close file
  end
  return value #return found port value
end
define :pvalue do #get internal server_port
  return @ports[:server_port]
end
#or you can read the server_port from the spider log instead


osc_send "localhost",pvalue,"/run-code",token,"play [72,76,79]"

My cli code produces a ruby gem which enables this from the command line.

2 Likes

I’ve expanded my first post about this to show a larger example program which sets up the server_port, the Token and then sends all the sonic-pi built in example program “Bach” to the server port with the correct syntax where it is played.

#example showing data being sent correctly to the server port to play the Bach example program

define :token do #this functions reads from spider.log
  #the dynamically allocated token value used in internal gui comms
  value='' #initialise variable
  #read spider.log
  File.open(ENV['HOME']+'/.sonic-pi/log/spider.log','r') do |f1|
    while l = f1.gets
      if l.include?"Token:" #find line containing Listen port:
        value = l.split(" ").last.to_i #extract port
        break
      end
    end
    f1.close #close file
  end
  return value #return found port value
end

define :pvalue do #get internal server_port
  return @ports[:server_port]
end
#Alternatively the server_port can also be extracted from the spider log

#########################################
#This next section would be normally be sent from an external OSC source like psonic
use_osc "127.0.0.1",4560 #set up destination for OSC calls
#now send the data for the Bach example file as a string to osc "/run-code"
osc "/run-code","# Bach
# Bach Minuet in G
#
# Coded by Robin Newman

use_bpm 60
use_synth_defaults release: 0.5, amp: 0.7, cutoff: 90
use_synth :beep

## Each section of the minuet is repeated
2.times do

  ## First start a thread for the first 8 bars of the bass left hand part
  in_thread do
    play_chord [55,59]#b1
    sleep 1
    play_pattern_timed [57],[0.5]
    play_pattern_timed [59],[1.5] #b2
    play_pattern_timed [60],[1.5] #b3
    play_pattern_timed [59],[1.5] #b4
    play_pattern_timed [57],[1.5] #b5
    play_pattern_timed [55],[1.5] #b6
    play_pattern_timed [62,59,55],[0.5] #b7
    play_pattern_timed [62],[0.5] #b8
    play_pattern_timed [50,60,59,57],[0.25]
  end

  ## Play concurrently the first 8 bars of the right hand part
  play_pattern_timed [74],[0.5]#b1
  play_pattern_timed [67,69,71,72],[0.25]
  play_pattern_timed [74,67,67],[0.5]#b2
  play_pattern_timed [76],[0.5]#b3
  play_pattern_timed [72,74,76,78],[0.25]
  play_pattern_timed [79,67,67],[0.5]#b4
  play_pattern_timed [72],[0.5] #b5
  play_pattern_timed [74,72,71,69],[0.25]
  play_pattern_timed [71],[0.5] #b6
  play_pattern_timed [72,71,69,67],[0.25]
  play_pattern_timed [66],[0.5] #b7
  play_pattern_timed [67,69,71,67],[0.25]
  play_pattern_timed [71,69],[0.5,1] #b8

  ## Start a new thread for bars 9-16 of the left hand part
  in_thread do
    play_chord [55,59]#b9=b1
    sleep 1
    play 57
    sleep 0.5
    play_pattern_timed [55,59,55],[0.5] #b10
    play_pattern_timed [60],[1.5] #b11=b3
    play_pattern_timed [59,60,59,57,5],[0.5,0.25,0.25,0.25,0.25] #b12=b4]
    play_pattern_timed [57,54],[1,0.5] #b13
    play_pattern_timed [55,59],[1,0.5] #b14
    play_pattern_timed [60,62,50],[0.5] #b15
    play_pattern_timed [55,43],[1,0.5] #b16
  end

  ## Play concurrently bars 9-16 of the right hand part the first six
  ## bars repeat bars 1-6
  play_pattern_timed [74],[0.5]#b9 = b1
  play_pattern_timed [67,69,71,72],[0.25]
  play_pattern_timed [74,67,67],[0.5]#b10=b2
  play_pattern_timed [76],[0.5]#b11=b3
  play_pattern_timed [72,74,76,78],[0.25]
  play_pattern_timed [79,67,67],[0.5]#b12=b4
  play_pattern_timed [72],[0.5] #b13=b5
  play_pattern_timed [74,72,71,69],[0.25]
  play_pattern_timed [71],[0.5] #b14=b6
  play_pattern_timed [72,71,69,67],[0.25]
  play_pattern_timed [69],[0.5] #b15
  play_pattern_timed [71,69,67,66],[0.25]
  play_pattern_timed [67],[1.5] #b16
end


## ==========second section starts here======
## The second section is also repeated
2.times do

  ## Start a thread for bars 17-24 of the left hand part
  in_thread do
    play_pattern_timed [55],[1.5] #b17
    play_pattern_timed [54],[1.5] #b18
    play_pattern_timed [52,54,52],[0.5] #b19
    play_pattern_timed [57,45],[1,0.5] #b20
    play_pattern_timed [57],[1.5] #b21
    play_pattern_timed [59,62,61],[0.5] #b22
    play_pattern_timed [62,54,57],[0.5] #b23
    play_pattern_timed [62,50,60],[0.5] #b24
  end

  ## Play bars 17 to 24 of the right hand concurrently with the left
  ## hand thread
  play_pattern_timed [83],[0.5] #b17
  play_pattern_timed [79,81,83,79],[0.25]
  play_pattern_timed [81],[0.5] #b18
  play_pattern_timed [74,76,78,74],[0.25]
  play_pattern_timed [79],[0.5] #b19
  play_pattern_timed [76,78,79,74],[0.25]
  play_pattern_timed [73,71,73,69],[0.5,0.25,0.25,0.5] #b20
  play_pattern_timed [69,71,73,74,76,78],[0.25] #b21
  play_pattern_timed [79,78,76],[0.5] #b22
  play_pattern_timed [78,69,73],[0.5] #b23
  play 74 #b24
  sleep 1.5

  ## Start a new thread for bars 25-32 of the left hand part
  in_thread do
    play_pattern_timed [59,62,59],[0.5] #b25
    play_pattern_timed [60,64,60],[0.5] #b26
    play_pattern_timed [59,57,55],[0.5] #b27
    play 62 #b28
    sleep 1.5 #includes a rest
    play_pattern_timed [50,54],[1,0.5] #b29
    play_pattern_timed [52,55,54],[0.5] #b30
    play_pattern_timed [55,47,50],[0.5] #b31
    play_pattern_timed [55,50,43],[0.5] #b32
  end

  ## Play bars 25-32 of the right hand part concurrently with the left
  ## hand thread
  play_pattern_timed [74,67,66,67],[0.5,0.25,0.25,0.5] #b25
  play_pattern_timed [76,67,66,67],[0.5,0.25,0.25,0.5] #b26
  play_pattern_timed [74,72,71],[0.5] #b27
  play_pattern_timed [69,67,66,67,69],[0.25,0.25,0.25,0.25,0.5] #b28
  play_pattern_timed [62,64,66,67,69,71],[0.25] #b29
  play_pattern_timed [72,71,69],[0.5] #b30
  play_pattern_timed [71,74,67,66],[0.25,0.25,0.5,0.5] #b31
  play_chord [67,59] #b32
  sleep 1.5
end
"
#######################################

live_loop :test do #live loop detects arrival of /osc*/run-code and extracts the data to n[0]
  n = sync "/osc*/run-code"
  #send the data in n[0] to the server_port with correct token and address /run-code
  #this will then be played by Sonic Pi
  osc_send "localhost",pvalue,"/run-code",token,n[0]
end
1 Like

Thank you so much for taking the time to answer my question. I managed to use your Ruby code to get my Python proof of concept working. This script will take a list of arguments and send them to Sonic Pi by extracting the ports and token from the log file (reading it in reverse).

#!/usr/bin/env python3

from pythonosc import udp_client, osc_message_builder
import argparse
import os
from pathlib import Path

def reverse_readline(filename, buf_size=8192):
    """A generator that returns the lines of a file in reverse order"""
    with open(filename) as fh:
        segment = None
        offset = 0
        fh.seek(0, os.SEEK_END)
        file_size = remaining_size = fh.tell()
        while remaining_size > 0:
            offset = min(file_size, offset + buf_size)
            fh.seek(file_size - offset)
            buffer = fh.read(min(remaining_size, buf_size))
            remaining_size -= buf_size
            lines = buffer.split('\n')
            if segment is not None:
                if buffer[-1] != '\n':
                    lines[-1] += segment
                else:
                    yield segment
            segment = lines[0]
            for index in range(len(lines) - 1, 0, -1):
                if lines[index]:
                    yield lines[index]
        if segment is not None:
            yield segment

def get_token_and_ports():
    file=reverse_readline(Path.home().joinpath( '.sonic-pi', 'log', 'spider.log' ))
    token_and_ports={}
    for line in file:
        if line.startswith('Token:'):
            token_and_ports['token']=line.split()[1]
        elif line.startswith('Ports:'):
            token_and_ports['ports']=dict(item.split('=>') for item in line[8:-1].replace(' ','').split(','))
        if len(token_and_ports) == 2:
            return token_and_ports

if __name__ == "__main__":
    token_and_ports=get_token_and_ports()
    parser = argparse.ArgumentParser()
    parser.add_argument("--ip", default="127.0.0.1",
                        help="The ip of the OSC server")
    parser.add_argument('commands', metavar='command', type=str, nargs='+',
                        help='Command to send to sonic pi')
    args = parser.parse_args()
    token=int(token_and_ports['token'])
    port=int(token_and_ports['ports'][':server_port'])
    client = udp_client.SimpleUDPClient(args.ip, port)
    print(f'Using token {token} port {port} command {args.commands}')
    client.send_message('/run-code', [token, '\n'.join(args.commands)])

When I initially read through the spider server code, I had thought reading the spider.log file and pattern matching for the token and active port would be the right way to have an external program inject code/commands into the Sonic Pi app to get it to run them. But when I mentioned that in the “CLI or API interface” issue on GitHub, Sam Aaron responded that retrieving that information from the logs files, while it may work at the moment, is not supported and is likely to break in future releases.

I would love to be able to use a lightweight listener to do code injection without having to create a full blown front-end (with the attendant overhead of managing heartbeat threads, UI, etc) and just being able to send some code to the OSC port to have it played by Sonic Pi (and being able to do so in a semi-automated fashion so I don’t have to pass in the port info I manually read from the GUI every time). If this is something that will actually be a thing longer term, I look forward to it, but would like to verify that’s the case.

1 Like

@sqarcle: I haven’t worked on it for very long but check out my Sonic Pipe CLI. It’s really bad code but it can show you how you can extract the needed information from the Sonic Pi daemon and send OSC from Python to SP. Sonic Pipe can also be used as a library if you want to use it like so. You might not need to use it but you can extrapolate by looking at the source :slight_smile:

2 Likes