Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 100 additions & 0 deletions MusicBeam/ArtNet.pde
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import java.net.DatagramSocket;
import java.net.DatagramPacket;
import java.util.Arrays;
import java.util.concurrent.atomic.AtomicInteger;

// Art-Net (DMX over Ethernet) effect selection.
// Reads a single DMX channel; its value selects the active effect by index
// (0 = Blackout / no effect, 1..N = the other effects). Opt-in via the
// "Art-Net (DMX)" toggle. Uses only the JDK, no extra libraries.

static final byte[] ART_NET_ID = "Art-Net\0".getBytes();
static final int ART_NET_OP_DMX = 0x5000; // ArtDMX opcode
static final int ART_NET_DMX_DATA = 18; // offset of the DMX data in an ArtDMX packet
static final int ART_NET_PORT = 6454; // Art-Net standard UDP port
static final int ART_NET_UNIVERSE = 0; // 15-bit universe to listen on
static final int ART_NET_CHANNEL = 1; // 1-indexed DMX channel = effect index

ArtNet artNet;

class ArtNet implements Runnable
{
final AtomicInteger pending = new AtomicInteger(Integer.MIN_VALUE);
volatile boolean running = false;
int lastValue = -1; // edge-trigger; only touched by the receive thread
DatagramSocket socket;

// Called from controlEvent() on the animation thread. Idempotent.
void setEnabled(boolean on)
{
if (on && !running) {
try {
socket = new DatagramSocket(ART_NET_PORT);
running = true;
lastValue = -1; // re-arm so the current console value is applied
new Thread(this).start();
println("Art-Net listening on UDP " + ART_NET_PORT + ", universe " + ART_NET_UNIVERSE + ", channel " + ART_NET_CHANNEL);
}
catch (Exception e) {
println("Art-Net failed to start: " + e.getMessage());
running = false;
socket = null;
}
} else if (!on && running) {
running = false;
if (socket != null)
socket.close();
socket = null;
}
}

// Receive loop; runs on its own thread and never touches ControlP5.
void run()
{
byte[] buf = new byte[530]; // 18 byte header + up to 512 DMX channels
DatagramPacket pkt = new DatagramPacket(buf, buf.length);
while (running) {
try {
pkt.setLength(buf.length); // receive() shrinks length; reset so large packets aren't truncated
socket.receive(pkt);
parse(pkt.getData(), pkt.getLength());
}
catch (Exception e) {
if (running)
println("Art-Net receive error: " + e.getMessage());
}
}
}

// Read the selected channel of an ArtDMX packet and hand its value to the
// animation thread. Anything else is ignored.
void parse(byte[] b, int len)
{
int i = ART_NET_DMX_DATA + ART_NET_CHANNEL - 1;
if (len > i
&& Arrays.equals(b, 0, ART_NET_ID.length, ART_NET_ID, 0, ART_NET_ID.length)
&& le16(b, 8) == ART_NET_OP_DMX
&& (le16(b, 14) & 0x7fff) == ART_NET_UNIVERSE)
{
int value = b[i] & 0xff;
if (value != lastValue) { // edge-trigger so the GUI stays usable between changes
lastValue = value;
pending.set(value);
}
}
}

// 16 bit little-endian read.
int le16(byte[] b, int i)
{
return (b[i] & 0xff) | ((b[i+1] & 0xff) << 8);
}

// Called once per frame from draw(); the only place ControlP5 is touched.
void update()
{
int v = pending.getAndSet(Integer.MIN_VALUE);
if (v >= 0 && v < effectArray.length)
effectArray[v].activate();
}
}
13 changes: 11 additions & 2 deletions MusicBeam/MusicBeam.pde
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ Effect[] effectArray;

DropdownList displays;

Toggle projectorToggle, randomToggle, blackoutOnSilenceToggle;
Toggle projectorToggle, randomToggle, blackoutOnSilenceToggle, artNetToggle;
Slider randomTimeSlider, beatDelaySlider, minLevelSlider;
Button nextButton;
RadioButton activeEffect, activeSetting;
Expand Down Expand Up @@ -93,6 +93,9 @@ void draw() {
effectArray[int(activeSetting.getValue())].showControls();
}

if (artNet != null)
artNet.update();

if (randomToggle!=null)
if (randomToggle.getState()&&randomTimer>=randomTimeSlider.getValue()*60)
{
Expand All @@ -115,6 +118,8 @@ void controlEvent(ControlEvent event)
{
if (event.getName()=="next")
nextRandom();
else if (event.getName()=="artNet" && artNet!=null)
artNet.setEnabled(artNetToggle.getState());
}

void drawBeatBoard()
Expand Down Expand Up @@ -222,6 +227,7 @@ void initControls()
PApplet.runSketch(args, stage);
stage.noLoop();
initEffects();
artNet = new ArtNet();
initSettings();
stage.loop();
}
Expand All @@ -237,9 +243,12 @@ void initRandomControls() {
nextButton = cp5.addButton("next").setSize(350, 45).setPosition(415, 60);
nextButton.getCaptionLabel().set("Next Effect").align(ControlP5.CENTER, ControlP5.CENTER);

blackoutOnSilenceToggle = cp5.addToggle("blackoutOnSilence").setSize(350, 45).setPosition(415, 110);
blackoutOnSilenceToggle = cp5.addToggle("blackoutOnSilence").setSize(172, 45).setPosition(415, 110);
blackoutOnSilenceToggle.getCaptionLabel().set("Blackout on Silence").align(ControlP5.CENTER, ControlP5.CENTER);

artNetToggle = cp5.addToggle("artNet").setSize(172, 45).setPosition(593, 110);
artNetToggle.getCaptionLabel().set("Art-Net (DMX)").align(ControlP5.CENTER, ControlP5.CENTER);

activeEffect = cp5.addRadioButton("activeEffects").setPosition(415, 170).setSize(250, 45).setItemsPerRow(1).setSpacingRow(5).setNoneSelectedAllowed(true);
activeSetting = cp5.addRadioButton("activeSettings").setPosition(720, 170).setSize(45, 45).setItemsPerRow(1).setSpacingRow(5);
}
Expand Down
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,17 @@ MusicBeam is written in [Processing][Processing] a JAVA based language that is e
The latest releases can be found at: [musicbeam.org](http://www.musicbeam.org)


## Art-Net (DMX) Control

The active effect can be selected over the network with Art-Net messages. The button **Art-Net (DMX)** starts a background thread that listens on UDP port 6454 for DMX frames and monitors channel 1 of universe 0. The channel value gets mapped to the available Effects. Channel value 0 maps to the Blackout effect.

It can be tried out using [`examples/artnet_test.py`](examples/artnet_test.py):

```console
python3 examples/artnet_test.py 3 # select effect 3
```


## Contributing

MusicBeam is writing in Processing a Java sublanguage. It is extremely easy to write even without prior coding experience.
Expand Down
38 changes: 38 additions & 0 deletions examples/artnet_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
#!/usr/bin/env python3
"""Test MusicBeam's Art-Net effect selection by sending ArtDMX frames.

Enable the "Art-Net (DMX)" toggle in MusicBeam first, then:

python3 artnet_test.py 3 select effect 3
python3 artnet_test.py 0 blackout / no effect
python3 artnet_test.py demo cycle through all effects, then blackout
python3 artnet_test.py 3 192.168.1.20 send to another machine
"""
import socket
import sys
import time

EFFECT_COUNT = 11 # effect indices 0..10, 0 = Blackout


def send(value, host):
packet = bytearray(b"Art-Net\x00")
packet += (0x5000).to_bytes(2, "little") # OpCode: ArtDMX
packet += (14).to_bytes(2, "big") # protocol version
packet += bytes([0, 0]) # sequence, physical
packet += bytes([0, 0]) # universe 0
packet += (1).to_bytes(2, "big") # one DMX channel
packet += bytes([value]) # channel 1 = effect index
socket.socket(socket.AF_INET, socket.SOCK_DGRAM).sendto(packet, (host, 6454))
print(f"sent effect index {value} to {host}")


if len(sys.argv) < 2:
sys.exit(__doc__)
host = sys.argv[2] if len(sys.argv) > 2 else "127.0.0.1"
if sys.argv[1] == "demo":
for value in list(range(EFFECT_COUNT)) + [0]:
send(value, host)
time.sleep(2)
else:
send(int(sys.argv[1]), host)
Loading