{:beat-link-trigger-version "0.6.1-SNAPSHOT-98-0xbfa6", :triggers [{:bar true, :start "Start", :channel 1, :start-stop false, :note 127, :gear false, :stop true, :expressions {:setup ";; Create a reusable `TrackPositionListener` that can be assigned from player\n;; to player as the trigger changes its focus (for example if it is following\n;; the master player). The listener will cause the SMPTE linear timecode output\n;; by the OrangePi-DMX to follow the position and speed of the current player\n;; when the trigger is enabled.\n(let [reversed (atom false) ; Tracks whether the timecode is running backwards.\n listener (reify org.deepsymmetry.beatlink.data.TrackPositionListener\n (movementChanged [this position-update]\n (let [osc-client (:orangepi-dmx @globals) ; Look up the OSC connection.\n prefix (:orangepi-prefix @globals)] ; And the OSC command prefix.\n (if position-update\n (do ; We have actual position data, so update the time code.\n ;; Update the current playback pitch. The OrangePi-DMX uses values\n ;; that are 1.0 less than those used by Beat Link.\n (osc/osc-send osc-client (str prefix \"pitch\") (float (- (.pitch position-update) 1.0)))\n (when (not= @reversed (.reverse position-update))\n ;; Send a direction command if the player direction differs from how the timecode is running.\n (reset! reversed (.reverse position-update))\n (osc/osc-send osc-client (str prefix \"direction/\" (if @reversed \"backward\" \"forward\"))))\n ;; Jump timecode to the new position, and reflect whether we are playing or stopped.\n (let [command (if (and (.playing position-update)\n (pos? (.pitch position-update)))\n \"start/\" ; We are playing at a non-zero pitch.\n \"goto/\") ; We are stopped, or pitch is zero, effectively the same.\n timecode (ms-to-timecode-segments (.milliseconds position-update) (:orangepi-frame-rate @globals) 0)]\n (osc/osc-send osc-client (str prefix command timecode))))\n (osc/osc-send osc-client (str prefix \"stop\"))))))] ; We have no player data, so stop the timecode.\n (swap! locals assoc :orangepi-listener listener)) ; Put it where the tracked update expression can find it.", :tracked "(if trigger-active?\n (let [current-player (:orangepi-tracking @locals)]\n ;; The trigger is active, so make sure we are tracking the correct player for timecode.\n (when (not= current-player device-number)\n (.addTrackPositionListener time-finder device-number (:orangepi-listener @locals)) ; Tie OrangePi-DMX to new player.\n (swap! locals assoc :orangepi-tracking device-number))) ; And record that we are now tracking this player. \n (when-let [current-player (:orangepi-tracking @locals)]\n ;; We are tracking a player but are no longer enabled. Remove the listener and stop the timecode.\n (.removeTrackPositionListener time-finder (:orangepi-listener @locals))\n (osc/osc-send (:orangepi-dmx @globals) (str (:orangepi-prefix @globals) \"/tc/stop\"))\n (swap! locals dissoc :orangepi-tracking))) ; Record that we are no longer tracking any player.", :activation ";; Nothing to actually do here, the track position listener does it all."}, :comment "Tie OrangePI SMPTE to on-air master player.", :outputs #beat_link_trigger.util.MidiChoice{:full-name "CoreMIDI4J - IAC Driver Bus 1"}, :send true, :players #beat_link_trigger.util.PlayerChoice{:number 0}, :channel-label "Channel:", :enabled "On-Air", :message "Custom"}], :expressions {:shared "(def orangepi-dmx-hostname\n \"The hostname of the OrangePi-DMX device, if that can be changed.\"\n \"allwinner_2C028E\")\n\n(defn ms-to-timecode-segments\n \"Given a track position in milliseconds, a frame rate (per second),\n and an offset in hours, returns a string in the format used by the\n OrangePi-DMX SMPTE generator, `/hh/mm/ss/ff`.\"\n [position frame-rate offset]\n (let [hours (.toHours java.util.concurrent.TimeUnit/MILLISECONDS position)\n remainder (- position (.toMillis java.util.concurrent.TimeUnit/HOURS hours))\n minutes (.toMinutes java.util.concurrent.TimeUnit/MILLISECONDS remainder)\n remainder (- remainder (.toMillis java.util.concurrent.TimeUnit/MINUTES minutes))\n seconds (.toSeconds java.util.concurrent.TimeUnit/MILLISECONDS remainder)\n remainder (- remainder (.toMillis java.util.concurrent.TimeUnit/SECONDS seconds))]\n (format \"%02d/%02d/%02d/%02d\" (+ hours offset) minutes seconds (quot (* frame-rate remainder) 1000))))", :online "(let [client (osc/osc-client \"192.168.1.225\" 8000) ; Edit this to match your OrangePi configuration.\n frame-rate 30 ; And edit this to match your desired frame rate.\n prefix (str \"/\" orangepi-dmx-hostname \"/tc/\")] ; All OSC commands start with this string.\n (swap! globals assoc :orangepi-dmx client) ; Put this where the trigger can find it.\n (osc/osc-send client (str prefix \"rate/set/\" frame-rate)) ; Tell the SMPTE generator the frame rate.\n (swap! globals assoc :orangepi-frame-rate frame-rate) ; The trigger needs to know this too.\n (swap! globals assoc :orangepi-prefix prefix) ; Save the trigger the trouble of recalculating this.\n (osc/osc-send client (str prefix \"direction/forward\"))) ; And start out assuming we will be playing forward.", :offline "(osc/osc-close (:orangepi-dmx @globals))\n(swap! globals dissoc :orangepi-dmx)"}, :tracks-using-playlists? nil, :request-metadata? true, :window-positions {}, :send-status? false, :carabiner {:port 17000, :latency 1, :bars true}}