All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.deepsymmetry.beatlink.BeatFinder Maven / Gradle / Ivy

There is a newer version: 7.4.0
Show newest version
package org.deepsymmetry.beatlink;

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicReference;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * 

Watches for devices to report new beats by broadcasting beat packets on port 50001, * and passes them on to registered listeners. When players are actively playing music, * they send beat packets at the start of each beat which, in addition to announcing the * start of the beat, provide information about where the beat falls within a measure of * music (assuming that the track was properly configured in rekordbox, and is in 4/4 time), * the current BPM of the track being played, and the current player pitch adjustment, from * which the actual effective BPM can be calculated.

* *

When players are stopped, they do not send beat packets, but the mixer continues sending them * at the last BPM reported by the master player, so it acts as the most reliable synchronization * source. The mixer does not make any effort to keep its notion of measures (down beats) consistent * with any player, however. So systems which want to stay in sync with measures as well as beats * will want to use the {@link VirtualCdj} to maintain awareness of which player is the master player.

* * This class also receives special sync and on-air control messages which are sent to the same port as * beat packets. If the {@link VirtualCdj} is sending status packets, it needs to be notified about these * so it can properly update its sync and on-air state if the mixer tells it to, so it will ensure the * {@code BeatFinder} is running whenever it is sending status updates. * * @author James Elliott */ @SuppressWarnings("WeakerAccess") public class BeatFinder extends LifecycleParticipant { private static final Logger logger = LoggerFactory.getLogger(BeatFinder.class); /** * The port to which devices broadcast beat messages. */ public static final int BEAT_PORT = 50001; /** * The socket used to listen for beat packets while we are active. */ private final AtomicReference socket = new AtomicReference(null); /** * Check whether we are presently listening for beat packets. * * @return {@code true} if our socket is open and monitoring for DJ Link beat packets on the network */ public boolean isRunning() { return socket.get() != null; } /** * Helper method to check that we got the right size packet. * * @param packet a packet that has been received * @param expectedLength the number of bytes we expect it to contain * @param name the description of the packet in case we need to report issues with the length * * @return {@code true} if enough bytes were received to process the packet */ private boolean isPacketLongEnough(DatagramPacket packet, int expectedLength, String name) { final int length = packet.getLength(); if (length < expectedLength) { logger.warn("Ignoring too-short " + name + " packet; expecting " + expectedLength + " bytes and got " + length + "."); return false; } if (length > expectedLength) { logger.warn("Processing too-long " + name + " packet; expecting " + expectedLength + " bytes and got " + length + "."); } return true; } /** * Start listening for beat announcements and sync commands. If already listening, has no effect. * * @throws SocketException if the socket to listen on port 50001 cannot be created */ public synchronized void start() throws SocketException { if (!isRunning()) { socket.set(new DatagramSocket(BEAT_PORT)); deliverLifecycleAnnouncement(logger, true); final byte[] buffer = new byte[512]; final DatagramPacket packet = new DatagramPacket(buffer, buffer.length); Thread receiver = new Thread(null, new Runnable() { @Override public void run() { boolean received; while (isRunning()) { try { socket.get().receive(packet); received = !DeviceFinder.getInstance().isAddressIgnored(packet.getAddress()); } catch (IOException e) { // Don't log a warning if the exception was due to the socket closing at shutdown. if (isRunning()) { // We did not expect to have a problem; log a warning and shut down. logger.warn("Problem reading from DeviceAnnouncement socket, stopping", e); stop(); } received = false; } try { if (received) { final Util.PacketType kind = Util.validateHeader(packet, BEAT_PORT); if (kind != null) { switch (kind) { case BEAT: if (isPacketLongEnough(packet, 96, "beat")) { deliverBeat(new Beat(packet)); } break; case CHANNELS_ON_AIR: if (isPacketLongEnough(packet, 0x2d, "channels on-air")) { byte[] data = packet.getData(); Set audibleChannels = new TreeSet(); for (int channel = 1; channel <= 4; channel++) { if (data[0x23 + channel] != 0) { audibleChannels.add(channel); } } audibleChannels = Collections.unmodifiableSet(audibleChannels); deliverOnAirUpdate(audibleChannels); } break; case SYNC_CONTROL: if (isPacketLongEnough(packet, 0x2c, "sync control command")) { deliverSyncCommand(packet.getData()[0x2b]); } break; case MASTER_HANDOFF_REQUEST: if (isPacketLongEnough(packet, 0x28, "tempo master handoff request")) { deliverMasterYieldCommand(packet.getData()[0x21]); } break; case MASTER_HANDOFF_RESPONSE: if (isPacketLongEnough(packet, 0x2c, "tempo master handoff response")) { byte[] data = packet.getData(); deliverMasterYieldResponse(data[0x21], data[0x2b] == 1); } break; case FADER_START_COMMAND: if (isPacketLongEnough(packet, 0x28, "fader start command")) { byte[] data = packet.getData(); Set playersToStart = new TreeSet(); Set playersToStop = new TreeSet(); for (int channel = 1; channel <= 4; channel++) { switch (data[0x23 + channel]) { case 0: playersToStart.add(channel); break; case 1: playersToStop.add(channel); break; case 2: // Leave this player alone break; default: logger.warn("Ignoring unrecognized fader start command, " + data[0x23 + channel] + ", for channel " + channel); } } playersToStart = Collections.unmodifiableSet(playersToStart); playersToStop = Collections.unmodifiableSet(playersToStop); deliverFaderStartCommand(playersToStart, playersToStop); } default: logger.warn("Ignoring packet received on beat port with unexpected type: " + kind); } } } } catch (Throwable t) { logger.warn("Problem processing beat packet", t); } } } }, "beat-link BeatFinder receiver"); receiver.setDaemon(true); receiver.setPriority(Thread.MAX_PRIORITY); receiver.start(); } } /** * Stop listening for beats. */ public synchronized void stop() { if (isRunning()) { socket.get().close(); socket.set(null); deliverLifecycleAnnouncement(logger, false); } } /** * Keeps track of the registered beat listeners. */ private final Set beatListeners = Collections.newSetFromMap(new ConcurrentHashMap()); /** *

Adds the specified beat listener to receive beat announcements when DJ Link devices broadcast * them on the network. If {@code listener} is {@code null} or already present in the list * of registered listeners, no exception is thrown and no action is performed.

* *

To reduce latency, beat announcements are delivered to listeners directly on the thread that is receiving them * them from the network, so if you want to interact with user interface objects in listener methods, you need to use * javax.swing.SwingUtilities.invokeLater(Runnable) * to do so on the Event Dispatch Thread. * * Even if you are not interacting with user interface objects, any code in the listener method * must finish quickly, or it will add latency for other listeners, and beat announcements will back up. * If you want to perform lengthy processing of any sort, do so on another thread.

* * @param listener the beat listener to add */ public void addBeatListener(BeatListener listener) { if (listener != null) { beatListeners.add(listener); } } /** * Removes the specified beat listener so that it no longer receives beat announcements when * DJ Link devices broadcast them to the network. If {@code listener} is {@code null} or not present * in the list of registered listeners, no exception is thrown and no action is performed. * * @param listener the beat listener to remove */ public void removeBeatListener(BeatListener listener) { if (listener != null) { beatListeners.remove(listener); } } /** * Get the set of beat listeners that are currently registered. * * @return the currently registered beat listeners */ public Set getBeatListeners() { // Make a copy so callers get an immutable snapshot of the current state. return Collections.unmodifiableSet(new HashSet(beatListeners)); } /** * Send a beat announcement to all registered listeners, and let the {@link VirtualCdj} know about it in case it * needs to notify the master beat listeners. * * @param beat the message announcing the new beat */ private void deliverBeat(final Beat beat) { VirtualCdj.getInstance().processBeat(beat); for (final BeatListener listener : getBeatListeners()) { try { listener.newBeat(beat); } catch (Throwable t) { logger.warn("Problem delivering beat announcement to listener", t); } } } /** * Keeps track of the registered sync command listeners. */ private final Set syncListeners = Collections.newSetFromMap(new ConcurrentHashMap()); /** *

Adds the specified sync command listener to receive sync commands when DJ Link devices send * them to Beat Link. If {@code listener} is {@code null} or already present in the list * of registered listeners, no exception is thrown and no action is performed.

* *

To reduce latency, sync commands are delivered to listeners directly on the thread that is receiving them * them from the network, so if you want to interact with user interface objects in listener methods, you need to use * javax.swing.SwingUtilities.invokeLater(Runnable) * to do so on the Event Dispatch Thread. * * Even if you are not interacting with user interface objects, any code in the listener method * must finish quickly, or it will add latency for other listeners, and beat announcements will back up. * If you want to perform lengthy processing of any sort, do so on another thread.

* * @param listener the sync listener to add */ public void addSyncListener(SyncListener listener) { if (listener != null) { syncListeners.add(listener); } } /** * Removes the specified sync listener so that it no longer receives sync commands when * DJ Link devices send them to Beat Link. If {@code listener} is {@code null} or not present * in the list of registered listeners, no exception is thrown and no action is performed. * * @param listener the sync listener to remove */ public void removeSyncListener(SyncListener listener) { if (listener != null) { syncListeners.remove(listener); } } /** * Get the set of sync command listeners that are currently registered. * * @return the currently registered sync listeners */ public Set getSyncListeners() { // Make a copy so callers get an immutable snapshot of the current state. return Collections.unmodifiableSet(new HashSet(syncListeners)); } /** * Send a sync command to all registered listeners. * * @param command the byte which identifies the type of sync command we received */ private void deliverSyncCommand(byte command) { for (final SyncListener listener : getSyncListeners()) { try { switch (command) { case 0x01: listener.becomeMaster(); case 0x10: listener.setSyncMode(true); break; case 0x20: listener.setSyncMode(false); break; } } catch (Throwable t) { logger.warn("Problem delivering sync command to listener", t); } } } /** * Keeps track of the registered master handoff command listeners. */ private final Set masterHandoffListeners = Collections.newSetFromMap(new ConcurrentHashMap()); /** *

Adds the specified master handoff listener to receive tempo master handoff commands when DJ Link devices send * them to Beat Link. If {@code listener} is {@code null} or already present in the list * of registered listeners, no exception is thrown and no action is performed.

* *

To reduce latency, handoff commands are delivered to listeners directly on the thread that is receiving them * them from the network, so if you want to interact with user interface objects in listener methods, you need to use * javax.swing.SwingUtilities.invokeLater(Runnable) * to do so on the Event Dispatch Thread. * * Even if you are not interacting with user interface objects, any code in the listener method * must finish quickly, or it will add latency for other listeners, and beat announcements will back up. * If you want to perform lengthy processing of any sort, do so on another thread.

* * @param listener the tempo master handoff listener to add */ public void addMasterHandoffListener(MasterHandoffListener listener) { if (listener != null) { masterHandoffListeners.add(listener); } } /** * Removes the specified master handoff listener so that it no longer receives tempo master handoff commands when * DJ Link devices send them to Beat Link. If {@code listener} is {@code null} or not present * in the list of registered listeners, no exception is thrown and no action is performed. * * @param listener the tempo master handoff listener to remove */ public void removeMasterHandoffListener(MasterHandoffListener listener) { if (listener != null) { masterHandoffListeners.remove(listener); } } /** * Get the set of master handoff command listeners that are currently registered. * * @return the currently registered tempo master handoff command listeners */ public Set getMasterHandoffListeners() { // Make a copy so callers get an immutable snapshot of the current state. return Collections.unmodifiableSet(new HashSet(masterHandoffListeners)); } /** * Send a master handoff yield command to all registered listeners. * * @param toPlayer the device number to which we are being instructed to yield the tempo master role */ private void deliverMasterYieldCommand(int toPlayer) { for (final MasterHandoffListener listener : getMasterHandoffListeners()) { try { listener.yieldMasterTo(toPlayer); } catch (Throwable t) { logger.warn("Problem delivering master yield command to listener", t); } } } /** * Send a master handoff yield response to all registered listeners. * * @param fromPlayer the device number that is responding to our request that it yield the tempo master role to us * @param yielded will be {@code true} if we should now be the tempo master */ private void deliverMasterYieldResponse(int fromPlayer, boolean yielded) { for (final MasterHandoffListener listener : getMasterHandoffListeners()) { try { listener.yieldResponse(fromPlayer, yielded); } catch (Throwable t) { logger.warn("Problem delivering master yield response to listener", t); } } } /** * Keeps track of the registered on-air listeners. */ private final Set onAirListeners = Collections.newSetFromMap(new ConcurrentHashMap()); /** *

Adds the specified on-air listener to receive channel on-air updates when the mixer broadcasts * them on the network. If {@code listener} is {@code null} or already present in the list * of registered listeners, no exception is thrown and no action is performed.

* *

To reduce latency, on-air updates are delivered to listeners directly on the thread that is receiving them * them from the network, so if you want to interact with user interface objects in listener methods, you need to use * javax.swing.SwingUtilities.invokeLater(Runnable) * to do so on the Event Dispatch Thread. * * Even if you are not interacting with user interface objects, any code in the listener method * must finish quickly, or it will add latency for other listeners, and beat announcements will back up. * If you want to perform lengthy processing of any sort, do so on another thread.

* * @param listener the on-air listener to add */ public void addOnAirListener(OnAirListener listener) { if (listener != null) { onAirListeners.add(listener); } } /** * Removes the specified on-air listener so that it no longer receives channel on-air updates when * the mixer broadcasts them to the network. If {@code listener} is {@code null} or not present * in the list of registered listeners, no exception is thrown and no action is performed. * * @param listener the on-air listener to remove */ public void removeOnAirListener(OnAirListener listener) { if (listener != null) { onAirListeners.remove(listener); } } /** * Get the set of on-air listeners that are currently registered. * * @return the currently registered on-air listeners */ public Set getOnAirListeners() { // Make a copy so callers get an immutable snapshot of the current state. return Collections.unmodifiableSet(new HashSet(onAirListeners)); } /** * Send a channels on-air update to all registered listeners. * * @param audibleChannels holds the device numbers of all channels that can currently be heard in the mixer output */ private void deliverOnAirUpdate(Set audibleChannels) { for (final OnAirListener listener : getOnAirListeners()) { try { listener.channelsOnAir(audibleChannels); } catch (Throwable t) { logger.warn("Problem delivering channels on-air update to listener", t); } } } /** * Keeps track of the registered fader start listeners. */ private final Set faderStartListeners = Collections.newSetFromMap(new ConcurrentHashMap()); /** *

Adds the specified fader start listener to receive fader start commands when the mixer broadcasts * them on the network. If {@code listener} is {@code null} or already present in the list * of registered listeners, no exception is thrown and no action is performed.

* *

To reduce latency, fader start commands are delivered to listeners directly on the thread that is receiving them * them from the network, so if you want to interact with user interface objects in listener methods, you need to use * javax.swing.SwingUtilities.invokeLater(Runnable) * to do so on the Event Dispatch Thread. * * Even if you are not interacting with user interface objects, any code in the listener method * must finish quickly, or it will add latency for other listeners, and beat announcements will back up. * If you want to perform lengthy processing of any sort, do so on another thread.

* * @param listener the fader start listener to add */ public void addFaderStartListener(FaderStartListener listener) { if (listener != null) { faderStartListeners.add(listener); } } /** * Removes the specified fader start listener so that it no longer receives fader start commands when * the mixer broadcasts them to the network. If {@code listener} is {@code null} or not present * in the list of registered listeners, no exception is thrown and no action is performed. * * @param listener the fader start listener to remove */ public void removeFaderStartListener(FaderStartListener listener) { if (listener != null) { faderStartListeners.remove(listener); } } /** * Get the set of fader start listeners that are currently registered. * * @return the currently registered fader start listeners */ public Set getFaderStartListeners() { // Make a copy so callers get an immutable snapshot of the current state. return Collections.unmodifiableSet(new HashSet(faderStartListeners)); } /** * Send a fader start command to all registered listeners. * * @param playersToStart contains the device numbers of all players that should start playing * @param playersToStop contains the device numbers of all players that should stop playing */ private void deliverFaderStartCommand(Set playersToStart, Set playersToStop) { for (final FaderStartListener listener : getFaderStartListeners()) { try { listener.fadersChanged(playersToStart, playersToStop); } catch (Throwable t) { logger.warn("Problem delivering fader start command to listener", t); } } } /** * Holds the singleton instance of this class. */ private static final BeatFinder ourInstance = new BeatFinder(); /** * Get the singleton instance of this class. * * @return the only instance of this class which exists. */ public static BeatFinder getInstance() { return ourInstance; } /** * Prevent direct instantiation. */ private BeatFinder() { // Nothing to do. } @Override public String toString() { return "BeatFinder[active:" + isRunning() + "]"; } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy