annotate Source/TouchKeys/LogPlayback.cpp @ 56:b4a2d2ae43cf tip

merge
author Andrew McPherson <andrewm@eecs.qmul.ac.uk>
date Fri, 23 Nov 2018 15:48:14 +0000
parents ff5d65c69e73
children
rev   line source
andrewm@0 1 /*
andrewm@0 2 TouchKeys: multi-touch musical keyboard control software
andrewm@0 3 Copyright (c) 2013 Andrew McPherson
andrewm@0 4
andrewm@0 5 This program is free software: you can redistribute it and/or modify
andrewm@0 6 it under the terms of the GNU General Public License as published by
andrewm@0 7 the Free Software Foundation, either version 3 of the License, or
andrewm@0 8 (at your option) any later version.
andrewm@0 9
andrewm@0 10 This program is distributed in the hope that it will be useful,
andrewm@0 11 but WITHOUT ANY WARRANTY; without even the implied warranty of
andrewm@0 12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
andrewm@0 13 GNU General Public License for more details.
andrewm@0 14
andrewm@0 15 You should have received a copy of the GNU General Public License
andrewm@0 16 along with this program. If not, see <http://www.gnu.org/licenses/>.
andrewm@0 17
andrewm@0 18 =====================================================================
andrewm@0 19
andrewm@0 20 LogPlayback.cpp: basic functions for playing back a recorded TouchKeys log.
andrewm@0 21 */
andrewm@0 22
andrewm@0 23 #include "LogPlayback.h"
andrewm@0 24
andrewm@0 25 LogPlayback::LogPlayback(PianoKeyboard& keyboard, MidiInputController& midi)
andrewm@0 26 : keyboard_(keyboard), midiInputController_(midi), open_(false), playing_(false), paused_(false),
andrewm@0 27 usingTouch_(false), usingMidi_(false), playbackRate_(1.0),
andrewm@0 28 nextTouchMidiNote_(0), nextTouchTimestamp_(0), nextMidiTimestamp_(0),
andrewm@0 29 lastMidiTimestamp_(0), timestampOffset_(0)
andrewm@0 30 {
andrewm@0 31 // Create a statically bound call to the performMapping() method that
andrewm@0 32 // we use each time we schedule a new mapping
andrewm@0 33 touchAction_ = boost::bind(&LogPlayback::nextTouchEvent, this);
andrewm@0 34 midiAction_ = boost::bind(&LogPlayback::nextMidiEvent, this);
andrewm@0 35 }
andrewm@0 36
andrewm@0 37 LogPlayback::~LogPlayback()
andrewm@0 38 {
andrewm@0 39 if(open_)
andrewm@0 40 closeLogFiles();
andrewm@0 41 }
andrewm@0 42
andrewm@0 43 // File management. Open a touch and/or MIDI file. Returns true on success.
andrewm@0 44 // Pass a blank string to either one of the paths to not use that form of data capture
andrewm@0 45 bool LogPlayback::openLogFiles(string const& touchPath, string const& midiPath) {
andrewm@0 46 touchLog_.open (touchPath.c_str(), ios::in | ios::binary);
andrewm@0 47 midiLog_.open (midiPath.c_str(), ios::in | ios::binary);
andrewm@0 48
andrewm@0 49 usingTouch_ = touchLog_.is_open();
andrewm@0 50 usingMidi_ = midiLog_.is_open();
andrewm@0 51
andrewm@0 52 // Check for bad file paths
andrewm@0 53 if(!usingTouch_ && touchPath != "")
andrewm@0 54 return false;
andrewm@0 55 if(!usingMidi_ && midiPath != "")
andrewm@0 56 return false;
andrewm@0 57 if(!usingTouch_ && !usingMidi_)
andrewm@0 58 return false;
andrewm@0 59
andrewm@0 60 // Set defaults
andrewm@0 61 open_ = true;
andrewm@0 62 playing_ = paused_ = false;
andrewm@0 63 playbackRate_ = 1.0;
andrewm@0 64 return true;
andrewm@0 65 }
andrewm@0 66
andrewm@0 67 // Close the current files
andrewm@0 68 void LogPlayback::closeLogFiles() {
andrewm@0 69 if(!open_)
andrewm@0 70 return;
andrewm@0 71 if(playing_)
andrewm@0 72 stopPlayback();
andrewm@0 73 touchLog_.close();
andrewm@0 74 midiLog_.close();
andrewm@0 75 open_ = playing_ = paused_ = false;
andrewm@0 76 }
andrewm@0 77
andrewm@0 78 // Start, stop, pause, resume
andrewm@0 79 void LogPlayback::startPlayback(timestamp_type startingTimestamp) {
andrewm@0 80 if(!open_)
andrewm@0 81 return;
andrewm@0 82
andrewm@0 83 // Start the playback scheduler thread
andrewm@0 84 playbackScheduler_.start(0);
andrewm@0 85
andrewm@0 86 timestamp_type firstTouchTimestamp, firstMidiTimestamp;
andrewm@0 87
andrewm@0 88 // Register actions on the scheduler thread
andrewm@0 89 if(usingTouch_) {
andrewm@0 90 readNextTouchFrame();
andrewm@0 91 firstTouchTimestamp = nextTouchTimestamp_;
andrewm@0 92 timestampOffset_ = playbackScheduler_.currentTimestamp() - firstTouchTimestamp;
andrewm@0 93 }
andrewm@0 94 if(usingMidi_) {
andrewm@0 95 readNextMidiFrame();
andrewm@0 96 firstMidiTimestamp = nextMidiTimestamp_;
andrewm@0 97 lastMidiTimestamp_ = nextMidiTimestamp_; // First timestamp difference is 0
andrewm@0 98
andrewm@0 99 // Timestamp offset is to first MIDI event, unless there's an earlier touch event
andrewm@0 100 if(!(usingTouch_ && (firstTouchTimestamp < firstMidiTimestamp)))
andrewm@0 101 timestampOffset_ = playbackScheduler_.currentTimestamp() - firstMidiTimestamp;
andrewm@0 102 }
andrewm@0 103
andrewm@53 104 cout << "Touch " << firstTouchTimestamp << " MIDI " << firstMidiTimestamp << " offset " << timestampOffset_ << endl;
andrewm@53 105
andrewm@0 106 playing_ = true;
andrewm@0 107 paused_ = false;
andrewm@0 108
andrewm@0 109 if(usingTouch_)
andrewm@0 110 playbackScheduler_.schedule(this, touchAction_, playbackScheduler_.currentTimestamp());
andrewm@0 111 if(usingMidi_)
andrewm@0 112 playbackScheduler_.schedule(this, midiAction_, playbackScheduler_.currentTimestamp());
andrewm@0 113 }
andrewm@0 114
andrewm@0 115 void LogPlayback::stopPlayback() {
andrewm@0 116 playing_ = paused_ = false;
andrewm@0 117
andrewm@0 118 // Stop the playback scheduler thread
andrewm@0 119 playbackScheduler_.stop();
andrewm@0 120 playbackScheduler_.unschedule(this);
andrewm@0 121 }
andrewm@0 122
andrewm@0 123 // Pause a currently playing file. Save the pause time so the offset
andrewm@0 124 // can be recalculated when it resumes
andrewm@0 125 void LogPlayback::pausePlayback() {
andrewm@0 126 if(open_ && playing_) {
andrewm@0 127 playbackScheduler_.unschedule(this);
andrewm@0 128
andrewm@0 129 // TODO: consider thread safety: what happens if this comes during one of the scheduled calls?
andrewm@0 130
andrewm@0 131 paused_ = true;
andrewm@0 132 pauseTimestamp_ = playbackScheduler_.currentTimestamp();
andrewm@0 133 }
andrewm@0 134 }
andrewm@0 135
andrewm@0 136 // Resume playback after a pause
andrewm@0 137 void LogPlayback::resumePlayback() {
andrewm@0 138 if(paused_) {
andrewm@0 139 paused_ = false;
andrewm@0 140 timestamp_type resumeTimestamp = playbackScheduler_.currentTimestamp();
andrewm@0 141
andrewm@0 142 // Update the timestamp offset
andrewm@0 143 timestampOffset_ += resumeTimestamp - pauseTimestamp_;
andrewm@0 144
andrewm@0 145 // Reschedule calls
andrewm@0 146 if(usingTouch_)
andrewm@0 147 playbackScheduler_.schedule(this, touchAction_, nextTouchTimestamp_ + timestampOffset_);
andrewm@0 148 if(usingMidi_)
andrewm@0 149 playbackScheduler_.schedule(this, touchAction_, nextMidiTimestamp_ + timestampOffset_);
andrewm@0 150 }
andrewm@0 151 }
andrewm@0 152
andrewm@0 153 // Seek to a timestamp in the file
andrewm@0 154 void LogPlayback::seekPlayback(timestamp_type newTimestamp) {
andrewm@0 155 // Advance through the file until we reach the indicated timestamp
andrewm@0 156
andrewm@0 157 if(!playing_ || !open_)
andrewm@0 158 return;
andrewm@0 159
andrewm@0 160 // Remove any future actions while we perform the seek
andrewm@0 161 playbackScheduler_.unschedule(this);
andrewm@0 162 //timestamp_diff_type offset = 0;
andrewm@0 163 timestamp_type firstUpcomingTimestamp = 0;
andrewm@0 164
andrewm@0 165 if(usingTouch_) {
andrewm@0 166 //timestamp_type lastTimestamp = nextTouchTimestamp_;
andrewm@0 167
andrewm@0 168 // TODO: this assumes the seek is moving forward
andrewm@0 169 while(nextTouchTimestamp_ <= newTimestamp) {
andrewm@0 170 if(!readNextTouchFrame()) { // EOF or error
andrewm@0 171 usingTouch_ = false;
andrewm@0 172 if(!usingMidi_)
andrewm@0 173 playing_ = paused_ = false;
andrewm@0 174 break;
andrewm@0 175 }
andrewm@0 176 }
andrewm@0 177
andrewm@0 178 // Now we have the first event scheduled after the seek location
andrewm@0 179 // Update timestamp offset to continue playback from here.
andrewm@0 180
andrewm@0 181 //offset = nextTouchTimestamp_ - lastTimestamp;
andrewm@0 182 firstUpcomingTimestamp = nextTouchTimestamp_;
andrewm@0 183 }
andrewm@0 184 if(usingMidi_) {
andrewm@0 185 //timestamp_type lastTimestamp = nextMidiTimestamp_;
andrewm@0 186
andrewm@0 187 // TODO: this assumes the seek is moving forward
andrewm@0 188 while(nextMidiTimestamp_ <= newTimestamp) {
andrewm@0 189 if(!readNextMidiFrame()) { // EOF or error
andrewm@0 190 usingMidi_ = false;
andrewm@0 191 if(!usingTouch_)
andrewm@0 192 playing_ = paused_ = false;
andrewm@0 193 break;
andrewm@0 194 }
andrewm@0 195 }
andrewm@0 196
andrewm@0 197 // Now we have the first event scheduled after the seek location
andrewm@0 198 // Update timestamp offset to continue playback from here.
andrewm@0 199 // Use whichever event came first
andrewm@0 200
andrewm@0 201 //if(!(usingTouch_ && (nextMidiTimestamp_ - lastTimestamp) > offset))
andrewm@0 202 // offset = (nextMidiTimestamp_ - lastTimestamp);
andrewm@0 203 if(!usingTouch_ || nextMidiTimestamp_ < nextTouchTimestamp_);
andrewm@0 204 firstUpcomingTimestamp = nextMidiTimestamp_;
andrewm@0 205 }
andrewm@0 206
andrewm@0 207 // Update the timestamp offset
andrewm@0 208 timestampOffset_ = playbackScheduler_.currentTimestamp() - firstUpcomingTimestamp;
andrewm@0 209
andrewm@0 210 if(usingTouch_)
andrewm@0 211 playbackScheduler_.schedule(this, touchAction_, nextTouchTimestamp_ + timestampOffset_);
andrewm@0 212 if(usingMidi_)
andrewm@0 213 playbackScheduler_.schedule(this, midiAction_, nextMidiTimestamp_ + timestampOffset_);
andrewm@0 214 }
andrewm@0 215
andrewm@0 216 // Change the playback rate (1.0 being the standard speed)
andrewm@0 217 void LogPlayback::changePlaybackRate(float rate) {
andrewm@0 218 playbackRate_ = rate;
andrewm@0 219 }
andrewm@0 220
andrewm@0 221 // Events the scheduler calls when the right time elapses. Find the
andrewm@0 222 // next touch or MIDI event and play it back
andrewm@0 223 timestamp_type LogPlayback::nextTouchEvent() {
andrewm@0 224 if(!playing_ || !open_ || paused_)
andrewm@0 225 return 0;
andrewm@0 226
andrewm@0 227 // TODO: handle playback rate
andrewm@0 228
andrewm@0 229 // Play the most recent stored touch frame
andrewm@0 230 if(nextTouchMidiNote_ >= 0 && nextTouchMidiNote_ < 128) {
andrewm@0 231 // Use PianoKeyboard timestamps for the messages we send since our scheduler
andrewm@0 232 // may have a different idea of time.
andrewm@0 233
andrewm@0 234 if(nextTouch_.count == 0) {
andrewm@0 235 if(keyboard_.key(nextTouchMidiNote_) != 0)
andrewm@0 236 keyboard_.key(nextTouchMidiNote_)->touchOff(keyboard_.schedulerCurrentTimestamp());
andrewm@0 237 /*
andrewm@0 238 // Send raw OSC message if enabled
andrewm@0 239 if(sendRawOscMessages_) {
andrewm@0 240 keyboard_.sendMessage("/touchkeys/raw-off", "iii",
andrewm@0 241 octave, key, frame,
andrewm@0 242 LO_ARGS_END );
andrewm@0 243 }
andrewm@0 244 */
andrewm@0 245 }
andrewm@0 246 else {
andrewm@0 247 if(keyboard_.key(nextTouchMidiNote_) != 0)
andrewm@0 248 keyboard_.key(nextTouchMidiNote_)->touchInsertFrame(nextTouch_,
andrewm@0 249 keyboard_.schedulerCurrentTimestamp());
andrewm@0 250 /*if(sendRawOscMessages_) {
andrewm@0 251 keyboard_.sendMessage("/touchkeys/raw", "iiifffffff",
andrewm@0 252 octave, key, frame,
andrewm@0 253 sliderPosition[0],
andrewm@0 254 sliderSize[0],
andrewm@0 255 sliderPosition[1],
andrewm@0 256 sliderSize[1],
andrewm@0 257 sliderPosition[2],
andrewm@0 258 sliderSize[2],
andrewm@0 259 sliderPositionH,
andrewm@0 260 LO_ARGS_END );
andrewm@0 261 }*/
andrewm@0 262 }
andrewm@0 263 }
andrewm@0 264
andrewm@0 265 bool newTouchFound = readNextTouchFrame();
andrewm@0 266
andrewm@0 267 // Go through next touch frames and send them as long as the timestamp is not in the future
andrewm@0 268 while(newTouchFound && (nextTouchTimestamp_ + timestampOffset_) <= playbackScheduler_.currentTimestamp()) {
andrewm@0 269 if(nextTouchMidiNote_ >= 0 && nextTouchMidiNote_ < 128) {
andrewm@0 270 // Use PianoKeyboard timestamps for the messages we send since our scheduler
andrewm@0 271 // may have a different idea of time.
andrewm@0 272
andrewm@0 273 if(keyboard_.key(nextTouchMidiNote_) != 0) {
andrewm@0 274 if(nextTouch_.count == 0)
andrewm@0 275 keyboard_.key(nextTouchMidiNote_)->touchOff(keyboard_.schedulerCurrentTimestamp());
andrewm@0 276 else
andrewm@0 277 keyboard_.key(nextTouchMidiNote_)->touchInsertFrame(nextTouch_,
andrewm@0 278 keyboard_.schedulerCurrentTimestamp());
andrewm@0 279 }
andrewm@0 280 }
andrewm@0 281
andrewm@53 282 newTouchFound = readNextTouchFrame();
andrewm@0 283 }
andrewm@0 284
andrewm@0 285 if(!newTouchFound) { // EOF or error
andrewm@0 286 usingTouch_ = false;
andrewm@0 287 if(!usingMidi_)
andrewm@0 288 playing_ = paused_ = false;
andrewm@0 289 return 0;
andrewm@0 290 }
andrewm@0 291 else // Return the timestamp of the next call
andrewm@0 292 return (nextTouchTimestamp_ + timestampOffset_);
andrewm@0 293 }
andrewm@0 294
andrewm@0 295 timestamp_type LogPlayback::nextMidiEvent() {
andrewm@0 296 if(!playing_ || !open_ || paused_)
andrewm@0 297 return 0;
andrewm@0 298
andrewm@0 299 // TODO: handle playback rate
andrewm@0 300
andrewm@0 301 // Play the most recent stored touch frame
andrewm@53 302 if(nextMidi_.size() >= 3) {
andrewm@53 303 if((nextMidi_[0] & 0xF0) == 0xD0) // channel aftertouch has 2 bytes
andrewm@53 304 midiInputController_.handleIncomingMidiMessage(0, MidiMessage(nextMidi_[0], nextMidi_[1]));
andrewm@53 305 else
andrewm@53 306 midiInputController_.handleIncomingMidiMessage(0, MidiMessage(nextMidi_[0], nextMidi_[1], nextMidi_[2]));
andrewm@53 307 }
andrewm@0 308 //midiInputController_.rtMidiCallback(nextMidiTimestamp_ - lastMidiTimestamp_, &nextMidi_, 0);
andrewm@0 309 lastMidiTimestamp_ = nextMidiTimestamp_;
andrewm@0 310
andrewm@0 311 bool newMidiEventFound = readNextMidiFrame();
andrewm@0 312
andrewm@0 313 // Go through next touch frames and send them as long as the timestamp is not in the future
andrewm@0 314 while(newMidiEventFound && (nextMidiTimestamp_ + timestampOffset_) <= playbackScheduler_.currentTimestamp()) {
andrewm@53 315 if(nextMidi_.size() >= 3) {
andrewm@53 316 if((nextMidi_[0] & 0xF0) == 0xD0) // channel aftertouch has 2 bytes
andrewm@53 317 midiInputController_.handleIncomingMidiMessage(0, MidiMessage(nextMidi_[0], nextMidi_[1]));
andrewm@53 318 else
andrewm@53 319 midiInputController_.handleIncomingMidiMessage(0, MidiMessage(nextMidi_[0], nextMidi_[1], nextMidi_[2]));
andrewm@53 320 }
andrewm@0 321 //midiInputController_.rtMidiCallback(nextMidiTimestamp_ - lastMidiTimestamp_, &nextMidi_, 0);
andrewm@0 322 lastMidiTimestamp_ = nextMidiTimestamp_;
andrewm@0 323
andrewm@0 324 readNextMidiFrame();
andrewm@0 325 }
andrewm@0 326
andrewm@0 327 if(!newMidiEventFound) { // EOF or error
andrewm@0 328 usingMidi_ = false;
andrewm@0 329 if(!usingTouch_)
andrewm@0 330 playing_ = paused_ = false;
andrewm@0 331 return 0;
andrewm@0 332 }
andrewm@0 333 else // Return the timestamp of the next call
andrewm@0 334 return (nextMidiTimestamp_ + timestampOffset_);
andrewm@0 335 }
andrewm@0 336
andrewm@0 337 // Retrieve the next key touch frame from the log file
andrewm@0 338 // Return true if a touch was found, false if EOF or an error occurred
andrewm@0 339 bool LogPlayback::readNextTouchFrame() {
andrewm@0 340 int frameCounter;
andrewm@0 341
andrewm@0 342 try {
andrewm@0 343 touchLog_.read((char *)&nextTouchTimestamp_, sizeof(timestamp_type));
andrewm@0 344 touchLog_.read((char *)&frameCounter, sizeof(int));
andrewm@0 345 touchLog_.read((char *)&nextTouchMidiNote_, sizeof(int));
andrewm@0 346 touchLog_.read((char *)&nextTouch_, sizeof(KeyTouchFrame));
andrewm@0 347 }
andrewm@0 348 catch(...) {
andrewm@0 349 cout << "error reading touch\n";
andrewm@0 350 return false;
andrewm@0 351 }
andrewm@0 352 if(touchLog_.eof()) {
andrewm@0 353 cout << "Touch log playback finished\n";
andrewm@0 354 return false;
andrewm@0 355 }
andrewm@0 356
andrewm@0 357 //cout << "read touch on key " << nextTouchMidiNote_ << " timestamp " << nextTouchTimestamp_ << endl;
andrewm@0 358
andrewm@0 359 // TODO: what about frameCounter
andrewm@0 360
andrewm@0 361 return true;
andrewm@0 362 }
andrewm@0 363
andrewm@0 364 // Retrieve the next MIDI frame from the log file
andrewm@0 365 // Return true if an event was found, false if EOF or an error occurred
andrewm@0 366 bool LogPlayback::readNextMidiFrame() {
andrewm@0 367 int midi0, midi1, midi2;
andrewm@0 368
andrewm@0 369 try {
andrewm@0 370 midiLog_.read((char*)&nextMidiTimestamp_, sizeof (timestamp_type));
andrewm@0 371 midiLog_.read((char*)&midi0, sizeof (int));
andrewm@0 372 midiLog_.read((char*)&midi1, sizeof (int));
andrewm@0 373 midiLog_.read((char*)&midi2, sizeof (int));
andrewm@0 374 }
andrewm@0 375 catch(...) {
andrewm@0 376 cout << "error reading MIDI\n";
andrewm@0 377 return false;
andrewm@0 378 }
andrewm@0 379
andrewm@0 380 if(midiLog_.eof()) {
andrewm@0 381 cout << "MIDI log playback finished\n";
andrewm@0 382 return false;
andrewm@0 383 }
andrewm@0 384
andrewm@0 385 nextMidi_.clear();
andrewm@0 386 nextMidi_.push_back((unsigned char)midi0);
andrewm@0 387 nextMidi_.push_back((unsigned char)midi1);
andrewm@0 388 nextMidi_.push_back((unsigned char)midi2);
andrewm@0 389
andrewm@0 390 //cout << "read MIDI data " << (int)midi0 << " " << (int)midi1 << " " << (int)midi2 << endl;
andrewm@0 391
andrewm@0 392 return true;
andrewm@0 393 }