// Automatically generated C++ file on Thu Aug 24 09:59:49 2023 // // To build with Digital Mars C++ Compiler: // // dmc -mn -WD wavsrc.cpp kernel32.lib /******************************************************************************* * WavSrc -- QSpice C-Block component to read *.WAV file data as a voltage * source. Sample values are normalized to +/-1.0V. * * Revision History: * 2023.08.26 - Proof of Concept (POC) * * Known Issues: * * This is a POC and supports only the most basic 16-Bit PCM WAV formats. * Frankly, I would expect this to be used only with simple WAV files * generated with tools like Audacity. OTOH, it should be possible to extend * the code for other formats, bit-depths, etc. * * Simulation time points don't seem to track data time points exactly. Output * lags WAV data by one sample. * * Input file is hardcoded. Waiting on QSpice support for string * parameters/attributes. * * No provision currently for simulation timeponts exceeding a single sample * data timepoint. Maybe should read until caught up. * * Debug messages are sent to QSpice output window (STDOUT). Consider * logging to data file. * * To be fair, this is just a POC. Much left to do and to understand about the * QSpice C-Block code interface. ******************************************************************************/ #include #include #include #include #include #include "WavSrc.h" /* * test WAV files for debugging -- QSpice doesn't allow passing string * attributes yet so, for now, we'll hardcode the paths. uncomment one or * provide your own. */ #define TestWavFile "sample_files\\Sin_1KHz_1.0pk_16bit_PCM_100ms.wav" // #define TestWavFile "sample_files\\Sqr_1KHz_1.0pk_16bit_PCM_100ms.wav" /* * standard QSpice template-generated parameter data structure */ union uData { bool b; char c; unsigned char uc; short s; unsigned short us; int i; unsigned int ui; float f; double d; long long int i64; unsigned long long int ui64; char *str; unsigned char *bytes; }; // int DllMain() must exist and return 1 for a process to load the .DLL // See https://docs.microsoft.com/en-us/windows/win32/dlls/dllmain for more // information. int __stdcall DllMain(void *module, unsigned int reason, void *reserved) { return 1; } // renamed template-generated display() function; will add writing to log file // later void msg(const char *fmt, ...) { msleep(30); // can't find where this is defined in DMC includes... fflush(stdout); va_list args = {0}; va_start(args, fmt); vprintf(fmt, args); va_end(args); fflush(stdout); msleep(30); } // removed the template-genreated bzero() code and replaced with standard C/C++ // memset() in initialization code. // #undef pin names lest they collide with names in any header file(s) you might // include. #undef Out /******************************************************************************* * constants & defines for convenience ******************************************************************************/ #define FileCLosed 0 #define FileOpen 1 #define FileError -1 /******************************************************************************* * Per-instance data. The QSpice template generator gives this structure a * unique name based on the C-Block mocule name for reasons that excape me. * * Note: As currently defined, this contains more than the minimal required * data. Will return to this/prune later. ******************************************************************************/ struct InstData { FILE *file; // file stream pointer for WAV data int fileState; // 0 = closed; -1 = error; 1 = open fpos_t startOfData; // file position of start of data for looping (not yet // implemented) int sampleCnt; // # of samples read so far int nbrSamples; // # of samples in file double lastOut; // last normalized value read/output double nextSampleTime; // time to fetch next sample double maxAmplitude; // max input amplitude value nomralized to +/-1.0 double sammpleTimeIncr; // calculated time between samples int nbrChannels; // number of channels per sample }; /******************************************************************************* * forward decls ******************************************************************************/ void init(InstData &inst, double t, union uData *data); void getSample(InstData &inst, double t); /******************************************************************************* * QSpice callbback entry points ******************************************************************************/ /* * note: The uData parameter passes an array of component port and attribute * data. the template generator should be used to generate proper indexes into * this data if the component is changed in the schematic. */ extern "C" __declspec(dllexport) void wavsrc( InstData **opaque, double t, union uData *data) { double &Out = data[0].d; // output // allocate per-instance if not already allocated if (!*opaque) { // allocate the memory block InstData *inst = (InstData *)malloc(sizeof(InstData)); if (!inst) { // can't get memory so terminate with prejudice msg("Unable to allocate instance memory. Aborting...\n"); exit(1); } // set the pointer in QSpice for this instance data *opaque = inst; // clear the memory using memset() instead of the template-generated bzero() memset(inst, 0, sizeof(InstData)); // replaces the bzero() call // initialize the instance data (open/parse file through header stuff) init(*inst, t, data); } // create/set the instance reference InstData &inst = **opaque; // if the current sample has "expired" and there are more samples, get next // sample if (t >= inst.nextSampleTime) { if (inst.sampleCnt < inst.nbrSamples) getSample(inst, t); else inst.lastOut = 0.0f; } // set component's out port value to current sample value Out = inst.lastOut; } /* * QSpice MaxExtStepSize() -- "implement a good choice of max timestep size that * depends on InstData". Not sure what that means or, in fact, exactly when * this gets called so, for now, make it the same as the time between samples. */ extern "C" __declspec(dllexport) double MaxExtStepSize(InstData *inst) { // note that we're assuming that the main initialization has been called // before we get here... return inst->sammpleTimeIncr; } /* * QSpice Trun() -- "limit the timestep to a tolerance if the circuit causes a * change in InstData". Not sure what this means or, in fact, exactly when this * gets called so, for now, not using it... */ #if 0 // limit the timestep to a tolerance if the circuit // causes a change in InstData extern "C" __declspec(dllexport) void Trunc( InstData *inst, double t, union uData *data, double *timestep) { const double ttol = 1e-9; if (*timestep > ttol) { double &Out = data[0].d; // output // Save output vector const double _Out = Out; InstData tmp = *inst; wavsrc(&(&tmp), t, data); // if(tmp != *inst) // implement a meaningful way to detect if the state has // changed // *timestep = ttol; // Restore output vector Out = _Out; } } #endif /* * QSpice Destroy() -- clean up upon end of simulation */ extern "C" __declspec(dllexport) void Destroy(InstData *inst) { fclose(inst->file); free(inst); } /******************************************************************************* * WavSrc component functions ******************************************************************************/ /*------------------------------------------------------------------------------ * init() - opens the WAV file, parses through fmt chunk, and stops at beginning * of sample data. initializes instance data for first data read. *----------------------------------------------------------------------------*/ void init(InstData &inst, double t, union uData *data) { // default instance file state to file error inst.fileState = FileError; // hardcoded filename until QSpice implements passing string parameters // See #define TestWavFile definition above. const char *fileName = TestWavFile; // open the WAV file msg("Opening file: %s...\n", fileName); inst.file = fopen(fileName, "rb"); if (!inst.file) { msg("Error opening WAV file (not found or unable to open for read).\n"); return; } // read file header info size_t bytes; WavFileHeaderChunk fileHdr; bytes = fread(&fileHdr, 1, sizeof(fileHdr), inst.file); if (bytes != sizeof(fileHdr)) { fclose(inst.file); msg("Error on first read of WAV file.\n"); return; } // show info for debugging msg("groupID = %4.4s, chunkSize = %d (total size = %d), riffType = %4.4s\n", fileHdr.groupID, fileHdr.chunkSize, fileHdr.chunkSize + 8, fileHdr.riffType); // check header for supported file type if (memcmp(fileHdr.groupID, "RIFF", 4) || memcmp(fileHdr.riffType, "WAVE", 4)) { fclose(inst.file); msg("Unsupported file format (expected \"RIFF\" & \"WAVE\").\n"); return; } // grab next chunk header -- should be a format chunk... WavChunkHeader chunkHdr; bytes = fread(&chunkHdr, 1, sizeof(chunkHdr), inst.file); if (bytes != sizeof(chunkHdr)) { // either EOF or unexpected error, don't really care which msg("Unexpected file read condition.\n"); fclose(inst.file); return; } // show info for debugging msg("Chunk Type: \"%4.4s\", Size: %d\n", chunkHdr.format, chunkHdr.chunkSize); // verify that it is a format chunk if (memcmp(chunkHdr.format, "fmt ", 4)) { msg("Unexpected chunk format (expected \"fmt \").\n"); fclose(inst.file); return; } // verify that the format chunk size is acceptable/expected switch (chunkHdr.chunkSize) { case 16: case 18: case 40: break; default: msg("Unexpected format chunk size (expected 16, 18, or 40).\n"); fclose(inst.file); return; } // read/save format chunk data in instance data WavFmtChunk fmtChunk; bytes = fread(&fmtChunk, 1, chunkHdr.chunkSize, inst.file); if (bytes != chunkHdr.chunkSize) { // either EOF or unexpected error, don't really care which msg("Unexpected file read condition.\n"); fclose(inst.file); return; } // display format chunk contents for debugging msg("Format Chunk Data:\n" " Format Code: %d\n" " # of Channels: %d\n" " Samples Per Second: %d\n" " Avg Bytes Per Second: %d\n" " Block Alignment: %d\n" " Bits Per Sample: %d\n", fmtChunk.fmtCode, fmtChunk.nbrChannels, fmtChunk.samplesPerSec, fmtChunk.avgBytesPerSec, fmtChunk.blkAlign, fmtChunk.bitsPerSample); // validate allowed format -- only 16-bit PCM is supported for now if (fmtChunk.fmtCode != FmtPCM) { msg("Unsupported format. Only simple PCM-encoding is currently " "supported.\n"); fclose(inst.file); return; } // temporarily allowing only mono samples for proof of concept (may add // stereo support later) if (fmtChunk.nbrChannels != 1) { msg("Unsupported format. Currently only simple single-channel (mono) is " "supported.\n"); fclose(inst.file); return; } // set amplitude nomalization for bit depth switch (fmtChunk.bitsPerSample) { // case 8: maxAmplitude = 0x7F; break; case 16: inst.maxAmplitude = 0x7fff; break; // case 20: inst.maxAmplitude = 0x07FFFF; break; // case 24: inst.maxAmplitude = 0x7FFFFF; break; // case 32: inst.maxAmplitude = 0x7FFFFFFF; break; default: msg("Unsupported format. Currently only 16-bit samples are supported.\n"); fclose(inst.file); return; } // finally, get the data chunk (should be next) bytes = fread(&chunkHdr, 1, sizeof(chunkHdr), inst.file); if (bytes != sizeof(chunkHdr)) { // either EOF or unexpected error, don't really care which msg("Unexpected file read condition.\n"); fclose(inst.file); return; } // show data for debugging msg("Chunk Type: \"%4.4s\", Size: %d\n", chunkHdr.format, chunkHdr.chunkSize); // if not "data", not expected/handled if (memcmp(chunkHdr.format, "data", 4)) { msg("Unexpected chunk format (expected \"data\").\n"); fclose(inst.file); return; } // save initial instance data values inst.sammpleTimeIncr = 1.0f / fmtChunk.samplesPerSec; inst.nbrSamples = chunkHdr.chunkSize / 2; inst.nextSampleTime = 0.0f; inst.lastOut = 0.0f; inst.nbrChannels = fmtChunk.nbrChannels; // show some debug information msg("SamplesPerSecond = %d, nbrSamples = %d\n", fmtChunk.samplesPerSec, inst.nbrSamples); // in theory, the file is positioned at the start of the data... inst.fileState = FileOpen; } /*------------------------------------------------------------------------------ * getData() - gets the next sample from the file. * * we're ignoring the time parameter at the moment -- may need to have the * function skip some samples if the simulator time advances more than a single * data sample point. *----------------------------------------------------------------------------*/ void getSample(InstData &inst, double t) { // belt + suspenders if (inst.fileState != FileOpen || inst.sampleCnt >= inst.nbrSamples) { inst.lastOut = 0.0f; return; } // get next sample int16_t sampleVal; // hardcoded for 16-bit samples at this point int bytes = fread(&sampleVal, 1, sizeof(int16_t), inst.file); if (bytes < sizeof(sampleVal)) { // read failed inst.fileState = FileError; inst.lastOut = 0.0f; return; } inst.lastOut = sampleVal / inst.maxAmplitude; // normalize to +/-1.0 inst.nextSampleTime += inst.sammpleTimeIncr; inst.sampleCnt++; // debugging if (inst.sampleCnt < 5) msg("Sample %d=%d, ampl=%f\n", inst.sampleCnt, sampleVal, inst.lastOut); } /*============================================================================== * EOF WavSrc.cpp *============================================================================*/