/* * SMPTESynthDlg.java * (FScape) * * Copyright (c) 2001-2016 Hanns Holger Rutz. All rights reserved. * * This software is published under the GNU General Public License v3+ * * * For further information, please contact Hanns Holger Rutz at * contact@sciss.de * * * Changelog: * 21-May-08 created */ package de.sciss.fscape.gui; import de.sciss.fscape.io.GenericFile; import de.sciss.fscape.prop.Presets; import de.sciss.fscape.prop.PropertyArray; import de.sciss.fscape.session.ModulePanel; import de.sciss.fscape.util.Param; import de.sciss.io.AudioFile; import de.sciss.io.AudioFileDescr; import javax.swing.*; import java.awt.*; import java.io.IOException; import java.text.MessageFormat; import java.text.NumberFormat; import java.text.ParseException; import java.util.Locale; /** * Processing module synthesizing * SMPTE longitudinal timecode. * * http://www.philrees.co.uk/articles/timecode.htm * http://en.wikipedia.org/wiki/SMPTE_time_code * http://en.wikipedia.org/wiki/RC_time_constant * http://en.wikipedia.org/wiki/Low-pass_filter * http://local.wasp.uwa.edu.au/~pbourke/other/interpolation/ */ public class SMPTESynthDlg extends ModulePanel { // -------- private variables -------- // Properties (defaults) private static final int PR_OUTPUTFILE = 0; // pr.text private static final int PR_STARTTIME = 1; private static final int PR_STOPTIME = 2; private static final int PR_OUTPUTTYPE = 0; // pr.intg private static final int PR_OUTPUTRES = 1; private static final int PR_OUTPUTRATE = 2; private static final int PR_FRAMES = 3; // private static final int PR_MINPHASE = 0; // pr.bool private static final int PR_GAIN = 0; // pr.para private static final String PRN_OUTPUTFILE = "OutputFile"; private static final String PRN_STARTTIME = "StartTime"; private static final String PRN_STOPTIME = "StopTime"; private static final String PRN_OUTPUTTYPE = "OutputType"; private static final String PRN_OUTPUTRES = "OutputRes"; private static final String PRN_OUTPUTRATE = "OutputRate"; private static final String PRN_FRAMES = "Frames"; private static final int FRAMES_24 = 0; private static final int FRAMES_25 = 1; private static final int FRAMES_29 = 2; // 29.97 Drop Frame private static final int FRAMES_30 = 3; private static final String[] FRAMES_LABELS = { "24", "25", "29.97 DF", "30" }; private static final String[] prText = { "", "00:00:00:00", "00:05:00:00" }; private static final String[] prTextName = { PRN_OUTPUTFILE, PRN_STARTTIME, PRN_STOPTIME }; private static final int[] prIntg = { 0, 0, 1, FRAMES_30 }; private static final String[] prIntgName = { PRN_OUTPUTTYPE, PRN_OUTPUTRES, PRN_OUTPUTRATE, PRN_FRAMES }; // private static final boolean[] prBool = { false }; // private static final String[] prBoolName = { PRN_MINPHASE }; private static final Param[] prPara = { new Param( -18.0, Param.DECIBEL_AMP )}; private static final String[] prParaName = { PRN_GAIN }; private static final int GG_OUTPUTFILE = GG_OFF_PATHFIELD + PR_OUTPUTFILE; private static final int GG_STARTTIME = GG_OFF_TEXTFIELD + PR_STARTTIME; private static final int GG_STOPTIME = GG_OFF_TEXTFIELD + PR_STOPTIME; private static final int GG_OUTPUTTYPE = GG_OFF_CHOICE + PR_OUTPUTTYPE; private static final int GG_OUTPUTRES = GG_OFF_CHOICE + PR_OUTPUTRES; private static final int GG_OUTPUTRATE = GG_OFF_CHOICE + PR_OUTPUTRATE; private static final int GG_FRAMES = GG_OFF_CHOICE + PR_FRAMES; // private static final int GG_MINPHASE = GG_OFF_CHECKBOX + PR_MINPHASE; private static final int GG_GAIN = GG_OFF_PARAMFIELD + PR_GAIN; private static PropertyArray static_pr = null; private static Presets static_presets = null; private final MessageFormat smpteMsgFrmt = new MessageFormat( "{0}:{1}:{2}:{3}" ); private final NumberFormat smpteNumFrmt = NumberFormat.getIntegerInstance( Locale.US ); // -------- public methods -------- public SMPTESynthDlg() { super( "SMPTE Synthesizer" ); init2(); } protected void buildGUI() { // einmalig PropertyArray initialisieren if( static_pr == null ) { static_pr = new PropertyArray(); static_pr.text = prText; static_pr.textName = prTextName; static_pr.intg = prIntg; static_pr.intgName = prIntgName; // static_pr.bool = prBool; // static_pr.boolName = prBoolName; static_pr.para = prPara; static_pr.paraName = prParaName; // static_pr.superPr = DocumentFrame.static_pr; fillDefaultAudioDescr( static_pr.intg, PR_OUTPUTTYPE, PR_OUTPUTRES, PR_OUTPUTRATE ); static_presets = new Presets( getClass(), static_pr.toProperties( true )); } presets = static_presets; pr = (PropertyArray) static_pr.clone(); // -------- build GUI -------- final GridBagConstraints con; final PathField ggOutputFile; final Component[] ggGain; final JComboBox ggFrames; final JFormattedTextField ggStartTime, ggStopTime; gui = new GUISupport(); con = gui.getGridBagConstraints(); con.insets = new Insets( 1, 2, 1, 2 ); // -------- Output Gadgets -------- con.fill = GridBagConstraints.BOTH; con.gridwidth = GridBagConstraints.REMAINDER; gui.addLabel( new GroupLabel( "LTC Output", GroupLabel.ORIENT_HORIZONTAL, GroupLabel.BRACE_NONE )); ggOutputFile = new PathField( PathField.TYPE_OUTPUTFILE + PathField.TYPE_FORMATFIELD + PathField.TYPE_RESFIELD + PathField.TYPE_RATEFIELD, "Select output file" ); ggOutputFile.handleTypes( GenericFile.TYPES_SOUND ); con.gridwidth = 1; con.weightx = 0.1; gui.addLabel( new JLabel( "Filename", SwingConstants.RIGHT )); con.gridwidth = GridBagConstraints.REMAINDER; con.weightx = 0.9; gui.addPathField( ggOutputFile, GG_OUTPUTFILE, null ); gui.registerGadget( ggOutputFile.getTypeGadget(), GG_OUTPUTTYPE ); gui.registerGadget( ggOutputFile.getResGadget(), GG_OUTPUTRES ); gui.registerGadget( ggOutputFile.getRateGadget(), GG_OUTPUTRATE ); ggGain = createGadgets( GGTYPE_GAIN ); con.weightx = 0.1; con.gridwidth = 1; gui.addLabel( new JLabel( "Amplitude", SwingConstants.RIGHT )); con.weightx = 0.4; con.gridwidth = GridBagConstraints.REMAINDER; gui.addParamField( (ParamField) ggGain[ 0 ], GG_GAIN, null ); // con.weightx = 0.5; // con.gridwidth = GridBagConstraints.REMAINDER; // gui.addChoice( (JComboBox) ggGain[ 1 ], GG_GAINTYPE, null ); // -------- Settings Gadgets -------- gui.addLabel( new GroupLabel( "Settings", GroupLabel.ORIENT_HORIZONTAL, GroupLabel.BRACE_NONE )); smpteNumFrmt.setMinimumIntegerDigits( 2 ); smpteNumFrmt.setMaximumIntegerDigits( 2 ); for( int i = 0; i < 4; i++ ) smpteMsgFrmt.setFormat( i, smpteNumFrmt ); ggStartTime = new JFormattedTextField(); con.gridwidth = 1; con.weightx = 0.1; gui.addLabel( new JLabel( "Start Time", SwingConstants.RIGHT )); con.weightx = 0.4; gui.addTextField( ggStartTime, GG_STARTTIME, null ); ggFrames = new JComboBox(); for( int i = 0; i < FRAMES_LABELS.length; i++ ) { ggFrames.addItem( FRAMES_LABELS[ i ]); } con.weightx = 0.1; gui.addLabel( new JLabel( "Frames/sec.", SwingConstants.RIGHT )); con.weightx = 0.4; con.gridwidth = GridBagConstraints.REMAINDER; gui.addChoice( ggFrames, GG_FRAMES, null ); ggStopTime = new JFormattedTextField(); con.gridwidth = 1; con.weightx = 0.1; gui.addLabel( new JLabel( "Stop Time", SwingConstants.RIGHT )); con.weightx = 0.4; gui.addTextField( ggStopTime, GG_STOPTIME, null ); initGUI( this, FLAGS_PRESETS | FLAGS_PROGBAR, gui ); } /** * Transfer values from prop-array to GUI */ public void fillGUI() { super.fillGUI(); super.fillGUI( gui ); } /** * Transfer values from GUI to prop-array */ public void fillPropertyArray() { super.fillPropertyArray(); super.fillPropertyArray( gui ); } // -------- Processor Interface -------- protected void process() { final Param ampRef = new Param( 1.0, Param.ABS_AMP ); // transform-Referenz final float amp; final int frames, framesPerBuf; final boolean drop; final byte[] word = new byte[ 10 ]; final float[] buf, bufR; final float[][] outBuf; final AudioFileDescr outStream; final PathField ggOutput; final int smpsPerHalfbit, smpsPerWord, calcRate; final int inBufPre = 2; final double tau, rf; final float alpha; final long outLength; final int inBufLen; AudioFile outF = null; byte hoursStart, minsStart, secsStart, framesStart; byte hoursStop, minsStop, secsStop, framesStop, tempB; int numStartFrames,numStopFrames, frameOffset, tempI; long progOff, progLen, sampleOffset, sampleOffsetR; int chunkLen, outBufOff, phase, outBufPre, inBufOff; float yold, y0, y1, y2, y3, mu, mu2, a0, a1, a2, a3; double d1; Object[] msgArgs; topLevel: try { // ---- open files ---- // if( true ) throw new IOException( "NOT YET WORKING" ); switch( pr.intg[ PR_FRAMES ]) { case FRAMES_24: frames = 24; drop = false; break; case FRAMES_25: frames = 25; drop = false; break; case FRAMES_29: frames = 30; drop = true; // break; throw new IllegalArgumentException( "Drop Frame is not supported yet!" ); case FRAMES_30: frames = 30; drop = false; break; default: throw new IllegalArgumentException( String.valueOf( pr.intg[ PR_FRAMES ])); } msgArgs = smpteMsgFrmt.parse( pr.text[ PR_STARTTIME ]); hoursStart = ((Number) msgArgs[ 0 ]).byteValue(); minsStart = ((Number) msgArgs[ 1 ]).byteValue(); secsStart = ((Number) msgArgs[ 2 ]).byteValue(); framesStart = ((Number) msgArgs[ 3 ]).byteValue(); numStartFrames = ((hoursStart * 60 + minsStart) * 60 + secsStart) * frames + framesStart; msgArgs = smpteMsgFrmt.parse( pr.text[ PR_STOPTIME ]); hoursStop = ((Number) msgArgs[ 0 ]).byteValue(); minsStop = ((Number) msgArgs[ 1 ]).byteValue(); secsStop = ((Number) msgArgs[ 2 ]).byteValue(); framesStop = ((Number) msgArgs[ 3 ]).byteValue(); numStopFrames = ((hoursStop * 60 + minsStop) * 60 + secsStop) * frames + framesStop; // allow swapping if( numStartFrames > numStopFrames ) { tempI = numStartFrames; numStartFrames = numStopFrames; numStopFrames = tempI; tempB = hoursStart; hoursStart = hoursStop; hoursStop = tempB; tempB = minsStart; minsStart = minsStop; minsStop = tempB; tempB = secsStart; secsStart = secsStop; secsStop = tempB; tempB = framesStart; framesStart = framesStop; framesStop = tempB; } ggOutput = (PathField) gui.getItemObj( GG_OUTPUTFILE ); if( ggOutput == null ) throw new IOException( ERR_MISSINGPROP ); outStream = new AudioFileDescr(); ggOutput.fillStream( outStream ); outStream.channels = 1; // outStream.rate = frames * 160 * smpsPerHalfbit; // XXX outF = AudioFile.openAsWrite( outStream ); // .... check running .... if( !threadRunning ) break topLevel; // smpsPerHalfbit = (int) Math.floor( outStream.rate / (frames * 160) ); smpsPerHalfbit = (int) Math.ceil( outStream.rate / (frames * 160) ); calcRate = frames * 160 * smpsPerHalfbit; // System.out.println( "calculation sample rate is " + calcRate ); tau = 25.0e-6 / Math.log( 9 ); // 25 micro-s rise time --> time constant alpha = (float) (1.0 / (tau * calcRate + 1)); rf = calcRate / outStream.rate; // reciprocal resampling factor // System.out.println( "tau " + tau + "; alpha " + alpha ); smpsPerWord = 160 * smpsPerHalfbit; progOff = 0; outLength = (long) Math.ceil( (long) (numStopFrames - numStartFrames) * smpsPerWord / rf ); progLen = outLength; frameOffset = numStartFrames; framesPerBuf = 50; yold = 0f; // buf = new float[ framesPerBuf * 160 ]; inBufLen = framesPerBuf * smpsPerWord + 3; buf = new float[ inBufLen ]; bufR = new float[ (int) Math.ceil( framesPerBuf * smpsPerWord / rf ) + 1 ]; outBuf = new float[][] { bufR }; outBufPre = 1; // 2; // 1; // delay compensation // ---- synthesize output ---- // Bits 0-3 : Frame Units (BCD-coded with LSB-first) // Bits 4-7 : User Bits 1 (all zero) // Bits 8-9 : Frame Tens (BCD-coded with LSB-first) // Bit 10 : Drop Frame Bool // Bit 11 : Colour Frame Bool (always zero) // Bits 12-15 : User Bits 2 (all zero) // Bits 16-19 : Secs Units (BCD-coded with LSB-first) // Bits 20-23 : User Bits 3 (all zero) // Bits 24-26 : Secs Tens (BCD-coded with LSB-first) // Bit 27 : Bi-Phase Mark Correction Bit (always zero) // Bits 28-31 : User Bits 4 (all zero) // Bits 32-35 : Mins Units (BCD-coded with LSB-first) // Bits 36-39 : User Bits 5 (all zero) // Bits 40-42 : Mins Tens (BCD-coded with LSB-first) // Bit 43 : Binary Group Flag Bit 1 (always zero) // Bits 44-47 : User Bits 6 (all zero) // Bits 48-51 : Hours Units (BCD-coded with LSB-first) // Bits 52-55 : User Bits 7 (all zero) // Bits 56-57 : Hours Tens (BCD-coded with LSB-first) // Bit 58 : Reserved (always zero) // Bit 59 : Binary Group Flag Bit 2 (always zero) // Bits 60-63 : User Bits 8 (all zero) // Bits 64-79 : Sync Word (0011 1111 1111 1101) if( drop ) word[ 1 ] |= (byte) 0x04; word[ 8 ] = (byte) 0xFC; // NOT: 0x3F; word[ 9 ] = (byte) 0xBF; amp = (float) (Param.transform( pr.para[ PR_GAIN ], Param.ABS_AMP, ampRef, null )).value; phase = -1; sampleOffset = 0; sampleOffsetR = 0; // y1 = 0; y2 = 0; y3 = 0; while( threadRunning && (sampleOffsetR < outLength) ) { chunkLen = Math.min( framesPerBuf, numStopFrames - frameOffset ); inBufOff = inBufPre; for( int i = 0; i < chunkLen; i++ ) { // if( drop ) ... XXX bcd( framesStart % 10, word, 0 ); bcd( framesStart / 10, word, 8, 2 ); bcd( secsStart % 10, word, 16 ); bcd( secsStart / 10, word, 24, 3 ); bcd( minsStart % 10, word, 32 ); bcd( minsStart / 10, word, 40, 3 ); bcd( hoursStart % 10, word, 48 ); bcd( hoursStart / 10, word, 56, 2 ); for( int j = 0; j < 10; j++ ) { for( int m = 0, n = word[ j ]; m < 8; m++, n >>= 1 ) { phase = -phase; for( int p = 0; p < smpsPerHalfbit; p++ ) buf[ inBufOff++ ] = phase; if( (n & 1) == 1 ) phase = -phase; for( int p = 0; p < smpsPerHalfbit; p++ ) buf[ inBufOff++ ] = phase; } } // increment if( ++framesStart == frames ) { framesStart = 0; if( ++secsStart == 60 ) { secsStart = 0; if( ++minsStart == 60 ) { minsStart = 0; if( ++hoursStart == 24 ) { hoursStart = 0; } } } } } for( int i = inBufOff; i < buf.length; i++ ) { buf[ i ] = 0f; } // low pass filter for( int i = inBufPre; i < buf.length; i++ ) { yold = yold + (alpha * (buf[ i ] - yold)); buf[ i ] = yold * amp; } // resample using cubic interolation d1 = sampleOffsetR * rf - sampleOffset + 1; // d1 = sampleOffset * rf - sampleOffsetR; outBufOff = 0; // assert ((int) d1) == 0; //if( (int) d1 != 0 ) { // System.out.println( "for sampleOffset " + sampleOffset + "; sampleOffsetR " + sampleOffsetR + "; rf " + rf + "; d1 is " + d1 ); //} // y1 = buf[ 0 ]; // y2 = buf[ 1 ]; // y3 = buf[ 2 ]; for( int j = (int) d1; j < inBufLen - inBufPre; outBufOff++ ) { y0 = buf[ j - 1 ]; y1 = buf[ j ]; y2 = buf[ j + 1]; y3 = buf[ j + 2 ]; mu = (float) (d1 % 1.0); mu2 = mu * mu; a0 = y3 - y2 - y0 + y1; a1 = y0 - y1 - a0; a2 = y2 - y0; a3 = y1; bufR[ outBufOff ] = a0 * mu * mu2 + a1 * mu2 + a2 * mu + a3; // bufR[ chunkLenR ] = y0 * (1f - mu) + y1 * mu; d1 += rf; j = (int) d1; } // handle overlap buf[ 0 ] = buf[ inBufLen - 2 ]; buf[ 1 ] = buf[ inBufLen - 1 ]; // buf[ 2 ] = y3; // sampleOffsetR += chunkLenR; // chunkLenR = (int) Math.min( chunkLenR - outBufOff, outLength - sampleOffsetR ); outBufOff = (int) Math.min( outBufOff, outLength - sampleOffsetR + outBufPre ); outF.writeFrames( outBuf, outBufPre, outBufOff - outBufPre ); frameOffset += chunkLen; sampleOffset += (inBufOff - inBufPre); sampleOffsetR += outBufOff; outBufPre = 0; progOff += outBufOff; // .... progress .... setProgression( (float) progOff / (float) progLen ); } // .... check running .... if( !threadRunning ) break topLevel; // ---- Finish ---- outF.close(); outF = null; // .... check running .... if( !threadRunning ) break topLevel; } catch( ParseException e1 ) { setError( e1 ); } catch( IOException e1 ) { setError( e1 ); } // ---- cleanup ---- if( outF != null ) { outF.cleanUp(); } } // -------- private Methods -------- private void bcd( int units, byte[] word, int bitOffset ) { bcd( units, word, bitOffset, 4 ); } private void bcd( int units, byte[] word, int bitOffset, int numBits ) { final int unitsShift = (bitOffset & 7); // final int unitsMask = ~(0x0F << unitsShift); final int unitsMask = ~(((1 << numBits) - 1) << unitsShift); final int unitsEnc = units << unitsShift; final int unitsIdx = bitOffset >> 3; word[ unitsIdx ] = (byte) (word[ unitsIdx ] & unitsMask | unitsEnc); word[ unitsIdx + 1 ] = (byte) (word[ unitsIdx + 1 ] & (unitsMask >> 8) | (unitsEnc >> 8)); } }