Smpte class, not yet a library

Hi, since I needed to read SMPTE LinearTimeCode) , and didn’t found anything already written for processing, I’ve written this class. It is commented, and I would like it to make a Library, but I feel I’m not able to do it, so if someone can do it, or would like to help me to translate it to a correct library, it will be wonderful.

It works with 44100 and 48000 Hz Sample Frequency, and with 8, 16 and 24 bits of Samples, 24 and 25 fps.

Feel free to use it, correct the code or improve it. just let me know…
here’s the code:

import javax.sound.sampled.*;

class SmpteReader implements Runnable {
  // Class that reads SMPTE LTC from the selected audio input.
  // tested with a sample rate of 44100 and 48000 Hz
  // tested with 24 and 25 frames per second
  // tested with a quantization of 8 and 16 bit (ready to work with 24 bit, but not tested yet).
  
  // TODO:
  // check and code for other non-integer fps with drop frame.
  
  private AudioFormat format;
  
  private float sampleRate;  // sample freq.
  private int sampleSizeInBits;   // quantization.
  private int channels;            // 1 = mono.
  private boolean signed;       // true = 0 in the middle.
  private boolean bigEndian;    // usually true.
  private int framePerSecond;  // PAL system = 25 fps.
  private int bitsPerMessage = 80;    // A SMPTE message is composed of 40 bits

  private TargetDataLine targetDataline; // audio input
  private Thread thread;
  private int precRead = 0;
  private int thisRead = 0;
  private int word;
  private boolean readComplete = true;
  private int dataIntCnt = 0;
  private int bCnt = 0;
  private int precCnt = 0;
  private int threshold;  
  private int bufferLengthInBytes, bufferSubLength, frameSizeInBytes;
  public int numBytesRead;
  private byte data[];
  private int dataInt[];
  private int bits[] = new int[bitsPerMessage];

  public int frame, second, minute, hour;
  public String timeStr = "";
  
  public SmpteReader() {  // Empty constructor for most common SMPTE:
    this(44100.0, 16, 1, true, true, 25);
  }
  
  public SmpteReader(float _sampleRate, int _sampleSizeInBits, int _channels, boolean _signed, boolean _bigEndian, int _framePerSecond) {
    sampleRate = _sampleRate;
    sampleSizeInBits = _sampleSizeInBits;
    channels = _channels;
    signed = _signed;
    bigEndian = _bigEndian;
    framePerSecond = _framePerSecond;
    
    format = new AudioFormat(sampleRate, sampleSizeInBits, channels, signed, bigEndian);

    // Opening the audio line in:
    DataLine.Info info = new DataLine.Info(TargetDataLine.class, format);
    if (!AudioSystem.isLineSupported(info)) {
      System.out.println("Line " + info + " is not supported.");
    } else {
      try {
        targetDataline = (TargetDataLine) AudioSystem.getLine(info);
        targetDataline.open(format);
      } 
      catch (LineUnavailableException ex) {
        System.out.println("Cannot open input line: " + ex);
        return;
      }
    }  

    targetDataline.start();

    // Reading audio format:
    frameSizeInBytes = format.getFrameSize();
    //println("Frame Size: "+frameSizeInBytes+" bytes");
    bufferSubLength = (int)(targetDataline.getBufferSize()/8);
    //println("Buffer sub length: "+bufferSubLength+" frames");
    bufferLengthInBytes = (bufferSubLength * frameSizeInBytes);
    //println("Buffer length: "+bufferLengthInBytes+" bytes");
    
    // This threshold is the number of samples read after the zero crossing for the half of a bit:
    // If those are more then Threshold means we have a bit = 0 (two consecutive equal readings);
    // Else, on the second consecutive change before threshold samples, we have a bit = 1 (two consecutive different readings);
    // More on that here: https://en.wikipedia.org/wiki/SMPTE_timecode
    threshold = int(((sampleRate/framePerSecond)/(bitsPerMessage*2))*1.5); // *2 is because we need 2 peaks to make a bit, and *1.5 is because we are reading the peak between one half bit and the other.

    // Array to record incoming bytes (samples):
    data = new byte[bufferLengthInBytes];
    if (sampleSizeInBits == 24) {
      dataInt = new int[data.length/3]; // 
    } else if (sampleSizeInBits == 16) {
      dataInt = new int[data.length/2];
    } else if (sampleSizeInBits == 8) {
      dataInt = new int[data.length];
    }
    println("SMPTE Thread started:");
    println("Sample Rate: "+sampleRate+" Hz");
    println("Sample Size: "+sampleSizeInBits+" bit");
    println("Frame Per Second: "+framePerSecond+" fps");
    thread = new Thread(this);
    thread.start();
  }

  public void stop() {
    thread = null;
  }

  public void run() {
    try {
      while (thread != null) {
        if (targetDataline.available() > 0) {

          // Let's sample audio into data[] and return read byte number:
          numBytesRead = targetDataline.read(data, 0, bufferLengthInBytes);
          // println("Read byte number: "+numBytesRead);
          if (sampleSizeInBits == 24) {
            
            // Sum bytes to get Ints (24 bit quantization):
            dataIntCnt = 0;
            for (int i = 2; i < data.length; i+=3) {            
              dataInt[dataIntCnt] = (data[i-2] << 16) + (data[i-1] << 8) + int(data[i]);
              dataIntCnt++;
            }
          } else if (sampleSizeInBits == 16) {
            
            // Sum bytes to get Ints (16 bit quantization):
            dataIntCnt = 0;
            for (int i = 1; i < data.length; i+=2) {            
              dataInt[dataIntCnt] = (data[i-1] << 8) + int(data[i]);
              dataIntCnt++;
            }
          } else if (sampleSizeInBits == 8) {
            
            // just copy bytes to int (8 bit quantization):
            dataInt = int(data);
          }

          // Samples reading iteration:
          for (int k = 0; k < dataInt.length; k++) {
            thisRead = dataInt[k];
            
            // If we read something:
            if (thisRead != 0 && precRead != 0) {
              
              // Sync on Zero Crossing (if reading goes from <0 to >0 , or from >0 to <0):
              if (thisRead/abs(thisRead) != precRead/abs(precRead)) {
                // precCnt is the last reading counter.
                if (k < precCnt) {
                  // means we started back to read from the beginning of dataInt[], without finding the end of the smpte message;
                  // so we translate it into a negative counter to sync the next reading.
                  precCnt = -(dataInt.length - precCnt);
                }                
                if ((k - precCnt) > threshold) {
                  // TODO: throw away the packet if > 2*threshold ?.
                  // We have read 2 consecutive high samples or low samples, means we suppose this bit = 0;
                  readComplete = true;
                  precCnt = k;
                  // bCnt is the smpte message counter.
                  bits[bCnt] = 0;
                  // word is the message as an int to check the sync (bits from 64 to 79).
                  word = word << 1;
                  bCnt++;
                } else if ((k - precCnt) < threshold && (k - precCnt) > 0) {                  
                  // readComplete = false, if it is the first peak we are reading;
                  readComplete = !readComplete;
                  precCnt = k;
                  // on the second different consecutive peak:
                  if (readComplete) {                    
                    // this bit = 1
                    bits[bCnt] = 1;                    
                    word = (word << 1) + 1;
                    bCnt++;
                  }
                }
                if (char(word) == 16381) {                  
                  // Sync word:
                  calcTime();
                  word = 0;        
                  bCnt = 0;
                }
                
                // check to avoid array overflow. We translate all the bits of one position to fill the last:
                if (bCnt > bits.length-1) {
                  for (int i = 1; i < bits.length; i++) {
                    bits[i-1] = bits[i];
                  }
                  bCnt--;
                }
              }
            }
            precRead = thisRead;
          }
        }
      }
      targetDataline.stop();
      targetDataline.close();
    }
    catch (Exception e) {
      println("Error: Force quit...");
      print(e);
    }
  }

  private void calcTime() {
    frame = ((bits[9] << 1) + bits[8])*10 + (bits[3] << 3) + (bits[2] << 2) + (bits[1] << 1) + bits[0];
    second = ((bits[26] << 2) + (bits[25] << 1) + bits[24])*10 + (bits[19] << 3) + (bits[18] << 2) + (bits[17] << 1) + bits[16];
    minute = ((bits[42] << 2) + (bits[41] << 1) + bits[40])*10 + (bits[35] << 3) + (bits[34] << 2) + (bits[33] << 1) + bits[32];
    hour = ((bits[57] << 1) + bits[56])*10 + (bits[51] << 3) + (bits[50] << 2) + (bits[49] << 1) + bits[48];
    timeStr = nf(hour, 2)+":"+nf(minute, 2)+":"+nf(second, 2)+"."+nf(frame, 2);
  }

  public String getTimeStr() {
    return timeStr;
  }
  public int getHour() {
    return hour;
  }
  public int getMinute() {
    return minute;
  }
  public int getSecond() {
    return second;
  }
  public int getFrame() {
    return frame;
  }
  public int[] getTimeArrayInt() {
    int time[] = {hour, minute, second, frame};
    return time;
  }
  public long getTimeInFramesLong() {
    long time = frame + (second * 25) + (minute * 1500) + (hour * 90000);
    return time;
  }
}

Use the complete constructor if needed, or just call:
smpte = new SmpteReader();
And you can see the timecode this way:
text(smpte.timeStr, width/2, height/2);

Hope it can be useful for others.
Davide.

Thank you for sharing this!

If you would like to turn this into a library, here are my recommendations:

  1. read the Library wiki pages
  2. get Eclipse
  3. download either the official template or the minimal template
  4. add those template files to a new github repo with your project name
  5. copy your code in and commit. don’t worry about whether it works yet.

At this point, you can start trying to get your library to compile and install yourself – or at any time you can post a link to your github and ask for help. Then people can download your latest repo and give you concrete advice on how to get it working. You should definitely include an example sketch so that it can be tested!

Once it works, you can submit it to have it included in Contributions Manager.