'use babel';
const os = require('os');
const process = require('process');
const fs = require('fs');
const path = require('path');
const { EventEmitter } = require('events');
const osc = require('node-osc');
const {SonicPiOSCServer} = require('./osc/osc-server');
const proc_utils = require('./utils/proc_utils.js');
// const logger = require('./utils/logger.js');
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
/**
* A class used to initiate, interface with, and shutdown the Sonic Pi server.
*/
class SonicPiAPI {
/**
* @constant
* @private
*/
string_number_name = [
"zero",
"one",
"two",
"three",
"four",
"five",
"six",
"seven",
"eight",
"nine",
"ten"
]
/**
* @typedef {Object} Result
* @memberof SonicPiAPI
* @property {boolean} success - Whether the operation was successful
* @property {string} error_message - An error message if something went wrong - only present if `success` is false
*/
/**
* @typedef {Object} BootDaemonOutput
* @memberof SonicPiAPI
* @private
* @property {number} token - null if failed to get output
* @property {Object[]} ports - null if failed to get output
* @property {number} ports.daemon
* @property {number} ports.gui_listen_to_spider
* @property {number} ports.gui_send_to_spider
* @property {number} ports.scsynth
* @property {number} ports.tau_osc_cues
* @property {number} ports.tau
* @property {number} ports.phx_http
*/
/**
* Creates an instance of the `SonicPiAPI` Class
* @constructor
* @classdesc A class used to initiate, interface with, and shutdown the Sonic Pi server.
*/
constructor() {
this.running = false;
this.use_udp = false;
this.m_paths = {
RootPath: "", // Sonic Pi Application root
RubyPath: "", // Path to ruby executable
BootDaemonPath: "", // Path to the ruby server script
SamplePath: "", // Path to the samples folder
UserPath: "",
ServerErrorLogPath: "",
ServerOutputLogPath: "",
ProcessLogPath: "",
SCSynthLogPath: "",
GUILogPath: ""
}
this.m_ports = {}
this.m_guid = "";
this.m_token = null;
this.m_homeDirWriteable = false;
this.osc_server = null;
this.osc_client = null;
this.status_emitter = new EventEmitter();
/**
* User settings
* @member {Object}
* @property {boolean} log_synths - Default: true
* @property {boolean} log_cues - Default: true
* @property {boolean} enable_external_synths - Default: false
* @property {boolean} enforce_timing_guarantees - Default: false
* @property {boolean} check_args - Default: false
* @property {number} default_midi_channel - Note that any number less than 0 corresponds to all channels. Must be an integer. Default: -1
*/
this.settings = {
log_synths: true,
log_cues: true,
enable_external_synths: false,
enforce_timing_guarantees: false,
check_args: false,
default_midi_channel: -1
}
}
/**
* Destructor for the Sonic Pi API Class
* @private
*/
destructor() {
this.shutdown();
}
/**
* Shuts down the Sonic Pi server
*/
shutdown() {
// Ask the server to exit
if (!this.m_bootDaemonProcess) {
console.log("Server daemon process is not running.");
} else {
// Stop the osc server and clients
if (this.osc_server) {
delete this.osc_server;
}
if (this.osc_client) {
delete this.osc_client;
}
// Stop the daemon keep alive thread
if (this.daemon_keep_alive) {
clearInterval(this.daemon_keep_alive);
}
// Tell the server to shutdown
this.daemon_osc_client.send("/daemon/exit", this.m_token)
if (this.daemon_osc_client) {
delete this.daemon_osc_client;
}
// Make sure the daemon has exited after 10 seconds
const _this = this;
setTimeout(() => {
if (_this.m_bootDaemonProcess) {
console.log("Daemon still running. Sending SIGSTOP to daemon...")
_this.m_bootDaemonProcess.kill("SIGSTOP");
}
}, 10000);
setTimeout(() => {
if (_this.m_bootDaemonProcess) {
console.log("Daemon still running. Sending SIGTERM to daemon...")
_this.m_bootDaemonProcess.kill("SIGTERM");
}
}, 11000);
setTimeout(() => {
if (_this.m_bootDaemonProcess) {
console.log("Daemon still running. Sending SIGKILL to daemon...")
_this.m_bootDaemonProcess.kill("SIGKILL");
}
}, 12000);
this.m_bootDaemonProcess.on('exit', (code, signal) => {
console.log(`Daemon exited with code ${code} and signal ${signal}`);
console.log("Shutdown process complete");
/**
* Emitted when the shutdown process is complete (i.e. when the boot daemon has exited)
*
* @event SonicPiAPI#shutdown_complete
*/
_this.status_emitter.emit("shutdown_complete");
_this.m_bootDaemonProcess = null;
_this.running = false;
});
}
}
/**
* Set the main volume
* @param {number} vol - A number from 0 to 200 indicating the volume percentage
* @param {number} [silent=0]
* @throws {RangeError} Argument vol must be between 0 and 200 inclusive.
*/
set_volume(vol, silent=0) {
if (vol < 0 || vol > 200) {
throw RangeError("Volume outside of valid range - `vol` must be between 0 and 200 inclusive");
}
console.log(`[SonicPiAPI] - Changing volume to ${vol}%`);
var v = (vol / 100);
this.osc_client.send('/mixer-amp', this.m_token, v, silent);
}
/**
* Handle and log a startup error
* @param {string} message - Error message
* @private
*/
startup_error(message) {
var stack = new Error().stack;
console.error(`Failed to start Sonic Pi server: ${message}\nStack trace: ${stack}`);
this.shutdown();
return {
success: false,
error_message: message
};
}
/**
* Start the boot daemon
* @private
* @returns {Promise<BootDaemonOutput>}
*/
start_boot_daemon() {
console.log("Launching Sonic Pi Boot Daemon:");
const _this = this;
var cmd = this.m_paths.RubyPath;
var args = [];
args.push(this.m_paths.BootDaemonPath);
console.log("Args: " + args.join(", "));
this.m_bootDaemonProcess = proc_utils.start_process(cmd, args);
if (!this.m_bootDaemonProcess) {
atom.notifications.addError("The Boot Daemon could not be started!");
console.error("Failed to start Boot Daemon!");
return false;
}
console.log("Attempting to read Boot Daemon output");
return new Promise((resolve, reject) => {
_this.m_bootDaemonProcess.stdout.once('data', (data) => {
console.log(`Received chunk ${data}`);
var list = data.toString().split(" ");
var token = list.pop();
var ports = list;
// Redirect stdout and stderr
var out_pipe = fs.createWriteStream(this.m_paths.DaemonLogPath);
_this.m_bootDaemonProcess.stdout.pipe(out_pipe);
_this.m_bootDaemonProcess.stderr.pipe(out_pipe);
console.log(`Token: ${token}`)
console.log(`Ports: ${ports}`)
if (ports.length == 7) {
resolve({
token: parseInt(token),
ports: {
daemon: parseInt(ports[0]),
gui_listen_to_spider: parseInt(ports[1]),
gui_send_to_spider: parseInt(ports[2]),
scsynth: parseInt(ports[3]),
tau_osc_cues: parseInt(ports[4]),
tau: parseInt(ports[5]),
phx_http: parseInt(ports[6])
}
});
} else {
resolve({
token: null,
ports: null
});
}
});
_this.m_bootDaemonProcess.once('error', (err) => {
reject(err);
});
});
}
/**
* Apply user settings to the code
* @private
* @param {string} code - Code to process
* @returns {string} - Processed code
*/
preprocess_code(code) {
var user_settings = "";
if (!this.settings.log_synths) {
user_settings += "use_debug false #__nosave__ set by user preferences.\n";
}
if (!this.settings.log_cues) {
user_settings += "use_cue_logging false #__nosave__ set by user preferences.\n";
}
if (this.settings.check_args) {
user_settings += "use_arg_checks true #__nosave__ set by user preferences.\n";
}
if (this.settings.enable_external_synths) {
user_settings += "use_external_synths true #__nosave__ set by user preferences.\n";
}
if (this.settings.enforce_timing_guarantees) {
user_settings += "use_timing_guarantees true #__nosave__ set by user preferences.\n";
}
var midi_channel = this.settings.default_midi_channel;
if (midi_channel <= 0) {
midi_channel = "*";
}
user_settings += `use_midi_defaults channel: "${midi_channel}" #__nosave__ set by user preferences.\n`;
return user_settings + code;
}
/**
* Run code
* @param {string} code - The code to run
*/
run_code(code) {
var processed_code = this.preprocess_code(code);
this.osc_client.send('/run-code', this.m_token, processed_code);
}
/**
* Buffer new line and indent
* @param {number} point_line
* @param {number} point_index
* @param {number} first_line
* @param {string} code
* @param {string} file_name
*/
buffer_new_line_and_indent(point_line, point_index, first_line, code, file_name) {
this.osc_client.send('/buffer-newline-and-indent', this.m_token, file_name, code, point_line, point_index, first_line);
}
/**
* Save and run buffer
* @param {string} buffer - Buffer identifier
* @param {string} code - The code to save and run
*/
save_and_run_buffer(buffer, code) {
var processed_code = this.preprocess_code(code);
this.osc_client.send("/save-and-run-buffer", this.m_token, buffer, processed_code, buffer);
}
/**
* Load workspaces
*/
load_workspaces() {
for (var i = 0; i < this.max_workspaces; i++) {
this.osc_client.send("/load-buffer", this.m_token, `workspace_${string_number_name(i)}`);
}
}
/**
* Save workspaces
* @param {Object[]} workspaces
* @param {string} workspaces.n - The code to save in workspace `n`, where `n` is an integer in the range: 0 ≤ n < `max_workspaces`
*/
save_workspaces(workspaces) {
for (var i = 0; i < this.max_workspaces; i++) {
if (i in workspaces) {
this.osc_client.send("/save-buffer", this.m_token, `workspace_${string_number_name(i)}`, workspaces[i]);
}
}
}
/**
* Stop all jobs
*/
stop_all_jobs() {
this.osc_client.send('/stop-all-jobs', this.m_token);
}
/**
* Initialise the Sonic Pi server
* @param {string} root - Root path of Sonic Pi
* @async
* @returns {Result} The result of the initialisation process
*/
async init(root) {
if (this.running) {
return this.startup_error("Sonic Pi server is already running");
}
if (!fs.existsSync(path.normalize(root))) {
return this.startup_error(`Could not find root path: ${root}`);
}
this.m_paths.RootPath = path.normalize(root);
// Find ruby path
if (process.platform == "win32") {
this.m_paths.RubyPath = path.join(this.m_paths.RootPath, "app/server/native/ruby/bin/ruby.exe");
} else {
this.m_paths.RubyPath = path.join(this.m_paths.RootPath, "app/server/native/ruby/bin/ruby");
}
if (!fs.existsSync(this.m_paths.RubyPath)) {
this.m_paths.RubyPath = "ruby";
}
// Check script paths
this.m_paths.BootDaemonPath = path.join(this.m_paths.RootPath, "app/server/ruby/bin/daemon.rb");
if (!fs.existsSync(this.m_paths.BootDaemonPath)) {
return this.startup_error(`Could not find boot daemon script path: ${this.m_paths.BootDaemonPath}`);
}
// Samples
this.m_paths.SamplePath = path.join(this.m_paths.RootPath, "etc/samples");
// Sonic Pi home directory
this.m_paths.UserPath = path.join(os.homedir(), ".sonic-pi");
var logPath = path.join(this.m_paths.UserPath, "log");
// Make the log folder and check we can write to it.
// This is ~/.sonic-pi/log
this.m_homeDirWriteable = true;
try {
if (!fs.existsSync(logPath)) {
fs.mkdirSync(logPath);
fs.writeFileSync(path.join(logPath, ".writeTest"), "test");
fs.unlinkSync(path.join(logPath, ".writeTest"));
}
} catch (err) {
console.error(`Home directory not writable: ${err}`);
this.m_homeDirWriteable = false;
}
// Our log paths
this.m_paths.DaemonLogPath = path.join(logPath, "daemon.log");
this.m_paths.ServerErrorLogPath = path.join(logPath, "server-errors.log");
this.m_paths.ServerOutputLogPath = path.join(logPath, "server-output.log");
this.m_paths.ProcessLogPath = path.join(logPath, "processes.log");
this.m_paths.SCSynthLogPath = path.join(logPath, "scsynth.log");
this.m_paths.GUILogPath = path.join(logPath, "gui.log");
// // Setup redirection of log from this app to our log file
// // stdout into ~/.sonic-pi/log/gui.log
// if (m_homeDirWriteable && (m_logOption == LogOption::File))
// {
// m_coutbuf = std::cout.rdbuf();
// m_stdlog.open(m_paths.GUILogPath.string().c_str());
// std::cout.rdbuf(m_stdlog.rdbuf());
// }
console.log("Welcome to Sonic Pi");
console.log("===================");
var result = await this.start_boot_daemon();
if (!result) {
return this.startup_error("Failed to start boot baemon")
}
this.m_token = result.token;
this.m_ports = result.ports;
console.log(`Token: ${this.m_token}`)
console.log(`Ports: ${this.m_ports}`)
/**
* Emits the ports used for communicating with different parts of the server
*
* @event SonicPiAPI#received_ports
* @type {object}
* @property {number} daemon - Port for sending commands to boot daemon
* @property {number} gui_listen_to_spider - Port for Spider (Ruby Server) to GUI communications
* @property {number} gui_send_to_spider - Port for GUI to Spider communications
* @property {number} scsynth - Port for sending commands to scsynth
* @property {number} tau_osc_cues - Port for receiving OSC cues
* @property {number} tau - Port for sending messages to Tau (Elixir Server)
* @property {number} phx_http - Port that the Tau webpage is served on
*/
this.status_emitter.emit("received_ports", this.m_ports);
if (this.m_token == null) {
return this.startup_error(`Unable to get client token from boot daemon`);
}
this.daemon_osc_client = new osc.Client("127.0.0.1", this.m_ports.daemon);
let _this = this;
this.daemon_keep_alive = setInterval(() => {
_this.daemon_osc_client.send("/daemon/keep-alive", _this.m_token);
}, 4000);
this.osc_client = new osc.Client("127.0.0.1", this.m_ports.gui_send_to_spider);
this.osc_server = new SonicPiOSCServer("127.0.0.1", this.m_ports.gui_listen_to_spider, "127.0.0.1", this.m_ports.gui_send_to_spider);
this.running = true;
console.log("Init SonicPi Succeeded...");
return {
success: true,
};
}
}
module.exports = {
SonicPiAPI
};