Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix Effects #5

Merged
merged 6 commits into from
Oct 28, 2024
Merged
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
72 changes: 23 additions & 49 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,29 +1,35 @@
# ⭕️ Halo: ShowControl for DJSL
<p align="center">
<img width="350" height="350" src="/_docs/halo_logo.png">
</p>

I wanted to perform a live show powered by Ableton.
Halo is a real-time lighting console built for the console. It's lighting engine supports beat synchronized effects
using Ableton Link and SMPTE Timecode.

TUI-based lighting console.
## Features

Halo is a lighting console built for the console.
* Control multiple groups of fixtures.
* Synchronize with an Ableton Link session.

Halo is designed for shows without a lighting operating where playback should happen automatically in sync with the music.
## Requirements

## Sunsetting Halo
* Network interface for Art-Net output
* Ableton Link compatible device/software (optional)

I'm discontinuing this project for now, for the following reasons:
## Usage

* I really want DJSL lights to sync to the music.
* It will be really hard to do this in Halo using only OSC cues.
* You really need MIDI or LTC (like other consoles) to do this properly so events fire right on cue.
* Sending OSC commands every split second may likely get out of sync.
* You don't want to invest months of time building a tracking lighting console that runs concurrent animations.
```bash
cargo run --release
```

## Features
## Getting Started

Halo doesn't use a programmer, editor or GUI. It is essentially only a playback engine. You define shows ahead of time
using the show file format.

* Control multiple groups of lights
* Control lights
* Lights running in different times
* Nature isn't one fade. It's lots of things coming and going.
## Planned Features

- [ ] LTC/SMPTE Timecode
- [ ] Show file live reloading

## Concepts

Expand Down Expand Up @@ -65,42 +71,10 @@ No programmer. No editor. Halo is only a playback engine. You do the programming
* Go
* Jump to Cue

## Milestones

1. **Milestone 1:** Playback scenes and sequences.
2. **Milestone 2:** Surfaces for input and output. Ableton Link.
3. **Milestone 3:** Venues. Camera Fixtures (for helping record in Capture)

## Requirements

* OLA

## Usage

Start OLAD in debug mode in another terminal window:

```bash
$ olad -l 3
$ open <YOUR_IP_ADDRESS:9090>
```

Then start Halo:

```bash
$ ./halo
```

## References

* https://opensource.com/article/17/5/open-source-lighting
* https://dev.to/davidsbond/golang-reverse-engineering-an-akai-mpd26-using-gousb-3b49
* https://corylulu.github.io/VDocs/NodeIODMX.html?itm=174
* https://github.com/node-dmx/dmx
* https://github.com/qmsk/dmx

## Libraries

* https://github.com/google/gousb: For reading/writing MIDI control surfaces.
* https://github.com/trimmer-io/go-timecode
* https://github.com/gomidi/midi
* https://github.com/fogleman/ease: Easing Functions
Binary file added _docs/halo_logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
70 changes: 15 additions & 55 deletions oldrust/src/artnet.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
use artnet_protocol::{ArtCommand, Output};
use log::{debug, trace};
use serde::{Deserialize, Serialize};
use std::{
io::Error,
net::{SocketAddr, ToSocketAddrs, UdpSocket},
time::{Duration, SystemTime},
time::SystemTime,
};

// The IP of the device running this SW
Expand All @@ -18,104 +15,67 @@ pub struct ArtNet {
socket: UdpSocket,
destination: SocketAddr,
channels: Vec<u8>,
update_interval: Duration,
last_sent: Option<SystemTime>,
mode: ArtNetMode,
}

#[derive(Serialize, Deserialize, Clone)]
#[serde(rename_all = "camelCase")]
#[derive(Clone, Debug)]
pub enum ArtNetMode {
Broadcast,
/// Specify from (interface) + to (destination) addresses
Unicast(SocketAddr, SocketAddr),
}

impl ArtNet {
pub fn new(mode: ArtNetMode, update_frequency: u64) -> Result<Self, anyhow::Error> {
pub fn new(mode: ArtNetMode) -> Result<Self, anyhow::Error> {
let channels = Vec::with_capacity(CHANNELS_PER_UNIVERSE as usize);

let update_interval = Duration::from_secs_f32(1.0 / update_frequency as f32);

match mode {
ArtNetMode::Broadcast => {
let socket = UdpSocket::bind((String::from("0.0.0.0"), 6455))?;
let broadcast_addr = ("255.255.255.255", 6454).to_socket_addrs()?.next().unwrap();
let broadcast_addr = (ART_NET_CONTROLLER_IP, 6454)
.to_socket_addrs()?
.next()
.unwrap();
socket.set_broadcast(true).unwrap();
debug!("Broadcast mode set up OK");
//debug!("Broadcast mode set up OK");
Ok(ArtNet {
socket,
destination: broadcast_addr,
channels,
update_interval,
last_sent: None,
mode: mode.clone(),
})
}

ArtNetMode::Unicast(src, destination) => {
debug!(
"Will connect from interface {} to destination {}",
&src, &destination
);
// debug!(
// "Will connect from interface {} to destination {}",
// &src, &destination
// );
let socket = UdpSocket::bind(src)?;

socket.set_broadcast(false)?;
Ok(ArtNet {
socket,
destination,
channels,
update_interval,
last_sent: None,
mode: mode.clone(),
})
}
}

let socket = UdpSocket::bind((DEVICE_IP, 6454))?;

let destination = (ART_NET_CONTROLLER_IP, 6454)
.to_socket_addrs()
.unwrap()
.next()
.unwrap();
socket.set_broadcast(true).unwrap();

Ok(Self {
socket: socket,
destination: destination,
})
}

pub fn send_data(&self, dmx: Vec<u8>) {
//ArtCommand::Output::BigEndian::from(dmx.clone())

// let length = ArtCommand::Output::BigEndianLength((dmx.len()));

pub fn send_data(&self, universe: u8, dmx: Vec<u8>) {
let command = ArtCommand::Output(Output {
// length: dmx.len() as u16,
//data: dmx.into(),
port_address: universe.into(),
data: dmx.into(),
..Output::default()
});

// let command = ArtCommand::Output(Output {
// //length: dmx.len() as u16,
// length: 5,
// data: dmx.clone().into(),
// ..Output::default() //..Output::default()
// });

// command
// .write_to_buffer()
// .map_err(|err| ArtnetError::Art(err))?;

// self.socket.send_to(&bytes, self.broadcast_addr).unwrap();

let bytes = command.write_to_buffer().unwrap();
self.socket.send_to(&bytes, self.broadcast_addr).unwrap();

//let bytes = command.into_buffer().unwrap();
//self.socket.send_to(&bytes, self.broadcast_addr).unwrap();
self.socket.send_to(&bytes, self.destination).unwrap();
}
}
79 changes: 39 additions & 40 deletions oldrust/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,18 +30,17 @@ fn main() {

let mut stdout = stdout();

let art_net_controller = artnet::ArtNet::new().unwrap();
//art_net_controller.s

let socket = UdpSocket::bind(("0.0.0.0", 6455)).unwrap();
let broadcast_addr = ("255.255.255.255", 6454)
.to_socket_addrs()
.unwrap()
.next()
.unwrap();
socket.set_broadcast(true).unwrap();
let buff = ArtCommand::Poll(Poll::default()).write_to_buffer().unwrap();
socket.send_to(&buff, &broadcast_addr).unwrap();
let art_net_controller = artnet::ArtNet::new(artnet::ArtNetMode::Broadcast).unwrap();

// let socket = UdpSocket::bind(("0.0.0.0", 6455)).unwrap();
// let broadcast_addr = ("255.255.255.255", 6454)
// .to_socket_addrs()
// .unwrap()
// .next()
// .unwrap();
// socket.set_broadcast(true).unwrap();
// let buff = ArtCommand::Poll(Poll::default()).write_to_buffer().unwrap();
// socket.send_to(&buff, &broadcast_addr).unwrap();

loop {
let delta = Instant::now() - last_instant; // Is this timer accurate enough?
Expand All @@ -55,34 +54,34 @@ fn main() {

println!("Calling socket.recv_from");
let mut buffer = [0u8; 1024];
let (length, addr) = socket.recv_from(&mut buffer).unwrap();
let command = ArtCommand::from_buffer(&buffer[..length]).unwrap();

println!("Received {:?}", command);
match command {
ArtCommand::Poll(poll) => {
// This will most likely be our own poll request, as this is broadcast to all devices on the network
println!("Recv poll {:?}", poll);
}
ArtCommand::PollReply(reply) => {
println!("Reply {:?}", reply);

// This is an ArtNet node on the network. We can send commands to it like this:
art_net_controller.send_data(vec![0xff; 512]);

let command = ArtCommand::Output(Output {
// length: dmx.len() as u16,
//data: dmx.into(),
port_address: 1.into(),
data: vec![0xff; 512].into(),
..Output::default()
});

let bytes = command.write_to_buffer().unwrap();
socket.send_to(&bytes, &addr).unwrap();
}
_ => {}
}
//let (length, addr) = socket.recv_from(&mut buffer).unwrap();
//let command = ArtCommand::from_buffer(&buffer[..length]).unwrap();

//println!("Received {:?}", command);
// match command {
// ArtCommand::Poll(poll) => {
// // This will most likely be our own poll request, as this is broadcast to all devices on the network
// println!("Recv poll {:?}", poll);
// }
// ArtCommand::PollReply(reply) => {
// println!("Reply {:?}", reply);

// // This is an ArtNet node on the network. We can send commands to it like this:
// art_net_controller.send_data(1.into(), vec![0xff; 512]);

// let command = ArtCommand::Output(Output {
// // length: dmx.len() as u16,
// //data: dmx.into(),
// port_address: 1.into(),
// data: vec![0xff; 512].into(),
// ..Output::default()
// });

// let bytes = command.write_to_buffer().unwrap();
// socket.send_to(&bytes, &addr).unwrap();
// }
// _ => {}
// }

print!(
"\rRunning Frq: {: >3}Hz Peers:{} BPM: {} ",
Expand Down
55 changes: 55 additions & 0 deletions src/ableton_link.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,59 @@ impl State {
pub fn commit_app_state(&mut self) {
self.link.commit_app_session_state(&self.session_state);
}

pub fn get_clock_state(&mut self) -> ClockState {
self.capture_app_state();
let time = self.link.clock_micros();
let enabled = match self.link.is_enabled() {
true => "yes",
false => "no ",
}
.to_string();
let num_peers = self.link.num_peers();
let start_stop = match self.link.is_start_stop_sync_enabled() {
true => "yes",
false => "no ",
}
.to_string();
let playing = match self.session_state.is_playing() {
true => "[playing]",
false => "[stopped]",
}
.to_string();
let tempo = self.session_state.tempo();
let beats = self.session_state.beat_at_time(time, self.quantum);
let phase = self.session_state.phase_at_time(time, self.quantum);
let mut metro = String::with_capacity(self.quantum as usize);
for i in 0..self.quantum as usize {
if i > phase as usize {
metro.push('O');
} else {
metro.push('X');
}
}

ClockState {
enabled,
num_peers,
start_stop,
playing,
tempo,
beats,
phase,
metro,
}
}
}

#[derive(Clone, Debug)]
pub struct ClockState {
pub enabled: String,
pub num_peers: u64,
pub start_stop: String,
pub playing: String,
pub tempo: f64,
pub beats: f64,
pub phase: f64,
pub metro: String,
}
4 changes: 2 additions & 2 deletions src/artnet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,10 +68,10 @@ impl ArtNet {
}
}

pub fn send_data(&self, dmx: Vec<u8>) {
pub fn send_data(&self, universe: u8, dmx: Vec<u8>) {
let command = ArtCommand::Output(Output {
// length: dmx.len() as u16,
//data: dmx.into(),
port_address: universe.into(),
data: dmx.into(),
..Output::default()
});
Expand Down
Loading