|
... |
|
|
|
// MARK: p0 create4LeadupDropHotcues() |
|
// Create a sequence of hotcues for DJ mixing: 6 leadup + drop + 1 outro |
|
// Each hotcue is positioned at specific beat intervals for smooth transitions |
|
var leadupCues = { |
|
"1": { control: "beatjump_256_backward", colour: 0x006838, label: "-265" }, // -265, dark green |
|
"2": { control: "beatjump_64_forward", colour: 0x006838, label: "-192" }, // -192, dark green |
|
"3": { control: "beatjump_64_forward", colour: 0x006838, label: "-128" }, // -128, dark green |
|
"4": { control: "beatjump_64_forward", colour: 0x006838, label: "-64" }, // -64, dark green |
|
"5": { control: "beatjump_32_forward", colour: 0xff8000, label: "-32" }, // -32, orange |
|
"6": { control: "beatjump_16_forward", colour: 0xff8000, label: "-16" }, // -16, orange |
|
"7": { control: "beatjump_16_forward", colour: 0xc71136, label: "DROP" }, // drop, red |
|
"8": { control: "beatjump_128_forward", colour: 0x5C3F97, label: "+128" }, // +128, purple |
|
} |
|
|
|
// Helper function to set hotcue label using new UTF-8 string controls |
|
LaunchpadProMK3.setHotcueLabel = function(group, hotcueNumber, labelText) { |
|
try { |
|
const labelControl = "hotcue_" + hotcueNumber + "_label_text"; |
|
engine.setStringValue(group, labelControl, labelText); |
|
print("LaunchpadProMK3: Set hotcue " + hotcueNumber + " label to: " + labelText); |
|
} catch (e) { |
|
print("LaunchpadProMK3: Error setting hotcue label: " + e); |
|
} |
|
}; |
|
|
|
// Helper function to get hotcue label using new UTF-8 string controls |
|
LaunchpadProMK3.getHotcueLabel = function(group, hotcueNumber) { |
|
try { |
|
const labelControl = "hotcue_" + hotcueNumber + "_label_text"; |
|
return engine.getStringValue(group, labelControl); |
|
} catch (e) { |
|
print("LaunchpadProMK3: Error getting hotcue label: " + e); |
|
return ""; |
|
} |
|
}; |
|
|
|
|
|
function isCloseEnough(array, num, precision = 2) { |
|
return array.some(n => Math.abs(n - num) < Math.pow(10, -precision)); |
|
} |
|
LaunchpadProMK3.create4LeadupDropHotcues = function (deck, value) { |
|
DEBUG(`create4LeadupDropHotcues: ## create hotcues ${C.Y} -192 -128 -64 -32 -16 ${C.R}drop ${C.RE}on ${C.O}${deck}`, C.G, 2); |
|
if (value === 0 || value === undefined) return 0; |
|
const group = `[Channel${deck}]`; |
|
|
|
// Check if deck has a track loaded before continuing |
|
const trackLoaded = engine.getValue(group, "track_loaded"); |
|
if (trackLoaded !== 1) { |
|
DEBUG(`create4LeadupDropHotcues: deck ${deck} has no track loaded (track_loaded: ${trackLoaded}), aborting`, C.R); |
|
return; |
|
} |
|
|
|
// what time is it right now? |
|
const now = Date.now() |
|
// original playPosition |
|
const originalPlayPosition = engine.getValue(group, "playposition"); |
|
// is now at least a second after the last time? |
|
if (now < (lastHotcueCreationTime + 1000)) { |
|
DEBUG("create4LeadupDropHotcues: DENIED " + lastHotcueCreationTime + " " + now, C.R); |
|
return |
|
} |
|
// record now as the new last time |
|
lastHotcueCreationTime = now |
|
// how long is the track in samples? |
|
const samplesTotal = engine.getValue(group, "track_samples"); |
|
const hotcuePositions = []; |
|
// get the first twenty hotcue positions, store in an array |
|
for (let h = 0; h <= 35; h++) { |
|
hotcuePositions[h] = engine.getValue(group, "hotcue_" + (+h + 1) + "_position") |
|
//if (hotcuePositions[h]) hotcueRightmost = h; |
|
} |
|
DEBUG("create4LeadupDropHotcues: hotcuePositions creation " + C.O + hotcuePositions); |
|
// for each of the controls in the object; |
|
DEBUG("create4LeadupDropHotcues: leadupCues " + C.O + JSON.stringify(leadupCues)); |
|
// Non-blocking sequence runner to avoid busy-wait sleeps |
|
const steps = Object.entries(leadupCues); |
|
let idx = 0; |
|
|
|
// Proactively cancel any in-flight sequence for this deck, then prep registries |
|
try { LaunchpadProMK3.cancelHotcueSequenceTimer(deck); } catch (e) {} |
|
LaunchpadProMK3._hotcueSequenceTimers = LaunchpadProMK3._hotcueSequenceTimers || {}; |
|
// Initialize per-deck cancellation token and store a local reference |
|
LaunchpadProMK3._hotcueSequenceTokens = LaunchpadProMK3._hotcueSequenceTokens || {}; |
|
const token = {}; |
|
LaunchpadProMK3._hotcueSequenceTokens[deck] = token; |
|
DEBUG("create4LeadupDropHotcues: token created for deck " + C.O + deck, C.G); |
|
|
|
const processStep = function () { |
|
// Abort early if sequence was canceled before this step |
|
if (LaunchpadProMK3._hotcueSequenceTokens && LaunchpadProMK3._hotcueSequenceTokens[deck] !== token) { |
|
DEBUG("create4LeadupDropHotcues: sequence canceled before step; abort", C.Y); |
|
return; |
|
} |
|
if (idx >= steps.length) { |
|
// Sequence finished: restore original play position |
|
try { engine.setValue(group, "playposition", originalPlayPosition); } catch (e) {} |
|
LaunchpadProMK3._hotcueSequenceTimers[deck] = null; |
|
if (LaunchpadProMK3._hotcueSequenceTokens) { LaunchpadProMK3._hotcueSequenceTokens[deck] = null; } |
|
return; |
|
} |
|
|
|
const number = steps[idx]; |
|
DEBUG(JSON.stringify(number)); |
|
DEBUG("number " + C.O + number[1].control); |
|
const control = number[1].control; |
|
const colour = number[1].colour; |
|
DEBUG(`control ${C.O}${control}${C.RE} colour ${C.O}#${colour.toString(16)}`, C.G, 1); |
|
|
|
// perform it - handle multiple controls separated by semicolon |
|
const controls = control.split(';'); |
|
DEBUG(`create4LeadupDropHotcues: controls ${C.O}${controls}`, C.G, 1); |
|
|
|
// Trigger control(s) immediately via script.triggerControl for proper on/off pulsing |
|
for (const singleControl of controls) { |
|
DEBUG(`create4LeadupDropHotcues: singleControl ${C.O}${singleControl}`, C.G, 1); |
|
const trimmedControl = singleControl.trim(); |
|
if (trimmedControl) { |
|
try { script.triggerControl(group, trimmedControl, 50); } catch (e) { try { engine.setValue(group, trimmedControl, 1); } catch (e2) {} } |
|
DEBUG(`create4LeadupDropHotcues: ${C.O}${trimmedControl}${C.RE}`, C.G, 1); |
|
} |
|
} |
|
|
|
// After a short delay, read position and set hotcue, then proceed |
|
let localTimerId = null; |
|
DEBUG("create4LeadupDropHotcues: scheduling timer; prev id " + C.O + (LaunchpadProMK3._hotcueSequenceTimers[deck] || "none") + C.RE + " deck " + C.O + deck, C.G); |
|
localTimerId = engine.beginTimer(100, function () { |
|
DEBUG("create4LeadupDropHotcues: timer fired id " + C.O + localTimerId + C.RE + " deck " + C.O + deck, C.G); |
|
// One-shot timer: just clear stored id |
|
LaunchpadProMK3._hotcueSequenceTimers[deck] = null; |
|
|
|
// Abort if sequence was canceled mid-tick |
|
if (LaunchpadProMK3._hotcueSequenceTokens && LaunchpadProMK3._hotcueSequenceTokens[deck] !== token) { |
|
DEBUG("create4LeadupDropHotcues: timer fired after cancel; abort", C.Y); |
|
try { engine.setValue(group, "playposition", originalPlayPosition); } catch (e) {} |
|
return; |
|
} |
|
|
|
// Validate deck is still loaded |
|
if (engine.getValue(group, "track_loaded") !== 1) { |
|
DEBUG("create4LeadupDropHotcues: deck unloaded mid-sequence; aborting", C.Y); |
|
return; |
|
} |
|
|
|
// how far through the track is the playhead now, between 0 and 1 |
|
const playPosition = engine.getValue(group, "playposition"); |
|
DEBUG("create4LeadupDropHotcues: playPosition " + C.O + playPosition); |
|
if (playPosition >= 0 && playPosition < 1) { |
|
// find the first unused hotcue |
|
DEBUG("create4LeadupDropHotcues: hotcuePositions mid " + C.O + hotcuePositions); |
|
// how many samples into the track right now? |
|
const samplesNow = samplesTotal * playPosition; |
|
DEBUG("create4LeadupDropHotcues: samplesNow " + C.O + samplesNow); |
|
// has this sample position got a hotcue already? |
|
if (!isCloseEnough(hotcuePositions, samplesNow, 3)) { |
|
const hotcueSpace = hotcuePositions.findIndex((hotcueSpaceFree) => hotcueSpaceFree === -1); |
|
DEBUG("create4LeadupDropHotcues: hotcueSpace " + C.O + hotcueSpace); |
|
// if there is no hotcue space then give up |
|
if (hotcueSpace !== -1) { |
|
const hotcueSpaceTitle = "hotcue_" + (hotcueSpace + 1); |
|
DEBUG("create4LeadupDropHotcues: hotcueSpaceTitle " + C.O + hotcueSpaceTitle); |
|
// create new hotcue |
|
engine.setValue(group, hotcueSpaceTitle + "_set", 1); |
|
// give that hotcue its colour |
|
engine.setValue(group, hotcueSpaceTitle + "_color", colour); |
|
// set the hotcue label using new string controls |
|
const hotcueNum = hotcueSpace + 1; |
|
const leadupCueKey = (idx + 1).toString(); |
|
if (leadupCues[leadupCueKey] && leadupCues[leadupCueKey].label) { |
|
LaunchpadProMK3.setHotcueLabel(group, hotcueNum, leadupCues[leadupCueKey].label); |
|
} |
|
// what is its pad? |
|
DEBUG("create4LeadupDropHotcues: LaunchpadProMK3.decks[deck].deckMainSliceStartIndex " + C.O + LaunchpadProMK3.decks[deck].deckMainSliceStartIndex); |
|
const pad = LaunchpadProMK3.decks[deck].deckMainSliceStartIndex + hotcueSpace; |
|
DEBUG("create4LeadupDropHotcues: pad " + C.O + pad); |
|
// add to undo list |
|
LaunchpadProMK3.lastHotcue.unshift([group, hotcueSpaceTitle, pad, deck, colour]); |
|
|
|
// add to existing check |
|
hotcuePositions[hotcueSpace] = samplesNow; |
|
DEBUG("create4LeadupDropHotcues: hotcuePositions end " + C.O + hotcuePositions, C.R, 0, 1); |
|
} else { |
|
DEBUG("create4LeadupDropHotcues: no hotcue space", C.R); |
|
} |
|
} |
|
} else { |
|
// out of bounds; skip |
|
DEBUG("create4LeadupDropHotcues: out-of-bounds playPosition; skipping", C.O); |
|
} |
|
|
|
// Next step |
|
idx += 1; |
|
// Check cancellation before proceeding to next step |
|
if (LaunchpadProMK3._hotcueSequenceTokens && LaunchpadProMK3._hotcueSequenceTokens[deck] !== token) { |
|
DEBUG("create4LeadupDropHotcues: canceled before next step; stop", C.Y); |
|
try { engine.setValue(group, "playposition", originalPlayPosition); } catch (e) {} |
|
return; |
|
} |
|
processStep(); |
|
}, true); |
|
LaunchpadProMK3._hotcueSequenceTimers[deck] = localTimerId; |
|
DEBUG("create4LeadupDropHotcues: scheduled id " + C.O + localTimerId + C.RE + " deck " + C.O + deck, C.G); |
|
}; |
|
|
|
processStep(); |
|
|
|
//for (let X = hotcueRightmost; X <= 19; X++) { |
|
// LaunchpadProMK3.sleep(25); |
|
|
|
DEBUG("create4LeadupDropHotcues: # end multi hotcue creation", C.R, 0, 2); |
|
}; |
|
|
|
... |