diff --git a/CMakeLists.txt b/CMakeLists.txt index 1c406e0..b94887d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,7 +2,7 @@ cmake_minimum_required(VERSION 3.15) project( ReiserRT_FlyingPhasor - VERSION 2.3.4 + VERSION 2.3.5 DESCRIPTION "ReiserRT Complex Flying Phasor Tone Generator" ) # Set up compiler requirements diff --git a/README.md b/README.md index 2a02688..08d9d5e 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,12 @@ # ReiserRT_FlyingPhasor Frank Reiser's C++11 implementation of a fast and accurate, sin/cos waveform pair (I/Q) generator. -This component has been tested to be interface-able with C++20 compiles. Note that the compiled library code -is built using the c++11 standard. ## Overview This tone generator evolved out of a desire to generate complex exponential waveforms (sinusoids) fast and accurate. The traditional way of doing this, involved repeated calls to sin and cos functions with an advancing, radian input argument. This produces accurate results, at least over a limited domain interval. -However, it is anything but fast. +However, it is computationally intensive (not fast), and subject to domain range issues. If a continual sequence of complex values are what is required for an application. The task of generating this sequence can be accomplished by simply rotating a phasor around the unit circle. @@ -20,43 +18,82 @@ Note that this is not necessarily true for implementations of std::sin and std:: input values may result in instability. ## Details -A little more needs to be said regarding the "loving care" mentioned above. +A little more should be said regarding the "loving care" mentioned above. This tone generator is taking advantage of Euler's mathematics of the unit circle. When you multiply two phasors, you multiply the magnitudes and add the angles. When applied to unit vectors (magnitudes of one), the resultant magnitude stays one, you simply add the angles. Our state data unit vectors are in rectangular form, we simply preform a complex multiply in rectangular form, the resultant magnitude of which may drift away from one. Because of this, a re-normalization cycle must be -preformed on some periodic basis and this adds to the cost. This tone generator performs +preformed on some periodic basis. This tone generator performs this re-normalization every other sample. This was chosen for two reasons. -One, it puts any resultant spur at the Nyquist (edge of bandwidth). -Two, because at every other sample, the re-normalization required is minimal, -keeping its noise contribution minimum. Additionally, with such small errors, +1) It puts any resultant re-normalization spur at the Nyquist rate (edge of bandwidth). +2) Re-normalizing at every other sample, keeps its noise contribution minimal. +Additionally, with such small errors at every other cycle, a simple and inexpensive linear approximation is all that is required to maintain stability. -Benchmarking indicates that this tone generator is approximately a factor of 5 times faster -than the traditional method. Accuracy is such that it is "almost" immeasurably worse. -You be the judge. Regarding the "state data", this tone generator was designed to generate a single tone per instance. An instance is constructed with an initial frequency and phase. When an initial number of samples are requested from an instance, they are delivered from the starting phase angle at a fixed radians per sample, rate. Subsequent sample requests, -are delivered in phase (continuous) with the previous samples delivered. However, an instance -may be "reset", to produce a different phased tone. Doing so, re-initializes all "state data" +are delivered in phase (continuous) with the previous samples delivered. An instance +may be "reset" however, to produce a different phased tone. Resetting re-initializes all "state data" as if the object were just constructed. The amount of state data maintained is fairly small. -If numerous tones are simultaneously required, instantiate multiple tone generators. +If numerous tones are simultaneously required, instantiate multiple tone generators and add the +results. + +# Example Data Characteristics +Here, we present some example data created with the 'streamFlyingPhasorGen' utility program included +with the project. We generated 1024 samples at pi/256 radians per sample with an initial phase of zero. +This data is plotted below: + +Figure 1 - Example Flying Phasor Sample Series Data + +![Figure 1](graphics/figure1.svg) + +From the figure, we see what looks like a cosine wave and a sine wave. It looks pretty good but, looks +can be deceiving. What does the spectrum look like? Are there any notable spurs in the frequency domain? +We will take a look at the power spectrum, plotted in decibels for an extended dynamic range view. We +did not apply any window to the sample series data here as our signal is right on a basis function. +Applying a window in this case would distract from our analysis. +This data is plotted below: + +Figure 2 - Example Flying Phasor Power Spectrum Data + +![Figure 2](graphics/figure2.svg) + +As can be seen, we have in excess of 300 dB of spur free dynamic range. This seems phenomenal but, how +does this compare to the legacy method? In order to compare, we use utility 'streamLegacyPhasorGen' +program included with the project, using the same parameters. +This data is plotted below: + +Figure 3 - Example Legacy Generator Power Spectrum Data + +![Figure 3](graphics/figure3.svg) + +The legacy method also has in excess of 300 dB of spur free dynamic range as would be +expected. It has a slightly tighter skirt than the FlyingPhasor but, the FlyingPhasor skirt +is reasonably within the noise floor of the legacy method. The FlyingPhasor generator noise floor actually +looks better than the legacy method. It is more uniformly distributed across the spectrum. +It also lacks the periodic spurs seen in the legacy method. +Benchmarking indicates that the FlyingPhasor tone generator is approximately a factor of 5 times +faster than the legacy method and is comparable in spur free dynamic range. You be the judge. + +# Interface Compatibility +This component has been tested to be interface-able with C++20 compiles. Note that the compiled library code +is built using the c++11 standard. API/ABI stability will be maintained between minor versions of this project. # Thread Safety -This tone generator is NOT "thread safe". There are no concurrent access mechanisms +This tone generator is not "thread safe". There are no concurrent access mechanisms in place and there is no good reason for addressing this. To the contrary, -state left by one thread would make no sense to another, never mind the concurrency issues. +state left by one thread would make little sense to another, never mind the concurrency issues. Have threads use their own unique instances. # Acknowledgements I cannot take credit for this algorithm. It is really just high school math. This implementation was derived from something I saw on Stack Exchange. What I have done is to actually utilize std::complex instead of a discrete reimplementation -of complex math, fine honed the re-normalization, and turned it into a reusable object that +of complex math, fine honed the re-normalization period, and turned it into a reusable object that meets my needs. Also, I provide some test harnesses that prove its worthiness. ## Building and Installation @@ -78,3 +115,5 @@ Roughly as follows: ``` sudo cmake --install . ``` +Please see the "tests" and "sundry" folders for examples on how to use the FlyingPhasor in +your own projects. diff --git a/graphics/PlotNotes.txt b/graphics/PlotNotes.txt new file mode 100755 index 0000000..a0bbe6a --- /dev/null +++ b/graphics/PlotNotes.txt @@ -0,0 +1,103 @@ +# I wrote this, so I can remember how I accomplished the graphics in the README +# in case I ever need to redo them. The project needs to be built and ideally installed +# beforehand. + +################### FIGURE 1 ####################### +# To Generate the "Figure 1 - Example Flying Phasor Sample Series Data" graphic contained in the README file, +# I used the below command off in some directory outside the source tree: +streamFlyingPhasorGen --includeX --streamFormat=b32 --chunkSize=1024 > flyingPhasorB32.in + +# The output binary file contains three binary columns (sampleNum, real, imaginary) for 1024 samples. +# The data is in 32 bit precision and by default has a radians per sample rate of pi/256 +# and a phase of zero. Note that 32 bit precision is more than adequate for a plot. + +# In the same directory where the file 'flyingPhasorB32.in' sits, enter gnuplot. +# You will need gnuplot to be installed obviously to do this. +gnuplot + +# Inside gnuplot +set term svg size 520, 360 font "Helvetica,12" background rgb "grey90" +set output 'figure1.svg' +set xrange [0:1023] +set yrange [-1.5:1.5] +set xtics 128 +set ytics 0.5 +set grid xtics +set grid ytics +set xlabel "Sample Number (n)" +set ylabel "Amplitude" +set title "FlyingPhasor rads/sample=pi/256, phase=0.0" + +# This is one long line, not two lines. Output goes directly to the file 'figure1.svg'. +plot 'flyingPhasorB32.in' binary format="%uint%float%float" using 1:2 with lines title "real(n)", 'flyingPhasorB32.in' binary format="%uint%float%float" using 1:3 with lines title "imag(n)" + +# You can exit gnuplot now +quit + +# The 'figure1.svg' file was then copied into the projects 'graphics' directory. + + + +################### FIGURE 2 ####################### +# To Generate the "Figure 2 - Example Flying Phasor Power Spectrum Data" graphic contained in the README file, +# I used the below command off in some directory outside the source tree: +streamFlyingPhasorGen --streamFormat=b64 --chunkSize=1024 > flyingPhasorB64_NoX.in + +# The output binary file contains two binary columns (real, imaginary) for 1024 samples. +# The data is in 64 bit precision and by default has a radians per sample rate of pi/256 +# and a phase of zero. Note that 64 bit precision is what we want to take an FFT and +# get a good look at spur free dynamic range. + +# Now for the FFT. I used a utility I wrote called 'streamFFT'. I have not published that +# as of 20231123 yet but intend to do so soon. + +# The 'streamFFT' utility always expects 64bit I/Q data as input but, we want 32bit out as this is +# adequate for plotting. The output binary two binary columns (fractionOfSampleRate, 10log10(mag^2)) +# for 1024 samples. It is essentially a power spectrum in decibels. Note that we did not specify +# a window for the FFT. This is because we put the signal right on a basis function and using a window +# would distract our visual spur analysis under this scenario. +streamFFT --includeX --streamFormat=b32 --chunkSize=1024 < flyingPhasorB64_NoX.in > flyingPhasorFFT_B32.in + +# In the same directory where the file 'flyingPhasorFFT_B32.in' sits, enter gnuplot. +gnuplot + +# Inside gnuplot +set term svg size 520, 360 font "Helvetica,12" background rgb "grey90" +set output 'figure2.svg' +set yrange[-400.0:50] +set xrange[-0.5:0.5] +set grid ytics +set grid xtics +set xlabel "Frequency (fraction of sample rate)" +set ylabel "Relative Power (dB)" +set title "FlyingPhasor rads/sample=pi/256, phase=0.0" +plot 'flyingPhasorFFT_B32.in' binary format="%float%float" using 1:2 with lines title "Power Spectrum" + + +################### FIGURE 3 ####################### +# To Generate the "Figure 3 - Example Legacy Generator Power Spectrum Data" graphic contained in the README file, +# I used the below command off in some directory outside the source tree: +streamLegacyPhasorGen --streamFormat=b64 --chunkSize=1024 > legacyPhasorB64_NoX.in + +# The output binary file contains two binary columns (real, imaginary) for 1024 samples. +# The data is in 64 bit precision and by default has a radians per sample rate of pi/256 +# and a phase of zero. Note that 64 bit precision is what we want to take an FFT and +# get a good look at spur free dynamic range. + +# Take FFT +streamFFT --includeX --streamFormat=b32 --chunkSize=1024 < legacyPhasorB64_NoX.in > legacyPhasorFFT_B32.in + +# In the same directory where the file 'legacyPhasorFFT_B32.in' sits, enter gnuplot. +gnuplot + +# Inside gnuplot +set term svg size 520, 360 font "Helvetica,12" background rgb "grey90" +set output 'figure3.svg' +set yrange[-400.0:50] +set xrange[-0.5:0.5] +set grid ytics +set grid xtics +set xlabel "Frequency (fraction of sample rate)" +set ylabel "Relative Power (dB)" +set title "Legacy Generator rads/sample=pi/256, phase=0.0" +plot 'legacyPhasorFFT_B32.in' binary format="%float%float" using 1:2 with lines title "Power Spectrum" diff --git a/graphics/figure1.svg b/graphics/figure1.svg new file mode 100644 index 0000000..35f6ce0 --- /dev/null +++ b/graphics/figure1.svg @@ -0,0 +1,552 @@ + + + +Gnuplot +Produced by GNUPLOT 5.2 patchlevel 4 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + -1.5 + + + + + + + + + + + + + -1 + + + + + + + + + + + + + -0.5 + + + + + + + + + + + + + 0 + + + + + + + + + + + + + 0.5 + + + + + + + + + + + + + 1 + + + + + + + + + + + + + 1.5 + + + + + + + + + + + + + 0 + + + + + + + + + + + + + 128 + + + + + + + + + + + + + 256 + + + + + + + + + + + + + 384 + + + + + + + + + + + + + 512 + + + + + + + + + + + + + 640 + + + + + + + + + + + + + 768 + + + + + + + + + + + + + 896 + + + + + + + + + + + + + + + + + Amplitude + + + + + Sample Number (n) + + + + + FlyingPhasor rads/sample=pi/256, phase=0.0 + + + real(n) + + + real(n) + + + + + + imag(n) + + + imag(n) + + + + + + + + + + + + + + + + + + diff --git a/graphics/figure2.svg b/graphics/figure2.svg new file mode 100644 index 0000000..5d34051 --- /dev/null +++ b/graphics/figure2.svg @@ -0,0 +1,407 @@ + + + +Gnuplot +Produced by GNUPLOT 5.2 patchlevel 4 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + -400 + + + + + + + + + + + + + -350 + + + + + + + + + + + + + -300 + + + + + + + + + + + + + -250 + + + + + + + + + + + + + -200 + + + + + + + + + + + + + -150 + + + + + + + + + + + + + -100 + + + + + + + + + + + + + -50 + + + + + + + + + + + + + 0 + + + + + + + + + + + + + 50 + + + + + + + + + + + + + -0.4 + + + + + + + + + + + + + -0.2 + + + + + + + + + + + + + 0 + + + + + + + + + + + + + 0.2 + + + + + + + + + + + + + 0.4 + + + + + + + + + Relative Power (dB) + + + + + Frequency (fraction of sample rate) + + + + + FlyingPhasor rads/sample=pi/256, phase=0.0 + + + Power Spectrum + + + Power Spectrum + + + + + + + + + + + + + + + + + + diff --git a/graphics/figure3.svg b/graphics/figure3.svg new file mode 100644 index 0000000..1028606 --- /dev/null +++ b/graphics/figure3.svg @@ -0,0 +1,407 @@ + + + +Gnuplot +Produced by GNUPLOT 5.2 patchlevel 4 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + -400 + + + + + + + + + + + + + -350 + + + + + + + + + + + + + -300 + + + + + + + + + + + + + -250 + + + + + + + + + + + + + -200 + + + + + + + + + + + + + -150 + + + + + + + + + + + + + -100 + + + + + + + + + + + + + -50 + + + + + + + + + + + + + 0 + + + + + + + + + + + + + 50 + + + + + + + + + + + + + -0.4 + + + + + + + + + + + + + -0.2 + + + + + + + + + + + + + 0 + + + + + + + + + + + + + 0.2 + + + + + + + + + + + + + 0.4 + + + + + + + + + Relative Power (dB) + + + + + Frequency (fraction of sample rate) + + + + + Legacy Generator rads/sample=pi/256, phase=0.0 + + + Power Spectrum + + + Power Spectrum + + + + + + + + + + + + + + + + + + diff --git a/src/FlyingPhasorToneGenerator.cpp b/src/FlyingPhasorToneGenerator.cpp index cef30ae..7dcdd3a 100644 --- a/src/FlyingPhasorToneGenerator.cpp +++ b/src/FlyingPhasorToneGenerator.cpp @@ -29,7 +29,8 @@ void FlyingPhasorToneGenerator::getSamples( FlyingPhasorElementBufferTypePtr pEl // Now advance (rotate) the phasor by our rate (complex multiply) phasor *= rate; - // Perform normalization + // Perform normalization work. This only actually normalized ever other invocation. + // We invoke it to maintain that part of the state machine. normalize(); } } @@ -46,7 +47,8 @@ void FlyingPhasorToneGenerator::getSamplesScaled( FlyingPhasorElementBufferTypeP // Now advance (rotate) the phasor by our rate (complex multiply) phasor *= rate; - // Perform normalization + // Perform normalization work. This only actually normalized ever other invocation. + // We invoke it to maintain that part of the state machine. normalize(); } } @@ -63,7 +65,8 @@ void FlyingPhasorToneGenerator::getSamplesScaled( FlyingPhasorElementBufferTypeP // Now advance (rotate) the phasor by our rate (complex multiply) phasor *= rate; - // Perform normalization + // Perform normalization work. This only actually normalized ever other invocation. + // We invoke it to maintain that part of the state machine. normalize(); } } @@ -79,7 +82,8 @@ void FlyingPhasorToneGenerator::accumSamples( FlyingPhasorElementBufferTypePtr p // Now advance (rotate) the phasor by our rate (complex multiply) phasor *= rate; - // Perform normalization + // Perform normalization work. This only actually normalized ever other invocation. + // We invoke it to maintain that part of the state machine. normalize(); } } @@ -96,7 +100,8 @@ void FlyingPhasorToneGenerator::accumSamplesScaled( FlyingPhasorElementBufferTyp // Now advance (rotate) the phasor by our rate (complex multiply) phasor *= rate; - // Perform normalization + // Perform normalization work. This only actually normalized ever other invocation. + // We invoke it to maintain that part of the state machine. normalize(); } } @@ -113,7 +118,8 @@ void FlyingPhasorToneGenerator::accumSamplesScaled( FlyingPhasorElementBufferTyp // Now advance (rotate) the phasor by our rate (complex multiply) phasor *= rate; - // Perform normalization + // Perform normalization work. This only actually normalized ever other invocation. + // We invoke it to maintain that part of the state machine. normalize(); } } @@ -134,7 +140,8 @@ FlyingPhasorElementType FlyingPhasorToneGenerator::getSample() // Now advance (rotate) the phasor by our rate (complex multiply) phasor *= rate; - // Perform normalization + // Perform normalization work. This only actually normalized ever other invocation. + // We invoke it to maintain that part of the state machine. normalize(); return retValue; diff --git a/sundry/CMakeLists.txt b/sundry/CMakeLists.txt index 0a1b86c..58369fd 100644 --- a/sundry/CMakeLists.txt +++ b/sundry/CMakeLists.txt @@ -1,8 +1,8 @@ -add_executable( streamLegacyGen "" ) -target_sources( streamLegacyGen PRIVATE streamLegacyGen.cpp ) -target_include_directories( streamLegacyGen PUBLIC ../src ../testUtilities ) -target_link_libraries( streamLegacyGen ReiserRT_FlyingPhasor TestUtilities ) -target_compile_options( streamLegacyGen PRIVATE +add_executable( streamLegacyPhasorGen "" ) +target_sources( streamLegacyPhasorGen PRIVATE streamLegacyPhasorGen.cpp) +target_include_directories( streamLegacyPhasorGen PUBLIC ../src ../testUtilities ) +target_link_libraries( streamLegacyPhasorGen ReiserRT_FlyingPhasor TestUtilities ) +target_compile_options( streamLegacyPhasorGen PRIVATE $<$:/W4 /WX> $<$>:-Wall -Wextra -Wpedantic -Werror> ) @@ -24,3 +24,33 @@ target_compile_options( twoToneTest PRIVATE $<$:/W4 /WX> $<$>:-Wall -Wextra -Wpedantic -Werror> ) + +# What this does is set up a relative path where we expect our custom libraries to be +# It will be used to patch the installation to find libraries relative to the binary. +file( RELATIVE_PATH _rel ${CMAKE_INSTALL_PREFIX}/${INSTALL_BINDIR} ${CMAKE_INSTALL_PREFIX}) +set( _rpath "\$ORIGIN/${_rel}" ) +file( TO_NATIVE_PATH "${_rpath}/${INSTALL_LIBDIR}" MY_LIBRARY_RPATH ) +message( STATUS "MY_LIBRARY_RPATH: ${MY_LIBRARY_RPATH}" ) + +# Install streamFlyingPhasorGen and streamLegacyPhasorGen with RPATH setting +set_target_properties( streamFlyingPhasorGen + PROPERTIES + SKIP_BUILD_RPATH OFF + BUILD_WITH_INSTALL_RPATH OFF + INSTALL_RPATH "${MY_LIBRARY_RPATH}" + INSTALL_RPATH_USE_LINK_PATH ON +) +set_target_properties( streamLegacyPhasorGen + PROPERTIES + SKIP_BUILD_RPATH OFF + BUILD_WITH_INSTALL_RPATH OFF + INSTALL_RPATH "${MY_LIBRARY_RPATH}" + INSTALL_RPATH_USE_LINK_PATH ON +) + +# Installation of our binary executable component. +install( + TARGETS streamFlyingPhasorGen streamLegacyPhasorGen + RUNTIME DESTINATION ${INSTALL_BINDIR} COMPONENT bin +) + diff --git a/sundry/streamFlyingPhasorGen.cpp b/sundry/streamFlyingPhasorGen.cpp index 312d83c..2e48b26 100644 --- a/sundry/streamFlyingPhasorGen.cpp +++ b/sundry/streamFlyingPhasorGen.cpp @@ -6,46 +6,171 @@ #include #include +#include using namespace ReiserRT::Signal; +void printHelpScreen() +{ + std::cout << "Usage:" << std::endl; + std::cout << " streamFlyingPhasorGen [options]" << std::endl; + std::cout << "Available Options:" << std::endl; + std::cout << " --help" << std::endl; + std::cout << " Displays this help screen and exits." << std::endl; + std::cout << " --radsPerSample=" << std::endl; + std::cout << " The number of radians per sample to be generated." << std::endl; + std::cout << " Defaults to pi/256 radians per sample if unspecified." << std::endl; + std::cout << " --phase=" << std::endl; + std::cout << " The initial phase of the starting sample in radians." << std::endl; + std::cout << " Defaults to 0.0 radians if unspecified." << std::endl; + std::cout << " --chunkSize=" << std::endl; + std::cout << " The number of samples to produce per chunk. If zero, no samples are produced." << std::endl; + std::cout << " Defaults to 4096 samples if unspecified." << std::endl; + std::cout << " --numChunks=" << std::endl; + std::cout << " The number of chunks to generate. If zero, runs continually up to max uint64 chunks." << std::endl; + std::cout << " This maximum value is inclusive of any skipped chunks." << std::endl; + std::cout << " Defaults to 1 chunk if unspecified." << std::endl; + std::cout << " --skipChunks=" << std::endl; + std::cout << " The number of chunks to skip before any chunks are output. Does not effect the numChunks output." << std::endl; + std::cout << " In essence if numChunks is 1 and skip chunks is 4, chunk number 5 is the only chunk output." << std::endl; + std::cout << " Defaults to 0 chunks skipped if unspecified." << std::endl; + std::cout << " --streamFormat=" << std::endl; + std::cout << " t32 - Outputs samples in text format with floating point precision of (9 decimal places)." << std::endl; + std::cout << " t64 - Outputs samples in text format with floating point precision (17 decimal places)." << std::endl; + std::cout << " b32 - Outputs data in raw binary with 32bit precision (uint32 and float), native endian-ness." << std::endl; + std::cout << " b64 - Outputs data in raw binary 64bit precision (uint64 and double), native endian-ness." << std::endl; + std::cout << " Defaults to t64 if unspecified." << std::endl; + std::cout << " --includeX" << std::endl; + std::cout << " Include sample count in the output stream. This is useful for gnuplot using any format." << std::endl; + std::cout << " Defaults to no inclusion if unspecified." << std::endl; + std::cout << std::endl; + std::cout << "Error Returns:" << std::endl; + std::cout << " 1 - Command Line Parsing Error - Unrecognized Long Option." << std::endl; + std::cout << " 2 - Command Line Parsing Error - Unrecognized Short Option (none supported)." << std::endl; + std::cout << " 3 - Invalid streamFormat specified." << std::endl; +} + int main( int argc, char * argv[] ) { // Parse potential command line. Defaults provided otherwise. CommandLineParser cmdLineParser{}; - if ( 0 != cmdLineParser.parseCommandLine(argc, argv) ) + + auto parseRes = cmdLineParser.parseCommandLine(argc, argv); + if ( 0 != parseRes ) { - std::cout << "Failed parsing command line" << std::endl; - std::cout << "Optional Arguments are:" << std::endl; - std::cout << "\t--radsPerSample=: The radians per sample to used." << std::endl; - std::cout << "\t--phase=: The initial phase in radians." << std::endl; + std::cerr << "streamFlyingPhasorGen Parse Error: Use command line argument --help for instructions" << std::endl; + exit(parseRes); + } - exit( -1 ); + if ( cmdLineParser.getHelpFlag() ) + { + printHelpScreen(); + exit( 0 ); } -#if 0 - else + + // Get Frequency and Starting Phase + auto radiansPerSample = cmdLineParser.getRadsPerSample(); + auto phi = cmdLineParser.getPhase(); + + // Get the Skip Chunk Count + const auto skipChunks = cmdLineParser.getSkipChunks(); + + // Get Chunk Size. + const auto chunkSize = cmdLineParser.getChunkSize(); + + // Condition Number of Chunks. If it's zero, we set to maximum less skipChunks + // because we will incorporate skipChunks into numChunks to simplify logic. + auto numChunks = cmdLineParser.getNumChunks(); + if ( 0 == numChunks ) + numChunks = std::numeric_limits::max() - skipChunks; + numChunks += skipChunks; + + // Do we have a valid stream output format to use? + auto streamFormat = cmdLineParser.getStreamFormat(); + if ( CommandLineParser::StreamFormat::Invalid == streamFormat ) { - std::cout << "Parsed: --radiansPerSample=" << cmdLineParser.getRadsPerSample() - << " --phase=" << cmdLineParser.getPhase() << std::endl << std::endl; + std::cerr << "streamFlyingPhasorGen Error: Invalid Stream Format Specified. Use --help for instructions" << std::endl; + exit( 3 ); } -#endif - double radiansPerSample = cmdLineParser.getRadsPerSample(); - double phi = cmdLineParser.getPhase(); - - // Generate Samples - std::unique_ptr< FlyingPhasorToneGenerator > pFlyingPhasorToneGen{ new FlyingPhasorToneGenerator{ radiansPerSample, phi } }; - constexpr size_t numSamples = 4096; - std::unique_ptr< FlyingPhasorElementType[] > pToneSeries{new FlyingPhasorElementType [ numSamples] }; - constexpr FlyingPhasorElementType j{0.0, 1.0 }; - FlyingPhasorElementBufferTypePtr p = pToneSeries.get(); - pFlyingPhasorToneGen->getSamples( p, numSamples ); - // Write to standard out. It can be redirected. - std::cout << std::scientific; - std::cout.precision(17); - for ( size_t n = 0; numSamples != n; ++n ) + // Instantiate a FlyingPhasor + FlyingPhasorToneGenerator flyingPhasorToneGenerator{ radiansPerSample, phi }; + + // Allocate Memory for Chunk Size + std::unique_ptr< FlyingPhasorElementType[] > pToneSeries{new FlyingPhasorElementType [ chunkSize ] }; + + // If we are using a text stream format, set the output precision + if ( CommandLineParser::StreamFormat::Text32 == streamFormat) + { + std::cout << std::scientific; + std::cout.precision(9); + } + else if ( CommandLineParser::StreamFormat::Text64 == streamFormat) { - std::cout << p[n].real() << " " << p[n].imag() << std::endl; + std::cout << std::scientific; + std::cout.precision(17); + } + + // Are we including Sample count in the output? + auto includeX = cmdLineParser.getIncludeX(); + + FlyingPhasorElementBufferTypePtr p = pToneSeries.get(); + size_t sampleCount = 0; + size_t skippedChunks = 0; + for ( size_t chunk = 0; numChunks != chunk; ++chunk ) + { + // Get Samples. If we are skipping chunks, we may not output, but we must + // maintain flying phasor state. + flyingPhasorToneGenerator.getSamples( p, chunkSize ); + + // Skip this Chunk? + if ( skipChunks != skippedChunks ) + { + ++skippedChunks; + sampleCount += chunkSize; + continue; + } + + if ( CommandLineParser::StreamFormat::Text32 == streamFormat || + CommandLineParser::StreamFormat::Text64 == streamFormat ) + { + for ( size_t n = 0; chunkSize != n; ++n ) + { + if ( includeX ) std::cout << sampleCount++ << " "; + std::cout << p[n].real() << " " << p[n].imag() << std::endl; + } + } + else if ( CommandLineParser::StreamFormat::Bin32 == streamFormat ) + { + for ( size_t n = 0; chunkSize != n; ++n ) + { + if ( includeX ) + { + auto sVal = uint32_t( sampleCount++); + std::cout.write( reinterpret_cast< const char * >(&sVal), sizeof( sVal ) ); + } + auto fVal = float( p[n].real() ); + std::cout.write( reinterpret_cast< const char * >(&fVal), sizeof( fVal ) ); + fVal = float( p[n].imag() ); + std::cout.write( reinterpret_cast< const char * >(&fVal), sizeof( fVal ) ); + } + } + else if ( CommandLineParser::StreamFormat::Bin64 == streamFormat ) + { + for ( size_t n = 0; chunkSize != n; ++n ) + { + if ( includeX ) + { + auto sVal = sampleCount++; + std::cout.write( reinterpret_cast< const char * >(&sVal), sizeof( sVal ) ); + } + auto fVal = p[n].real(); + std::cout.write( reinterpret_cast< const char * >(&fVal), sizeof( fVal ) ); + fVal = p[n].imag(); + std::cout.write( reinterpret_cast< const char * >(&fVal), sizeof( fVal ) ); + } + } + std::cout.flush(); } exit( 0 ); diff --git a/sundry/streamLegacyGen.cpp b/sundry/streamLegacyGen.cpp deleted file mode 100644 index c7de18a..0000000 --- a/sundry/streamLegacyGen.cpp +++ /dev/null @@ -1,56 +0,0 @@ -// Created on 20220109 - -#include "FlyingPhasorToneGenerator.h" - -#include "CommandLineParser.h" - -#include -#include - -using namespace ReiserRT::Signal; - -int main( int argc, char * argv[] ) -{ - // Parse potential command line. Defaults provided otherwise. - CommandLineParser cmdLineParser{}; - if ( 0 != cmdLineParser.parseCommandLine(argc, argv) ) - { - std::cout << "Failed parsing command line" << std::endl; - std::cout << "Optional Arguments are:" << std::endl; - std::cout << "\t--radsPerSample=: The radians per sample to used." << std::endl; - std::cout << "\t--phase=: The initial phase in radians." << std::endl; - - exit( -1 ); - } -#if 0 - else - { - std::cout << "Parsed: --radiansPerSample=" << cmdLineParser.getRadsPerSample() - << " --phase=" << cmdLineParser.getPhase() << std::endl << std::endl; - } -#endif - double radiansPerSample = cmdLineParser.getRadsPerSample(); - double phi = cmdLineParser.getPhase(); - - // Generate Samples - constexpr size_t numSamples = 4096; - std::unique_ptr< FlyingPhasorElementType[] > pToneSeries{new FlyingPhasorElementType [ numSamples] }; - constexpr FlyingPhasorElementType j{0.0, 1.0 }; - FlyingPhasorElementBufferTypePtr p = pToneSeries.get(); - for ( size_t n = 0; numSamples != n; ++n ) - { - // Legacy Complex Exponential Equivalent - p[n] = exp( j * ( double( n ) * radiansPerSample + phi ) ); - } - - // Write to standard out. It can be redirected. - std::cout << std::scientific; - std::cout.precision(17); - for ( size_t n = 0; numSamples != n; ++n ) - { - std::cout << p[n].real() << " " << p[n].imag() << std::endl; - } - - exit( 0 ); - return 0; -} diff --git a/sundry/streamLegacyPhasorGen.cpp b/sundry/streamLegacyPhasorGen.cpp new file mode 100644 index 0000000..b32de2b --- /dev/null +++ b/sundry/streamLegacyPhasorGen.cpp @@ -0,0 +1,178 @@ +// Created on 20220109 + +#include "FlyingPhasorToneGenerator.h" + +#include "CommandLineParser.h" + +#include +#include +#include + +using namespace ReiserRT::Signal; + +void printHelpScreen() { + std::cout << "Usage:" << std::endl; + std::cout << " streamLegacyPhasorGen [options]" << std::endl; + std::cout << "Available Options:" << std::endl; + std::cout << " --help" << std::endl; + std::cout << " Displays this help screen and exits." << std::endl; + std::cout << " --radsPerSample=" << std::endl; + std::cout << " The number of radians per sample to be generated." << std::endl; + std::cout << " Defaults to pi/256 radians per sample if unspecified." << std::endl; + std::cout << " --phase=" << std::endl; + std::cout << " The initial phase of the starting sample in radians." << std::endl; + std::cout << " Defaults to 0.0 radians if unspecified." << std::endl; + std::cout << " --chunkSize=" << std::endl; + std::cout << " The number of samples to produce per chunk. If zero, no samples are produced." << std::endl; + std::cout << " Defaults to 4096 samples if unspecified." << std::endl; + std::cout << " --numChunks=" << std::endl; + std::cout << " The number of chunks to generate. If zero, runs continually up to max uint64 chunks." << std::endl; + std::cout << " This maximum value is inclusive of any skipped chunks." << std::endl; + std::cout << " Defaults to 1 chunk if unspecified." << std::endl; + std::cout << " --skipChunks=" << std::endl; + std::cout << " The number of chunks to skip before any chunks are output. Does not effect the numChunks output." << std::endl; + std::cout << " In essence if numChunks is 1 and skip chunks is 4, chunk number 5 is the only chunk output." << std::endl; + std::cout << " Defaults to 0 chunks skipped if unspecified." << std::endl; + std::cout << " --streamFormat=" << std::endl; + std::cout << " t32 - Outputs samples in text format with floating point precision of (9 decimal places)." << std::endl; + std::cout << " t64 - Outputs samples in text format with floating point precision (17 decimal places)." << std::endl; + std::cout << " b32 - Outputs data in raw binary with 32bit precision (uint32 and float), native endian-ness." << std::endl; + std::cout << " b64 - Outputs data in raw binary 64bit precision (uint64 and double), native endian-ness." << std::endl; + std::cout << " Defaults to t64 if unspecified." << std::endl; + std::cout << " --includeX" << std::endl; + std::cout << " Include sample count in the output stream. This is useful for gnuplot using any format" << std::endl; + std::cout << " Defaults to no inclusion if unspecified." << std::endl; + std::cout << std::endl; + std::cout << "Error Returns:" << std::endl; + std::cout << " 1 - Command Line Parsing Error - Unrecognized Long Option." << std::endl; + std::cout << " 2 - Command Line Parsing Error - Unrecognized Short Option (none supported)." << std::endl; + std::cout << " 3 - Invalid streamFormat specified." << std::endl; +} + +int main( int argc, char * argv[] ) +{ + // Parse potential command line. Defaults provided otherwise. + CommandLineParser cmdLineParser{}; + + auto parseRes = cmdLineParser.parseCommandLine(argc, argv); + if ( 0 != parseRes ) + { + std::cerr << "streamLegacyPhasorGen Parse Error: Use command line argument --help for instructions" << std::endl; + exit(parseRes); + } + + if ( cmdLineParser.getHelpFlag() ) + { + printHelpScreen(); + exit( 0 ); + } + + // Get Frequency and Starting Phase + auto radiansPerSample = cmdLineParser.getRadsPerSample(); + auto phi = cmdLineParser.getPhase(); + + // Get the Skip Chunk Count + const auto skipChunks = cmdLineParser.getSkipChunks(); + + // Get Chunk Size. + const auto chunkSize = cmdLineParser.getChunkSize(); + + // Condition Number of Chunks. If it's zero, we set to maximum less skipChunks + // because we will incorporate skipChunks into numChunks to simplify logic. + auto numChunks = cmdLineParser.getNumChunks(); + if ( 0 == numChunks ) + numChunks = std::numeric_limits::max() - skipChunks; + numChunks += skipChunks; + + // If one of the text formats, set output precision appropriately + auto streamFormat = cmdLineParser.getStreamFormat(); + if ( CommandLineParser::StreamFormat::Invalid == streamFormat ) + { + std::cerr << "streamLegacyPhasorGen Error: Invalid Stream Format Specified. Use --help for instructions" << std::endl; + exit( 3 ); + } + + // Allocate Memory for Chunk Size + std::unique_ptr< FlyingPhasorElementType[] > pToneSeries{ new FlyingPhasorElementType [ chunkSize ] }; + + // If we are using a text stream format, set the output precision + if ( CommandLineParser::StreamFormat::Text32 == streamFormat) + { + std::cout << std::scientific; + std::cout.precision(9); + } + else if ( CommandLineParser::StreamFormat::Text64 == streamFormat) + { + std::cout << std::scientific; + std::cout.precision(17); + } + + // Are we including Sample count in the output? + auto includeX = cmdLineParser.getIncludeX(); + + constexpr FlyingPhasorElementType j{ 0.0, 1.0 }; + FlyingPhasorElementBufferTypePtr p = pToneSeries.get(); + size_t sampleCount = 0; + size_t skippedChunks = 0; + for ( size_t chunk = 0; numChunks != chunk; ++chunk ) + { + // Skip this Chunk? + if ( skipChunks != skippedChunks ) + { + ++skippedChunks; + sampleCount += chunkSize; + continue; + } + + // Get Samples using complex exponential function + for ( size_t n = 0; chunkSize != n; ++n ) + { + // Legacy Complex Exponential Equivalent + p[n] = std::exp( j * ( double( chunkSize * chunk + n ) * radiansPerSample + phi ) ); + } + + if ( CommandLineParser::StreamFormat::Text32 == streamFormat || + CommandLineParser::StreamFormat::Text64 == streamFormat ) + { + for ( size_t n = 0; chunkSize != n; ++n ) + { + if ( includeX ) std::cout << sampleCount++ << " "; + std::cout << p[n].real() << " " << p[n].imag() << std::endl; + } + } + else if ( CommandLineParser::StreamFormat::Bin32 == streamFormat ) + { + for ( size_t n = 0; chunkSize != n; ++n ) + { + if ( includeX ) + { + auto sVal = uint32_t( sampleCount++ ); + std::cout.write( reinterpret_cast< const char * >(&sVal), sizeof( sVal ) ); + } + auto fVal = float( p[n].real() ); + std::cout.write( reinterpret_cast< const char * >(&fVal), sizeof( fVal ) ); + fVal = float( p[n].imag() ); + std::cout.write( reinterpret_cast< const char * >(&fVal), sizeof( fVal ) ); + } + } + else if ( CommandLineParser::StreamFormat::Bin64 == streamFormat ) + { + for ( size_t n = 0; chunkSize != n; ++n ) + { + if ( includeX ) + { + auto sVal = sampleCount++; + std::cout.write( reinterpret_cast< const char * >(&sVal), sizeof( sVal ) ); + } + auto fVal = p[n].real(); + std::cout.write( reinterpret_cast< const char * >(&fVal), sizeof( fVal ) ); + fVal = p[n].imag(); + std::cout.write( reinterpret_cast< const char * >(&fVal), sizeof( fVal ) ); + } + } + std::cout.flush(); + } + + exit( 0 ); + return 0; +} diff --git a/testUtilities/CommandLineParser.cpp b/testUtilities/CommandLineParser.cpp index ff920a5..6be2590 100644 --- a/testUtilities/CommandLineParser.cpp +++ b/testUtilities/CommandLineParser.cpp @@ -11,19 +11,28 @@ int CommandLineParser::parseCommandLine( int argc, char * argv[] ) // int digitOptIndex = 0; int retCode = 0; - enum eOptions { RadsPerSample=1, Phase=2 }; + enum eOptions { RadsPerSample=1, Phase, ChunkSize, NumChunks, SkipChunks, StreamFormat, Help, IncludeX }; + // While options still left to parse while (true) { // int thisOptionOptIndex = optind ? optind : 1; int optionIndex = 0; static struct option longOptions[] = { {"radsPerSample", required_argument, nullptr, RadsPerSample }, {"phase", required_argument, nullptr, Phase }, + {"chunkSize", required_argument, nullptr, ChunkSize }, + {"numChunks", required_argument, nullptr, NumChunks }, + {"skipChunks", required_argument, nullptr, SkipChunks }, + {"streamFormat", required_argument, nullptr, StreamFormat }, + {"help", no_argument, nullptr, Help }, + {"includeX", no_argument, nullptr, IncludeX }, {nullptr, 0, nullptr, 0 } }; c = getopt_long(argc, argv, "", longOptions, &optionIndex); + + // When getopt_long has completed the parsing of the command line, it returns -1. if (c == -1) { break; } @@ -31,50 +40,59 @@ int CommandLineParser::parseCommandLine( int argc, char * argv[] ) switch (c) { case RadsPerSample: radsPerSampleIn = std::stod( optarg ); -#if 0 - std::cout << "The getopt_long call detected the --radsPerSample=" << optarg - << ". Value extracted = " << radsPerSampleIn << "." << std::endl; -#endif break; + case Phase: phaseIn = std::stod( optarg ); -#if 0 - std::cout << "The getopt_long call detected the --phase=" << optarg - << ". Value extracted = " << phaseIn << "." << std::endl; -#endif break; - case '?': - std::cout << "The getopt_long call returned '?'" << std::endl; - retCode = -1; + + case ChunkSize: + chunkSizeIn = std::stoul( optarg ); break; - default: - std::cout << "The getopt_long call returned character code" << c << std::endl; - retCode = -1; + + case NumChunks: + numChunksIn = std::stoul( optarg ); break; - } - } - return retCode; -} + case SkipChunks: + skipChunksIn = std::stoul( optarg ); + break; -#if 0 -int main( int argc, char * argv[] ) { + case StreamFormat: + { + // This one is more complicated. We either detect a valid string here, or we don't. + const std::string streamFormatStr{ optarg }; + if ( streamFormatStr == "t32" ) + streamFormatIn = StreamFormat::Text32; + else if ( streamFormatStr == "t64" ) + streamFormatIn = StreamFormat::Text64; + else if ( streamFormatStr == "b32" ) + streamFormatIn = StreamFormat::Bin32; + else if ( streamFormatStr == "b64" ) + streamFormatIn = StreamFormat::Bin64; + else + streamFormatIn = StreamFormat::Invalid; + break; + } - std::cout << "Hello, World!" << " The value of argc is " << argc << std::endl; + case Help: + helpFlagIn = true; + break; - if (1 < argc) - { - if ( 0 != parseCommandLine(argc, argv) ) - { - std::cout << "Failed parsing command line" << std::endl; - return -1; + case IncludeX: + includeX_In = true; + break; + + case '?': +// std::cout << "The getopt_long call returned '?'" << std::endl; + retCode = 1; + break; + default: +// std::cout << "The getopt_long call returned character code" << c << std::endl; + retCode = 2; + break; } } - else - { - std::cout << "No program arguments, entering interactive mode" << std::endl; - } - return 0; + return retCode; } -#endif diff --git a/testUtilities/CommandLineParser.h b/testUtilities/CommandLineParser.h index d0b60f8..4f299bc 100644 --- a/testUtilities/CommandLineParser.h +++ b/testUtilities/CommandLineParser.h @@ -3,6 +3,8 @@ #ifndef TSG_COMPLEXTONEGEN_COMMANDLINEPARSER_H #define TSG_COMPLEXTONEGEN_COMMANDLINEPARSER_H +#include + class CommandLineParser { public: @@ -14,9 +16,26 @@ class CommandLineParser inline double getRadsPerSample() const { return radsPerSampleIn; } inline double getPhase() const { return phaseIn; } + inline unsigned long getChunkSize() const { return chunkSizeIn; } + inline unsigned long getNumChunks() const { return numChunksIn; } + inline unsigned long getSkipChunks() const { return skipChunksIn; } + + enum class StreamFormat : short { Invalid=0, Text32, Text64, Bin32, Bin64 }; + StreamFormat getStreamFormat() const { return streamFormatIn; } + + inline bool getHelpFlag() const { return helpFlagIn; } + inline bool getIncludeX() const { return includeX_In; } + private: - double radsPerSampleIn{ 1.0 }; + double radsPerSampleIn{ M_PI / 256 }; double phaseIn{ 0.0 }; + unsigned long chunkSizeIn{ 4096 }; + unsigned long numChunksIn{ 1 }; + unsigned long skipChunksIn{ 0 }; + bool helpFlagIn{ false }; + bool includeX_In{ false }; + + StreamFormat streamFormatIn{ StreamFormat::Text64 }; };