andrewm@0: /* andrewm@0: TouchKeys: multi-touch musical keyboard control software andrewm@0: Copyright (c) 2013 Andrew McPherson andrewm@0: andrewm@0: This program is free software: you can redistribute it and/or modify andrewm@0: it under the terms of the GNU General Public License as published by andrewm@0: the Free Software Foundation, either version 3 of the License, or andrewm@0: (at your option) any later version. andrewm@0: andrewm@0: This program is distributed in the hope that it will be useful, andrewm@0: but WITHOUT ANY WARRANTY; without even the implied warranty of andrewm@0: MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the andrewm@0: GNU General Public License for more details. andrewm@0: andrewm@0: You should have received a copy of the GNU General Public License andrewm@0: along with this program. If not, see . andrewm@0: andrewm@0: ===================================================================== andrewm@0: andrewm@0: LogPlayback.cpp: basic functions for playing back a recorded TouchKeys log. andrewm@0: */ andrewm@0: andrewm@0: #include "LogPlayback.h" andrewm@0: andrewm@0: LogPlayback::LogPlayback(PianoKeyboard& keyboard, MidiInputController& midi) andrewm@0: : keyboard_(keyboard), midiInputController_(midi), open_(false), playing_(false), paused_(false), andrewm@0: usingTouch_(false), usingMidi_(false), playbackRate_(1.0), andrewm@0: nextTouchMidiNote_(0), nextTouchTimestamp_(0), nextMidiTimestamp_(0), andrewm@0: lastMidiTimestamp_(0), timestampOffset_(0) andrewm@0: { andrewm@0: // Create a statically bound call to the performMapping() method that andrewm@0: // we use each time we schedule a new mapping andrewm@0: touchAction_ = boost::bind(&LogPlayback::nextTouchEvent, this); andrewm@0: midiAction_ = boost::bind(&LogPlayback::nextMidiEvent, this); andrewm@0: } andrewm@0: andrewm@0: LogPlayback::~LogPlayback() andrewm@0: { andrewm@0: if(open_) andrewm@0: closeLogFiles(); andrewm@0: } andrewm@0: andrewm@0: // File management. Open a touch and/or MIDI file. Returns true on success. andrewm@0: // Pass a blank string to either one of the paths to not use that form of data capture andrewm@0: bool LogPlayback::openLogFiles(string const& touchPath, string const& midiPath) { andrewm@0: touchLog_.open (touchPath.c_str(), ios::in | ios::binary); andrewm@0: midiLog_.open (midiPath.c_str(), ios::in | ios::binary); andrewm@0: andrewm@0: usingTouch_ = touchLog_.is_open(); andrewm@0: usingMidi_ = midiLog_.is_open(); andrewm@0: andrewm@0: // Check for bad file paths andrewm@0: if(!usingTouch_ && touchPath != "") andrewm@0: return false; andrewm@0: if(!usingMidi_ && midiPath != "") andrewm@0: return false; andrewm@0: if(!usingTouch_ && !usingMidi_) andrewm@0: return false; andrewm@0: andrewm@0: // Set defaults andrewm@0: open_ = true; andrewm@0: playing_ = paused_ = false; andrewm@0: playbackRate_ = 1.0; andrewm@0: return true; andrewm@0: } andrewm@0: andrewm@0: // Close the current files andrewm@0: void LogPlayback::closeLogFiles() { andrewm@0: if(!open_) andrewm@0: return; andrewm@0: if(playing_) andrewm@0: stopPlayback(); andrewm@0: touchLog_.close(); andrewm@0: midiLog_.close(); andrewm@0: open_ = playing_ = paused_ = false; andrewm@0: } andrewm@0: andrewm@0: // Start, stop, pause, resume andrewm@0: void LogPlayback::startPlayback(timestamp_type startingTimestamp) { andrewm@0: if(!open_) andrewm@0: return; andrewm@0: andrewm@0: // Start the playback scheduler thread andrewm@0: playbackScheduler_.start(0); andrewm@0: andrewm@0: timestamp_type firstTouchTimestamp, firstMidiTimestamp; andrewm@0: andrewm@0: // Register actions on the scheduler thread andrewm@0: if(usingTouch_) { andrewm@0: readNextTouchFrame(); andrewm@0: firstTouchTimestamp = nextTouchTimestamp_; andrewm@0: timestampOffset_ = playbackScheduler_.currentTimestamp() - firstTouchTimestamp; andrewm@0: } andrewm@0: if(usingMidi_) { andrewm@0: readNextMidiFrame(); andrewm@0: firstMidiTimestamp = nextMidiTimestamp_; andrewm@0: lastMidiTimestamp_ = nextMidiTimestamp_; // First timestamp difference is 0 andrewm@0: andrewm@0: // Timestamp offset is to first MIDI event, unless there's an earlier touch event andrewm@0: if(!(usingTouch_ && (firstTouchTimestamp < firstMidiTimestamp))) andrewm@0: timestampOffset_ = playbackScheduler_.currentTimestamp() - firstMidiTimestamp; andrewm@0: } andrewm@0: andrewm@53: cout << "Touch " << firstTouchTimestamp << " MIDI " << firstMidiTimestamp << " offset " << timestampOffset_ << endl; andrewm@53: andrewm@0: playing_ = true; andrewm@0: paused_ = false; andrewm@0: andrewm@0: if(usingTouch_) andrewm@0: playbackScheduler_.schedule(this, touchAction_, playbackScheduler_.currentTimestamp()); andrewm@0: if(usingMidi_) andrewm@0: playbackScheduler_.schedule(this, midiAction_, playbackScheduler_.currentTimestamp()); andrewm@0: } andrewm@0: andrewm@0: void LogPlayback::stopPlayback() { andrewm@0: playing_ = paused_ = false; andrewm@0: andrewm@0: // Stop the playback scheduler thread andrewm@0: playbackScheduler_.stop(); andrewm@0: playbackScheduler_.unschedule(this); andrewm@0: } andrewm@0: andrewm@0: // Pause a currently playing file. Save the pause time so the offset andrewm@0: // can be recalculated when it resumes andrewm@0: void LogPlayback::pausePlayback() { andrewm@0: if(open_ && playing_) { andrewm@0: playbackScheduler_.unschedule(this); andrewm@0: andrewm@0: // TODO: consider thread safety: what happens if this comes during one of the scheduled calls? andrewm@0: andrewm@0: paused_ = true; andrewm@0: pauseTimestamp_ = playbackScheduler_.currentTimestamp(); andrewm@0: } andrewm@0: } andrewm@0: andrewm@0: // Resume playback after a pause andrewm@0: void LogPlayback::resumePlayback() { andrewm@0: if(paused_) { andrewm@0: paused_ = false; andrewm@0: timestamp_type resumeTimestamp = playbackScheduler_.currentTimestamp(); andrewm@0: andrewm@0: // Update the timestamp offset andrewm@0: timestampOffset_ += resumeTimestamp - pauseTimestamp_; andrewm@0: andrewm@0: // Reschedule calls andrewm@0: if(usingTouch_) andrewm@0: playbackScheduler_.schedule(this, touchAction_, nextTouchTimestamp_ + timestampOffset_); andrewm@0: if(usingMidi_) andrewm@0: playbackScheduler_.schedule(this, touchAction_, nextMidiTimestamp_ + timestampOffset_); andrewm@0: } andrewm@0: } andrewm@0: andrewm@0: // Seek to a timestamp in the file andrewm@0: void LogPlayback::seekPlayback(timestamp_type newTimestamp) { andrewm@0: // Advance through the file until we reach the indicated timestamp andrewm@0: andrewm@0: if(!playing_ || !open_) andrewm@0: return; andrewm@0: andrewm@0: // Remove any future actions while we perform the seek andrewm@0: playbackScheduler_.unschedule(this); andrewm@0: //timestamp_diff_type offset = 0; andrewm@0: timestamp_type firstUpcomingTimestamp = 0; andrewm@0: andrewm@0: if(usingTouch_) { andrewm@0: //timestamp_type lastTimestamp = nextTouchTimestamp_; andrewm@0: andrewm@0: // TODO: this assumes the seek is moving forward andrewm@0: while(nextTouchTimestamp_ <= newTimestamp) { andrewm@0: if(!readNextTouchFrame()) { // EOF or error andrewm@0: usingTouch_ = false; andrewm@0: if(!usingMidi_) andrewm@0: playing_ = paused_ = false; andrewm@0: break; andrewm@0: } andrewm@0: } andrewm@0: andrewm@0: // Now we have the first event scheduled after the seek location andrewm@0: // Update timestamp offset to continue playback from here. andrewm@0: andrewm@0: //offset = nextTouchTimestamp_ - lastTimestamp; andrewm@0: firstUpcomingTimestamp = nextTouchTimestamp_; andrewm@0: } andrewm@0: if(usingMidi_) { andrewm@0: //timestamp_type lastTimestamp = nextMidiTimestamp_; andrewm@0: andrewm@0: // TODO: this assumes the seek is moving forward andrewm@0: while(nextMidiTimestamp_ <= newTimestamp) { andrewm@0: if(!readNextMidiFrame()) { // EOF or error andrewm@0: usingMidi_ = false; andrewm@0: if(!usingTouch_) andrewm@0: playing_ = paused_ = false; andrewm@0: break; andrewm@0: } andrewm@0: } andrewm@0: andrewm@0: // Now we have the first event scheduled after the seek location andrewm@0: // Update timestamp offset to continue playback from here. andrewm@0: // Use whichever event came first andrewm@0: andrewm@0: //if(!(usingTouch_ && (nextMidiTimestamp_ - lastTimestamp) > offset)) andrewm@0: // offset = (nextMidiTimestamp_ - lastTimestamp); andrewm@0: if(!usingTouch_ || nextMidiTimestamp_ < nextTouchTimestamp_); andrewm@0: firstUpcomingTimestamp = nextMidiTimestamp_; andrewm@0: } andrewm@0: andrewm@0: // Update the timestamp offset andrewm@0: timestampOffset_ = playbackScheduler_.currentTimestamp() - firstUpcomingTimestamp; andrewm@0: andrewm@0: if(usingTouch_) andrewm@0: playbackScheduler_.schedule(this, touchAction_, nextTouchTimestamp_ + timestampOffset_); andrewm@0: if(usingMidi_) andrewm@0: playbackScheduler_.schedule(this, midiAction_, nextMidiTimestamp_ + timestampOffset_); andrewm@0: } andrewm@0: andrewm@0: // Change the playback rate (1.0 being the standard speed) andrewm@0: void LogPlayback::changePlaybackRate(float rate) { andrewm@0: playbackRate_ = rate; andrewm@0: } andrewm@0: andrewm@0: // Events the scheduler calls when the right time elapses. Find the andrewm@0: // next touch or MIDI event and play it back andrewm@0: timestamp_type LogPlayback::nextTouchEvent() { andrewm@0: if(!playing_ || !open_ || paused_) andrewm@0: return 0; andrewm@0: andrewm@0: // TODO: handle playback rate andrewm@0: andrewm@0: // Play the most recent stored touch frame andrewm@0: if(nextTouchMidiNote_ >= 0 && nextTouchMidiNote_ < 128) { andrewm@0: // Use PianoKeyboard timestamps for the messages we send since our scheduler andrewm@0: // may have a different idea of time. andrewm@0: andrewm@0: if(nextTouch_.count == 0) { andrewm@0: if(keyboard_.key(nextTouchMidiNote_) != 0) andrewm@0: keyboard_.key(nextTouchMidiNote_)->touchOff(keyboard_.schedulerCurrentTimestamp()); andrewm@0: /* andrewm@0: // Send raw OSC message if enabled andrewm@0: if(sendRawOscMessages_) { andrewm@0: keyboard_.sendMessage("/touchkeys/raw-off", "iii", andrewm@0: octave, key, frame, andrewm@0: LO_ARGS_END ); andrewm@0: } andrewm@0: */ andrewm@0: } andrewm@0: else { andrewm@0: if(keyboard_.key(nextTouchMidiNote_) != 0) andrewm@0: keyboard_.key(nextTouchMidiNote_)->touchInsertFrame(nextTouch_, andrewm@0: keyboard_.schedulerCurrentTimestamp()); andrewm@0: /*if(sendRawOscMessages_) { andrewm@0: keyboard_.sendMessage("/touchkeys/raw", "iiifffffff", andrewm@0: octave, key, frame, andrewm@0: sliderPosition[0], andrewm@0: sliderSize[0], andrewm@0: sliderPosition[1], andrewm@0: sliderSize[1], andrewm@0: sliderPosition[2], andrewm@0: sliderSize[2], andrewm@0: sliderPositionH, andrewm@0: LO_ARGS_END ); andrewm@0: }*/ andrewm@0: } andrewm@0: } andrewm@0: andrewm@0: bool newTouchFound = readNextTouchFrame(); andrewm@0: andrewm@0: // Go through next touch frames and send them as long as the timestamp is not in the future andrewm@0: while(newTouchFound && (nextTouchTimestamp_ + timestampOffset_) <= playbackScheduler_.currentTimestamp()) { andrewm@0: if(nextTouchMidiNote_ >= 0 && nextTouchMidiNote_ < 128) { andrewm@0: // Use PianoKeyboard timestamps for the messages we send since our scheduler andrewm@0: // may have a different idea of time. andrewm@0: andrewm@0: if(keyboard_.key(nextTouchMidiNote_) != 0) { andrewm@0: if(nextTouch_.count == 0) andrewm@0: keyboard_.key(nextTouchMidiNote_)->touchOff(keyboard_.schedulerCurrentTimestamp()); andrewm@0: else andrewm@0: keyboard_.key(nextTouchMidiNote_)->touchInsertFrame(nextTouch_, andrewm@0: keyboard_.schedulerCurrentTimestamp()); andrewm@0: } andrewm@0: } andrewm@0: andrewm@53: newTouchFound = readNextTouchFrame(); andrewm@0: } andrewm@0: andrewm@0: if(!newTouchFound) { // EOF or error andrewm@0: usingTouch_ = false; andrewm@0: if(!usingMidi_) andrewm@0: playing_ = paused_ = false; andrewm@0: return 0; andrewm@0: } andrewm@0: else // Return the timestamp of the next call andrewm@0: return (nextTouchTimestamp_ + timestampOffset_); andrewm@0: } andrewm@0: andrewm@0: timestamp_type LogPlayback::nextMidiEvent() { andrewm@0: if(!playing_ || !open_ || paused_) andrewm@0: return 0; andrewm@0: andrewm@0: // TODO: handle playback rate andrewm@0: andrewm@0: // Play the most recent stored touch frame andrewm@53: if(nextMidi_.size() >= 3) { andrewm@53: if((nextMidi_[0] & 0xF0) == 0xD0) // channel aftertouch has 2 bytes andrewm@53: midiInputController_.handleIncomingMidiMessage(0, MidiMessage(nextMidi_[0], nextMidi_[1])); andrewm@53: else andrewm@53: midiInputController_.handleIncomingMidiMessage(0, MidiMessage(nextMidi_[0], nextMidi_[1], nextMidi_[2])); andrewm@53: } andrewm@0: //midiInputController_.rtMidiCallback(nextMidiTimestamp_ - lastMidiTimestamp_, &nextMidi_, 0); andrewm@0: lastMidiTimestamp_ = nextMidiTimestamp_; andrewm@0: andrewm@0: bool newMidiEventFound = readNextMidiFrame(); andrewm@0: andrewm@0: // Go through next touch frames and send them as long as the timestamp is not in the future andrewm@0: while(newMidiEventFound && (nextMidiTimestamp_ + timestampOffset_) <= playbackScheduler_.currentTimestamp()) { andrewm@53: if(nextMidi_.size() >= 3) { andrewm@53: if((nextMidi_[0] & 0xF0) == 0xD0) // channel aftertouch has 2 bytes andrewm@53: midiInputController_.handleIncomingMidiMessage(0, MidiMessage(nextMidi_[0], nextMidi_[1])); andrewm@53: else andrewm@53: midiInputController_.handleIncomingMidiMessage(0, MidiMessage(nextMidi_[0], nextMidi_[1], nextMidi_[2])); andrewm@53: } andrewm@0: //midiInputController_.rtMidiCallback(nextMidiTimestamp_ - lastMidiTimestamp_, &nextMidi_, 0); andrewm@0: lastMidiTimestamp_ = nextMidiTimestamp_; andrewm@0: andrewm@0: readNextMidiFrame(); andrewm@0: } andrewm@0: andrewm@0: if(!newMidiEventFound) { // EOF or error andrewm@0: usingMidi_ = false; andrewm@0: if(!usingTouch_) andrewm@0: playing_ = paused_ = false; andrewm@0: return 0; andrewm@0: } andrewm@0: else // Return the timestamp of the next call andrewm@0: return (nextMidiTimestamp_ + timestampOffset_); andrewm@0: } andrewm@0: andrewm@0: // Retrieve the next key touch frame from the log file andrewm@0: // Return true if a touch was found, false if EOF or an error occurred andrewm@0: bool LogPlayback::readNextTouchFrame() { andrewm@0: int frameCounter; andrewm@0: andrewm@0: try { andrewm@0: touchLog_.read((char *)&nextTouchTimestamp_, sizeof(timestamp_type)); andrewm@0: touchLog_.read((char *)&frameCounter, sizeof(int)); andrewm@0: touchLog_.read((char *)&nextTouchMidiNote_, sizeof(int)); andrewm@0: touchLog_.read((char *)&nextTouch_, sizeof(KeyTouchFrame)); andrewm@0: } andrewm@0: catch(...) { andrewm@0: cout << "error reading touch\n"; andrewm@0: return false; andrewm@0: } andrewm@0: if(touchLog_.eof()) { andrewm@0: cout << "Touch log playback finished\n"; andrewm@0: return false; andrewm@0: } andrewm@0: andrewm@0: //cout << "read touch on key " << nextTouchMidiNote_ << " timestamp " << nextTouchTimestamp_ << endl; andrewm@0: andrewm@0: // TODO: what about frameCounter andrewm@0: andrewm@0: return true; andrewm@0: } andrewm@0: andrewm@0: // Retrieve the next MIDI frame from the log file andrewm@0: // Return true if an event was found, false if EOF or an error occurred andrewm@0: bool LogPlayback::readNextMidiFrame() { andrewm@0: int midi0, midi1, midi2; andrewm@0: andrewm@0: try { andrewm@0: midiLog_.read((char*)&nextMidiTimestamp_, sizeof (timestamp_type)); andrewm@0: midiLog_.read((char*)&midi0, sizeof (int)); andrewm@0: midiLog_.read((char*)&midi1, sizeof (int)); andrewm@0: midiLog_.read((char*)&midi2, sizeof (int)); andrewm@0: } andrewm@0: catch(...) { andrewm@0: cout << "error reading MIDI\n"; andrewm@0: return false; andrewm@0: } andrewm@0: andrewm@0: if(midiLog_.eof()) { andrewm@0: cout << "MIDI log playback finished\n"; andrewm@0: return false; andrewm@0: } andrewm@0: andrewm@0: nextMidi_.clear(); andrewm@0: nextMidi_.push_back((unsigned char)midi0); andrewm@0: nextMidi_.push_back((unsigned char)midi1); andrewm@0: nextMidi_.push_back((unsigned char)midi2); andrewm@0: andrewm@0: //cout << "read MIDI data " << (int)midi0 << " " << (int)midi1 << " " << (int)midi2 << endl; andrewm@0: andrewm@0: return true; andrewm@0: }