import os import sys import copy import glob import wave from Riff import * from pprint import pprint """ ======================================================================== Convert Stardust Dust Flux Monitor Instrument (DFMI) encounter data to a WAVE Audio file. ======================================================================== Based on DFMI SoundMaker 2, written by Pasquale DiDonna in Visual Basic. ======================================================================== 1) Parse the command line arguments: 1.1) WAVE directory [required] 1.2) Ouput WAVE files name [required] 1.3) Threshold ID for LEFT audio channel [default M1, optional, one of A1b,A1a,A2b,A2a, M1,M2,M3,M4, m1,m2,m3,m4, N1,N2,N3,N4] 1.4) Threshold ID for RIGHT audio channel [default m1, optional, one of A1b,A1a,A2b,A2a, M1,M2,M3,M4, m1,m2,m3,m4, N1,N2,N3,N4] 2) Read in sampled WAVE files from WAVE directory (1.1 above): - tick.wav: contains one click over 100ms - tick11.wave - tick120.wav: contain 11 to 120 clicks over 1s each - create a no-sound WAVE sample 2.1) In the Cornell Science Data Center, these WAVE files are in subdirectory wavs/ relative to where this file, tab2wav.py resides 3) Read STDIN as the comma-separated-values from a DFMI data file, e.g. dfmienc_v2_yyyymmdd_hhmmss.tab one line at a time 4) Check the DFMI SYNC, mode and clock columns: 4.1) DFMI SYNC must be "A5", column 4, or the line is skipped 4.2) DFMI mode must be "NORM", column 5, or the line is skipped 5) Calculate changes across twelve Threshold counters in columns 9-20 from previous line, adding 8- or 16-bit rollover so changes are always non-negative 6) Compare 1-second DFMI clock, column 3, against previous line's DFMI clock 6.1) When the DFMI clock does not change, accumulate counter changes within that DFMI clock second 6.2) When the DFMI clock does change, use the accumulated counts from the previous second, and from the Threshold counter columns selected (1.3 and 1.4 above), and assign to that second the two WAVE files with the maximum numbers of clicks less than each of those counters. 6.2.1) If an accumulated count is greater than 0 and less than 11, the script uses the WAVE data from tick.wav (one click in 100ms to generate a one-second sample with the correct number of clicks. 7) When all lines have been read, the concatenate and write out the samples to the output WAVE file (1.2 above) ======================================================================== Usage: python tab2wav.py wavs/ out.wav [aLeft[ aRight]] < dfmienc_v2_yyyymmdd_hhmmss.tab wavs/ => directory containing tick.wav, tick11.wav to tick120.wav out.wav => output WAVE filepath aLeft => threshold to use for left speaker: A[12][ab] M[1234] m[1234] N[1234] aLeft => threshold to use for right speaker: " e.g. to use all the data from the Tempel 1 encounter from 04:38:50 to 04:39:30, use this command: awk -F, \ '$1<"\\"2011-02-15T04:38:50{next}$1>"\\"2011-02-15T04:39:30{next}{print}' \ dfmienc_v2_110215_162959.tab \ | python csv2wav.py wavs out.wav M1 m1 < dfmienc_v1_110215_162959.csv """ ######################################################################## ### Utility routines ######################################################################## bytPerSec=20000 ######################################################################## ### find the key in tickDict that is closest to nInp without being ### greater than nInp. If 0<nInp<11 and nInp is not in tickDict, create ### a 1s Riff from nInp x 100ms in tickDict[-1] and a lot of nulls. def getRiffN(nInp): ### If nInp is a list or tuple, return the list of keys if type(nInp) is list or type(nInp) is tuple: rtn=[] for n in nInp: rtn += [getRiffN(n)] return rtn ### Matching key is in tickDict, return it if nInp in tickDict: return nInp ### Find largest match less than nInp if nInp>10: bestN=0 for N in tickDict: if nInp>N and N>bestN: bestN = N return bestN ### create matches 1 to 10 from 100ms in tickDict[-1] ... nPad = max( 0 ,(bytPerSec/nInp) - tickDict[-1].subChunks['data'][0].Length ) riff1Data = tickDict[-1].subChunks['data'][0].Data + chr(0) * nPad newRiff = emptyRiff.deepcopy() while newRiff.subChunks['data'][0].Length<bytPerSec: newRiff.subChunks['data'][0].addData( (riff1Data*(nInp+1))[:bytPerSec] ) tickDict[nInp] = newRiff return nInp ######################################################################## ### Start ... emptyRiff = Riff(fyleArg=stryle('RIFF' + struct.pack('<l',36) + 'WAVE' + 'fmt ' + struct.pack('<l',16) + (chr(0)*16) + 'data' + struct.pack('<l',0) + '' ) ) ###emptyRiff.pout(dataShow=4) ######################################################################## ### Parse RIFF WAVE files tick.wav and tickNN.wav and tickNNN.wav in ### directory sys.argv[1] into Riff instances. Each one contains ### 100ms or 1s of 10kHz-sampled ### PCM sound with a number (NN or NNN) ### of clicks. av=sys.argv[1:] tickDict={} for tickFn in glob.glob( os.path.join(av.pop(0),'tick*.wav') ): bn=os.path.basename(tickFn) lenbn=len(bn) if len(bn)<8 : continue ### should be impossible, ignore if len(bn)==8: id=-1 ### tick.wav, 100ms one click elif lenbn==9: continue ### tick1.wav or tickN.wav, ignore elif lenbn>11: continue ### tickNNNN*.wav, too high else: id=int(bn[4:].split('.')[0]) ### tickNN.wav or tickNNN.wav, 1s, multiple clicks tickDict[id] = Riff(tickFn) ### Append zeroes if this Riff is shorter than 20,000 bytes L =tickDict[id].subChunks['data'][0].Length if id>10 and L<bytPerSec: tickDict[id].subChunks['data'][0].addData( chr(0)*(bytPerSec-L) ) L =tickDict[id].subChunks['data'][0].Length ######################################################################## ### sys.argv[2] is output .wav file outWavFn = av.pop(0) ######################################################################## ### Create RIFF instance of 1s of no sound ### N.B. this will overwrite any tickDict[0] (e.g. tick00.wav) for id in tickDict: if id<10: continue waveRiff=tickDict[id] tickDict[0] = emptyRiff.deepcopy() tickDict[0].replaceSubChunk(tickDict[id].subChunks['fmt '][0]) tickDict[0].subChunks['data'][0].addData( chr(0)*tickDict[id].subChunks['data'][0].Length ) break ######################################################################## ### Lookup for sys.argv[3:5] to select thresholds ### A[12][ab] - acoustic sensor thresholds ### M[1234] - Large PVDS sensor thresholds ### m[1234] - Small PVDS sensor thresholds ### N[1234] - duplicate acoustic sensor thresholds A[12][ab] threshDict={} for i,n in enumerate( 'A1b A1a A2b A2a M1 M2 M3 M4 m1 m2 m3 m4 ,LAST,'.split()): threshDict[n]=i for i,n in enumerate( 'N1 N2 N3 N4'.split()): threshDict[n]=threshDict['A1b']+i rollovers = (256,)*4 + (65536,)*8 ### four 8-bit acoustic and eight 16-bit PVDF counters aLeft = 'M1' ### Default to two channels, M1 and m1 aRight = 'm1' ######################################################################## ### Optionally override default channels ### Column offsets of left & right columns, offset from first column ### Calculate where columns are in CSV raw_input() line ### Initialze counters and DFMI clock ### initialize number of output seconds and .Length if av: aLeft=av.pop(0) ### sys.argv[3] may override aLeft if av: aRight=av.pop(0) ### sys.argv[3] may override aRight iLeft,iRight = (threshDict[aLeft],threshDict[aRight]) nCol = threshDict[',LAST,'] firstCol=8 lastCol=firstCol + nCol newDfmiClk='' zeroCounters=[0]*nCol newCounters= copy.copy(zeroCounters) sumCounters= copy.copy(zeroCounters) zeroCt = [0]*3 secIds = [zeroCt]*345 ### init buffer at 1k, double as needed iSeconds = 0 Length = 0 ######################################################################## ### Loop through lines try: while True: #################################################################### ### Split line at commas, skip bad sync and non-NORMal lines toks=raw_input().split(',') if toks[3]!='"A5"': continue if toks[4]!='"NORM"': continue ### Copy last new counters and DFMI clock to old oldDfmiClk = newDfmiClk oldCounters=copy.copy(newCounters) newCounters=eval( '[' + ','.join(toks[firstCol:lastCol]) + ']' ) newDfmiClk=toks[2].strip() #################################################################### ### if this is a new second, save info for old second if newDfmiClk!=oldDfmiClk: if not oldDfmiClk: continue ### skip to next line if this is the first second ### Double buffer as needed if iSeconds==len(secIds): secIds += [zeroCt]*len(secIds) iL,iR = getRiffN( [sumCounters[iLeft],sumCounters[iRight]] ) dL = min(tickDict[secIds[iSeconds][0]].subChunks['data'][0].Length ,tickDict[secIds[iSeconds][1]].subChunks['data'][0].Length ) secIds[iSeconds] = [iL,iR,dL] Length += dL iSeconds+=1 sumCounters = copy.copy( zeroCounters ) #################################################################### ### add counter changes, accounting for rollover for i,newV in enumerate( newCounters): sumCounters[i] += (newV - oldCounters[i]) + rollovers[i] * (newV<oldCounters[i]) except: pass pprint( dict(Length=Length,iSeconds=iSeconds,lensecIds=len(secIds),oldDfmiClk=oldDfmiClk,newDfmiClk=newDfmiClk) ) ###pprint( secIds ) f=wave.open(outWavFn,"wb") f.setnchannels(2) ### two channels f.setsampwidth(2) ### 16-bit samples f.setframerate(10000) ### 10k sample s^-1 f.setcomptype("NONE",'') ### No compression f.setnframes(Length) for iL,iR,L in secIds: dL = tickDict[iL].subChunks['data'][0].Data dR = tickDict[iR].subChunks['data'][0].Data for i in range(0,L,2): f.writeframesraw( dL[i:i+2] + dR[i:i+2] ) f.close() ##pprint( locals() ) """ "2011-02-15T04:37:53.418","0982212286:147", 1252,"A5","NORM","ENCOUNTER",0,"L", 0, 17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 289.30,... "2011-02-15T04:37:54.316","0982212287:121", 1253,"A5","NORM","ENCOUNTER",0,"L", 12, 64, 0, 0, 0, 0, 0, 0, 62, 0, 0, 0, 289.30,... "2011-02-15T04:37:54.418","0982212287:147", 1253,"A5","NORM","ENCOUNTER",0,"L", 12, 86, 0, 0, 0, 0, 0, 0, 62, 0, 0, 0, 289.30,... "2011-02-15T04:37:55.019","0982212288:045", 1254,"A5","NORM","ENCOUNTER",0,"L", 12, 86, 0, 0, 0, 0, 0, 0, 63, 0, 0, 0, 289.30,... "2011-02-15T04:37:55.406","0982212288:144", 1254,"A5","NORM","ENCOUNTER",0,"L", 12, 86, 0, 0, 0, 0, 0, 0, 63, 0, 0, 0, 289.54,... "2011-02-15T04:37:56.406","0982212289:144", 1255,"A5","NORM","ENCOUNTER",0,"L", 12, 86, 0, 0, 0, 0, 0, 0, 63, 0, 0, 0, 289.54,... "2011-02-15T04:37:57.418","0982212290:147", 1256,"A5","NORM","ENCOUNTER",0,"L", 12, 86, 0, 0, 0, 0, 0, 0, 63, 0, 0, 0, 289.54,... "2011-02-15T04:37:58.418","0982212291:147", 1257,"A5","NORM","ENCOUNTER",0,"L", 12, 86, 0, 0, 0, 0, 0, 0, 63, 0, 0, 0, 289.54,... "2011-02-15T04:37:59.418","0982212292:147", 1258,"A5","NORM","ENCOUNTER",0,"L", 12, 86, 0, 0, 0, 0, 0, 0, 63, 0, 0, 0, 289.54,... "2011-02-15T04:38:00.418","0982212293:147", 1259,"A5","NORM","ENCOUNTER",0,"L", 12, 86, 0, 0, 0, 0, 0, 0, 63, 0, 0, 0, 289.54,... "2011-02-15T04:38:01.406","0982212294:144", 1260,"A5","NORM","ENCOUNTER",0,"L", 12, 86, 0, 0, 0, 0, 0, 0, 63, 0, 0, 0, 289.54,... "2011-02-15T04:38:01.703","0982212294:220", 1260,"A5","NORM","ENCOUNTER",0,"L", 13,110, 0, 0, 0, 0, 0, 0, 90, 0, 0, 0, 289.54,... "2011-02-15T04:38:02.406","0982212295:144", 1261,"A5","NORM","ENCOUNTER",0,"L", 13,134, 0, 0, 0, 0, 0, 0, 90, 0, 0, 0, 289.54,... """