{:tracks-using-playlists? nil, :carabiner {:port 17000, :latency 1, :bars true}, :window-positions {:carabiner [1577 195], :waveform-detail-1 [482 143 600 200], :triggers [32 70 771 173], :overlay [1424 208], :nrepl [284 95], :player-status [1347 35]}, :send-status? false, :triggers [{:bar true, :start "Start", :channel 1, :start-stop false, :note 127, :gear false, :stop true, :expressions {:beat ";; We can only run when the TimeFinder is running.\n(when track-time-reached\n ;; Record that this beat has been handled, and the Tracked Update expression can ignore it.\n (swap! qlc-cue-times assoc-in [:sent device-number] beat-number)\n ;; Send the MIDI cues, if any, falling on this beat.\n (send-qlc-cues-near-time track-time-reached device-number globals))", :deactivation ";; Clear record of last beat handled since the player is stopping.\n;; If we restart in this same location, we should evaluate cues again.\n(swap! qlc-cue-times update :sent dissoc device-number)", :activation "", :tracked ";; We can only run when playing and the TimeFinder is running.\n(when (and playing? track-time-reached)\n ;; Do nothing if the current beat has already been handled.\n (when (not= beat-number (get-in @qlc-cue-times [:sent device-number]))\n (swap! qlc-cue-times assoc-in [:sent device-number] beat-number) ; Note this beat's been handled.\n ;; Press the QLC+ buttons, if any, for the point where playback began.\n ;; We assume playback began at the start of the current beat.\n (let [grid (.getLatestBeatGridFor beatgrid-finder device-number)\n started (.getTimeWithinTrack grid beat-number)]\n (send-qlc-cues-near-time started device-number globals))))"}, :comment "Cue-driven QLC+ Button Presser", :outputs #beat_link_trigger.util.MidiChoice{:full-name "CoreMIDI4J - IAC Driver Bus 1"}, :send true, :players #beat_link_trigger.util.PlayerChoice{:number 0}, :enabled "Always", :message "Custom"}], :overlay {}, :nrepl {:cider true}, :expressions {:setup "(.addTrackMetadataListener metadata-finder qlc-cue-indexer)\n(swap! globals assoc :http (http/create-client))", :shutdown "(.removeTrackMetadataListener metadata-finder qlc-cue-indexer)\n(when-let [ws (:ws @globals)]\n (.close ws))\n(.close (:http @globals))", :shared "(defonce ^{:doc \"Holds a map from player number to a map of cue times for that player.\n The cue time maps are indexed by track position (in milliseconds),\n and their values are sets of QLC+ button IDs to press when we reach\n a beat that is within 50 milliseconds of that time. This map is\n built by `find-qlc-cues` below whenever the track metadata for a\n player changes.\"}\n qlc-cue-times (atom {}))\n\n(defn find-qlc-web-socket ;; <1>\n \"Checks to see if there is already an open QLC+ web socket; if so,\n returns it. Otherwise, tries to create one, logging an error and\n returning `nil` if it fails.\"\n [globals] ;; <2>\n (let [ws (:qlc-ws @globals)] ;; <3>\n (or ws\n (try ; The web socket isn't already open, try creating it.\n (let [ws (http/websocket\n (:http @globals) \"ws://127.0.0.1:9999/qlcplusWS\" ;; <4>\n :close (fn [_ws code reason] ;; <5>\n (timbre/info \"QLC+ web socket closed, code\" code\n \"reason\" reason)\n (swap! globals dissoc :qlc-ws))\n :error (fn [_ws error]\n (timbre/error \"QLC+ web socket error:\" error))\n :text (fn [_ws message]\n (timbre/info \"QLC+ web socket received message:\"\n message)))]\n (swap! globals assoc :qlc-ws ws) ;; <6>\n ws)\n (catch Exception _e\n (timbre/error \"Unable to open web socket connection to QLC+\"))))))\n\n(defn send-qlc-message ;; <7>\n \"Sends a web socket message to QLC+, opening the web socket connection\n if it isn't already. Needs to be given the globals, so it can look\n up the connection, or use the async http client to create it if\n needed.\"\n [globals message]\n (when-let [ws (find-qlc-web-socket globals)]\n (http/send ws :text message))) ;; <8>\n\n(defn set-qlc-widget-value\n \"Formats and sends a message to QLC+ telling it to set a specific\n virtual console widget to a particular value. If the widget is a\n button and the value is 255, QLC+ will act like that button has\n been pressed.\"\n [globals widget-id value]\n (send-qlc-message globals (str widget-id \"|\" value)))\n\n(defn find-qlc-cues\n \"Scans all the cues and loops found in the supplied track metadata\n looking for any that contain the string QLC: followed immediately\n by a number. Returns a map whose keys are the track time at which\n each such cue or loop begins, and whose values are sets of the\n number that was found in the cue name(s) that started at that time.\n If there is no track metadata, or it has no cue list, returns\n `nil`.\"\n [^TrackMetadata md]\n (when md\n (when-let [cue-list (.getCueList md)]\n (reduce (fn [result cue]\n (if-let [[_ ids] (re-find #\"QLC:(\\d+(,\\d+)*)\" (.-comment cue))] ; Cue name matches.\n (update result (.-cueTime cue) (fnil clojure.set/union #{})\n (clojure.string/split ids #\",\"))\n result))\n {} (.-entries cue-list)))))\n\n(def qlc-cue-indexer\n \"Responds to the coming and going of track metadata, and updates our\n list of cue-defined beats on which QLC+ button presses need to be sent.\"\n (reify org.deepsymmetry.beatlink.data.TrackMetadataListener\n (metadataChanged [this md-update]\n (swap! qlc-cue-times assoc (.player md-update) (find-qlc-cues (.metadata md-update))))))\n\n(defn send-qlc-cues-near-time\n \"Finds all QLC cues close enough to the specified time for the\n specified device and sends the corresponding button press messages\n to the QLC+ web socket, which we can look up through the globals.\"\n [time device-number globals]\n (doseq [[_ ids] (filter (fn [[cue-time]] (> 50 (Math/abs (- time cue-time))))\n (get @qlc-cue-times device-number))]\n (doseq [widget-id ids] ; Send button presses for each id specified by one of the cues we reached.\n (set-qlc-widget-value globals widget-id 255))))", :online "(triggers/show-player-status)"}, :beat-link-trigger-version "7.0.0-SNAPSHOT-147-0x32e1-DIRTY", :my-settings {"autoPlayMode" "SINGLE", "ejectLoadLock" "ON", "quantizeBeatValue" "BEAT", "sync" "ON", "masterTempo" "ON"}}