v8hd
Version:
Control interface for Roland V-8HD video switchers
434 lines (433 loc) • 15.7 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.RolandV8HD = void 0;
const midi_1 = require("midi");
const enums_1 = require("../enums");
const CC_1 = require("../midi/CC");
const DEBUG = false;
/**
* Class for controlling Roland V-8HD video switcher via MIDI
* Provides methods to control video transitions, effects, and audio levels
*/
class RolandV8HD {
/**
* Creates a new instance of RolandV8HD controller
* Automatically searches for and connects to the Roland V-8HD device
* @throws {Error} If Roland V-8HD MIDI device is not found
*/
constructor() {
this.MIDI_CHANNEL = 0; // Fixed at 00H
this.output = new midi_1.Output();
this.initializeMidi();
}
/**
* Initializes MIDI connection with Roland V-8HD
* @private
* @throws {Error} If Roland V-8HD device cannot be found
*/
initializeMidi() {
// List available MIDI output ports
const portCount = this.output.getPortCount();
const ports = [];
for (let i = 0; i < portCount; i++) {
ports.push(this.output.getPortName(i));
}
// Find and open the Roland V-8HD port
for (let i = 0; i < portCount; i++) {
const portName = this.output.getPortName(i);
if (portName.includes("Roland") || portName.includes("V-8HD")) {
this.output.openPort(i);
if (DEBUG)
console.log(`Connected to Roland V-8HD on port ${i}: ${portName}`);
return;
}
}
throw new Error("Roland V-8HD not found. Available MIDI outputs: " + ports.join(", "));
}
/**
* Sends a MIDI Control Change message to the device
* @private
* @param {CC} cc - Control Change number
* @param {number} value - Value to send (0-127)
*/
sendControlChange(cc, value) {
// Format: [status, control, value]
// status is 0xB0 (Control Change on Channel 0) + channel number
this.output.sendMessage([0xB0 + this.MIDI_CHANNEL, cc, value]);
}
/**
* Sets the video fader position
* @param {number} position - Position value (0-127, bottom to top)
* @throws {Error} If position is out of range
*/
setVideoFader(position) {
if (position < 0 || position > 127) {
throw new Error("Video fader position must be between 0 and 127");
}
this.sendControlChange(CC_1.CC.PANPOT, position);
}
/**
* Sets the transition type
* @param {TransitionType} type - Type of transition (MIX or WIPE)
*/
setTransitionType(type) {
this.sendControlChange(CC_1.CC.EXPRESSION, type);
}
/**
* Sets the Mix/Wipe transition time
* @param {number} seconds - Transition time in seconds (0.0-4.0)
* @throws {Error} If time is out of range
*/
setMixWipeTime(seconds) {
if (seconds < 0 || seconds > 4.0) {
throw new Error("Mix/Wipe time must be between 0.0 and 4.0 seconds");
}
// Convert seconds to MIDI value (0-40)
const value = Math.round(seconds * 10);
this.sendControlChange(CC_1.CC.EFFECT_CONTROL_1, value);
}
/**
* Sets the PinP 1 transition time
* @param {number} seconds - Transition time in seconds (0.0-4.0)
* @throws {Error} If time is out of range
*/
setPinP1Time(seconds) {
if (seconds < 0 || seconds > 4.0) {
throw new Error("PinP 1 time must be between 0.0 and 4.0 seconds");
}
const value = Math.round(seconds * 10);
this.sendControlChange(CC_1.CC.EFFECT_CONTROL_2, value);
}
/**
* Sets the PinP 2 transition time
* @param {number} seconds - Transition time in seconds (0.0-4.0)
* @throws {Error} If time is out of range
*/
setPinP2Time(seconds) {
if (seconds < 0 || seconds > 4.0) {
throw new Error("PinP 2 time must be between 0.0 and 4.0 seconds");
}
const value = Math.round(seconds * 10);
this.sendControlChange(CC_1.CC.UNDEFINED_14, value);
}
/**
* Sets the DSK transition time
* @param {number} seconds - Transition time in seconds (0.0-4.0)
* @throws {Error} If time is out of range
*/
setDSKTime(seconds) {
if (seconds < 0 || seconds > 4.0) {
throw new Error("DSK time must be between 0.0 and 4.0 seconds");
}
const value = Math.round(seconds * 10);
this.sendControlChange(CC_1.CC.UNDEFINED_15, value);
}
/**
* Sets the PinP 1 input source
* @param {InputSource} source - Source to use for PinP 1
*/
setPinP1Source(source) {
this.sendControlChange(CC_1.CC.GENERAL_PURPOSE_1, source);
}
/**
* Sets the PinP 1 horizontal position
* @param {number} percent - Position as percentage (-50 to 50%)
* @throws {Error} If position is out of range
*/
setPinP1PositionH(percent) {
if (percent < -50 || percent > 50) {
throw new Error("PinP 1 horizontal position must be between -50 and 50%");
}
// Convert percent to MIDI value (10-100)
const value = Math.round(percent + 50) + 10;
this.sendControlChange(CC_1.CC.GENERAL_PURPOSE_2, value);
}
/**
* Sets the PinP 1 vertical position
* @param {number} percent - Position as percentage (-50 to 50%)
* @throws {Error} If position is out of range
*/
setPinP1PositionV(percent) {
if (percent < -50 || percent > 50) {
throw new Error("PinP 1 vertical position must be between -50 and 50%");
}
// Convert percent to MIDI value (10-100)
const value = Math.round(percent + 50) + 10;
this.sendControlChange(CC_1.CC.GENERAL_PURPOSE_3, value);
}
/**
* Sets the PinP 1 size
* @param {number} percent - Size as percentage (10-100%)
* @throws {Error} If size is out of range
*/
setPinP1Size(percent) {
if (percent < 10 || percent > 100) {
throw new Error("PinP 1 size must be between 10 and 100%");
}
this.sendControlChange(CC_1.CC.GENERAL_PURPOSE_4, percent);
}
/**
* Sets the PinP 1 view zoom
* @param {number} percent - Zoom as percentage (100-1000%)
* @throws {Error} If zoom is out of range
*/
setPinP1ViewZoom(percent) {
if (percent < 100 || percent > 1000) {
throw new Error("PinP 1 view zoom must be between 100 and 1000%");
}
// Convert percent to MIDI value (10-100)
const value = Math.round(((percent - 100) / 900) * 90) + 10;
this.sendControlChange(CC_1.CC.UNDEFINED_20, value);
}
/**
* Sets the PinP 2 input source
* @param {InputSource} source - Source to use for PinP 2
*/
setPinP2Source(source) {
this.sendControlChange(CC_1.CC.UNDEFINED_21, source);
}
/**
* Sets the PinP 2 horizontal position
* @param {number} percent - Position as percentage (-50 to 50%)
* @throws {Error} If position is out of range
*/
setPinP2PositionH(percent) {
if (percent < -50 || percent > 50) {
throw new Error("PinP 2 horizontal position must be between -50 and 50%");
}
const value = Math.round(percent + 50) + 10;
this.sendControlChange(CC_1.CC.UNDEFINED_22, value);
}
/**
* Sets the PinP 2 vertical position
* @param {number} percent - Position as percentage (-50 to 50%)
* @throws {Error} If position is out of range
*/
setPinP2PositionV(percent) {
if (percent < -50 || percent > 50) {
throw new Error("PinP 2 vertical position must be between -50 and 50%");
}
const value = Math.round(percent + 50) + 10;
this.sendControlChange(CC_1.CC.UNDEFINED_23, value);
}
/**
* Sets the PinP 2 size
* @param {number} percent - Size as percentage (10-100%)
* @throws {Error} If size is out of range
*/
setPinP2Size(percent) {
if (percent < 10 || percent > 100) {
throw new Error("PinP 2 size must be between 10 and 100%");
}
this.sendControlChange(CC_1.CC.UNDEFINED_24, percent);
}
/**
* Sets the PinP 2 view zoom
* @param {number} percent - Zoom as percentage (100-1000%)
* @throws {Error} If zoom is out of range
*/
setPinP2ViewZoom(percent) {
if (percent < 100 || percent > 1000) {
throw new Error("PinP 2 view zoom must be between 100 and 1000%");
}
const value = Math.round(((percent - 100) / 900) * 90) + 10;
this.sendControlChange(CC_1.CC.UNDEFINED_25, value);
}
/**
* Sets the DSK source
* @param {InputSource} source - Source to use for DSK
*/
setDSKSource(source) {
this.sendControlChange(CC_1.CC.UNDEFINED_26, source);
}
/**
* Sets the DSK level
* @param {number} level - Level value (0-127)
* @throws {Error} If level is out of range
*/
setDSKLevel(level) {
if (level < 0 || level > 127) {
throw new Error("DSK level must be between 0 and 127");
}
this.sendControlChange(CC_1.CC.UNDEFINED_27, level);
}
/**
* Sets the DSK gain
* @param {number} gain - Gain value (0-127, converts to 0-255)
* @throws {Error} If gain is out of range
*/
setDSKGain(gain) {
if (gain < 0 || gain > 127) {
throw new Error("DSK gain must be between 0 and 127");
}
this.sendControlChange(CC_1.CC.DSK_GAIN, gain);
}
/**
* Sets the DSK mix level
* @param {number} level - Mix level value (0-127, converts to 0-255)
* @throws {Error} If level is out of range
*/
setDSKMixLevel(level) {
if (level < 0 || level > 127) {
throw new Error("DSK mix level must be between 0 and 127");
}
this.sendControlChange(CC_1.CC.DSK_MIX_LEVEL, level);
}
/**
* Sets the SPLIT/VFX A switch state
* @param {ButtonState} state - ON or OFF state, defaults to ON
*/
setSplitVfxASwitch(state = enums_1.ButtonState.ON) {
this.sendControlChange(CC_1.CC.SPLIT_VFX_A_SW, state);
}
/**
* Sets the SPLIT/VFX A effect type
* @param {SplitVfxType} type - Effect type to use
*/
setSplitVfxAType(type) {
this.sendControlChange(CC_1.CC.SPLIT_VFX_A_TYPE, type);
}
/**
* Sets the SPLIT/VFX B switch state
* @param {ButtonState} state - ON or OFF state, defaults to ON
*/
setSplitVfxBSwitch(state = enums_1.ButtonState.ON) {
this.sendControlChange(CC_1.CC.SPLIT_VFX_B_SW, state);
}
/**
* Sets the SPLIT/VFX B effect type
* @param {SplitVfxType} type - Effect type to use
*/
setSplitVfxBType(type) {
this.sendControlChange(CC_1.CC.SPLIT_VFX_B_TYPE, type);
}
/**
* Sets the OUTPUT FADE knob counter-clockwise value
* @param {number} value - Fade value (0-63)
* @throws {Error} If value is out of range
*/
setOutputFadeCCW(value) {
if (value < 0 || value > 63) {
throw new Error("OUTPUT FADE counter-clockwise value must be between 0 and 63");
}
this.sendControlChange(CC_1.CC.OUTPUT_FADE_CCW, value);
}
/**
* Sets the OUTPUT FADE knob clockwise value
* @param {number} value - Fade value (0-63)
* @throws {Error} If value is out of range
*/
setOutputFadeCW(value) {
if (value < 0 || value > 63) {
throw new Error("OUTPUT FADE clockwise value must be between 0 and 63");
}
this.sendControlChange(CC_1.CC.OUTPUT_FADE_CW, value);
}
/**
* Sets the audio input level for a specific input channel
* @param {number} input - Input channel (1-8)
* @param {number} level - Level value (0-127)
* @throws {Error} If input or level is out of range
*/
setAudioInputLevel(input, level) {
if (level < 0 || level > 127) {
throw new Error("Audio input level must be between 0 and 127");
}
if (input < 1 || input > 8) {
throw new Error("Input number must be between 1 and 8");
}
const ccNumber = CC_1.CC.AUDIO_LEVEL_INPUT_1 + (input - 1);
this.sendControlChange(ccNumber, level);
}
/**
* Sets the audio level for the AUDIO IN source
* @param {number} level - Level value (0-127)
* @throws {Error} If level is out of range
*/
setAudioInLevel(level) {
if (level < 0 || level > 127) {
throw new Error("Audio input level must be between 0 and 127");
}
this.sendControlChange(CC_1.CC.AUDIO_LEVEL_AUDIO_IN, level);
}
/**
* Sets the audio output level
* @param {number} level - Level value (0-127)
* @throws {Error} If level is out of range
*/
setAudioOutputLevel(level) {
if (level < 0 || level > 127) {
throw new Error("Audio output level must be between 0 and 127");
}
this.sendControlChange(CC_1.CC.AUDIO_OUTPUT_LEVEL, level);
}
/**
* Sets the CUT button state
* @param {ButtonState} state - ON or OFF state, defaults to ON
*/
setCutButton(state = enums_1.ButtonState.ON) {
this.sendControlChange(CC_1.CC.CUT_BUTTON, state);
}
/**
* Sets the AUTO button state
* @param {ButtonState} state - ON or OFF state, defaults to ON
*/
setAutoButton(state = enums_1.ButtonState.ON) {
this.sendControlChange(CC_1.CC.AUTO_BUTTON, state);
}
/**
* Triggers the H CUT I video switch
* Performs an immediate cut transition
*/
triggerHCutI() {
this.sendControlChange(CC_1.CC.H_CUT_I, 0x01); // Any value works, using 1
}
/**
* Triggers the H AUTO TAKE I video switch
* Performs an auto transition based on current settings
*/
triggerHAutoTakeI() {
this.sendControlChange(CC_1.CC.H_AUTO_TAKE_I, 0x01); // Any value works, using 1
}
/**
* Sets the audio input mute state for a specific input channel
* @param {number} input - Input channel (1-8)
* @param {ButtonState} state - Mute state (ON to mute, OFF to unmute)
* @throws {Error} If input is out of range
*/
setAudioInputMute(input, state) {
if (input < 1 || input > 8) {
throw new Error("Input number must be between 1 and 8");
}
const ccNumber = CC_1.CC.AUDIO_MUTE_INPUT_1 + (input - 1);
// For audio mutes, the value corresponds to the input number when ON
const value = state === enums_1.ButtonState.ON ? input : 0x00;
this.sendControlChange(ccNumber, value);
}
/**
* Sets the AUDIO IN mute state
* @param {ButtonState} state - Mute state (ON to mute, OFF to unmute)
*/
setAudioInMute(state) {
// For AUDIO IN mute, the value is 0x09 when ON
const value = state === enums_1.ButtonState.ON ? 0x09 : 0x00;
this.sendControlChange(CC_1.CC.AUDIO_MUTE_AUDIO_IN, value);
}
/**
* Sets the audio output mute state
* @param {ButtonState} state - Mute state (ON to mute, OFF to unmute)
*/
setAudioOutputMute(state) {
// For output mute, the value is 0x10 when ON
const value = state === enums_1.ButtonState.ON ? 0x10 : 0x00;
this.sendControlChange(CC_1.CC.AUDIO_MUTE_OUTPUT, value);
}
/**
* Closes the MIDI connection and releases resources
* Call this method when finished using the controller
*/
close() {
this.output.closePort();
}
}
exports.RolandV8HD = RolandV8HD;