Accurate timing without audio


#1

Sorry for all the traffic here, I’m back on rapid development!

I need to get accurate timing in a Juce program that doesn’t make sound. (It’s a lighting sequencer.)

Is there a way to do this without actually reading or writing sound from anywhere? I don’t mind setting up an AudioProcessor or similar, but I’d really rather not be moving bytes around at the audio rate on the Raspberry Pi (one of the target machines).


#2

You want an audio device I/O callback that doesn’t actually communicate with a device? Here’s some code I have written. It does allocate memory for a small buffer but does nothing with it. You do not need to write any bytes into it. It properly simulates the timing of the specified sampleRate.

---- SNIP ----

//==============================================================================
/**
  A Thread for the NullAudioIODevice.

  This mimics the behavior of a real time device audio thread.
*/
class NullAudioThread : protected Thread
{
private:
  AudioIODeviceCallback* m_callback;
  AudioIODevice* m_device;

private:
  class TimerWithHistory
  {
  private:
    enum
    {
      maxHist = 20
    };

    int64 m_ticksPerSecond;
    double m_ticksPerCall;
    int64 m_curTime;
    int64 m_startTime;
    int64 m_hist[maxHist];
    int m_index; // of oldest

  private:
    void record (int64 when)
    {
      m_hist [m_index] = when;
      m_index = (m_index + 1) % maxHist;
    }

  public:
    TimerWithHistory (double milliSecondsPerCall)
      : m_index (0)
    {
      m_ticksPerSecond = Time::getHighResolutionTicksPerSecond ();
      m_ticksPerCall = m_ticksPerSecond * milliSecondsPerCall / 1000;
      m_curTime = Time::getHighResolutionTicks();
      // stuff history with perfect timings
      for (int i = maxHist; i > 0; --i)
        record (static_cast <int64> (m_curTime - i * m_ticksPerCall));
    }

    void call ()
    {
      m_startTime = m_curTime;

      record (m_startTime);
    }

    void wait (Thread* thread)
    {
      m_curTime = Time::getHighResolutionTicks();

      int64 const nextTime = static_cast <int64> (
        m_hist [m_index] + maxHist * m_ticksPerCall + 0.5);
     
      if (nextTime > m_curTime)
      {
        int const milliSeconds = static_cast <int> (
          (1000 * (nextTime - m_curTime) + (m_ticksPerSecond/2)) / m_ticksPerSecond);
        
        thread->wait (milliSeconds);
      }
      else
      {
        // call took too long
      }
    }
  };

public:
  NullAudioThread () : Thread ("NullAudioThread")
  {
  }

  ~NullAudioThread ()
  {
    stop ();
  }

  void start (AudioIODeviceCallback* callback,
              AudioIODevice* device)
  {
    m_callback = callback;
    m_device = device;

    startThread ();
    setPriority (10);
  }

  void stop ()
  {
    stopThread (-1);
  }

  void run ()
  {
    m_callback->audioDeviceAboutToStart (m_device);

    const int bufferSamples = m_device->getCurrentBufferSizeSamples();
    const double sampleRate = m_device->getCurrentSampleRate();

    const int inputCount = m_device->getActiveInputChannels().countNumberOfSetBits();
    const int outputCount = m_device->getActiveOutputChannels().countNumberOfSetBits();
    
    AudioSampleBuffer buffer (inputCount + outputCount, bufferSamples);

    TimerWithHistory timer (1000 * bufferSamples / sampleRate);

    while (!threadShouldExit())
    {
      timer.call ();

      m_callback->audioDeviceIOCallback (const_cast<const float**>
                                          (buffer.getArrayOfChannels() + outputCount),
                                          inputCount,
                                          buffer.getArrayOfChannels(),
                                          outputCount,
                                          bufferSamples);

      timer.wait (this);
    }

    m_callback->audioDeviceStopped ();
  }
};

//==============================================================================
/**
  AudioIODevice with thread that sends output to null.
*/
class NullAudioIODevice : public AudioIODevice
{
private:
  bool m_isOpen;
  AudioIODeviceCallback* m_callback;
  int m_bufferSizeSamples;
  double m_sampleRate;
  NullAudioThread m_thread;
  BigInteger m_activeInputs;
  BigInteger m_activeOutputs;

public:
  NullAudioIODevice ()
    : AudioIODevice ("None", "None")
    , m_isOpen (false)
    , m_callback (0)
  {
    m_activeInputs.clear();
    m_activeOutputs.clear();
  }

  StringArray getOutputChannelNames ()
  {
    StringArray names;
    names.add ("1");
    names.add ("2");
    names.add ("3");
    names.add ("4");
    names.add ("5");
    names.add ("6");
    return names;
  }

  StringArray getInputChannelNames ()
  {
    StringArray names;
    names.add ("1");
    return names;
  }

  int getNumSampleRates()
  {
    return 5;
  }

  /* We need to support as many sample rates as possible so that we
     can encompass all possible real settings if a device fails to open.
  */
  double getSampleRate (int index)
  {
    double rate=0;

    switch (index)
    {
    default:
    case 0: rate = 44100; break;
    case 1: rate = 48000; break;
    case 2: rate = 96000; break;
    case 3: rate = 176400; break;
    case 4: rate = 192000; break;
    };

    return rate;
  }

  int getNumBufferSizesAvailable ()
  {
    return 8;
  }

  int getBufferSizeSamples (int index)
  {
    int bufferSize;

    switch (index)
    {
    default:
    case 0: bufferSize = 48; break;
    case 1: bufferSize = 64; break;
    case 2: bufferSize = 128; break;
    case 3: bufferSize = 256; break;
    case 4: bufferSize = 512; break;
    case 5: bufferSize = 1024; break;
    case 6: bufferSize = 2048; break;
    case 7: bufferSize = 2560; break;
    };
    
    return bufferSize;
  }

  int getDefaultBufferSize()
  {
    return 64;
  }

  String open (BigInteger const& inputChannels,
               BigInteger const& outputChannels,
               double sampleRate,
               int bufferSizeSamples)
  {
    if (m_isOpen)
      close ();

    m_isOpen = true;
    m_sampleRate = sampleRate;
    m_bufferSizeSamples = bufferSizeSamples;
    m_activeInputs = inputChannels;
    m_activeOutputs = outputChannels;

    return String::empty;
  }

  void close()
  {
    if (m_isOpen)
      m_isOpen = false;
  }

  bool isOpen()
  {
    return m_isOpen;
  }

  void start (AudioIODeviceCallback* callback)
  {
    m_callback = callback;
    m_thread.start (callback, this);
  }

  void stop()
  {
    m_thread.stop ();
  }

  bool isPlaying ()
  {
    return m_callback != 0;
  }

  String getLastError ()
  {
    return String::empty;
  }

  int getCurrentBufferSizeSamples ()
  {
    return m_bufferSizeSamples;
  }

  double getCurrentSampleRate ()
  {
    return m_sampleRate;
  }

  int getCurrentBitDepth ()
  {
    return 16;
  }

  BigInteger getActiveOutputChannels () const
  {
    return m_activeOutputs;
  }

  BigInteger getActiveInputChannels () const
  {
    return m_activeInputs;
  }

  int getOutputLatencyInSamples ()
  {
    return 0;
  }

  int getInputLatencyInSamples ()
  {
    return 0;
  }
};

//==============================================================================
/**
  AudioIODeviceType that provides the NullAudioIODevice.
*/
class NullAudioIODeviceType : public AudioIODeviceType
{
public:
  explicit NullAudioIODeviceType (String deviceName)
    : AudioIODeviceType (deviceName)
  {
  }

  void scanForDevices ()
  {
  }

  StringArray getDeviceNames (bool wantInputNames) const
  {
    StringArray names;
    names.add (NullAudioDeviceManager::getNullDeviceName ());
    return names;
  }

  int getDefaultDeviceIndex (bool forInput) const
  {
    return 0;
  }

  int getIndexOfDevice (AudioIODevice* device, bool asInput) const
  {
    return 0;
  }

  bool hasSeparateInputsAndOutputs () const
  {
    return false;
  }

  AudioIODevice* createDevice (
    const String& outputDeviceName,
    const String& inputDeviceName)
  {
    return new NullAudioIODevice;
  }
};

//==============================================================================

NullAudioDeviceManager::NullAudioDeviceManager ()
{
}

NullAudioDeviceManager::~NullAudioDeviceManager()
{
}

String NullAudioDeviceManager::getNullDeviceName ()
{
  return "None";
}

void NullAudioDeviceManager::createAudioDeviceTypes (OwnedArray <AudioIODeviceType>& list)
{
  AudioDeviceManager::createAudioDeviceTypes (list);

  list.add (new NullAudioIODeviceType (getNullDeviceName ()));
}

Header file:

//==============================================================================
/**
  An AudioDeviceManager that also provides the NullAudioIODevice

  To allow the Mixer to synchronize after failing to open an actual audio
  device, the null device is substituted. Otherwise, user actions such as loading
  a track or manipulating parameters would fail to produce visible results.
*/
class NullAudioDeviceManager : public AudioDeviceManager
{
public:
  static String getNullDeviceName ();

  NullAudioDeviceManager ();
  ~NullAudioDeviceManager ();
  void createAudioDeviceTypes (OwnedArray <AudioIODeviceType>& list);
};

#3

Yowza! Very neat indeed.

It seems to do the trick when dropped in… !

Thanksss muchly!


#4

What’s the next piece of that puzzle? Are you tracking how many samples have been consumed in the callback to get a clock value?

Bruce


#5

Well, in the case I’m considering, I just need to emit the state of the world really often, so I’m fine.

Later on, when I need to do more precise subtimings, I’ll have to solve that part…