├── .gitignore ├── .gitmodules ├── CMakeLists.txt ├── README.md ├── compile.sh ├── dirtyudp.h ├── main.cpp └── protocol.txt /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | build/ 3 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "link"] 2 | path = link 3 | url = https://github.com/Ableton/link.git 4 | [submodule "oscpack"] 5 | path = oscpack 6 | url = https://github.com/MariadeAnton/oscpack.git 7 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.0) 2 | project(TidaLink) 3 | include(${CMAKE_SOURCE_DIR}/link/AbletonLinkConfig.cmake) 4 | #add_subdirectory(${CMAKE_SOURCE_DIR}/oscpack) 5 | set(INCLUDES 6 | ${INCLUDES} 7 | ${CMAKE_SOURCE_DIR}/dirtyudp.h 8 | ${CMAKE_SOURCE_DIR}/oscpack/osc/OscOutboundPacketStream.h 9 | ${CMAKE_SOURCE_DIR}/oscpack/osc/OscReceivedElements.h 10 | ${CMAKE_SOURCE_DIR}/oscpack/osc/OscPrintReceivedElements.h 11 | ${CMAKE_SOURCE_DIR}/oscpack/osc/OscTypes.h 12 | #${CMAKE_SOURCE_DIR}/oscpack/osc/OscPacketListener.h 13 | #${CMAKE_SOURCE_DIR}/oscpack/ip/UdpSocket.h 14 | #${CMAKE_SOURCE_DIR}/oscpack/ip/IpEndpointName.h 15 | #${CMAKE_SOURCE_DIR}/oscpack/ip/NetworkingUtils.h 16 | ) 17 | set(SOURCES 18 | ${SOURCES} 19 | ${CMAKE_SOURCE_DIR}/main.cpp 20 | ${CMAKE_SOURCE_DIR}/oscpack/osc/OscOutboundPacketStream.cpp 21 | ${CMAKE_SOURCE_DIR}/oscpack/osc/OscReceivedElements.cpp 22 | ${CMAKE_SOURCE_DIR}/oscpack/osc/OscPrintReceivedElements.cpp 23 | ${CMAKE_SOURCE_DIR}/oscpack/osc/OscTypes.cpp 24 | #${CMAKE_SOURCE_DIR}/oscpack/ip/UdpSocket.cpp 25 | #${CMAKE_SOURCE_DIR}/oscpack/ip/NetworkingUtils.cpp 26 | ) 27 | add_executable(TidaLink ${SOURCES} ${INCLUDES}) 28 | find_package (Threads) 29 | target_link_libraries(TidaLink Ableton::Link ${CMAKE_THREAD_LIBS_INIT}) 30 | #target_link_libraries(TidaLink oscpack) 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TidaLink 2 | Standalone server for automatically syncing [TidalCycles](http://tidalcycles.org) and [Ableton Link](https://www.ableton.com/en/link/). 3 | 4 | Quoting from: 5 | http://lurk.org/r/post/7AM6Qt7DlvJtorbIuVLMFA 6 | 7 | You'll need `cmake` as well as a c++ compiler etc. If you're on a mac, 8 | you will probably have to at least do this, which in turn requires 9 | homebrew (get it from https://brew.sh/ if you don't already have it): 10 | 11 | `brew install cmake` 12 | 13 | Then install TidaLink with this: 14 | 15 | ``` 16 | mkdir src 17 | cd src 18 | git clone https://github.com/tidalcycles/TidaLink.git 19 | cd TidaLink 20 | /bin/bash compile.sh 21 | ``` 22 | 23 | To run TidaLink, do this: 24 | 25 | ``` 26 | cd build 27 | ./TidaLink 28 | ``` 29 | 30 | Restart your editor and run some tidal code. It should now be in 31 | sync with any link compatible app that's running on the same network. 32 | You might need to change the cps in TidaLink (with 'w' or 'e') or in 33 | another link app before the sync kicks in. 34 | 35 | You might also need to make a slight adjustment to get it bang on. You 36 | can do this with the `nudger` function which isn't yet available in 37 | atom. You can make it available with this command [add and evaluate this line in your tidal source file]: 38 | 39 | ``` 40 | (cps, nudger, getNow) <- cpsUtils' 41 | ``` 42 | 43 | then you can do slight adjustments with e.g.: 44 | 45 | ``` 46 | nudger (-0.02) 47 | ``` 48 | 49 | to bring it back two hundredths of a second. 50 | 51 | 52 | -------------------------------------------------------------------------------- /compile.sh: -------------------------------------------------------------------------------- 1 | cp ./oscpack/ip/posix/UdpSocket.cpp ./oscpack/ip/UdpSocket.cpp 2 | cp ./oscpack/ip/posix/NetworkingUtils.cpp ./oscpack/ip/NetworkingUtils.cpp 3 | cd ./link 4 | git submodule update --init --recursive 5 | cmake . 6 | cd .. 7 | rm -rf build 8 | mkdir -p build 9 | cd build 10 | cmake .. 11 | make 12 | -------------------------------------------------------------------------------- /dirtyudp.h: -------------------------------------------------------------------------------- 1 | #ifndef DIRTYUDP_HPP 2 | #define DIRTYUDP_HPP 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include "oscpack/osc/OscOutboundPacketStream.h" 15 | #include "oscpack/osc/OscReceivedElements.h" 16 | #include "oscpack/osc/OscPrintReceivedElements.h" 17 | void error(const char* message) { 18 | perror(message); 19 | exit(0); 20 | } 21 | class UdpSender { 22 | private: 23 | int socketFile; 24 | int port; 25 | int bufferSize; 26 | char* buffer; 27 | const char* host; 28 | struct sockaddr_in serverAddress; 29 | struct hostent* server; 30 | int broadcast = 1; 31 | public: 32 | UdpSender(const char* host, int port, int bufferSize) { 33 | this->host = host; 34 | this->port = port; 35 | this->bufferSize = bufferSize; 36 | this->buffer = (char*)calloc(this->bufferSize, sizeof(char)); 37 | // create socket 38 | this->socketFile = socket(AF_INET, SOCK_DGRAM, 0); 39 | if(this->socketFile < 0) error("ERROR: Could not open socket\n"); 40 | //setsockopt(this->socketFile, SOL_SOCKET, SO_BROADCAST, &broadcast, sizeof(broadcast)); 41 | // get DNS entry for server 42 | this->server = gethostbyname(this->host); 43 | if(this->server == NULL) { 44 | fprintf(stderr, "ERROR: Could not find host %s\n", this->host); 45 | exit(0); 46 | } 47 | // construct server address 48 | bzero((char*)&this->serverAddress, sizeof(this->serverAddress)); 49 | this->serverAddress.sin_family = AF_INET; 50 | bcopy((char*)this->server->h_addr, 51 | (char*)&this->serverAddress.sin_addr.s_addr, 52 | this->server->h_length); 53 | this->serverAddress.sin_port = htons(this->port); 54 | } 55 | void Send(char* packet, int packetSize) { 56 | int tempFile; 57 | if(packetSize > this->bufferSize) 58 | error("ERROR: Packet length exceeds buffer size\n"); 59 | bzero(this->buffer, this->bufferSize*sizeof(char)); 60 | bcopy(packet, this->buffer, packetSize); 61 | tempFile = sendto(this->socketFile, 62 | this->buffer, 63 | packetSize, 64 | 0, 65 | (struct sockaddr *)&this->serverAddress, 66 | sizeof(this->serverAddress)); 67 | if(tempFile < 0) 68 | error("ERROR: There was an issue sending UDP"); 69 | return; 70 | } 71 | }; 72 | class UdpReceiver { 73 | private: 74 | int socketFile; 75 | int port; 76 | int bufferSize; 77 | int socketOptions; 78 | char* buffer; 79 | char* clientIP; 80 | struct sockaddr_in serverAddress; 81 | struct sockaddr_in clientAddress; 82 | struct hostent* client; 83 | socklen_t clientLength; 84 | public: 85 | UdpReceiver(int port, int bufferSize) { 86 | this->port = port; 87 | this->bufferSize = bufferSize; 88 | this->buffer = (char*)calloc(this->bufferSize, sizeof(char)); 89 | // create socket 90 | this->socketFile = socket(AF_INET, SOCK_DGRAM, 0); 91 | if(this->socketFile < 0) error("ERROR: Could not open socket\n"); 92 | // speedhax 93 | this->socketOptions = 1; 94 | setsockopt( this->socketFile, 95 | SOL_SOCKET, 96 | SO_REUSEADDR, 97 | (const void*)&this->socketOptions, 98 | sizeof(int)); 99 | // construct server address 100 | bzero((char*)&this->serverAddress, sizeof(this->serverAddress)); 101 | this->serverAddress.sin_family = AF_INET; 102 | this->serverAddress.sin_addr.s_addr = htonl(INADDR_ANY); 103 | this->serverAddress.sin_port = htons((unsigned short)this->port); 104 | // bind socket 105 | if(bind(this->socketFile, 106 | (struct sockaddr *)&this->serverAddress, 107 | sizeof(this->serverAddress)) < 0) 108 | error("ERROR: There was an issue binding the socket"); 109 | this->clientLength = sizeof(this->clientAddress); 110 | } 111 | void Loop(void (*udpCallback)(char* packet, int packetSize, void* data), void* data) { 112 | int packetSize; 113 | bzero(this->buffer, this->bufferSize*sizeof(char)); 114 | packetSize = recvfrom( this->socketFile, 115 | this->buffer, 116 | this->bufferSize, 117 | 0, 118 | (struct sockaddr *)&this->clientAddress, 119 | &this->clientLength); 120 | if(packetSize < 0) 121 | error("ERROR: There was a problem receiving data from a client"); 122 | this->client = gethostbyaddr( (const char*)&this->clientAddress.sin_addr.s_addr, 123 | sizeof(this->clientAddress.sin_addr.s_addr), 124 | AF_INET); 125 | if(this->client == NULL) 126 | error("ERROR: Error getting client host info"); 127 | this->clientIP = inet_ntoa(this->clientAddress.sin_addr); 128 | if(this->clientIP == NULL) 129 | error("ERROR: Error getting client IP."); 130 | // ----------------------------- // 131 | // --- Some old info logging --- // 132 | // ----------------------------- // 133 | /* 134 | printf("Received datagram from %s (%s):\n", 135 | this->client->h_name, 136 | this->clientIP); 137 | for(int i=0; ibuffer)&&ibufferSize; i++) 138 | printf("%c", this->buffer[i]); 139 | printf("\n"); 140 | */ 141 | udpCallback(this->buffer, packetSize, data); 142 | } 143 | }; 144 | #endif 145 | -------------------------------------------------------------------------------- /main.cpp: -------------------------------------------------------------------------------- 1 | /* Copyright 2016, ableton AG, Berlin. All rights reserved. 2 | * 3 | * This program is free software: you can redistribute it and/or modify 4 | * it under the terms of the GNU General Public License as published by 5 | * the Free Software Foundation, either version 2 of the License, or 6 | * (at your option) any later version. 7 | * 8 | * This program is distributed in the hope that it will be useful, 9 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | * GNU General Public License for more details. 12 | * 13 | * You should have received a copy of the GNU General Public License 14 | * along with this program. If not, see . 15 | * 16 | * If you would like to incorporate Link into a proprietary software application, 17 | * please contact . 18 | */ 19 | 20 | #include 21 | #include 22 | #include "oscpack/osc/OscOutboundPacketStream.h" 23 | #include "oscpack/osc/OscReceivedElements.h" 24 | #include "oscpack/osc/OscPrintReceivedElements.h" 25 | #include "dirtyudp.h" 26 | #include 27 | #include 28 | #include 29 | #include 30 | #include 31 | #include 32 | #if defined(LINK_PLATFORM_UNIX) 33 | #include 34 | #include 35 | #include 36 | #include 37 | #include 38 | #endif 39 | 40 | 41 | // taken from https://stackoverflow.com/a/3911102 42 | extern "C" void my_function_to_handle_aborts(int signal_number) 43 | { 44 | /*Your code goes here. You can output debugging info. 45 | If you return from this function, and it was called 46 | because abort() was called, your program will exit or crash anyway 47 | (with a dialog box on Windows). 48 | */ 49 | std::cout << "aborted with signal number " << signal_number << std::endl; 50 | exit(1); 51 | } 52 | 53 | // #define NTP_UT_EPOCH_DIFF ((70 * 365 + 17) * 24 * 60 * 60) 54 | #define OUTPUT_BUFFER_SIZE 1024 55 | 56 | // referencing this to make sure everything is working properly 57 | osc::OutboundPacketStream* stream; 58 | 59 | /* 60 | class UdpBroadcastSocket : public UdpSocket{ 61 | public: 62 | UdpBroadcastSocket( const IpEndpointName& remoteEndpoint ) { 63 | SetEnableBroadcast(true); 64 | Connect( remoteEndpoint ); 65 | } 66 | }; 67 | */ 68 | UdpSender* sender; 69 | UdpReceiver* receiver; 70 | 71 | void sendTempo(ableton::Link& link, double quantum, double latency, long int beatOffset) { 72 | auto timeline = link.captureAppTimeline(); 73 | const auto time = link.clock().micros(); 74 | const auto tempo = timeline.tempo(); 75 | const auto beats = (timeline.beatAtTime(time, quantum) - beatOffset); 76 | const auto phase = timeline.phaseAtTime(time, quantum); 77 | const auto cycle = beats / quantum; 78 | const double cps = (timeline.tempo() / quantum) / 60; 79 | const auto t = std::chrono::microseconds(time).count(); 80 | static long diff = 0; 81 | static double last_cps = -1; 82 | char buffer[OUTPUT_BUFFER_SIZE]; 83 | if (diff == 0) { 84 | unsigned long milliseconds_since_epoch = 85 | std::chrono::duration_cast 86 | (std::chrono::system_clock::now().time_since_epoch()).count(); 87 | // POSIX is millis and Link is micros.. Not sure if that `+500` helps 88 | diff = ((milliseconds_since_epoch*1000 + 500) - t); 89 | } 90 | double timetag_ut = ((double) (t + diff)) / ((double) 1000000); 91 | timetag_ut -= latency; 92 | int sec = floor(timetag_ut); 93 | int usec = floor(1000000 * (timetag_ut - sec)); 94 | osc::OutboundPacketStream p( buffer, OUTPUT_BUFFER_SIZE ); 95 | std::cout << "\nnew cps: " << cps << " | last cps: " << last_cps << "\n"; 96 | last_cps = cps; 97 | p << osc::BeginMessage( "/tempo" ) 98 | << sec << usec 99 | << (float) cycle << (float) cps << "True" << osc::EndMessage; 100 | //s.Send( p.Data(), p.Size() ); 101 | sender->Send((char *)p.Data(), p.Size()); 102 | } 103 | 104 | 105 | struct State 106 | { 107 | std::atomic running; 108 | ableton::Link link; 109 | double quantum; 110 | double latency = 0.4; 111 | long int beatOffset = 0; 112 | State() 113 | : running(true) 114 | , link(120.) 115 | { 116 | link.enable(true); 117 | link.setTempoCallback([this](const double bpm) { 118 | sendTempo(std::ref(link), quantum, latency, beatOffset); 119 | }); 120 | 121 | quantum=4; 122 | } 123 | }; 124 | 125 | void disableBufferedInput() 126 | { 127 | #if defined(LINK_PLATFORM_UNIX) 128 | termios t; 129 | tcgetattr(STDIN_FILENO, &t); 130 | t.c_lflag &= ~ICANON; 131 | tcsetattr(STDIN_FILENO, TCSANOW, &t); 132 | #endif 133 | } 134 | 135 | void enableBufferedInput() 136 | { 137 | #if defined(LINK_PLATFORM_UNIX) 138 | termios t; 139 | tcgetattr(STDIN_FILENO, &t); 140 | t.c_lflag |= ICANON; 141 | tcsetattr(STDIN_FILENO, TCSANOW, &t); 142 | #endif 143 | } 144 | 145 | void clearLine() 146 | { 147 | std::cout << " \r" << std::flush; 148 | std::cout.fill(' '); 149 | } 150 | 151 | void printHelp() 152 | { 153 | std::cout << std::endl << " < T I D A L I N K >" << std::endl << std::endl; 154 | std::cout << "usage:" << std::endl; 155 | std::cout << " start / stop: space" << std::endl; 156 | std::cout << " decrease / increase tempo: w / e" << std::endl; 157 | std::cout << " decrease / increase quantum: r / t" << std::endl; 158 | std::cout << " quit: q" << std::endl << std::endl; 159 | } 160 | 161 | void printState(const std::chrono::microseconds time, 162 | const ableton::Link::Timeline timeline, 163 | const std::size_t numPeers, 164 | const double quantum, 165 | State& state) 166 | { 167 | const auto beats = timeline.beatAtTime(time, quantum); 168 | const auto phase = timeline.phaseAtTime(time, quantum); 169 | const auto cycle = (beats - state.beatOffset) / quantum; 170 | const double cps = (timeline.tempo() / quantum) / 60; 171 | const auto t = std::chrono::microseconds(time).count(); 172 | static long diff = 0; 173 | static double last_cps = -1; 174 | //const auto time = state.link.clock().micros(); 175 | 176 | if (diff == 0) { 177 | unsigned long milliseconds_since_epoch = 178 | std::chrono::duration_cast 179 | (std::chrono::system_clock::now().time_since_epoch()).count(); 180 | // POSIX is millis and Link is micros.. Not sure if that `+500` helps 181 | diff = ((milliseconds_since_epoch*1000 + 500) - t); 182 | } 183 | double timetag_ut = ((double) (t + diff)) / ((double) 1000000); 184 | timetag_ut -= state.latency; 185 | int sec = floor(timetag_ut); 186 | int usec = floor(1000000 * (timetag_ut - sec)); 187 | 188 | std::cout << std::defaultfloat << "peers: " << numPeers << " | " 189 | << "quantum: " << quantum << " | " 190 | << "tempo: " << timeline.tempo() << " | " << std::fixed << "beats: " << (beats - state.beatOffset) 191 | //<< " | sec: " << sec 192 | // << " | usec: " << usec 193 | << " | lat: " << state.latency << " | "; 194 | for (int i = 0; i < ceil(quantum); ++i) 195 | { 196 | if (i < phase) 197 | { 198 | std::cout << 'X'; 199 | } 200 | else 201 | { 202 | std::cout << 'O'; 203 | } 204 | } 205 | clearLine(); 206 | } 207 | 208 | void input(State& state) 209 | { 210 | char in; 211 | 212 | #if defined(LINK_PLATFORM_WINDOWS) 213 | HANDLE stdinHandle = GetStdHandle(STD_INPUT_HANDLE); 214 | DWORD numCharsRead; 215 | INPUT_RECORD inputRecord; 216 | do 217 | { 218 | ReadConsoleInput(stdinHandle, &inputRecord, 1, &numCharsRead); 219 | } while ((inputRecord.EventType != KEY_EVENT) || inputRecord.Event.KeyEvent.bKeyDown); 220 | in = inputRecord.Event.KeyEvent.uChar.AsciiChar; 221 | #elif defined(LINK_PLATFORM_UNIX) 222 | in = std::cin.get(); 223 | #endif 224 | auto timeLine = state.link.captureAppTimeline(); 225 | const auto tempo = timeLine.tempo(); 226 | 227 | std::chrono::microseconds updateAt = state.link.clock().micros(); 228 | 229 | switch (in) 230 | { 231 | case 'q': 232 | state.running = false; 233 | clearLine(); 234 | return; 235 | case 'w': 236 | timeLine.setTempo(tempo-1,updateAt); 237 | break; 238 | case 'e': 239 | timeLine.setTempo(tempo+1,updateAt); 240 | break; 241 | case 'r': 242 | state.quantum -= 1; 243 | break; 244 | case 't': 245 | state.quantum += 1; 246 | break; 247 | default: 248 | clearLine(); 249 | printHelp(); 250 | break; 251 | } 252 | state.link.commitAppTimeline(timeLine); 253 | input(state); 254 | } 255 | 256 | void oscRecvHandler(char* packet, int packetSize, void* data) { 257 | State* state = (State*)data; 258 | const auto time = state->link.clock().micros(); 259 | auto timeline = state->link.captureAppTimeline(); 260 | osc::ReceivedPacket* oscPacket = new osc::ReceivedPacket(packet, packetSize); 261 | if(oscPacket->IsMessage()) { 262 | osc::ReceivedMessage* message = new osc::ReceivedMessage(*oscPacket); 263 | if(std::strcmp(message->AddressPattern(), "/cps") == 0) { 264 | auto timeLine = state->link.captureAppTimeline(); 265 | const auto tempo = timeLine.tempo(); 266 | // --- // 267 | osc::ReceivedMessage::const_iterator arg = message->ArgumentsBegin(); 268 | float cps = (arg++)->AsFloat(); 269 | if(arg != message->ArgumentsEnd()) 270 | throw osc::ExcessArgumentException(); 271 | // --- // 272 | std::chrono::microseconds updateAt = state->link.clock().micros(); 273 | timeLine.setTempo((double)(cps*state->quantum*60), updateAt); 274 | state->link.commitAppTimeline(timeLine); 275 | } 276 | if(std::strcmp(message->AddressPattern(), "/latency") == 0) { 277 | auto timeLine = state->link.captureAppTimeline(); 278 | const auto tempo = timeLine.tempo(); 279 | // --- // 280 | osc::ReceivedMessage::const_iterator arg = message->ArgumentsBegin(); 281 | state->latency = (arg++)->AsFloat(); 282 | if(arg != message->ArgumentsEnd()) 283 | throw osc::ExcessArgumentException(); 284 | sendTempo(std::ref(state->link), state->quantum, state->latency, state->beatOffset); 285 | } 286 | if(std::strcmp(message->AddressPattern(), "/nudge") == 0) { 287 | auto timeLine = state->link.captureAppTimeline(); 288 | const auto tempo = timeLine.tempo(); 289 | // --- // 290 | osc::ReceivedMessage::const_iterator arg = message->ArgumentsBegin(); 291 | state->latency += (arg++)->AsFloat(); 292 | if(arg != message->ArgumentsEnd()) 293 | throw osc::ExcessArgumentException(); 294 | sendTempo(std::ref(state->link), state->quantum, state->latency, state->beatOffset); 295 | std::cout << "\n\n" << "got nudge, latency now set to " << state->latency << "\n\n"; 296 | } 297 | if(std::strcmp(message->AddressPattern(), "/resetbeat") == 0) { 298 | auto timeLine = state->link.captureAppTimeline(); 299 | const auto tempo = timeLine.tempo(); 300 | auto beats = timeLine.beatAtTime(time, state->quantum); 301 | 302 | osc::ReceivedMessage::const_iterator arg = message->ArgumentsBegin(); 303 | state->beatOffset = floor(beats+0.5); 304 | state->beatOffset -= (arg++)->AsInt32(); 305 | if(arg != message->ArgumentsEnd()) 306 | throw osc::ExcessArgumentException(); 307 | sendTempo(std::ref(state->link), state->quantum, state->latency, state->beatOffset); 308 | std::cout << "\n\ngot resetbeat\n\n"; 309 | } 310 | if(std::strcmp(message->AddressPattern(), "/ping") == 0) { 311 | auto timeLine = state->link.captureAppTimeline(); 312 | const auto tempo = timeLine.tempo(); 313 | // --- // 314 | sendTempo(std::ref(state->link), state->quantum, state->latency, state->beatOffset); 315 | } 316 | } 317 | } 318 | 319 | void oscRecvThreadFunc(State& state) { 320 | receiver->Loop(oscRecvHandler, (void*)(&state)); 321 | oscRecvThreadFunc(state); 322 | } 323 | 324 | int main(int, char**) 325 | { 326 | /*Do this early in your program's initialization */ 327 | signal(SIGABRT, &my_function_to_handle_aborts); 328 | 329 | sender = new UdpSender("127.0.0.1", 6043, OUTPUT_BUFFER_SIZE); 330 | receiver = new UdpReceiver(6042, OUTPUT_BUFFER_SIZE); 331 | State state; 332 | printHelp(); 333 | std::thread thread(input, std::ref(state)); 334 | std::thread oscRecvThread(oscRecvThreadFunc, std::ref(state)); 335 | disableBufferedInput(); 336 | 337 | while (state.running) 338 | { 339 | const auto time = state.link.clock().micros(); 340 | auto timeline = state.link.captureAppTimeline(); 341 | printState(time, timeline, state.link.numPeers(), state.quantum, state); 342 | std::this_thread::sleep_for(std::chrono::milliseconds(10)); 343 | } 344 | 345 | enableBufferedInput(); 346 | thread.join(); 347 | return 0; 348 | } 349 | 350 | // ------------------------------------------ // 351 | // --- DIRTYUDP OSCPACK INTEGRATION TESTS --- // 352 | // ------------------------------------------ // 353 | #define BUFFERSIZE 4096 354 | void udpHandler(char* packet, int packetSize, void* data) { 355 | std::cout << osc::ReceivedPacket(packet, packetSize); 356 | } 357 | int main_udprcv(int argc, char** argv) { 358 | UdpReceiver* receiver = new UdpReceiver(7000, BUFFERSIZE); 359 | while(1) receiver->Loop(udpHandler, NULL); 360 | } 361 | char buffer [BUFFERSIZE]; 362 | void main_udptx(int argc, char** argv) { 363 | UdpSender* sender = new UdpSender("127.0.0.1", 7000, BUFFERSIZE); 364 | osc::OutboundPacketStream p(buffer, BUFFERSIZE); 365 | p << osc::BeginBundleImmediate 366 | << osc::BeginMessage( "/test1" ) 367 | << true << 23 << (float)3.1415 << "hello" << osc::EndMessage 368 | << osc::BeginMessage( "/test2" ) 369 | << true << 24 << (float)10.8 << "world" << osc::EndMessage 370 | << osc::EndBundle; 371 | sender->Send((char *)p.Data(), p.Size()); 372 | } 373 | -------------------------------------------------------------------------------- /protocol.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | - Tempo receivers currently listen on UDP port 6040 (on all interfaces) 4 | - Tempo sender currently listens on UDP port 6041 (on all interfaces) 5 | 6 | Currently the tempo sender only sends this message: 7 | 8 | - A message with timestamp (expressed as two integers - sec and 9 | microsecs) set to the moment when the cps last changed: 10 | - path /tempo 11 | - [i] timestamp - seconds 12 | - [i] timestamp - microseconds 13 | - [f] floating point cycle value when cps last changed 14 | - [f] floating point cps 15 | - [s] True if clock is currently paused, False if not (seems to not work yet) 16 | 17 | The tempo receivers can send these messages: 18 | 19 | - path /cps - cycles per second change, causes new /tempo message to be sent to all receivers 20 | - [f] the new cps 21 | 22 | - path /nudge, nudge phase offset in seconds, causes new /tempo message to be sent to all 23 | receivers (local to the tempo receivers and not wider link network) 24 | - [f] nudge value (phase offset, or 0 to reset) 25 | 26 | - path /ping - causes the /tempo message to be (re)sent to all receivers 27 | 28 | - path /shutdown - stops the process 29 | 30 | 31 | ====== 32 | TODO 33 | ------ 34 | 35 | [ ] allow cps changes to be scheduled at particular beat values 36 | --------------------------------------------------------------------------------