Skip to content

Commit

Permalink
Add rest of Link API for @bdyetton in #1
Browse files Browse the repository at this point in the history
Although he only wanted time-at-beat, it makes sense to finish off the
other Link API calls since people are now using this in ways beyond
what Beat Link Trigger needed.

Also update the documentation, version number, and add an explanation
of what Link timestamps are, and how they differ from the wall clock.
  • Loading branch information
James Elliott committed Jul 23, 2017
1 parent 3028910 commit 73a2020
Show file tree
Hide file tree
Showing 3 changed files with 172 additions and 26 deletions.
10 changes: 8 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,13 @@ This change log follows the conventions of

## [Unreleased][unreleased]

Nothing so far.

## 0.1.3 - 2017-07-23

- Updated the embedded Link library to incorporate fixes.
- Added phase-at-time command, to implement more of the Link API.
- Added `phase-at-time`, `time-at-beat`, and `request-beat-at-time`
commands, to implement the rest of the Link API.

## 0.1.2 - 2017-04-22

Expand Down Expand Up @@ -36,6 +41,7 @@ This change log follows the conventions of
- Chose communication and configuration frameworks.
- Built an implementation which meets the needs of beat-link-trigger.

[unreleased]: https://github.com/brunchboy/beat-link-trigger/compare/v0.1.2...HEAD
[unreleased]: https://github.com/brunchboy/beat-link-trigger/compare/v0.1.3...HEAD
[0.1.3]: https://github.com/brunchboy/beat-link-trigger/compare/v0.1.2...v0.1.3
[0.1.2]: https://github.com/brunchboy/beat-link-trigger/compare/v0.1.1...v0.1.2
[0.1.1]: https://github.com/brunchboy/beat-link-trigger/compare/v0.1.0...v0.1.1
141 changes: 122 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ simple single-packet messages. The currently supported messages are:
Sending the string `status` to Carabiner requests an update containing
the current status. It will respond with a packet like the following:

status { :peers 1 :bpm 128.500019 :start 128928334480 :beat 106.412459 }
status { :peers 0 :bpm 120.000000 :start 73743731220 :beat 597.737570 }

As with all non-error messages from Carabiner, this consists of a
message type identifier followed by an
Expand All @@ -88,6 +88,18 @@ A status response will also be sent when you first connect to
Carabiner, and whenever the Link session tempo changes, as well as
whenever the number of Link peers changes.

> :stopwatch: **About Link Timestamps:** The `:start` value in the
> above message, and the time and `:when` values sent and returned in
> the commands below, are expressed in microseconds, but they are
> *not* interchangeable with values returned by the normal "wall
> clock" system clock you use to determine the current time of day.
> Link needs to use a monotonically increasing clock that is not
> affected by things like leap seconds or NTP server synchronization.
> In Java you can obtain Link-compatible timestamps using the
> `System.nanoTime()` method. If you are working in other languages,
> you will need to experiment in order to find out how to read the
> same clock that Link is using.
### bpm

Sending the string `bpm ` followed by a floating-point value, for
Expand All @@ -108,49 +120,112 @@ followed by the argument you supplied.
### beat-at-time

Sending the string `beat-at-time ` followed by a nanosecond timestamp
Sending the string `beat-at-time ` followed by a microsecond timestamp
(an integer relative to the `:start` value returned in the `status`
response), and a quantum value (which identifies the number of beats
that make up a bar or loop as described in the Phase Synchronization
section of the [Link documentation](http://ableton.github.io/link/)),
asks Carabiner to identify the beat which falls at the specified point
on the Link session timeline. For example, sending `beat-at-time
129426900000 4` to the link session used in the above examples
would result in a response like the following:
73746356220 4` to the link session used in the above examples would
result in a response like the following:

beat-at-time { :when 73746356220 :quantum 4.000000 :beat 5.250000 }

This response indicates that at the specified microsecond along the
Link session timeline, we are a quarter of the way from the sixth to
the seventh beat (Link numbers beats starting with 0).

If one of the parameters is missing or cannot be parsed, Carabiner
responds with `bad-time ` or `bad-quantum ` followed by the arguments
you gave it.

beat-at-time { :when 129426900000 :quantum 4.000000 :beat 1.278066 }
### phase-at-time

This response indicates that at the specified nanosecond along the
Sending the string `phase-at-time ` followed by a microsecond
timestamp (an integer relative to the `:start` value returned in the
`status` response), and a quantum value (which identifies the number
of beats that make up a bar or loop as described in the Phase
Synchronization section of
the [Link documentation](http://ableton.github.io/link/)), asks
Carabiner to identify the phase which falls at the specified point on
the Link session timeline. A phase is a floating point value ranging
fom 0.0 to just less than the quantum. For example, sending
`phase-at-time 73746356220 4` to the link session used in the above
examples would result in a response like the following:

phase-at-time { :when 73746356220 :quantum 4.000000 :phase 1.250000 }

This response indicates that at the specified microsecond along the
Link session timeline, we are just over a quarter of the way from the
second to the third beat.
second to the third beat of the four beats in a quantum period.

If one of the parameters is missing or cannot be parsed, Carabiner
responds with `bad-time ` or `bad-quantum ` followed by the arguments
you gave it.

### time-at-beat

Sending the string `time-at-beat ` followed by a beat number (a
floating point value, since you can inquire about points that fall
between beats), and a quantum value (which identifies the number of
beats that make up a bar or loop as described in the Phase
Synchronization section of
the [Link documentation](http://ableton.github.io/link/)), asks
Carabiner to return the microsecond timestamp (an integer relative to
the `:start` value returned in the `status` response) at which the
specified beat (or fraction of a beat) falls on the Link session
timeline. For example, sending `time-at-beat 100 4` to the link
session used in the above examples would result in a response like the
following:

time-at-beat { :beat 100.000000 :quantum 4.000000 :when 73793731220 }

This response indicates that the hundred and first beat falls at the
specified microsecond within the timeline. As a sanity check, you can
ask about the first beat, and verify that the `:when` value matches
the `:start` value reported by the `status` message. Sending
`time-at-beat 0 4` in the session used in these examples results in
the following response, which you can compare to the `status` response
above:

time-at-beat { :beat 0.000000 :quantum 4.000000 :when 73743731220 }

Sending another `status` command now shows that the current beat has
moved on while these examples were being written, but the timeline
start point has remained the same:

status { :peers 0 :bpm 120.000000 :start 73743731220 :beat 2560.623742 }

If one of the parameters is missing or cannot be parsed, Carabiner
responds with `bad-beat ` or `bad-quantum ` followed by the arguments
you gave it.

### force-beat-at-time

Sending the string `force-beat-at-time ` followed by a floating-point
beat number, a nanosecond timestamp (an integer relative to the
beat number, a microsecond timestamp (an integer relative to the
`:start` value returned in the `status` response), and a quantum value
(as described above) tells Carabiner to forcibly and abruptly adjust
the Link session timeline so that the specified beat falls at the
specified point in time. The change will be communicated to all
participants, and will result in audible shifts in playback.

Continuing the previous example, sending `force-beat-at-time 1.0
129426900000 4` will tell Carabiner to adjust the Link session
timeline so the second beat starts as close as possible to the
specified moment (which previously was 28% of the way to the third
73746356220 4` will tell Carabiner to adjust the Link session timeline
so the second beat starts as close as possible to the specified moment
(which previously was 25% of the way from the sixth to the seventh
beat). Carabiner responds with a `status` message which reports the
new `:start` timestamp of the timeline.

status { :peers 1 :bpm 140.000000 :start 129426471429 :beat 115.286846 }
status { :peers 0 :bpm 120.000000 :start 73745856220 :beat 2989.161370 }

At this point, repeating the `beat-at-time` command from the previous
section will return a beat value that is very close to 1.0.
At this point, repeating the `beat-at-time` command we used in the
section explaining that command, `beat-at-time 73746356220 4`, will
return a beat value that is very close to 1.0 (in this example it was
exact):

beat-at-time { :when 129426900000 :quantum 4.000000 :beat 0.999984 }
beat-at-time { :when 73746356220 :quantum 4.000000 :beat 1.000000 }

If one of the parameters is missing or cannot be parsed, Carabiner
responds with `bad-beat `, `bad-time `, or `bad-quantum ` followed by
Expand All @@ -164,9 +239,37 @@ jitter does not lead to excessive adjustments.

> If you are building an application that can perform quantized
> starts, and thereby participate in a Link session more graciously,
> without requiring the other participants to shift the timeline, then
> open an [issue](https://github.com/brunchboy/carabiner/issues) to
> have the `request-beat-at-time` command implemented by Carabiner.
> without requiring the other participants to shift the timeline, you
> should use the following command instead:
### request-beat-at-time

Sending the string `request-beat-at-time ` followed by a
floating-point beat number, a microsecond timestamp (an integer
relative to the `:start` value returned in the `status` response), and
a quantum value (as described above) tells Carabiner to ask Link to
try to gracefully adjust its timeline so that the specified beat will
occur at the specified time. If there are no other peers in the Link
session, this will behave the same as `force-beat-at-time`, above.
However, if there are any peers, it will avoid the kinds of audible
discontinuities described above, by adjusting the local timeline so
that the specified beat will instead fall at the next point in time
*after* the requested time which has the same *phase* as the specified
beat.

Carabiner responds with a `status` message which reports the
new `:start` timestamp of the timeline.

If one of the parameters is missing or cannot be parsed, Carabiner
responds with `bad-beat `, `bad-time `, or `bad-quantum ` followed by
the arguments you gave it.

As the Link documentation explains, this command is specifically
designed to enable the concept of "quantized launch". If there are no
peers, the deisred mapping is established immediately when requested.
Otherwise, we wait until the next time at which the session phase
matches the desired event, so we can seamlessly join the peers that
are already in the session.

## Apology

Expand All @@ -179,7 +282,7 @@ definitely welcome!

<img align="right" alt="Deep Symmetry" src="doc/assets/DS-logo-bw-200-padded-left.png">

Carabiner is Copyright © 2016 [Deep Symmetry, LLC](http://deepsymmetry.org)
Carabiner is Copyright © 2016-2017 [Deep Symmetry, LLC](http://deepsymmetry.org)

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
Expand Down
47 changes: 42 additions & 5 deletions carabiner.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -135,8 +135,37 @@ static void handlePhaseAtTime(std::string args, struct mg_connection *nc) {
}
}

// Process a request to realign the timeline
static void handleForceBeatAtTime(std::string args, struct mg_connection *nc) {
// Process a request to determine when a specific beat falls on the timeline
static void handleTimeAtBeat(std::string args, struct mg_connection *nc) {
std::stringstream ss(args);
double beat;
double quantum;

ss >> beat;
if (ss.fail()) {
// Unparsed beat value, report error
std::string response = "bad-beat " + args;
mg_send(nc, response.data(), response.length());
} else {
ss >> quantum;
if (ss.fail() || (quantum < 2.0) || (quantum > 16.0)) {
// Unparsed quantum value, report error
std::string response = "bad-quantum " + args;
mg_send(nc, response.data(), response.length());
} else {
ableton::Link::Timeline timeline = linkInstance.captureAppTimeline();
std::chrono::microseconds time = timeline.timeAtBeat(beat, quantum);
auto micros = std::chrono::duration_cast<std::chrono::microseconds>(time);
std::string response = "time-at-beat { :beat " + std::to_string(beat) +
" :quantum " + std::to_string(quantum) +
" :when " + std::to_string(micros.count()) + " }";
mg_send(nc, response.data(), response.length());
}
}
}

// Process a request to gracefully or forcibly realign the timeline
static void handleAdjustBeatAtTime(std::string args, struct mg_connection *nc, bool force) {
std::stringstream ss(args);
double beat;
long when;
Expand All @@ -161,7 +190,11 @@ static void handleForceBeatAtTime(std::string args, struct mg_connection *nc) {
mg_send(nc, response.data(), response.length());
} else {
ableton::Link::Timeline timeline = linkInstance.captureAppTimeline();
timeline.forceBeatAtTime(beat, std::chrono::microseconds(when), quantum);
if (force) {
timeline.forceBeatAtTime(beat, std::chrono::microseconds(when), quantum);
} else {
timeline.requestBeatAtTime(beat, std::chrono::microseconds(when), quantum);
}
linkInstance.commitAppTimeline(timeline);
reportStatus(nc);
}
Expand Down Expand Up @@ -191,8 +224,12 @@ static void processMessage(std::string msg, struct mg_connection *nc) {
handleBeatAtTime(args, nc);
} else if (matchesCommand(msg, "phase-at-time ", args)) {
handlePhaseAtTime(args, nc);
} else if (matchesCommand(msg, "time-at-beat ", args)) {
handleTimeAtBeat(args, nc);
} else if (matchesCommand(msg, "force-beat-at-time ", args)) {
handleForceBeatAtTime(args, nc);
handleAdjustBeatAtTime(args, nc, true);
} else if (matchesCommand(msg, "request-beat-at-time ", args)) {
handleAdjustBeatAtTime(args, nc, false);
} else if (matchesCommand(msg, "status", args)) {
handleStatus(args, nc);
} else {
Expand Down Expand Up @@ -255,7 +292,7 @@ void peersCallback(std::size_t numPeers) {
int main(int argc, char* argv[]) {
gflags::SetUsageMessage("Bridge to an Ableton Link session. Sample usage:\n" + std::string(argv[0]) +
" --port 1234 --poll 10");
gflags::SetVersionString("0.1.1");
gflags::SetVersionString("0.1.3");
gflags::ParseCommandLineFlags(&argc, &argv, true);
if (argc > 1) {
std::cerr << "Unrecognized argument, " << argv[1] << std::endl;
Expand Down

0 comments on commit 73a2020

Please sign in to comment.