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;
}
}
@In(1) AudioIn in; @Out(1) AudioOut out; @P(1) double last; @P(2) double cnt; @Override public void init() { log(INFO, "Sample rate is " + sampleRate); log(INFO, "Buffer size is " + blockSize);
link(in, fn(s -> { cnt++; if (s > 0 && last < 0 || s < 0 && last > 0) { log(INFO, "zero crossing at time " + millis()); log(INFO, "cnt: "+cnt); cnt = 0;
..And also, it is difficult for me to read the function you wrote, I mean, where does 's' take its value? is it a prebuilt var?
Didn't know fn() creates a ugen, is that written in the docs?
@In(1) AudioIn in; @Out(1) AudioOut out; @AuxOut(1) Output smpteTime; private double last; private double sampleCnt; private int bitsPerMessage = 80; private int bits[] = new int[bitsPerMessage]; private int bitArrayCnt; private boolean bitReadComplete = true; private int word; public int frame, second, minute, hour; public String timeStr = ""; @UGen Gain gain; @P(2) @Type.Number(min=0, max=2, skew=4) Property level; @Override public void init() { level.link(gain::level); log(INFO, "Sample rate is " + sampleRate); log(INFO, "Buffer size is " + blockSize);
link(in, gain, fn(s -> { sampleCnt++; if (s > 0 && last < 0 || s < 0 && last > 0) { //log(INFO, "cnt: "+sampleCnt); // Zero Crossing: if (sampleCnt < 13 && sampleCnt > 10) { // every half bit is: (44100 / 25)/(80*2) = 11.025 // 11.025 frames bitReadComplete = !bitReadComplete; if (bitReadComplete) { //log(INFO, "Adding a UNO 1"); word = (word << 1) + 1; bits[bitArrayCnt] = 1; bitArrayCnt++; } else { //log(INFO, "Read first short......."); } } else if (sampleCnt > 21 && sampleCnt < 24) { // 22.050 frames means a bit = 0; //log(INFO, "Adding a ZERO"); bitReadComplete = true; word = word << 1; bits[bitArrayCnt] = 0; bitArrayCnt++; } //log(INFO, "Word = "+Integer.toBinaryString(word & 65535)); if ((word & 65535) == 16381) { // Sync word: //log(INFO, "Sync word!"); calcTime(); sendTime(); word = 0; bitArrayCnt = 0; for (int i = 0; i < bits.length-1; i++) { bits[i] = 0; } } if (bitArrayCnt > bits.length-1) { for (int i = 1; i < bits.length; i++) { bits[i-1] = bits[i]; } bitArrayCnt--; } sampleCnt = 0; } last = s; return s; }), out); }
@Override public void update() {
} 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 = String.format("%02d" , hour)+":"+String.format("%02d" , minute)+":"+String.format("%02d" , second)+"."+String.format("%02d" , frame); log(INFO, timeStr); } private void sendTime() { try { smpteTime.send(timeStr); } catch (Exception ex) { log(ERROR, ex); } }I'm confused by this - how do you get SMPTE at 44100 into a soundcard
running at 48000? If it's from a file then that might be at a
different sample rate, but it should be resampled automatically IIRC.