UNPKG

reaper-osc

Version:
405 lines 17.3 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.ReaperOscClient = exports.parseMessage = void 0; /** * Stateless OSC client for communicating with Reaper * @module */ const events_1 = require("events"); const osc = __importStar(require("osc")); const Messages_1 = require("../Messages"); const Events_1 = require("./Events"); const Commands_1 = require("./Commands"); const Config_1 = require("../Config"); // --- Value extractors --- function boolFrom(msg) { var _a; return ((_a = msg.args[0]) === null || _a === void 0 ? void 0 : _a.value) === 1; } function floatFrom(msg) { var _a; return (_a = msg.args[0]) === null || _a === void 0 ? void 0 : _a.value; } function intFrom(msg) { var _a; return (_a = msg.args[0]) === null || _a === void 0 ? void 0 : _a.value; } function stringFrom(msg) { var _a; return (_a = msg.args[0]) === null || _a === void 0 ? void 0 : _a.value; } // --- Exact address parsers (global + transport) --- const EXACT_PARSERS = new Map([ // Global ['/click', msg => Events_1.MetronomeEvent(boolFrom(msg))], ['/autorecarm', msg => Events_1.AutoRecordArmEvent(boolFrom(msg))], ['/anysolo', msg => Events_1.AnySoloEvent(boolFrom(msg))], // Transport ['/play', msg => Events_1.PlayEvent(boolFrom(msg))], ['/stop', msg => Events_1.StopEvent(boolFrom(msg))], ['/pause', msg => Events_1.PauseEvent(boolFrom(msg))], ['/record', msg => Events_1.RecordEvent(boolFrom(msg))], ['/rewind', msg => Events_1.RewindEvent(boolFrom(msg))], ['/forward', msg => Events_1.FastForwardEvent(boolFrom(msg))], ['/repeat', msg => Events_1.RepeatEvent(boolFrom(msg))], ['/time', msg => Events_1.TimeChanged(floatFrom(msg))], ['/beat/str', msg => Events_1.BeatChanged(stringFrom(msg))], ['/frames/str', msg => Events_1.FramesChanged(stringFrom(msg))], ['/loop/start/time', msg => Events_1.LoopStartChanged(floatFrom(msg))], ['/loop/end/time', msg => Events_1.LoopEndChanged(floatFrom(msg))], ['/bymarker', msg => Events_1.RewindByMarkerEvent(boolFrom(msg))], ['/editloop', msg => Events_1.SetLoopEvent(boolFrom(msg))], // Last marker ['/lastmarker/name', msg => Events_1.LastMarkerNameChanged(stringFrom(msg))], ['/lastmarker/number/str', msg => Events_1.LastMarkerNumberChanged(stringFrom(msg))], ['/lastmarker/time', msg => Events_1.LastMarkerTimeChanged(floatFrom(msg))], // Last region ['/lastregion/name', msg => Events_1.LastRegionNameChanged(stringFrom(msg))], ['/lastregion/number/str', msg => Events_1.LastRegionNumberChanged(stringFrom(msg))], ['/lastregion/time', msg => Events_1.LastRegionTimeChanged(floatFrom(msg))], ['/lastregion/length', msg => Events_1.LastRegionLengthChanged(floatFrom(msg))], ]); const TRACK_PARSERS = new Map([ ['mute', (n, m) => Events_1.TrackMuteEvent(n, boolFrom(m))], ['solo', (n, m) => Events_1.TrackSoloEvent(n, boolFrom(m))], ['recarm', (n, m) => Events_1.TrackRecordArmEvent(n, boolFrom(m))], ['select', (n, m) => Events_1.TrackSelectEvent(n, boolFrom(m))], ['name', (n, m) => Events_1.TrackNameChanged(n, stringFrom(m))], ['pan', (n, m) => Events_1.TrackPanChanged(n, floatFrom(m))], ['pan2', (n, m) => Events_1.TrackPan2Changed(n, floatFrom(m))], ['panmode', (n, m) => Events_1.TrackPanModeChanged(n, stringFrom(m))], ['volume', (n, m) => Events_1.TrackVolumeChanged(n, floatFrom(m))], ['volume/db', (n, m) => Events_1.TrackVolumeDbChanged(n, floatFrom(m))], ['vu', (n, m) => Events_1.TrackVuChanged(n, floatFrom(m))], ['vu/L', (n, m) => Events_1.TrackVuLeftChanged(n, floatFrom(m))], ['vu/R', (n, m) => Events_1.TrackVuRightChanged(n, floatFrom(m))], ['monitor', (n, m) => Events_1.TrackMonitoringModeChanged(n, intFrom(m))], ]); const TRACK_FX_PARSERS = new Map([ // bypass is inverted: Reaper sends 0 when bypassed ['name', (t, f, m) => Events_1.TrackFxNameChanged(t, f, stringFrom(m))], ['bypass', (t, f, m) => Events_1.TrackFxBypassEvent(t, f, !boolFrom(m))], ['openui', (t, f, m) => Events_1.TrackFxOpenUiEvent(t, f, boolFrom(m))], ['preset', (t, f, m) => Events_1.TrackFxPresetChanged(t, f, stringFrom(m))], ]); const TRACK_SEND_PARSERS = new Map([ ['name', (t, s, m) => Events_1.TrackSendNameChanged(t, s, stringFrom(m))], ['volume', (t, s, m) => Events_1.TrackSendVolumeChanged(t, s, floatFrom(m))], ['volume/str', (t, s, m) => Events_1.TrackSendVolumeStrChanged(t, s, stringFrom(m))], ['pan', (t, s, m) => Events_1.TrackSendPanChanged(t, s, floatFrom(m))], ['pan/str', (t, s, m) => Events_1.TrackSendPanStrChanged(t, s, stringFrom(m))], ]); // --- Track receive suffix parsers (for /track/N/recv/M/suffix) --- const TRACK_RECV_PARSERS = new Map([ ['name', (t, r, m) => Events_1.TrackReceiveNameChanged(t, r, stringFrom(m))], ['volume', (t, r, m) => Events_1.TrackReceiveVolumeChanged(t, r, floatFrom(m))], ['volume/str', (t, r, m) => Events_1.TrackReceiveVolumeStrChanged(t, r, stringFrom(m))], ['pan', (t, r, m) => Events_1.TrackReceivePanChanged(t, r, floatFrom(m))], ['pan/str', (t, r, m) => Events_1.TrackReceivePanStrChanged(t, r, stringFrom(m))], ]); const SELECTED_TRACK_SEND_PARSERS = new Map([ ['name', (s, m) => Events_1.SelectedTrackSendNameChanged(s, stringFrom(m))], ['volume', (s, m) => Events_1.SelectedTrackSendVolumeChanged(s, floatFrom(m))], ['volume/str', (s, m) => Events_1.SelectedTrackSendVolumeStrChanged(s, stringFrom(m))], ['pan', (s, m) => Events_1.SelectedTrackSendPanChanged(s, floatFrom(m))], ['pan/str', (s, m) => Events_1.SelectedTrackSendPanStrChanged(s, stringFrom(m))], ]); // --- Selected track receive suffix parsers (for /track/recv/M/suffix) --- const SELECTED_TRACK_RECV_PARSERS = new Map([ ['name', (r, m) => Events_1.SelectedTrackReceiveNameChanged(r, stringFrom(m))], ['volume', (r, m) => Events_1.SelectedTrackReceiveVolumeChanged(r, floatFrom(m))], ['volume/str', (r, m) => Events_1.SelectedTrackReceiveVolumeStrChanged(r, stringFrom(m))], ['pan', (r, m) => Events_1.SelectedTrackReceivePanChanged(r, floatFrom(m))], ['pan/str', (r, m) => Events_1.SelectedTrackReceivePanStrChanged(r, stringFrom(m))], ]); const SELECTED_TRACK_PARSERS = new Map([ ['mute', m => Events_1.SelectedTrackMuteEvent(boolFrom(m))], ['solo', m => Events_1.SelectedTrackSoloEvent(boolFrom(m))], ['recarm', m => Events_1.SelectedTrackRecordArmEvent(boolFrom(m))], ['select', m => Events_1.SelectedTrackSelectEvent(boolFrom(m))], ['name', m => Events_1.SelectedTrackNameChanged(stringFrom(m))], ['pan', m => Events_1.SelectedTrackPanChanged(floatFrom(m))], ['pan2', m => Events_1.SelectedTrackPan2Changed(floatFrom(m))], ['panmode', m => Events_1.SelectedTrackPanModeChanged(stringFrom(m))], ['volume', m => Events_1.SelectedTrackVolumeChanged(floatFrom(m))], ['volume/db', m => Events_1.SelectedTrackVolumeDbChanged(floatFrom(m))], ['vu', m => Events_1.SelectedTrackVuChanged(floatFrom(m))], ['vu/L', m => Events_1.SelectedTrackVuLeftChanged(floatFrom(m))], ['vu/R', m => Events_1.SelectedTrackVuRightChanged(floatFrom(m))], ['monitor', m => Events_1.SelectedTrackMonitoringModeChanged(intFrom(m))], ]); const SELECTED_TRACK_FX_PARSERS = new Map([ ['name', (f, m) => Events_1.SelectedTrackFxNameChanged(f, stringFrom(m))], ['bypass', (f, m) => Events_1.SelectedTrackFxBypassEvent(f, !boolFrom(m))], ['openui', (f, m) => Events_1.SelectedTrackFxOpenUiEvent(f, boolFrom(m))], ['preset', (f, m) => Events_1.SelectedTrackFxPresetChanged(f, stringFrom(m))], ]); const SELECTED_FX_PARSERS = new Map([ ['name', m => Events_1.SelectedFxNameChanged(stringFrom(m))], ['bypass', m => Events_1.SelectedFxBypassEvent(!boolFrom(m))], ['openui', m => Events_1.SelectedFxOpenUiEvent(boolFrom(m))], ['preset', m => Events_1.SelectedFxPresetChanged(stringFrom(m))], ]); const MARKER_PARSERS = new Map([ ['name', (n, m) => Events_1.MarkerNameChanged(n, stringFrom(m))], ['number/str', (n, m) => Events_1.MarkerNumberChanged(n, stringFrom(m))], ['time', (n, m) => Events_1.MarkerTimeChanged(n, floatFrom(m))], ]); const REGION_PARSERS = new Map([ ['name', (n, m) => Events_1.RegionNameChanged(n, stringFrom(m))], ['number/str', (n, m) => Events_1.RegionNumberChanged(n, stringFrom(m))], ['time', (n, m) => Events_1.RegionTimeChanged(n, floatFrom(m))], ['length', (n, m) => Events_1.RegionLengthChanged(n, floatFrom(m))], ]); // --- Message parsing --- function parseMarkerMessage(msg) { const parts = msg.addressParts; const slotIndex = parseInt(parts[1]); if (isNaN(slotIndex)) return null; const suffix = parts.slice(2).join('/'); const parser = MARKER_PARSERS.get(suffix); return parser ? parser(slotIndex, msg) : null; } function parseRegionMessage(msg) { const parts = msg.addressParts; const slotIndex = parseInt(parts[1]); if (isNaN(slotIndex)) return null; const suffix = parts.slice(2).join('/'); const parser = REGION_PARSERS.get(suffix); return parser ? parser(slotIndex, msg) : null; } function parseTrackMessage(msg) { const parts = msg.addressParts; const trackNumber = parseInt(parts[1]); if (isNaN(trackNumber)) { // Selected track sends/receives: /track/send/N/suffix, /track/recv/N/suffix if (parts[1] === 'send' || parts[1] === 'recv') { const sendNumber = parseInt(parts[2]); if (isNaN(sendNumber)) return null; const suffix = parts.slice(3).join('/'); const parsers = parts[1] === 'send' ? SELECTED_TRACK_SEND_PARSERS : SELECTED_TRACK_RECV_PARSERS; const parser = parsers.get(suffix); return parser ? parser(sendNumber, msg) : null; } // Selected track property: /track/mute, /track/name, etc. const suffix = parts.slice(1).join('/'); const parser = SELECTED_TRACK_PARSERS.get(suffix); return parser ? parser(msg) : null; } // Track FX: /track/N/fx/M/property if (parts[2] === 'fx') { const fxNumber = parseInt(parts[3]); if (isNaN(fxNumber)) return null; const suffix = parts.slice(4).join('/'); const parser = TRACK_FX_PARSERS.get(suffix); return parser ? parser(trackNumber, fxNumber, msg) : null; } // Track sends/receives: /track/N/send/M/suffix, /track/N/recv/M/suffix if (parts[2] === 'send' || parts[2] === 'recv') { const sendNumber = parseInt(parts[3]); if (isNaN(sendNumber)) return null; const suffix = parts.slice(4).join('/'); const parsers = parts[2] === 'send' ? TRACK_SEND_PARSERS : TRACK_RECV_PARSERS; const parser = parsers.get(suffix); return parser ? parser(trackNumber, sendNumber, msg) : null; } // Track property: /track/N/property const suffix = parts.slice(2).join('/'); const parser = TRACK_PARSERS.get(suffix); return parser ? parser(trackNumber, msg) : null; } function parseFxMessage(msg) { const parts = msg.addressParts; const fxNumber = parseInt(parts[1]); if (isNaN(fxNumber)) { // Selected FX: /fx/name, /fx/bypass, etc. const suffix = parts.slice(1).join('/'); const parser = SELECTED_FX_PARSERS.get(suffix); return parser ? parser(msg) : null; } // Indexed FX on selected track: /fx/M/name, /fx/M/bypass, etc. const suffix = parts.slice(2).join('/'); const parser = SELECTED_TRACK_FX_PARSERS.get(suffix); return parser ? parser(fxNumber, msg) : null; } function parseMessage(msg) { // O(1) exact match for global + transport const exact = EXACT_PARSERS.get(msg.address); if (exact) return exact(msg); // Prefix-based routing const firstPart = msg.addressParts[0]; if (firstPart === 'track') { const event = parseTrackMessage(msg); if (event) return event; } if (firstPart === 'fx') { const event = parseFxMessage(msg); if (event) return event; } if (firstPart === 'marker') { const event = parseMarkerMessage(msg); if (event) return event; } if (firstPart === 'region') { const event = parseRegionMessage(msg); if (event) return event; } return { type: 'unknown', message: msg }; } exports.parseMessage = parseMessage; /** * A stateless OSC client for communicating with Reaper. * * Translates incoming OSC messages into strongly typed events, and accepts * strongly typed commands for sending to Reaper. Does not maintain any state * about track properties, transport, or FX — use {@link Reaper} for that. * * @example * ```typescript * const client = new ReaperOscClient(); * await client.start(); * * // Receive typed events * client.on('message', event => { * switch (event.type) { * case 'track:mute': * console.log(`Track ${event.trackNumber} muted: ${event.muted}`); * break; * case 'transport:play': * console.log(`Playing: ${event.playing}`); * break; * } * }); * * // Send typed commands * client.send({ type: 'transport:play' }); * client.send({ type: 'track:mute', trackNumber: 3, muted: true }); * ``` */ class ReaperOscClient extends events_1.EventEmitter { constructor(config = new Config_1.ReaperConfiguration()) { super(); this._isReady = false; this._log = config.log; this._osc = new osc.UDPPort({ localAddress: config.localAddress, localPort: config.localPort, remoteAddress: config.remoteAddress, remotePort: config.remotePort, broadcast: true, metadata: true, }); this.initOsc(); } /** Indicates whether OSC is ready to send and receive messages */ get isReady() { return this._isReady; } /** * Send a typed command to Reaper. * @param command The command to send */ send(command) { this.sendRaw(Commands_1.commandToOscMessage(command)); } /** * Send a raw OSC message to Reaper. * @param message The OSC message to send */ sendRaw(message) { if (!this._isReady) { throw new Error("Can't send while OSC is not ready"); } this._osc.send(message); this._log('debug', 'OSC Message sent', message); } /** Open the OSC port and start listening for messages */ async start() { if (this._isReady) { return; } let errorCallback; let readyCallback; const promise = new Promise((resolve, reject) => { errorCallback = (err) => { this._log('debug', 'Error opening OSC connection', err); reject(err); }; readyCallback = () => { this._log('debug', 'OSC listener ready'); this._isReady = true; resolve(); }; this._osc.once('ready', readyCallback); this._osc.once('error', errorCallback); }); const removeListeners = () => { this._osc.removeListener('ready', readyCallback); this._osc.removeListener('error', errorCallback); }; this._osc.open(); try { await promise; } finally { removeListeners(); } } /** Stop listening for OSC messages */ async stop() { if (!this._isReady) { return; } const promise = new Promise(resolve => { this._osc.once('close', () => { this._log('debug', 'OSC listener stopped'); resolve(); }); }); this._isReady = false; this._osc.close(); return promise; } initOsc() { this._osc.on('error', (err) => { this._log('error', 'OSC error received', err); }); this._osc.on('message', (rawMessage) => { const message = new Messages_1.OscMessage(rawMessage.address, rawMessage.args); const event = parseMessage(message); this.emit('message', event); this.emit('rawMessage', message); }); } } exports.ReaperOscClient = ReaperOscClient; //# sourceMappingURL=Client.js.map