Live Synchronization, Tap Tempo, MIDI, and abusing the Transport

classic Classic list List threaded Threaded
9 messages Options
Reply | Threaded
Open this post in threaded view
|

Live Synchronization, Tap Tempo, MIDI, and abusing the Transport

Gabriel M. Beddingfield

Hello!  I've been working on a Jack Timebase master that can be
manipulated to provide synchronization of the computers to a live band.
  Would any of you be willing to peek at the code?  Is this abusing the
Transport concept too much?

I'm creating a setup for using audio in live performances, and I haven't
found anything for synchronizing the computers to the people (in the
event of a discrepency).  In almost every case, the common conclusion
has been "make the people match the computer."

Not satisifed with that, I've been working on a way to provide a
tap-tempo sync that will adjust the Jack Transport to the tempo of the
live ensemble.  It also provides a means of "skipping" a beat or
"inserting" a beat (for times when the singer comes in early -- a common
occurrence in our band).  The intent is that taps and commands can be
given via a MIDI footswitch to control the transport.  Changes to the
transport will be reflected in the sequencer (a timebase slave).

The code (C++) is attached, and has quite a bit of documentation
embedded.  I've been testing it with jack 0.101.1 and using seq24 as the
sequencer.

Peace,
Gabriel


/**********************************************-*- indent-tabs-mode:nil; -*-
 *                                                                         *
 *   livetime.cpp                                                          *
 *                                                                         *
 *   Copyright (C) 2008 by Gabriel M. Beddingfield                         *
 *   [hidden email]                                                    *
 *                                                                         *
 *   "For of him [God], and through him, and unto him, are all things.     *
 *   To him be the glory for ever. Amen." (Romans 11:36)                   *
 *                                                                         *
 *   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  *
 *   the Free Software Foundation; version 2 of the License                *
 *                                                                         *
 *   This program is distributed in the hope that it will be useful,       *
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
 *   GNU General Public License for more details.                          *
 *                                                                         *
 *   You should have received a copy of the GNU General Public License     *
 *   along with this program; if not, write to the                         *
 *   Free Software Foundation, Inc.,                                       *
 *   59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.             *
 ***************************************************************************/

/* livetime.cpp
   Gabriel M. Beddingfield
   2008-01-29

   Jack Transport master with tap-tempo sync.  For use with the Jack
   Audio Connection Toolkit.  Tested with jackd 0.101.1 on Debian 4.0
   (etch).

   The purpose of this program is to provide a Jack Transport
   synchronization for the purpose of live performance.  Using MIDI on
   stage often forces the band to follow the tempo of the sequencer.
   If a musician makes an error (e.g. a singer comes in 1 beat early),
   there is usually no means to recover.  This program is intended to
   fill that need for live performance.

   This program registers itself as a Jack Timebase Master.  Once the
   transport is started, Giving tempo taps will change the tempo, and
   ramp to synchronize the beat with the taps that were given.  It
   also has the ability to skip a beat (by changing the tempo to 2x
   for one beat), and to insert a beat (by changing the tempo to .5x
   for one beat).

   See below for build instructions.

   To understand the code, the main objects are these:

   main()   - a scaffolding for testing purposes.

   LiveTime - This is the Jack Timebase master.  It keeps track of
              where you are and makes the adjustments that you
              request.

   TimeBase - This struct is only for use with the class LiveTime.
              It provides a reference point from which time is
              measured.  For example, it will know that frame
              562891 was bar:beat:tick = 10:2:0134, and the tempo
              there is 97.0 beats per minute.  It can calculate
              the frame of 11:1:0000 (or any bar:beat:tick) and
              and also the bar:beat:tick for any frame.

   TapTempo - This class manages the tap tempo feature.  It will
              accept 2 or more taps (configurable).  If more than two
              taps are given, the next beat and tempo are
              statistically determined (i.e. least squares) as opposed
              to averaging the taps (i.e. disregarding everything
              between the first and last tap).

   Known limitations:

   o If you change tempo, insert a beat, skip a beat, etc., then time
     0.00 will _not_ be bar 1, beat 1, tick 0.  It takes a special
     command to allow this to happen... and that command is not yet
     implemented.  :-P

   o It is assumed that you are making minor tempo corrections.  If
     you use the tap tempo to make drastic tempo corrections, it does
     not ramp smoothly.  For example, I've seen it dwell at 80 bpm
     when changing from 120 bpm to 180 bpm.

   o The taps don't work if the transport isn't running.  In fact, if
     you try it, you will probably crash the program (division by
     zero).

   o Yeah, tapping with the keyboard by typing T, <Enter> is
     wierd.  I intend to replace that with MIDI.
   
   Known Bugs:

   o When changing the tempo with TapTempo, there's a time between NOW
     and LiveTime::m_ramp.m_frame where we're using the NEW
     LiveTime::m_normal before we're supposed to.  This makes the tap
     tempo even jumpier.

   To-do:

   o Divide up the files like a normal project.  :-)

   o Implement the stuff mentioned above as "I intend to..."

   o Change the Mutex to a ReadWriteMutex (many readers, one writer).
     The Mutex code is currently the bottleneck.

   o Some private methods require that the mutex already be locked.
     There's no good way to test that... and it just doesn't seem
     right.  There's no protection against some rogue programmer (me)
     from using these methods without checking the header to see
     "mutex must be locked."

   o Implement the 'const' keyword on class methods where appropriate.
     Note that this typically requires that the Mutex in the class be
     a pointer rather than a heavy object -- which makes copy
     constructors more complicated.

   o Better debugging message scheme.

   BUILD INSTRUCTIONS
   ==================

   Library requirements:
     o C Standard Libraries
     o C++ Standard Libraries
     o POSIX libraries (especially pthread)
     o Jack Audio Connection Kit library

   Compiling:

   g++ -o livetime livetime.cpp -ljack -lpthread

 */

#define _REENTRANT

// Standard C++ libraries
#include <iostream>
#include <iomanip>
#include <cstdio>
#include <memory>
#include <stdexcept>
#include <cmath>
#include <cassert>

// POSIX libraries
#include <pthread.h>
#include <unistd.h> // sleep()

// Jack libraries
#include <jack/jack.h>
#include <jack/transport.h>
#include <jack/types.h>
// #include <alsa/asoundlib.h> // For midi with jack < 103

#define MARK(x) cerr << #x << endl

//Darn!  Requires libjack >= 0.103.0, but etch uses 0.101
//#define JACK_DEFAULT_MIDI_TYPE "8 bit raw midi"

using namespace std;

// class Mutex
//
// Provides a portable Mutex class interface for synchronizing
// concurrent tasks.
//
class Mutex
{
   
public:
    typedef enum { Locked, Unlocked } MutexState;

    Mutex(void) throw(std::runtime_error);
    Mutex(const Mutex& o) throw(std::runtime_error);

    ~Mutex();

    void lock(void) throw(std::runtime_error);
    bool trylock(void) throw();
    bool unlock(void) throw();
    MutexState status(void) const { return state; }

private:    
    pthread_mutex_t mutex;
    MutexState state;
};

// class MutexLocker
//
// Provides an exception-safe interfact to a Mutex.
// If an exception is called, the destructor will
// be called and the mutex will be unlocked.
// Otherwise you have to do something like:
//
class MutexLocker
{
    Mutex* mutex;
public:
    MutexLocker(Mutex* m) : mutex(m) {
        mutex->lock();
    }

    ~MutexLocker() {
        mutex->unlock();
    }

    bool unlock(void) {
        return mutex->unlock();
    }
};

// struct TimeBase
//
// This is a convenience class for the LiveTime class.
// It provides an anchor point and BBT calculation.
// This allows the LiveTime class to prepare new anchor
// points and quickly switch between them.
//
struct TimeBase
{
    jack_nframes_t m_frame;
    int32_t m_bar,
        m_beat,
        m_tick,
        m_beats_per_bar,
        m_ticks_per_beat;
    double m_beats_per_minute,
        m_frames_per_beat,
        m_frames_per_tick,
        m_frames_per_measure,
        m_anchor_beat_abs;  // first beat is 0.0
    Mutex m_mutex;

    void init_frame_rates(jack_nframes_t rate) {
        m_anchor_beat_abs = (double)(m_bar-1.0)*m_beats_per_bar
            + (m_beat-1.0)
            + (double)m_tick/m_ticks_per_beat;
        m_frames_per_beat = (double)rate * 60.0 / m_beats_per_minute;
        m_frames_per_tick = m_frames_per_beat / m_ticks_per_beat;
        m_frames_per_measure = m_frames_per_beat * m_beats_per_bar;
    }

    void set_jack_position_meta(jack_position_t* pos) {
        pos->valid = JackPositionBBT;
        pos->beats_per_bar = (int32_t)m_beats_per_bar;
        pos->beat_type = 4;
        pos->ticks_per_beat = (int32_t)m_ticks_per_beat;
        pos->beats_per_minute = m_beats_per_minute;
    }

    void getBBT(jack_nframes_t fr,
                int32_t& bar,
                int32_t& beat,
                int32_t& tick);

    jack_nframes_t getFrame(double bar,
                            double beat,
                            double tick);
};

ostream& operator<<(ostream& os, TimeBase x)
{
    os << "[" << x.m_bar
       << ":" << x.m_beat
       << ":" << x.m_tick
       << "] " << x.m_frame
       << " (" << x.m_anchor_beat_abs
       << ") @ " << x.m_frames_per_beat
       << " f/beat (" << x.m_beats_per_minute
       << " bpm) and |" << x.m_beats_per_bar
       << ":" << x.m_ticks_per_beat << "|";
}

// class TapTempo
//
// This class provides calculation of tempo and downbeat,
// given a series of 2 or more taps.  See the tap() method.
//
class TapTempo
{
public:
    TapTempo() :
        m_timeout(2.0),
        m_taps_required(4),
        m_taps_received(0),
        m_last_tap(0.0),
        m_sum_taps(0.0),
        m_sum_i_taps(0.0),
        m_frame_rate(44100.0)
        {
        }

    // Properties
    unsigned getTapsRequired(void) const { return m_taps_required; }
    void setTapsRequired(unsigned t) {
        if(t>0) {
            MutexLocker mutex(&m_mutex);
            m_taps_required = t;
            invalidate_();
        }
    }
    double getFrameRate(void) const { return m_frame_rate; }
    void setFrameRate(double rate) {
        MutexLocker mutex(&m_mutex);
        m_frame_rate = rate;
        invalidate_();
    }
    double getTimeout(void) const { return m_timeout; }
    void setTimeout(double t) {
        MutexLocker mutex(&m_mutex);
        m_timeout = t;
    }
    void invalidate(void) {
        MutexLocker mutex(&m_mutex);
        invalidate_();
    }

    // TapTempo::tap()
    //
    // Register a tap to define a new tempo and downbeat sync.  Supply
    // a frame number (tap_) and a reference to a double where the new
    // tempo may be written.
    //
    // Returns 0 and leaves beats_per_minute unchanged if we have not
    // reached the desired number of taps.  If this tap is the last
    // needed tap, it returns the frame number of the next beat, and
    // changes beats_per_minute to the new tempo.
    //
    // See last tap for explaination of the math.  Briefly,
    // it's a least-squares implementation.
    //
    // Future: Using the frame number is worthless unless the
    // transport is running.  Need to change that implementation so
    // that it can record valid taps even while the transport is stopped.
    //
    jack_nframes_t tap(jack_nframes_t tap_, double& beats_per_minute) {
        assert(tap_ > m_last_tap);

        MutexLocker mutex(&m_mutex);
        double tap = (double)tap_/m_frame_rate;

        // Check for timeout and invalid condition.
        if((m_timeout < (tap - m_last_tap)) || (m_taps_received >= m_taps_required)) {
            invalidate_();
        }

        // Push the tap onto the accumulators
        // Asses ++m_taps_received because of zero-offset indices.
        // See derivation.
        m_last_tap = tap;
        m_sum_taps += tap;
        m_sum_i_taps += (double)m_taps_received * tap;
        ++m_taps_received;

        // Check if we've got the last one.
        if(m_taps_received == m_taps_required) {
            return lastTap(beats_per_minute);
        }
        return 0;
    }

private:
    // Mutex must already be locked before calling
    void invalidate_(void) {
        m_sum_taps = m_sum_i_taps = 0.0;
        m_taps_received = 0;
    }

    // Mutex must already be locked before calling
    jack_nframes_t lastTap(double& beats_per_minute);  

private:
    double m_timeout; // seconds
    unsigned m_taps_required;
    unsigned m_taps_received;
    double m_last_tap;  // In seconds (frame/m_frame_rate)
    double m_sum_taps;
    double m_sum_i_taps;
    double m_frame_rate; // frames per second
    Mutex m_mutex;
};

// class LiveTime
//
// This class acts as a Jack Transport Master for synchronizing
// the computer audio with a live ensemble during the perfomance.
// It features a Tap Tempo interface and an interface to insert
// or skip a beat.
//
class LiveTime
{
public:
    // Steady = m_normal is the current time base.
    //
    // Ramping = m_ramp is the current time base, until we reach
    //           m_normal.m_frame.
    //
    // RampQueued = when m_ramp.m_frame is reached, switch over to
    //              Ramping.
    //
    typedef enum { Steady, Ramping, RampQueued } livetime_state;

private:
    livetime_state m_state;
    Mutex* m_state_mutex;
    TimeBase m_normal, m_ramp;
    TapTempo m_taps;
    bool m_time_reset;  // Reset the timebase now.

    // Jack stuff
    jack_client_t* m_client;
    // snd_rawmidi_t* m_midi_in;

public:
    LiveTime();
    ~LiveTime();

    livetime_state getState(void) { return m_state; }

    // Tap settings
    unsigned getTapsRequired(void) const { return m_taps.getTapsRequired(); }
    void setTapsRequired(unsigned ct) { m_taps.setTapsRequired(ct); }
    double getTapsTimeout(void) const { return m_taps.getTimeout(); }
    void setTapsTimeout(double t) { m_taps.setTimeout(t); }

    // methods

    // Register a tap for redefining the beat and tempo.
    void tap(void) {
        double new_tempo;
        jack_nframes_t target;
        MutexLocker mutex(m_state_mutex);
        target = jack_get_current_transport_frame(m_client);
        if(target = m_taps.tap(target, new_tempo)) {
            rampAndSnapTo(target, new_tempo);
        }
    }
    void skip_beat(void);
    void insert_beat(void);

    // Jack timebase callback, routes to the private timebase_normal()
    // and timebase_ramping methods.
    static void timebase(
        jack_transport_state_t state,
        jack_nframes_t nframes,
        jack_position_t *pos,
        int new_pos,
        void *arg);

private:
    // Jack timebase callback when ramping
    void timebase_ramping(
        jack_transport_state_t state,
        jack_nframes_t nframes,
        jack_position_t *pos,
        int new_pos);

    // Jack timebase callback when in normal (Steady) mode.
    void timebase_normal(
        jack_transport_state_t state,
        jack_nframes_t nframes,
        jack_position_t *pos,
        int new_pos);

    // Called after all the taps have been given to m_taps.
    //
    // Mutex must already be locked before calling this
    void rampAndSnapTo(jack_nframes_t target, double new_tempo);
};

// main()
//
// A scaffolding to test the code.  Note that some debugging messages
// will be dumped to your screen... but you can still enter a command
// independant of this.
//
// Commands:
//
// f - skip forward by one beat.
// b - insert a beat ("back")
// t - enter a "tap" for the tap tempo feature.
// q - quit.
//
// You will have to follow each command by pressing <Enter>
//
int main(void)
{
    LiveTime t;

    char c[1024];
    bool running = true;
    while(running) {
        cin >> c;
        switch(c[0]) {
        case 'f':
            t.skip_beat();
            break;
        case 'b':
            t.insert_beat();
            break;
        case 't':
            t.tap();
            break;
        case 'q':
            running = false;
        }
        //snd_rawmidi_read(m_midi_in, c, 1);
        //printf("%02x \n", (unsigned)c[0]);
    }
    return 0;
}

LiveTime::LiveTime(void) :
    m_state(Steady),
    m_client(0),
    m_state_mutex(0),
    m_time_reset(false)
{
    m_state_mutex = new Mutex;

    // Initialize m_normal
    m_normal.m_frame = 0;
    m_normal.m_bar = 1;
    m_normal.m_beat = 1;
    m_normal.m_tick = 0;
    m_normal.m_beats_per_bar = 4;
    m_normal.m_ticks_per_beat = 360;
    m_normal.m_beats_per_minute = 120.0;
    m_ramp = m_normal;

    m_client = jack_client_open("LiveTime", JackNullOption, 0);
//     if(snd_rawmidi_open(&m_midi_in, 0, "virtual", 0)) {
//      cerr << "Couldn't open MIDI port" << endl;
//     }
    jack_activate(m_client);
    jack_set_timebase_callback(
        m_client,
        0,
        this->timebase,
        this);

    m_taps.setFrameRate((double)jack_get_sample_rate(m_client));
}

LiveTime::~LiveTime()
{
    jack_client_close(m_client);
}

// This should ONLY be called from LiveTime::timebase()
// otherwise, it would need to lock m_state_mutex.
void LiveTime::timebase_ramping(
        jack_transport_state_t state,
        jack_nframes_t nframes,
        jack_position_t *pos,
        int new_pos)
{
    assert(pos->frame >= m_ramp.m_frame);
    assert(pos->frame < m_normal.m_frame);
    assert(m_state == Ramping);
    //assert(m_state_mutex->status() == Mutex::Locked);

    if(new_pos || m_time_reset) {
        m_ramp.init_frame_rates(pos->frame_rate);
        m_ramp.set_jack_position_meta(pos);
        m_taps.setFrameRate((double)pos->frame_rate);
        m_time_reset = false;
    }
    m_ramp.getBBT(pos->frame, pos->bar, pos->beat, pos->tick);
}

// This should ONLY be called from LiveTime::timebase()
// otherwise, it would need to lock m_state_mutex.
void LiveTime::timebase_normal(
        jack_transport_state_t state,
        jack_nframes_t nframes,
        jack_position_t *pos,
        int new_pos)
{
    // assert((m_state != Ramping) && (pos->frame > m_anchor_frame));
    //assert(m_state_mutex->status() == Mutex::Locked);

    if(new_pos || m_time_reset) {
        m_normal.init_frame_rates(pos->frame_rate);
        m_normal.set_jack_position_meta(pos);
        m_taps.setFrameRate((double)pos->frame_rate);
        m_time_reset = false;
    }
    m_normal.getBBT(pos->frame, pos->bar, pos->beat, pos->tick);
}

// Jack TimeBase callback.
//
// This determines if we're in normal mode (Steady), ramping mode
// (Ramping), or if a ramp has been queued to start (RampQueued).  It
// calls the correct method depending on the state.
//
void LiveTime::timebase(
        jack_transport_state_t state,
        jack_nframes_t nframes,
        jack_position_t *pos,
        int new_pos,
        void *arg)
{
    LiveTime* o = static_cast<LiveTime*>(arg);
    MutexLocker mutex(o->m_state_mutex);

    if(((o->m_state == Ramping)||(o->m_state == RampQueued))
       && (pos->frame >= o->m_ramp.m_frame)) {

        if(pos->frame < o->m_normal.m_frame) {
            if(o->m_state == RampQueued) {
                o->m_state = Ramping;
                o->m_time_reset = true;
            }
            o->timebase_ramping(state, nframes, pos, new_pos);
        } else {
            cerr << "Going steady: (nframes, rs, af) = ("
                 << pos->frame << ", "
                 << o->m_ramp.m_frame << ", "
                 << o->m_normal.m_frame
                 << ")" << endl;
            o->m_state = Steady;
            o->m_time_reset = true;
            o->timebase_normal(state, nframes, pos, new_pos);
        }
    } else {
        o->timebase_normal(state, nframes, pos, new_pos);
    }
}

// TapTempo has given us a frame to snap our beat to.  The trick
// to making this function work is to find the nearest beat to
// snap to, and to make sure that we don't skip any frames by
// accidentally setting m_ramp in the past.
void LiveTime::rampAndSnapTo(jack_nframes_t target, double new_tempo)
{
    TimeBase ramp;
    TimeBase normal = m_normal;
    double dummy;
    jack_nframes_t frame_rate = jack_get_sample_rate(m_client);

    // Find the nearest BBT for our new normal anchor
    normal.getBBT(target, normal.m_bar, normal.m_beat, normal.m_tick);
    normal.m_frame = target;
    if((normal.m_ticks_per_beat-normal.m_tick) <= normal.m_tick) {
        ++normal.m_beat;
    }
    normal.m_tick = 0;
    normal.m_beats_per_minute = round(10.0*new_tempo)/10.0;
    normal.init_frame_rates(frame_rate);

    // Set up the ramp.  Don't start the ramp until the current
    // frame + max_buffer_size
    ramp = m_normal;
    ramp.m_frame = jack_get_buffer_size(m_client) + jack_get_current_transport_frame(m_client);
    assert(normal.m_frame > ramp.m_frame);
    m_normal.getBBT(ramp.m_frame, ramp.m_bar, ramp.m_beat, ramp.m_tick);
    ramp.init_frame_rates(frame_rate);
    assert(normal.m_anchor_beat_abs > ramp.m_anchor_beat_abs);
    ramp.m_beats_per_minute = 60.0*frame_rate*(normal.m_anchor_beat_abs - ramp.m_anchor_beat_abs)
        / (normal.m_frame - ramp.m_frame);
    ramp.init_frame_rates(frame_rate);

    cerr << "Old normal: " << m_normal << endl;
    cerr << "New normal: " << normal << endl;
    cerr << "Ramp:       " << ramp << endl;
    m_normal = normal;
    m_ramp = ramp;
    m_state = RampQueued;
}

// skip a beat by double-timing for one beat.
// Snaps to the downbeats.
//
// Create a new m_normal and m_ramp and queues the ramp (RampQueued).
//
void LiveTime::skip_beat(void)
{
    if(m_state == Ramping) {
        cout << "Already ramping... not skipping" << endl;
        return;
    }
    cout << "skipping beat..." << endl;
    jack_position_t pos;
    jack_transport_query(m_client, &pos);
    assert(pos.valid == JackPositionBBT);

    TimeBase ramp, normal;
    ramp.m_beats_per_bar = (int32_t)pos.beats_per_bar;
    ramp.m_ticks_per_beat = (int32_t)pos.ticks_per_beat;
    if(pos.beat == ramp.m_beats_per_bar) {
        ramp.m_bar = pos.bar + 1;
        ramp.m_beat = 1;
    } else {
        ramp.m_bar = pos.bar;
        ramp.m_beat = pos.beat + 1;
    }
    ramp.m_tick = 0;

    normal = ramp;
    if(normal.m_beat == normal.m_beats_per_bar) {
        ++normal.m_bar;
        normal.m_beat = 1;
    } else {
        ++normal.m_beat;
    }

    MutexLocker mutex(m_state_mutex);
    ramp.m_frame = m_normal.getFrame(ramp.m_bar, ramp.m_beat, ramp.m_tick);
    normal.m_frame = m_normal.getFrame(normal.m_bar, normal.m_beat, normal.m_tick);
    ramp.m_beats_per_minute = 2.0 * pos.beats_per_minute;
    normal.m_beats_per_minute = pos.beats_per_minute;
    if(normal.m_beat == normal.m_beats_per_bar) {
        ++normal.m_bar;
        normal.m_beat = 1;
    } else {
        ++normal.m_beat;
    }
    normal.init_frame_rates(pos.frame_rate);
    ramp.init_frame_rates(pos.frame_rate);
    m_normal = normal;
    m_ramp = ramp;
    m_state = RampQueued;

    // Ok, we set the ramp... now it's up to to
    // timebase() to do the Right Thing.
}

// skip a beat by half-timing for one beat.
// Snaps to the downbeats.
//
// Create a new m_normal and m_ramp and queues the ramp (RampQueued).
//
void LiveTime::insert_beat(void)
{
    if(m_state == Ramping) {
        cout << "Already ramping... not skipping" << endl;
        return;
    }
    cout << "inserting beat..." << endl;
    jack_position_t pos;
    jack_transport_query(m_client, &pos);
    assert(pos.valid == JackPositionBBT);

    TimeBase ramp, normal;

    ramp.m_beats_per_bar = (int32_t)pos.beats_per_bar;
    ramp.m_ticks_per_beat = (int32_t)pos.ticks_per_beat;
    if(pos.beat == ramp.m_beats_per_bar) {
        ramp.m_bar = pos.bar + 1;
        ramp.m_beat = 1;
    } else {
        ramp.m_bar = pos.bar;
        ramp.m_beat = pos.beat + 1;
    }
    ramp.m_tick = 0;

    normal = ramp;
    if(normal.m_beat >= (normal.m_beats_per_bar-1)) {
        ++normal.m_bar;
        normal.m_beat = normal.m_beat + 2 - normal.m_beats_per_bar;
    } else {
        normal.m_beat += 2;
    }

    MutexLocker mutex(m_state_mutex);
    ramp.m_frame = m_normal.getFrame(ramp.m_bar, ramp.m_beat, ramp.m_tick);
    normal.m_frame = m_normal.getFrame(normal.m_bar, normal.m_beat, normal.m_tick);
    ramp.m_beats_per_minute = 0.5 * pos.beats_per_minute;
    normal.m_beats_per_minute = pos.beats_per_minute;
    if(normal.m_beat == 1) {
        --normal.m_bar;
        normal.m_beat = normal.m_beats_per_bar;
    } else {
        --normal.m_beat;
    }
    normal.init_frame_rates(pos.frame_rate);
    ramp.init_frame_rates(pos.frame_rate);
    m_normal = normal;
    m_ramp = ramp;
    m_state = RampQueued;

    // Ok, we set the ramp... now it's up to to
    // timebase() to do the Right Thing.
}

// TimeBase::getBBT()
//
// Calculates the bar:beat:tick of the given frame, based on the
// properties of the TimeBase struct.
//
// Make sure that this can be called on itself.  i.e, that this code:
//
// TimeBase t;
// t.getBBT(frame, t.bar, t.beat, t.tick);
//
// Does what you expect.
//
void TimeBase::getBBT(jack_nframes_t frame,
                      int32_t& _bar,
                      int32_t& _beat,
                      int32_t& _tick)
{
    double dbeat, bar, beat, tick;
    MutexLocker mutex(&m_mutex);
    // Find the differenc ein beats from the anchor
    dbeat = ((double)frame - (double)m_frame)/m_frames_per_beat;
    // Now add in the anchor
    dbeat += m_anchor_beat_abs;
    assert(dbeat >= 0.0);
    // Now convert to bbt.
    tick = modf(dbeat, &beat) * m_ticks_per_beat;
    mutex.unlock();  // We're done with the data
    bar = beat/m_beats_per_bar; // floor later
    beat = fmod(beat, m_beats_per_bar);

    assert(bar >= 0.0);
    assert(beat >= 0.0);
    assert(tick >= 0.0);
   
    _bar = (int32_t)floor(bar+1.0);
    _beat = (int32_t)round(beat+1.0);  // beat should be nearly an integer
    _tick = (int32_t)floor(tick);
}

// TimeBase::getFrame()
//
// Calculates the frame number of the given bar:beat:tick, based on
// the properties of the TimeBase struct.
//
jack_nframes_t TimeBase::getFrame(double bar, double beat, double tick)
{
    MutexLocker mutex(&m_mutex);
    // Calculate absolute beats (decimal w/ticks)
    double f = (bar-1.0)*m_beats_per_bar
        + (beat-1.0)
        + tick/m_ticks_per_beat;
    // offset by the anchor
    f -= m_anchor_beat_abs;
    // Convert to frames
    f *= m_frames_per_beat;
    // and offset by anchor
    f += m_frame;
    mutex.unlock();

    if( f < 0.0 ) {
        return (jack_nframes_t) 0;
    }
    return (jack_nframes_t) round(f);    
}

Mutex::Mutex(void) throw(runtime_error)
{
    if(pthread_mutex_init(&mutex, 0)) {
        throw runtime_error("Could not initialize mutex");
    }
}

Mutex::Mutex(const Mutex& /*o*/) throw(runtime_error)
{
    // Don't copy the mutex!!!
    if(pthread_mutex_init(&mutex, 0)) {
        throw runtime_error("Could not initialize mutex");
    }
}

Mutex::~Mutex()
{
    unlock();
    pthread_mutex_destroy(&mutex);
}

void Mutex::lock(void) throw(runtime_error)
{
    if(pthread_mutex_lock(&mutex)) {
        throw runtime_error("Error locking mutex");
    }
    state = Locked;
}

bool Mutex::trylock(void) throw()
{
    if(0 == pthread_mutex_trylock(&mutex)) {
        state = Unlocked;
        return true;
    }
    return false;
}

bool Mutex::unlock(void) throw()
{
    if(0 == pthread_mutex_unlock(&mutex)) {
        state = Unlocked;
        return true;
    }
    return false;
}

// TapTempo::lastTap()
//
// Predicts tap and rate based on previous N taps using a least
// squares analysis for each tap.  Assumes that an ideal tempo is
// desired, but represented by imperfect taps that follow a normal
// distribution on the error.
//
// Presume N+1 ideal tempo B0, B1, B2, ... BN being represented by N+1
// imperfect taps T0, T1, T2, ... TN.  Each Bi is evenly spaced, such
// that:
//
// Bi = B0 + (i*D)
//
// Where D is the space between beats.  Thus, we want to find the B0
// and D that best represents that taps that we've been given.
//
// Since each Ti represents each Bi, it has an error associated with
// it.  Thus Bi = Ti + Ei, or:
//
// Ei = Bi - Ti
//
// Where Ei is the error of the tap relative to the ideal tempo.  Without
// going into great detail, Ei^2 is directly related to the statistical
// probability distribution (bell curve, standard devation).
//
// We get an indication of the total error by summing the squares:
//
// SUM(Ei^2) = E0^2 + E1^2 + E2^2 + ... + EN^2
//
// Which, when substituting the above equations, becomes:
//
// SUM(Ei^2) = (B0-T0)^2 + (B1-T1)^2 + ... + (BN-TN)^2
//           = (B0-T0)^2 + (B0+D-T1)^2 + ... + (B0+N*D-TN)^2
//           = SUM( (B0 + i*D - Ti)^2 ) for i=0..N
//           = EE
//
// We seek to minimize this function.  This may be done by taking
// the partial derivative of each parameter, and setting it equal to
// zero.
//
// d(EE) = d(EE)/d(B0) + d(EE)/d(D)
//
// Where d() is the differential operator.  To minimize, we set
// both terms equal to zero:
//
// d(EE)/d(B0) = 0
//
// d(EE)/d(D) = 0
//
// And solve.
//
// d(EE)/d(B0) = SUM( 2*(B0+i*D-Ti) ) for i=0..N
//             = 2*SUM(B0+i*D-Ti) for i=0..N
//             = 2*SUM(B0) + 2*SUM(i*D) - 2*SUM(Ti), all for i=0..N
//             = 2*B0*(N+1) + 2*D*SUM(i) - 2*SUM(Ti), all for i=0..N
//
// d(EE)/d(D)  = SUM( 2*(B0+i*D-Ti)*i ) for i=0..N
//             = SUM( 2*i*B0 + 2*i^2*D - 2*i*Ti ) for i=0..N
//             = SUM( 2*i*B0 ) + SUM(2*i^2*D) - SUM(2*i*Ti), all for i=0..N
//             = 2*B0*SUM(i) + 2*D*SUM(i^2) - 2*SUM(i*Ti), all for i=0..N
//
// Setting these to zero gives us a linear system of equation.  Note that
// from here on out, SUM(...) is implied to be over the range i=0..N.
//
// 0 = 2*(N+1)  * B0 + 2*SUM(i)   * D - 2*SUM(Ti)      {d(EE)/d(B0)}
// 0 = 2*SUM(i) * B0 + 2*SUM(i^2) * D - 2*SUM(i*Ti)    {d(EE)/d(D)}
//
// [ 0   = [ 2*(N+1)    2*SUM(i)     * [ B0     - 2 * [ SUM(Ti)
//   0 ]     2*SUM(i)   2*SUM(i^2) ]     D  ]           SUM(i*Ti) ]
//
// Multiplying all bey 1/2 and moving the constant term to the left:
//
// [ SUM(Ti)     = [ (N+1)    SUM(i)     * [ B0
//   SUM(i*Ti) ]     SUM(i)   SUM(i^2) ]     D  ]
//
// For simplicity, let the coefficient matrix be represented by:
//
// [ m1  m2
//   m2  m4 ]  (note that m3 = m2, so it's omitted)
//
// Note that it's simple to prove mi > 0 for all N > 0.
//
// Solving by row reduction, we eliminate the bottom left entry by
// multiplying the first row by -m2/m1 and adding it to the second
// row.
//
// [ SUM(Ti)                    = [ m1  m2             [ B0
//   SUM(i*Ti)-(m2/m1)SUM(Ti) ]      0  m4-(m2^2/m1) ]    D ]
//
// Solving by back-substitution:
//
// D = [SUM(i*Ti)-(m2/m1)SUM(Ti)] / [m4-(m2^2/m1)]
//
// SUM(Ti) = m1*B0 + m2*D
// m1*B0 = SUM(Ti) - m2*D
// B0 = (SUM(Ti) - m2*D)/m1 = (1/m1)*SUM(Ti) - (m2/m1)*D
//
// Notice that B0 and D only depend on the following 3 terms:
//
// Ca = (m2/m1)
// Cb = 1/(m4-(m2^2/m1))
//    = m1 / (m4*m1 - m2^2)
// Cc = (1/m1)
//
// Now, SUM(i) and SUM(i^2) (and thus, each mi) can be calculated
// knowing only N.  I once derived the solution for SUM(i), but I
// verified and found the solution in an online database called "The
// On-Line Encyclopedia of Integer Sequences"
// http://www.research.att.com/~njas/sequences
//
// There, I found:
//
// SUM(i) for i=0..N = N*(N+1)/2
// SUM(i^2) for i=0..N = N*(N+1)*(2N+1)/6
//
// Reducing Ca, Cb, and Cc...
//
// m1 = (N+1)
// m2 = SUM(i) = N*(N+1)/2
// m4 = SUM(i^2) = N*(N+1)*(2N+1)/6
// Ca = (m2/m1)
//    = (N*(N+1)/2) / (N+1)
//    = N/2
// Cb = 1/(m4-(m2^2/m1))
//    = m1 / (m4*m1 - m2^2)
//    = (N+1) / [(N*(N+1)*(2N+1)/6) * (N+1) - (N*(N+1)/2)^2]
//    = (N+1) / [ N*(N+1)^2*(2N+1)/6 - N^2*(N+1)^2/4 ]
//    =  1 / [N*(N+1)*(2N+1)/6 - N^2*(N+1)/4]
//    =  1 / [N*(N+1)*(2N+1)*4/24 - N^2*(N+1)*6/24]
//    = 24 / [N*(N+1)*(2N+1)*4 - N^2*(N+1)*6]
//    = 24 / N*(N+1) * [ 4*(2N+1) - 6*N ]
// Cc = (1/m1)
//    = 1/(N+1)
//
//
// D = [SUM(i*Ti)-(m2/m1)SUM(Ti)] / [m4-(m2^2/m1)]
//   = [SUM(i*Ti)-Ca*SUM(Ti)] * Cb
// B0 = Cc*SUM(Ti) - Ca*D
//
// To reduce calculation and lag time, Ca, Cb, and Cc can be
// calculated before-hand.  Meanwhile, SUM(Ti) and SUM(i*Ti) can be
// tabulated as the taps come in.
//
jack_nframes_t TapTempo::lastTap(double& beats_per_minute)
{
    double D, B0;
    double next_frame;

    if(m_taps_required == 2) {
        B0 = m_sum_taps - m_last_tap;
        D = m_last_tap - B0;
        next_frame = (B0 + 2.0*D)*m_frame_rate;
    } else {
        double Ca, Cb, Cc;
        double N = (double)m_taps_required - 1.0;
        double Np1 = (double)m_taps_required; // N+1

        Ca = N/2.0;
        Cb = N*(N+1.0) * ( 4.0*(2.0*N+1) - (6.0)*N );
        Cb = 24.0 / Cb;
        Cc = 1/Np1;

        D = (m_sum_i_taps - Ca * m_sum_taps) * Cb;
        B0 = Cc*m_sum_taps - Ca*D;

        assert(D > .001);  // Crazy tapping.
        beats_per_minute = 60.0/D;
        next_frame = (B0 + Np1*D)*m_frame_rate;
    }
    invalidate_();
    return (jack_nframes_t)next_frame;
}

// end of livetime.cpp

-------------------------------------------------------------------------
This SF.net email is sponsored by: Microsoft
Defy all challenges. Microsoft(R) Visual Studio 2008.
http://clk.atdmt.com/MRT/go/vse0120000070mrt/direct/01/
_______________________________________________
Jackit-devel mailing list
[hidden email]
https://lists.sourceforge.net/lists/listinfo/jackit-devel
Reply | Threaded
Open this post in threaded view
|

Re: Live Synchronization, Tap Tempo, MIDI, and abusing the Transport

Daniel Schregenberger
Hi,

this is very very interesting to me! I've been looking for a solution to
sync the computer to the band as well, but havent found the time to do
it myself.
Right now I'm very busy (again, sigh), but I'm definately interested in
trying this!

If there is (will be) a project page, please let me know. Otherwise I
will contact you when I find the time to try it. I hope this is ok.

cheers,
Daniel


On Son, 2008-02-03 at 08:56 -0600, Gabriel M. Beddingfield wrote:

> Hello!  I've been working on a Jack Timebase master that can be
> manipulated to provide synchronization of the computers to a live band.
>   Would any of you be willing to peek at the code?  Is this abusing the
> Transport concept too much?
>
> I'm creating a setup for using audio in live performances, and I haven't
> found anything for synchronizing the computers to the people (in the
> event of a discrepency).  In almost every case, the common conclusion
> has been "make the people match the computer."
>
> Not satisifed with that, I've been working on a way to provide a
> tap-tempo sync that will adjust the Jack Transport to the tempo of the
> live ensemble.  It also provides a means of "skipping" a beat or
> "inserting" a beat (for times when the singer comes in early -- a common
> occurrence in our band).  The intent is that taps and commands can be
> given via a MIDI footswitch to control the transport.  Changes to the
> transport will be reflected in the sequencer (a timebase slave).
>
> The code (C++) is attached, and has quite a bit of documentation
> embedded.  I've been testing it with jack 0.101.1 and using seq24 as the
> sequencer.
>
> Peace,
> Gabriel

--
http://www.despite.ch/ -- http://www.klyde.ch/


-------------------------------------------------------------------------
This SF.net email is sponsored by: Microsoft
Defy all challenges. Microsoft(R) Visual Studio 2008.
http://clk.atdmt.com/MRT/go/vse0120000070mrt/direct/01/
_______________________________________________
Jackit-devel mailing list
[hidden email]
https://lists.sourceforge.net/lists/listinfo/jackit-devel
Reply | Threaded
Open this post in threaded view
|

Re: Live Synchronization, Tap Tempo, MIDI, and abusing the Transport

Frank Brickle
You might also be interested in some projects from the soundlab at Princeton:
http://soundlab.cs.princeton.edu/software/
especially ChucK
http://chuck.cs.princeton.edu/
and miniAudicle
http://audicle.cs.princeton.edu/mini/

Regards
Frank


On Feb 3, 2008 10:15 AM, Daniel Schregenberger <[hidden email]> wrote:
Hi,

this is very very interesting to me! I've been looking for a solution to
sync the computer to the band as well, but havent found the time to do
it myself.
Right now I'm very busy (again, sigh), but I'm definately interested in
trying this!

If there is (will be) a project page, please let me know. Otherwise I
will contact you when I find the time to try it. I hope this is ok.

cheers,
Daniel


On Son, 2008-02-03 at 08:56 -0600, Gabriel M. Beddingfield wrote:
> Hello!  I've been working on a Jack Timebase master that can be
> manipulated to provide synchronization of the computers to a live band.
>   Would any of you be willing to peek at the code?  Is this abusing the
> Transport concept too much?
>
> I'm creating a setup for using audio in live performances, and I haven't
> found anything for synchronizing the computers to the people (in the
> event of a discrepency).  In almost every case, the common conclusion
> has been "make the people match the computer."
>
> Not satisifed with that, I've been working on a way to provide a
> tap-tempo sync that will adjust the Jack Transport to the tempo of the
> live ensemble.  It also provides a means of "skipping" a beat or
> "inserting" a beat (for times when the singer comes in early -- a common
> occurrence in our band).  The intent is that taps and commands can be
> given via a MIDI footswitch to control the transport.  Changes to the
> transport will be reflected in the sequencer (a timebase slave).
>
> The code (C++) is attached, and has quite a bit of documentation
> embedded.  I've been testing it with jack 0.101.1 and using seq24 as the
> sequencer.
>
> Peace,
> Gabriel

--
http://www.despite.ch/ -- http://www.klyde.ch/


-------------------------------------------------------------------------
This SF.net email is sponsored by: Microsoft
Defy all challenges. Microsoft(R) Visual Studio 2008.
http://clk.atdmt.com/MRT/go/vse0120000070mrt/direct/01/
_______________________________________________
Jackit-devel mailing list
[hidden email]
https://lists.sourceforge.net/lists/listinfo/jackit-devel



--
The only thing we have to fear is whatever comes along next. -- Austin Cline
-------------------------------------------------------------------------
This SF.net email is sponsored by: Microsoft
Defy all challenges. Microsoft(R) Visual Studio 2008.
http://clk.atdmt.com/MRT/go/vse0120000070mrt/direct/01/
_______________________________________________
Jackit-devel mailing list
[hidden email]
https://lists.sourceforge.net/lists/listinfo/jackit-devel
Reply | Threaded
Open this post in threaded view
|

Re: Live Synchronization, Tap Tempo, MIDI, and abusing the Transport

Daniel James-2
Hi Daniel, hi Frank

>> this is very very interesting to me! I've been looking for a solution to
>> sync the computer to the band as well

See also TapStart:

http://www.arnoldarts.de/drupal/?q=TapStart

Cheers!

Daniel

-------------------------------------------------------------------------
This SF.net email is sponsored by: Microsoft
Defy all challenges. Microsoft(R) Visual Studio 2008.
http://clk.atdmt.com/MRT/go/vse0120000070mrt/direct/01/
_______________________________________________
Jackit-devel mailing list
[hidden email]
https://lists.sourceforge.net/lists/listinfo/jackit-devel
Reply | Threaded
Open this post in threaded view
|

Re: Live Synchronization, Tap Tempo, MIDI, and abusing the Transport

Gabriel M. Beddingfield
In reply to this post by Daniel Schregenberger
Daniel Schregenberger wrote:
>
> If there is (will be) a project page, please let me know. Otherwise I
> will contact you when I find the time to try it. I hope this is ok.

I appreciate it!  No project page, yet.  I let you know when I make one.

Peace,
Gabriel


-------------------------------------------------------------------------
This SF.net email is sponsored by: Microsoft
Defy all challenges. Microsoft(R) Visual Studio 2008.
http://clk.atdmt.com/MRT/go/vse0120000070mrt/direct/01/
_______________________________________________
Jackit-devel mailing list
[hidden email]
https://lists.sourceforge.net/lists/listinfo/jackit-devel
Reply | Threaded
Open this post in threaded view
|

Re: Live Synchronization, Tap Tempo, MIDI, and abusing the Transport

Gabriel M. Beddingfield
In reply to this post by Frank Brickle
Frank Brickle wrote:
> You might also be interested in some projects from the soundlab at
> Princeton:
> http://soundlab.cs.princeton.edu/software/
> especially ChucK
> http://chuck.cs.princeton.edu/
> and miniAudicle
> http://audicle.cs.princeton.edu/mini/

Yes, I am... but I'm having trouble connecting the dots.  I don't
understand how this is an answer to the problem.  Would you mind giving
me a little more direction?

This looks like a programming environment for music work (like pd, max,
supercollider), with and emphasis on OSC synchronization.  I'm trying to
find/create an application to accomplish a specific task with Jack clients.

Peace,
Gabriel

-------------------------------------------------------------------------
This SF.net email is sponsored by: Microsoft
Defy all challenges. Microsoft(R) Visual Studio 2008.
http://clk.atdmt.com/MRT/go/vse0120000070mrt/direct/01/
_______________________________________________
Jackit-devel mailing list
[hidden email]
https://lists.sourceforge.net/lists/listinfo/jackit-devel
Reply | Threaded
Open this post in threaded view
|

Re: Live Synchronization, Tap Tempo, MIDI, and abusing the Transport

Gabriel M. Beddingfield
In reply to this post by Daniel James-2
Daniel James wrote:
> See also TapStart:
>
> http://www.arnoldarts.de/drupal/?q=TapStart

Excellent!  Thanks for the link!

Peace,
Gabriel


-------------------------------------------------------------------------
This SF.net email is sponsored by: Microsoft
Defy all challenges. Microsoft(R) Visual Studio 2008.
http://clk.atdmt.com/MRT/go/vse0120000070mrt/direct/01/
_______________________________________________
Jackit-devel mailing list
[hidden email]
https://lists.sourceforge.net/lists/listinfo/jackit-devel
Reply | Threaded
Open this post in threaded view
|

Re: Live Synchronization, Tap Tempo, MIDI, and abusing the Transport

Frank Brickle
In reply to this post by Gabriel M. Beddingfield
ChucK is intended to realize distributed, mutable, synchronized realtime sound processes. Locally these processes can be JACK clients, or ALSA, or OSS. miniAudicle is merely the IDE for ChucK programs. It occurred to me that perhaps ChucK was a suitable framework for controlling other local JACK processes, since fine-grained timing is part of the language design.

Frank

On Feb 4, 2008 1:21 PM, Gabriel M. Beddingfield <[hidden email]> wrote:
Frank Brickle wrote:
> You might also be interested in some projects from the soundlab at
> Princeton:
> http://soundlab.cs.princeton.edu/software/
> especially ChucK
> http://chuck.cs.princeton.edu/
> and miniAudicle
> http://audicle.cs.princeton.edu/mini/

Yes, I am... but I'm having trouble connecting the dots.  I don't
understand how this is an answer to the problem.  Would you mind giving
me a little more direction?

This looks like a programming environment for music work (like pd, max,
supercollider), with and emphasis on OSC synchronization.  I'm trying to
find/create an application to accomplish a specific task with Jack clients.

Peace,
Gabriel

-------------------------------------------------------------------------
This SF.net email is sponsored by: Microsoft
Defy all challenges. Microsoft(R) Visual Studio 2008.
http://clk.atdmt.com/MRT/go/vse0120000070mrt/direct/01/
_______________________________________________
Jackit-devel mailing list
[hidden email]
https://lists.sourceforge.net/lists/listinfo/jackit-devel



--
The only thing we have to fear is whatever comes along next. -- Austin Cline
-------------------------------------------------------------------------
This SF.net email is sponsored by: Microsoft
Defy all challenges. Microsoft(R) Visual Studio 2008.
http://clk.atdmt.com/MRT/go/vse0120000070mrt/direct/01/
_______________________________________________
Jackit-devel mailing list
[hidden email]
https://lists.sourceforge.net/lists/listinfo/jackit-devel
Reply | Threaded
Open this post in threaded view
|

Re: Live Synchronization, Tap Tempo, MIDI, and abusing the Transport

Gabriel M. Beddingfield
Frank Brickle wrote:
> ChucK is intended to realize distributed, mutable, synchronized realtime
> sound processes. Locally these processes can be JACK clients, or ALSA,
> or OSS. miniAudicle is merely the IDE for ChucK programs. It occurred to
> me that perhaps ChucK was a suitable framework for controlling other
> local JACK processes, since fine-grained timing is part of the language
> design.

Thanks!  That helps me understand where you're coming from.

-Gabriel

-------------------------------------------------------------------------
This SF.net email is sponsored by: Microsoft
Defy all challenges. Microsoft(R) Visual Studio 2008.
http://clk.atdmt.com/MRT/go/vse0120000070mrt/direct/01/
_______________________________________________
Jackit-devel mailing list
[hidden email]
https://lists.sourceforge.net/lists/listinfo/jackit-devel