You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
588 lines
19 KiB
588 lines
19 KiB
/*
|
|
* Copyright (C) 2016 The Android Open Source Project
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
#import "WALTClient.h"
|
|
|
|
#include <ctype.h>
|
|
#include <dispatch/dispatch.h>
|
|
#include <mach/clock.h>
|
|
#include <mach/mach.h>
|
|
#include <mach/mach_host.h>
|
|
#include <stdlib.h>
|
|
#include <time.h>
|
|
|
|
#import "MIDIEndpoint.h"
|
|
#import "MIDIMessage.h"
|
|
|
|
NSString * const WALTClientErrorDomain = @"WALTClientErrorDomain";
|
|
|
|
static NSString * const kWALTVersion = @"v 4";
|
|
|
|
static const MIDIChannel kWALTChannel = 1;
|
|
static const MIDIByte kWALTSerialOverMIDIProgram = 1;
|
|
static const MIDIMessageType kWALTCommandType = MIDIMessageChannelPressure;
|
|
|
|
const NSTimeInterval kWALTReadTimeout = 0.2;
|
|
static const NSTimeInterval kWALTDuplicateTimeout = 0.01;
|
|
|
|
static const int kWALTSyncIterations = 7;
|
|
#define kWALTSyncDigitMax 9 // #define to avoid variable length array warnings.
|
|
|
|
/** Similar to atoll(), but only reads a maximum of n characters from s. */
|
|
static unsigned long long antoull(const char *s, size_t n) {
|
|
unsigned long long result = 0;
|
|
while (s && n-- && *s && isdigit(*s)) {
|
|
result = result * 10 + (*s - '0');
|
|
++s;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/** Converts a mach_timespec_t to its equivalent number of microseconds. */
|
|
static int64_t TimespecToMicroseconds(const mach_timespec_t ts) {
|
|
return ((int64_t)ts.tv_sec) * USEC_PER_SEC + ts.tv_nsec / NSEC_PER_USEC;
|
|
}
|
|
|
|
/** Returns the current time (in microseconds) on a clock. */
|
|
static int64_t CurrentTime(clock_serv_t clock) {
|
|
mach_timespec_t time = {0};
|
|
clock_get_time(clock, &time);
|
|
return TimespecToMicroseconds(time);
|
|
}
|
|
|
|
/** Sleeps the current thread for us microseconds. */
|
|
static void Sleep(int64_t us) {
|
|
const struct timespec ts = {
|
|
.tv_sec = (long)(us / USEC_PER_SEC),
|
|
.tv_nsec = (us % USEC_PER_SEC) * NSEC_PER_USEC,
|
|
};
|
|
nanosleep(&ts, NULL);
|
|
}
|
|
|
|
@interface WALTClient ()
|
|
@property (readwrite, nonatomic, getter=isConnected) BOOL connected;
|
|
|
|
- (void)drainResponseQueue;
|
|
- (BOOL)improveSyncBoundsWithError:(NSError **)error;
|
|
- (BOOL)improveMinBoundWithError:(NSError **)error;
|
|
- (BOOL)improveMaxBoundWithError:(NSError **)error;
|
|
- (BOOL)readRemoteTimestamps:(uint64_t[kWALTSyncDigitMax])times error:(NSError **)error;
|
|
- (WALTTrigger)readTrigger:(NSData *)response;
|
|
@end
|
|
|
|
@implementation WALTClient {
|
|
MIDIClient *_client;
|
|
|
|
// Responses from the MIDIClient are queued up here with a signal to the semaphore.
|
|
NSMutableArray<NSData *> *_responseQueue; // TODO(pquinn): Lock-free circular buffer?
|
|
dispatch_semaphore_t _responseSemaphore;
|
|
|
|
BOOL _syncCompleted;
|
|
|
|
clock_serv_t _clock;
|
|
|
|
NSData *_lastData;
|
|
NSTimeInterval _lastDataTimestamp;
|
|
|
|
struct {
|
|
// All microseconds.
|
|
int64_t base;
|
|
int64_t minError;
|
|
int64_t maxError;
|
|
} _sync;
|
|
}
|
|
|
|
- (instancetype)initWithError:(NSError **)error {
|
|
if ((self = [super init])) {
|
|
_responseQueue = [[NSMutableArray<NSData *> alloc] init];
|
|
_responseSemaphore = dispatch_semaphore_create(0);
|
|
|
|
// NB: It's important that this is the same clock used as the base for UIEvent's -timestamp.
|
|
kern_return_t result = host_get_clock_service(mach_host_self(), SYSTEM_CLOCK, &_clock);
|
|
|
|
if (result != KERN_SUCCESS || ![self checkConnectionWithError:error]) {
|
|
self = nil;
|
|
}
|
|
}
|
|
return self;
|
|
}
|
|
|
|
- (void)dealloc {
|
|
[self drainResponseQueue];
|
|
mach_port_deallocate(mach_task_self(), _clock);
|
|
}
|
|
|
|
// Ensure only one KVO notification is sent when the connection state is changed.
|
|
+ (BOOL)automaticallyNotifiesObserversOfConnected {
|
|
return NO;
|
|
}
|
|
|
|
- (void)setConnected:(BOOL)connected {
|
|
if (_connected != connected) {
|
|
[self willChangeValueForKey:@"connected"];
|
|
_connected = connected;
|
|
[self didChangeValueForKey:@"connected"];
|
|
}
|
|
}
|
|
|
|
- (BOOL)checkConnectionWithError:(NSError **)error {
|
|
if (_client.source.isOnline && _client.destination.isOnline && _syncCompleted) {
|
|
self.connected = YES;
|
|
return YES; // Everything's fine.
|
|
}
|
|
|
|
_syncCompleted = NO; // Reset the sync state.
|
|
[self drainResponseQueue];
|
|
|
|
// Create a new client.
|
|
// This probably isn't strictly necessary, but solves some of the flakiness on iOS.
|
|
_client.delegate = nil;
|
|
_client = [[MIDIClient alloc] initWithName:@"WALT" error:error];
|
|
_client.delegate = self;
|
|
if (!_client) {
|
|
self.connected = NO;
|
|
return NO;
|
|
}
|
|
|
|
if (!_client.source.isOnline) {
|
|
// Try to connect to the first available input source.
|
|
// TODO(pquinn): Make this user-configurable.
|
|
NSArray<MIDISource *> *sources = [MIDISource allSources];
|
|
if (sources.count) {
|
|
if (![_client connectToSource:sources.firstObject error:error]) {
|
|
self.connected = NO;
|
|
return NO;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!_client.destination.isOnline) {
|
|
// Try to connect to the first available input source.
|
|
// TODO(pquinn): Make this user-configurable.
|
|
NSArray<MIDIDestination *> *destinations = [MIDIDestination allDestinations];
|
|
if (destinations.count) {
|
|
if (![_client connectToDestination:destinations.firstObject error:error]) {
|
|
self.connected = NO;
|
|
return NO;
|
|
}
|
|
}
|
|
|
|
if (_client.destination.isOnline) {
|
|
// Switch to Serial-over-MIDI mode.
|
|
NSData *message = MIDIMessageCreateSimple1(MIDIMessageProgramChange,
|
|
kWALTChannel,
|
|
kWALTSerialOverMIDIProgram);
|
|
if (![_client sendData:message error:error]) {
|
|
self.connected = NO;
|
|
return NO;
|
|
}
|
|
|
|
// Make sure it's using a known protocol version.
|
|
message = MIDIMessageCreateSimple1(kWALTCommandType, kWALTChannel, WALTVersionCommand);
|
|
if (![_client sendData:message error:error]) {
|
|
self.connected = NO;
|
|
return NO;
|
|
}
|
|
|
|
NSData *response = [self readResponseWithTimeout:kWALTReadTimeout];
|
|
NSString *versionString = [[NSString alloc] initWithData:response
|
|
encoding:NSASCIIStringEncoding];
|
|
if (![versionString isEqualToString:kWALTVersion]) {
|
|
if (error) {
|
|
*error = [NSError errorWithDomain:WALTClientErrorDomain
|
|
code:0
|
|
userInfo:@{NSLocalizedDescriptionKey:
|
|
[@"Unknown WALT version: "
|
|
stringByAppendingString:versionString]}];
|
|
}
|
|
self.connected = NO;
|
|
return NO;
|
|
}
|
|
|
|
if (![self syncClocksWithError:error]) {
|
|
self.connected = NO;
|
|
return NO;
|
|
}
|
|
|
|
_syncCompleted = YES;
|
|
}
|
|
}
|
|
|
|
self.connected = (_client.source.isOnline && _client.destination.isOnline && _syncCompleted);
|
|
return YES;
|
|
}
|
|
|
|
#pragma mark - Clock Synchronisation
|
|
|
|
- (BOOL)syncClocksWithError:(NSError **)error {
|
|
_sync.base = CurrentTime(_clock);
|
|
|
|
if (![self sendCommand:WALTZeroSyncCommand error:error]) {
|
|
return NO;
|
|
}
|
|
|
|
NSData *data = [self readResponseWithTimeout:kWALTReadTimeout];
|
|
if (![self checkResponse:data forCommand:WALTZeroSyncCommand]) {
|
|
if (error) {
|
|
*error = [NSError errorWithDomain:WALTClientErrorDomain
|
|
code:0
|
|
userInfo:@{NSLocalizedDescriptionKey:
|
|
[NSString stringWithFormat:@"Bad acknowledgement for WALTZeroSyncCommand: %@", data]}];
|
|
}
|
|
return NO;
|
|
}
|
|
|
|
_sync.maxError = CurrentTime(_clock) - _sync.base;
|
|
_sync.minError = 0;
|
|
|
|
for (int i = 0; i < kWALTSyncIterations; ++i) {
|
|
if (![self improveSyncBoundsWithError:error]) {
|
|
return NO;
|
|
}
|
|
}
|
|
|
|
// Shift the time base so minError == 0
|
|
_sync.base += _sync.minError;
|
|
_sync.maxError -= _sync.minError;
|
|
_sync.minError = 0;
|
|
return YES;
|
|
}
|
|
|
|
- (BOOL)updateSyncBoundsWithError:(NSError **)error {
|
|
// Reset the bounds to unrealistic values
|
|
_sync.minError = -1e7;
|
|
_sync.maxError = 1e7;
|
|
|
|
for (int i = 0; i < kWALTSyncIterations; ++i) {
|
|
if (![self improveSyncBoundsWithError:error]) {
|
|
return NO;
|
|
}
|
|
}
|
|
|
|
return YES;
|
|
}
|
|
|
|
- (int64_t)minError {
|
|
return _sync.minError;
|
|
}
|
|
|
|
- (int64_t)maxError {
|
|
return _sync.maxError;
|
|
}
|
|
|
|
- (BOOL)improveSyncBoundsWithError:(NSError **)error {
|
|
return ([self improveMinBoundWithError:error] && [self improveMaxBoundWithError:error]);
|
|
}
|
|
|
|
- (BOOL)improveMinBoundWithError:(NSError **)error {
|
|
if (![self sendCommand:WALTResetCommand error:error]) {
|
|
return NO;
|
|
}
|
|
|
|
NSData *data = [self readResponseWithTimeout:kWALTReadTimeout];
|
|
if (![self checkResponse:data forCommand:WALTResetCommand]) {
|
|
if (error) {
|
|
*error = [NSError errorWithDomain:WALTClientErrorDomain
|
|
code:0
|
|
userInfo:@{NSLocalizedDescriptionKey:
|
|
[NSString stringWithFormat:@"Bad acknowledgement for WALTResetCommand: %@", data]}];
|
|
}
|
|
return NO;
|
|
}
|
|
|
|
const uint64_t kMaxSleepTime = 700; // µs
|
|
const uint64_t kMinSleepTime = 70; // µs
|
|
const uint64_t kSleepTimeDivider = 10;
|
|
|
|
uint64_t sleepTime = (_sync.maxError - _sync.minError) / kSleepTimeDivider;
|
|
if (sleepTime > kMaxSleepTime) { sleepTime = kMaxSleepTime; }
|
|
if (sleepTime < kMinSleepTime) { sleepTime = kMinSleepTime; }
|
|
|
|
struct {
|
|
uint64_t local[kWALTSyncDigitMax];
|
|
uint64_t remote[kWALTSyncDigitMax];
|
|
} digitTimes = {0};
|
|
|
|
// Send the digits 1 through 9 and record the times they were sent in digitTimes.local.
|
|
for (int i = 0; i < kWALTSyncDigitMax; ++i) {
|
|
digitTimes.local[i] = CurrentTime(_clock) - _sync.base;
|
|
|
|
char c = '1' + i;
|
|
if (![self sendCommand:c error:error]) {
|
|
if (error) {
|
|
*error = [NSError errorWithDomain:WALTClientErrorDomain
|
|
code:0
|
|
userInfo:@{NSLocalizedDescriptionKey:
|
|
[NSString stringWithFormat:@"Error sending digit %d", i + 1],
|
|
NSUnderlyingErrorKey: *error}];
|
|
}
|
|
return NO;
|
|
}
|
|
// Sleep between digits
|
|
Sleep(sleepTime);
|
|
}
|
|
|
|
if (![self readRemoteTimestamps:digitTimes.remote error:error]) {
|
|
return NO;
|
|
}
|
|
|
|
// Adjust minError to be the largest delta between local and remote.
|
|
for (int i = 0; i < kWALTSyncDigitMax; ++i) {
|
|
int64_t delta = digitTimes.local[i] - digitTimes.remote[i];
|
|
if (digitTimes.local[i] != 0 && digitTimes.remote[i] != 0 && delta > _sync.minError) {
|
|
_sync.minError = delta;
|
|
}
|
|
}
|
|
return YES;
|
|
}
|
|
|
|
- (BOOL)improveMaxBoundWithError:(NSError **)error {
|
|
struct {
|
|
uint64_t local[kWALTSyncDigitMax];
|
|
uint64_t remote[kWALTSyncDigitMax];
|
|
} digitTimes = {0};
|
|
|
|
// Ask the WALT to send the digits 1 through 9, and record the times they are received in
|
|
// digitTimes.local.
|
|
if (![self sendCommand:WALTSendSyncCommand error:error]) {
|
|
return NO;
|
|
}
|
|
|
|
for (int i = 0; i < kWALTSyncDigitMax; ++i) {
|
|
NSData *data = [self readResponseWithTimeout:kWALTReadTimeout];
|
|
if (data.length != 1) {
|
|
if (error) {
|
|
*error = [NSError errorWithDomain:WALTClientErrorDomain
|
|
code:0
|
|
userInfo:@{NSLocalizedDescriptionKey:
|
|
[NSString stringWithFormat:@"Error receiving digit %d: %@", i + 1, data]}];
|
|
}
|
|
return NO;
|
|
}
|
|
|
|
char c = ((const char *)data.bytes)[0];
|
|
if (!isdigit(c)) {
|
|
if (error) {
|
|
*error = [NSError errorWithDomain:WALTClientErrorDomain
|
|
code:0
|
|
userInfo:@{NSLocalizedDescriptionKey:
|
|
[NSString stringWithFormat:@"Error parsing digit response: %c", c]}];
|
|
}
|
|
return NO;
|
|
}
|
|
|
|
int digit = c - '0';
|
|
digitTimes.local[digit - 1] = CurrentTime(_clock) - _sync.base;
|
|
}
|
|
|
|
if (![self readRemoteTimestamps:digitTimes.remote error:error]) {
|
|
return NO;
|
|
}
|
|
|
|
// Adjust maxError to be the smallest delta between local and remote
|
|
for (int i = 0; i < kWALTSyncDigitMax; ++i) {
|
|
int64_t delta = digitTimes.local[i] - digitTimes.remote[i];
|
|
if (digitTimes.local[i] != 0 && digitTimes.remote[i] != 0 && delta < _sync.maxError) {
|
|
_sync.maxError = delta;
|
|
}
|
|
}
|
|
return YES;
|
|
}
|
|
|
|
- (BOOL)readRemoteTimestamps:(uint64_t [9])times error:(NSError **)error {
|
|
for (int i = 0; i < kWALTSyncDigitMax; ++i) {
|
|
// Ask the WALT for each digit's recorded timestamp
|
|
if (![self sendCommand:WALTReadoutSyncCommand error:error]) {
|
|
return NO;
|
|
}
|
|
|
|
NSData *data = [self readResponseWithTimeout:kWALTReadTimeout];
|
|
if (data.length < 3) {
|
|
if (error) {
|
|
*error = [NSError errorWithDomain:WALTClientErrorDomain
|
|
code:0
|
|
userInfo:@{NSLocalizedDescriptionKey:
|
|
[NSString stringWithFormat:@"Error receiving sync digit %d: %@", i + 1, data]}];
|
|
}
|
|
return NO;
|
|
}
|
|
|
|
// The reply data is formatted as n:xxxx, where n is a digit between 1 and 9, and xxxx
|
|
// is a microsecond timestamp.
|
|
int digit = (int)antoull(data.bytes, 1);
|
|
uint64_t timestamp = antoull(((const char *)data.bytes) + 2, data.length - 2);
|
|
|
|
if (digit != (i + 1) || timestamp == 0) {
|
|
if (error) {
|
|
*error = [NSError errorWithDomain:WALTClientErrorDomain
|
|
code:0
|
|
userInfo:@{NSLocalizedDescriptionKey:
|
|
[NSString stringWithFormat:@"Error parsing remote time response for %d: %@", i, data]}];
|
|
}
|
|
return NO;
|
|
}
|
|
times[digit - 1] = timestamp;
|
|
}
|
|
return YES;
|
|
}
|
|
|
|
#pragma mark - MIDIClient Delegate
|
|
|
|
// TODO(pquinn): Errors from these callbacks aren't propoagated anywhere.
|
|
|
|
- (void)MIDIClientEndpointAdded:(MIDIClient *)client {
|
|
[self performSelectorOnMainThread:@selector(checkConnectionWithError:)
|
|
withObject:nil
|
|
waitUntilDone:NO];
|
|
}
|
|
|
|
- (void)MIDIClientEndpointRemoved:(MIDIClient *)client {
|
|
[self performSelectorOnMainThread:@selector(checkConnectionWithError:)
|
|
withObject:nil
|
|
waitUntilDone:NO];
|
|
}
|
|
|
|
- (void)MIDIClientConfigurationChanged:(MIDIClient *)client {
|
|
[self performSelectorOnMainThread:@selector(checkConnectionWithError:)
|
|
withObject:nil
|
|
waitUntilDone:NO];
|
|
}
|
|
|
|
- (void)MIDIClient:(MIDIClient *)client receivedError:(NSError *)error {
|
|
// TODO(pquinn): What's the scope of these errors?
|
|
NSLog(@"WALTClient received unhandled error: %@", error);
|
|
}
|
|
|
|
- (void)MIDIClient:(MIDIClient *)client receivedData:(NSData *)message {
|
|
NSData *body = MIDIMessageBody(message);
|
|
@synchronized (_responseQueue) {
|
|
// Sometimes a message will be received twice in quick succession. It's not clear where the bug
|
|
// is (the WALT, CoreMIDI, or this application), and it cannot be reliably reproduced. As a
|
|
// hack, simply ignore messages that appear to be duplicates and arrive within
|
|
// kWALTDuplicateTimeout seconds.
|
|
if (self.currentTime - _lastDataTimestamp <= kWALTDuplicateTimeout &&
|
|
[body isEqualToData:_lastData]) {
|
|
NSLog(@"Ignoring duplicate response within kWALTDuplicateTimeout: %@", message);
|
|
return;
|
|
}
|
|
|
|
[_responseQueue addObject:body];
|
|
_lastData = body;
|
|
_lastDataTimestamp = self.currentTime;
|
|
}
|
|
dispatch_semaphore_signal(_responseSemaphore);
|
|
}
|
|
|
|
#pragma mark - Send/Receive
|
|
|
|
- (void)drainResponseQueue {
|
|
@synchronized (_responseQueue) {
|
|
// Drain out any stale responses or the semaphore destructor will assert.
|
|
while (_responseQueue.count) {
|
|
dispatch_semaphore_wait(_responseSemaphore, DISPATCH_TIME_FOREVER);
|
|
[_responseQueue removeObjectAtIndex:0];
|
|
}
|
|
}
|
|
}
|
|
|
|
- (NSData *)readResponseWithTimeout:(NSTimeInterval)timeout {
|
|
if (dispatch_semaphore_wait(_responseSemaphore,
|
|
dispatch_time(DISPATCH_TIME_NOW, timeout * NSEC_PER_SEC))) {
|
|
return nil;
|
|
}
|
|
|
|
@synchronized (_responseQueue) {
|
|
NSAssert(_responseQueue.count > 0, @"_responseQueue is empty!");
|
|
NSData *response = _responseQueue.firstObject;
|
|
[_responseQueue removeObjectAtIndex:0];
|
|
return response;
|
|
}
|
|
}
|
|
|
|
- (BOOL)sendCommand:(WALTCommand)command error:(NSError **)error {
|
|
NSData *message = MIDIMessageCreateSimple1(kWALTCommandType, kWALTChannel, command);
|
|
[self drainResponseQueue];
|
|
return [_client sendData:message error:error];
|
|
}
|
|
|
|
- (BOOL)checkResponse:(NSData *)response forCommand:(WALTCommand)command {
|
|
const WALTCommand flipped = isupper(command) ? tolower(command) : toupper(command);
|
|
if (response.length < 1 || ((const char *)response.bytes)[0] != flipped) {
|
|
return NO;
|
|
} else {
|
|
return YES;
|
|
}
|
|
}
|
|
|
|
#pragma mark - Specific Commands
|
|
|
|
- (NSTimeInterval)lastShockTimeWithError:(NSError **)error {
|
|
if (![self sendCommand:WALTGShockCommand error:error]) {
|
|
return -1;
|
|
}
|
|
|
|
NSData *response = [self readResponseWithTimeout:kWALTReadTimeout];
|
|
if (!response) {
|
|
if (error) {
|
|
*error = [NSError errorWithDomain:WALTClientErrorDomain
|
|
code:0
|
|
userInfo:@{NSLocalizedDescriptionKey:
|
|
@"Error receiving shock response."}];
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
uint64_t microseconds = antoull(response.bytes, response.length);
|
|
return ((NSTimeInterval)microseconds + _sync.base) / USEC_PER_SEC;
|
|
}
|
|
|
|
- (WALTTrigger)readTrigger:(NSData *)response {
|
|
NSString *responseString =
|
|
[[NSString alloc] initWithData:response encoding:NSASCIIStringEncoding];
|
|
NSArray<NSString *> *components = [responseString componentsSeparatedByString:@" "];
|
|
|
|
WALTTrigger result = {0};
|
|
|
|
if (components.count != 5 ||
|
|
![[components objectAtIndex:0] isEqualToString:@"G"] ||
|
|
[components objectAtIndex:1].length != 1) {
|
|
return result;
|
|
}
|
|
|
|
result.tag = [[components objectAtIndex:1] characterAtIndex:0];
|
|
|
|
uint64_t microseconds = atoll([components objectAtIndex:2].UTF8String);
|
|
result.t = ((NSTimeInterval)microseconds + _sync.base) / USEC_PER_SEC;
|
|
result.value = (int)atoll([components objectAtIndex:3].UTF8String);
|
|
result.count = (unsigned int)atoll([components objectAtIndex:4].UTF8String);
|
|
return result;
|
|
}
|
|
|
|
- (WALTTrigger)readTriggerWithTimeout:(NSTimeInterval)timeout {
|
|
return [self readTrigger:[self readResponseWithTimeout:timeout]];
|
|
}
|
|
|
|
#pragma mark - Time
|
|
|
|
- (NSTimeInterval)baseTime {
|
|
return ((NSTimeInterval)_sync.base) / USEC_PER_SEC;
|
|
}
|
|
|
|
- (NSTimeInterval)currentTime {
|
|
return ((NSTimeInterval)CurrentTime(_clock)) / USEC_PER_SEC;
|
|
}
|
|
@end
|