diff --git a/CHANGELOG.md b/CHANGELOG.md
index 52a9f84..6e746fc 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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
@@ -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
diff --git a/README.md b/README.md
index aafba2d..04e5a76 100644
--- a/README.md
+++ b/README.md
@@ -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
@@ -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
@@ -108,30 +120,91 @@ 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
@@ -139,18 +212,20 @@ 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
@@ -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
@@ -179,7 +282,7 @@ definitely welcome!
-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
diff --git a/carabiner.cpp b/carabiner.cpp
index eed4e4c..85089aa 100644
--- a/carabiner.cpp
+++ b/carabiner.cpp
@@ -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(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;
@@ -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);
}
@@ -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 {
@@ -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;