//| Copyright: (C) 2009-2020 Kevin Larke //| License: GNU GPL version 3.0 or above. See the accompanying LICENSE file. #include "cmGlobal.h" #include "cmFloatTypes.h" #include "cmRpt.h" #include "cmErr.h" #include "cmCtx.h" #include "cmMem.h" #include "cmMallocDebug.h" #include "cmTime.h" #include "cmFile.h" #include "cmAudioFile.h" #include "cmSyncRecd.h" #include "cmVectOpsTemplateMain.h" #include "cmMidi.h" typedef enum { kInvalidSrId, kMidiSrId, kAudioSrId } cmSrTypeId_t; typedef struct cmSrMidi_str { cmTimeSpec_t timestamp; unsigned status; unsigned d0; unsigned d1; } cmSrMidi_t; typedef struct cmSrAudio_str { cmTimeSpec_t timestamp; unsigned smpIdx; } cmSrAudio_t; typedef struct cmSrRecd_str { cmSrTypeId_t tid; union { cmSrMidi_t m; cmSrAudio_t a; } u; } cmSrRecd_t; enum { kReadSrFl = 0x01, // This is a read (not a write) file kFileUUSrId = 0xf00d }; typedef struct cmSr_str { cmErr_t err; cmFileH_t fH; cmAudioFileH_t afH; unsigned flags; // See kXXXSrFl cmSrRecd_t* cache; // cache[cn] cmSrAudio_t* map; // unsigned cn; // count of records in cache[]. unsigned ci; // next cache recd index unsigned fn; // count of recds written to file long offs; cmAudioFileInfo_t afInfo; } cmSr_t; cmSyncRecdH_t cmSyncRecdNullHandle = cmSTATIC_NULL_HANDLE; cmSr_t* _cmSrHtoP( cmSyncRecdH_t h ) { cmSr_t* p = (cmSr_t*)h.h; assert(p!=NULL); return p; } cmSyRC_t _cmSrWriteCache( cmSr_t* p ) { if( cmFileWrite(p->fH,p->cache,p->ci * sizeof(cmSrRecd_t)) != kOkFileRC ) return cmErrMsg(&p->err,kFileFailSyRC,"File write failed."); p->fn += p->ci; p->ci = 0; return kOkSyRC; } cmSyRC_t _cmSrFinal( cmSr_t* p ) { cmSyRC_t rc = kOkSyRC; // write any remaining cache records if( cmIsFlag(p->flags,kReadSrFl) == false ) { if((rc = _cmSrWriteCache(p)) == kOkSyRC ) { if(cmFileSeek(p->fH,kBeginFileFl,p->offs) != kOkFileRC ) rc = cmErrMsg(&p->err,kFileFailSyRC, "File seek fail on file offset positioning."); else if(cmFileWriteUInt(p->fH,&p->fn,1) != kOkFileRC ) rc = cmErrMsg(&p->err,kFileFailSyRC, "File write failed on record count."); } } // release the audio file object if( cmAudioFileIsValid(p->afH) ) if( cmAudioFileDelete(&p->afH) != kOkAfRC ) return cmErrMsg(&p->err,kAudioFileFailSyRC,"Audio file object delete failed."); // release the sync-recd file object if( cmFileIsValid(p->fH) ) if( cmFileClose(&p->fH) != kOkFileRC ) return cmErrMsg(&p->err,kFileFailSyRC,"File close failed."); cmMemFree(p->cache); cmMemFree(p->map); cmMemFree(p); return rc; } cmSr_t* _cmSrAlloc( cmCtx_t* ctx, unsigned flags ) { cmSr_t* p = cmMemAllocZ(cmSr_t,1); cmErrSetup(&p->err,&ctx->rpt,"SyncRecd"); p->cn = 2048; p->cache = cmMemAllocZ(cmSrRecd_t,p->cn); return p; } cmSyRC_t cmSyncRecdCreate( cmCtx_t* ctx, cmSyncRecdH_t* hp, const cmChar_t* srFn, const cmChar_t* audioFn, double srate, unsigned chCnt, unsigned bits ) { cmSyRC_t rc = kOkSyRC; cmRC_t afRC = kOkAfRC; assert( audioFn != NULL ); if((rc = cmSyncRecdFinal(hp)) != kOkSyRC ) return rc; cmSr_t* p = _cmSrAlloc(ctx,0); if( cmFileOpen(&p->fH,srFn,kWriteFileFl,&ctx->rpt) != kOkFileRC ) { rc = cmErrMsg(&p->err,kFileFailSyRC,"Unable to create the sync-recd file '%s'.",cmStringNullGuard(srFn)); goto errLabel; } if( cmAudioFileIsValid(p->afH = cmAudioFileNewCreate(audioFn,srate,bits,chCnt,&afRC,&ctx->rpt))==false) { rc = cmErrMsg(&p->err,kAudioFileFailSyRC,"Unable to create the sync-recd audio file '%s'.",cmStringNullGuard(audioFn)); goto errLabel; } unsigned fileUUId = kFileUUSrId; unsigned audioFnCnt = strlen(audioFn)+1; if( cmFileWriteUInt(p->fH,&fileUUId,1) != kOkFileRC ) { rc = cmErrMsg(&p->err,kFileFailSyRC,"File write failed on UUID."); goto errLabel; } if( cmFileWriteUInt(p->fH,&audioFnCnt,1) != kOkFileRC ) { rc = cmErrMsg(&p->err,kFileFailSyRC,"File write failed on audio file length write count."); goto errLabel; } if( cmFileWriteChar(p->fH,audioFn,audioFnCnt) != kOkFileRC ) { rc = cmErrMsg(&p->err,kFileFailSyRC,"File write failed on audio file string."); goto errLabel; } if( cmFileTell(p->fH,&p->offs) != kOkFileRC ) { rc = cmErrMsg(&p->err,kFileFailSyRC,"Unable to determine file offset."); goto errLabel; } if( cmFileWriteUInt(p->fH,&p->fn,1) != kOkFileRC ) { rc = cmErrMsg(&p->err,kFileFailSyRC,"File write failed on initial record count."); goto errLabel; } hp->h = p; errLabel: if( rc != kOkSyRC ) _cmSrFinal(p); return rc; } cmSyRC_t cmSyncRecdOpen( cmCtx_t* ctx, cmSyncRecdH_t* hp, const cmChar_t* srFn ) { cmSyRC_t rc = kOkSyRC; cmRC_t afRC = kOkAfRC; unsigned fileUUId = cmInvalidId; unsigned audioFnCnt = 0; cmChar_t* audioFn = NULL; unsigned i; unsigned acnt = 0; unsigned mcnt = 0; unsigned* tiV = NULL; if((rc = cmSyncRecdFinal(hp)) != kOkSyRC ) return rc; cmSr_t* p = _cmSrAlloc(ctx,kReadSrFl); if( cmFileOpen(&p->fH,srFn,kReadFileFl,&ctx->rpt) != kOkFileRC ) { rc = cmErrMsg(&p->err,kFileFailSyRC,"Unable to open the sync-recd file '%s'.",cmStringNullGuard(srFn)); goto errLabel; } if( cmFileReadUInt(p->fH,&fileUUId,1) != kOkFileRC ) { rc = cmErrMsg(&p->err,kFileFailSyRC,"File read failed on UUId."); goto errLabel; } if( cmFileReadUInt(p->fH,&audioFnCnt,1) != kOkFileRC ) { rc = cmErrMsg(&p->err,kFileFailSyRC,"File read failed on audio file name count."); goto errLabel; } audioFn = cmMemAllocZ(cmChar_t,audioFnCnt); if( cmFileReadChar(p->fH,audioFn,audioFnCnt) != kOkFileRC ) { rc = cmErrMsg(&p->err,kFileFailSyRC,"File read failed on audio file string."); goto errLabel; } if( cmFileReadUInt(p->fH,&p->fn,1) != kOkFileRC ) { rc = cmErrMsg(&p->err,kFileFailSyRC,"File read failed on record count."); goto errLabel; } // store the file offset to the first recd if( cmFileTell(p->fH,&p->offs) != kOkFileRC ) { rc = cmErrMsg(&p->err,kFileFailSyRC,"Unable to determine the current file offset."); goto errLabel; } // read each file - and count the types for(i=0; ifn; ++i) { cmSrRecd_t r; if( cmFileRead(p->fH,&r,sizeof(r)) != kOkFileRC ) { rc = cmErrMsg(&p->err,kFileFailSyRC,"Unable to read the record at index %i."); goto errLabel; } switch(r.tid) { case kMidiSrId: mcnt += 1; break; case kAudioSrId: acnt += 1; break; default: { assert(0); } } } printf("%i %i = %i\n",mcnt,acnt,p->fn); // rewind to the begining of the records if( cmFileSeek(p->fH,kBeginFileFl,p->offs) != kOkFileRC ) { rc = cmErrMsg(&p->err,kFileFailSyRC,"Unable to seek to first recd offset."); goto errLabel; } // allocate space to hold the MIDI records p->cn = mcnt; p->ci = 0; p->cache = cmMemResizeZ(cmSrRecd_t,p->cache,p->cn); p->map = cmMemAllocZ(cmSrAudio_t,p->cn); tiV = cmMemAllocZ(unsigned,p->cn); for(i=0; p->cicn && ifn; ++i) { if( cmFileRead(p->fH,p->cache + p->ci,sizeof(cmSrRecd_t)) != kOkFileRC ) { rc = cmErrMsg(&p->err,kFileFailSyRC,"Unable to read the record at index %i."); goto errLabel; } if( p->cache[p->ci].tid == kMidiSrId ) p->ci += 1; } // assert that all the MIDI records were read assert( p->ci == p->cn); // rewind to the first recd if( cmFileSeek(p->fH,kBeginFileFl,p->offs) != kOkFileRC ) { rc = cmErrMsg(&p->err,kFileFailSyRC,"Unable to seek to first recd offset."); goto errLabel; } // for each recd in the file for(i=0; ifn; ++i) { cmSrRecd_t r; if( cmFileRead(p->fH,&r,sizeof(r)) != kOkFileRC ) { rc = cmErrMsg(&p->err,kFileFailSyRC,"Unable to read the record at index %i."); goto errLabel; } // if this is an audio record if( r.tid == kAudioSrId ) { unsigned j; // for each midi record for(j=0; jcn; ++j) { // measure the time interval between this midi and this audio recd unsigned time_interval_micros = cmTimeAbsElapsedMicros(&r.u.a.timestamp,&p->cache[j].u.m.timestamp); // if the audio recd is closer to this midi recd than prior audio records ... if( time_interval_micros < tiV[j] || i==0 ) { // ... then store the audio time stamp in the map tiV[j] = time_interval_micros; p->map[j].timestamp = r.u.a.timestamp; p->map[j].smpIdx = r.u.a.smpIdx; } } } } // open the audio file if( cmAudioFileIsValid(p->afH = cmAudioFileNewOpen(audioFn,&p->afInfo,&afRC,&ctx->rpt ))==false) { rc = cmErrMsg(&p->err,kAudioFileFailSyRC,"Unable to open the sync-recd audio file '%s'.",cmStringNullGuard(audioFn)); goto errLabel; } p->flags = cmSetFlag(p->flags,kReadSrFl); hp->h = p; errLabel: cmMemFree(tiV); cmMemFree(audioFn); if( rc != kOkSyRC ) _cmSrFinal(p); return rc; } cmSyRC_t cmSyncRecdFinal( cmSyncRecdH_t* hp ) { cmSyRC_t rc = kOkSyRC; if( hp==NULL || cmSyncRecdIsValid(*hp)==false) return rc; cmSr_t* p = _cmSrHtoP(*hp); if((rc = _cmSrFinal(p)) != kOkSyRC ) return rc; hp->h = NULL; return rc; } bool cmSyncRecdIsValid( cmSyncRecdH_t h ) { return h.h != NULL; } cmSyRC_t cmSyncRecdMidiWrite( cmSyncRecdH_t h, const cmTimeSpec_t* timestamp, unsigned status, unsigned d0, unsigned d1 ) { cmSyRC_t rc = kOkSyRC; cmSr_t* p = _cmSrHtoP(h); cmSrRecd_t* rp = p->cache + p->ci; rp->tid = kMidiSrId; rp->u.m.timestamp = *timestamp; rp->u.m.status = status; rp->u.m.d0 = d0; rp->u.m.d1 = d1; p->ci += 1; if( p->ci == p->cn ) rc = _cmSrWriteCache(p); return rc; } cmSyRC_t cmSyncRecdAudioWrite( cmSyncRecdH_t h, const cmTimeSpec_t* timestamp, unsigned smpIdx, const cmSample_t* ch[], unsigned chCnt, unsigned frmCnt ) { cmSyRC_t rc = kOkSyRC; cmSr_t* p = _cmSrHtoP(h); cmSrRecd_t* rp = p->cache + p->ci; rp->tid = kAudioSrId; rp->u.a.timestamp = *timestamp; rp->u.a.smpIdx = smpIdx; p->ci += 1; if( p->ci == p->cn ) if((rc = _cmSrWriteCache(p)) != kOkSyRC ) goto errLabel; if( cmAudioFileWriteSample(p->afH,frmCnt,chCnt,(cmSample_t**)ch) != kOkAfRC ) rc = cmErrMsg(&p->err,kAudioFileFailSyRC,"Audio file write failed."); errLabel: return rc; } cmSyRC_t cmSyncRecdPrint( cmSyncRecdH_t h ) { cmSyRC_t rc = kOkSyRC; cmSr_t* p = _cmSrHtoP(h); unsigned i; if( cmIsFlag(p->flags,kReadSrFl)==false) return cmErrMsg(&p->err,kInvalidOpSyRC,"The 'print' operation is only valid on sync-recd files opened for reading."); for(i=0; icn; ++i) { cmSrRecd_t* r = p->cache + i; cmRptPrintf(p->err.rpt,"0x%x %3i %3i %ld %5.3f : %ld %5.3f %i",r->u.m.status,r->u.m.d0,r->u.m.d1,r->u.m.timestamp.tv_sec,r->u.m.timestamp.tv_nsec/1000000000.0,p->map[i].timestamp.tv_sec,p->map[i].timestamp.tv_nsec/1000000000.0,p->map[i].smpIdx); } return rc; } cmSyRC_t cmSyncRecdAudioFile( cmSyncRecdH_t h, const cmChar_t* fn ) { cmSyRC_t rc = kOkSyRC; cmSr_t* p = _cmSrHtoP(h); cmAudioFileH_t afH = cmNullAudioFileH; unsigned chCnt = 2; unsigned frmCnt = 1024; unsigned chIdx = 0; cmRC_t afRC = kOkAfRC; unsigned actFrmCnt = 0; unsigned smpIdx = 0; cmSample_t* chs[chCnt]; cmSample_t buf[frmCnt*2]; chs[0] = buf; chs[1] = buf+frmCnt; if( cmIsFlag(p->flags,kReadSrFl)==false) return cmErrMsg(&p->err,kInvalidOpSyRC,"The 'audio-file-output' operation is only valid on sync-recd files opened for reading."); /// Open an audio file for writing if(cmAudioFileIsValid(afH = cmAudioFileNewCreate(fn, p->afInfo.srate, p->afInfo.bits, chCnt, &afRC, p->err.rpt))==false) { rc = cmErrMsg(&p->err,kAudioFileFailSyRC,"Unable to create the synchronized audio file '%s'.",cmStringNullGuard(fn)); goto errLabel; } // rewind the input audio file if( cmAudioFileSeek(p->afH,0) != kOkAfRC ) { rc = cmErrMsg(&p->err,kAudioFileFailSyRC,"Seek failed during synchronized audio file output."); goto errLabel; } actFrmCnt = frmCnt; // for each buffer of audio for(smpIdx=0; actFrmCnt==frmCnt; smpIdx+=actFrmCnt) { unsigned i; // read frmCnt samples from the first channel of the input audio file if( cmAudioFileReadSample(p->afH, frmCnt, chIdx, 1, chs, &actFrmCnt ) != kOkAfRC ) { rc = cmErrMsg(&p->err,kAudioFileFailSyRC,"Audio file read failed."); break; } // zero the output buffer for the second audio channel cmVOS_Zero(chs[1],frmCnt); // insert impulses at the location of the MIDI messages. for(i=0; icn; ++i) if( p->cache[i].u.m.status==kNoteOnMdId && smpIdx <= p->map[i].smpIdx && p->map[i].smpIdx < smpIdx+frmCnt ) chs[1][ p->map[i].smpIdx - smpIdx ] = 1.0; // write the audio output samples if( cmAudioFileWriteSample(afH, frmCnt, chCnt, chs ) != kOkAfRC ) { rc = cmErrMsg(&p->err,kAudioFileFailSyRC,"Synchronized audio file write failed."); break; } } errLabel: if( cmAudioFileDelete(&afH) != kOkAfRC ) rc = cmErrMsg(&p->err,kAudioFileFailSyRC,"Synchronized audio file close failed."); return rc; } cmSyRC_t cmSyncRecdTest( cmCtx_t* ctx ) { enum { kOkTestRC, kTestFailRC, }; cmSyRC_t rc = kOkSyRC; const cmChar_t* srFn = "/home/kevin/temp/kr/sr/sr0.sr"; const cmChar_t* aFn = "/home/kevin/temp/kr/sr/sync_af.aiff"; cmErr_t err; cmSyncRecdH_t srH = cmSyncRecdNullHandle; cmErrSetup(&err,&ctx->rpt,"SyncRecdTest"); if((rc = cmSyncRecdOpen(ctx, &srH, srFn )) != kOkSyRC ) { cmErrMsg(&err,kTestFailRC,"Sync-recd open failed."); goto errLabel; } cmSyncRecdPrint(srH); cmSyncRecdAudioFile(srH,aFn); errLabel: if((rc = cmSyncRecdFinal(&srH)) != kOkSyRC ) cmErrMsg(&err,kTestFailRC,"Sync-recd close failed."); return rc; }