Generate_PAL_Samples.java
// $Id: Generate_PAL_Samples.java,v 1.3 2003/11/23 05:30:38 vickery Exp $
/*
* Generates hexadecimal constants for initializing an array or rom
* of sine wave intensities for use with Celoxica's PAL macros for
* audio output.
*
* Created on: Nov 5, 2003
* Author: C. Vickery
*
* $Log: Generate_PAL_Samples.java,v $
* Revision 1.3 2003/11/23 05:30:38 vickery
* Corrected syntax of generated macro expressions.
*
* Revision 1.2 2003/11/23 05:05:30 vickery
* Cleaned up parameters for controlling the fidelity of the generated
* waveform.
*
* Revision 1.1 2003/11/22 05:07:29 vickery
* Initial version.
*
*/
import java.text.DecimalFormat;
// Class Generate_PAL_Samples
// ------------------------------------------------------------------
/**
* <p>Generates sound intensity values for a sine wave in a form
* suitable for initializing Handel-C signed integer values.</p>
* <p>The values generated assume the Handel-C program has already
* defined a macro expression named bps, presumably with a
* statment like:</p>
* <pre>
* macro expr bps = ( PalAudioOutGetMaxDataWidthCT() );
* </pre>
* <p>This program generates 32 bit sample values for the sound
* intensities, but it outputs Handel-C constants that drop low
* order bits to result in a width of bps.</p>
*
* <p>The output of this program includes syntax markers so that
* it may be #included as initializers for a Handel-C array or
* rom of width bps. It also generates macro expressions for the
* number of samples (numSamples_<i>fff</i>) and sampling rate
* (sampleRate_<i>fff</i>), where <i>fff</i> is the sine wave's
* frequency in Hz.</p>
*
* <p>The program generates enough sound samples for one complete
* sine wave cycle. However, the period of the generated wave
* only approximates the period of the desired wave unless the
* period of the desired wave is an exact multiple of the sampling
* period. The output of the program includes comments that tell
* the nominal and actual frequency values.</p>
*
* <p>There are command line options that can be used to control
* the operation of the program. These are summarized in the Field
* list and in the documentation for the <i>main()</i> method.</p>
*
*/
public class Generate_PAL_Samples
{
// User settable parameters and their defaults
// --------------------------------------------------------------
/** <p>Sampling rate, in Hz.</p>
* <p>Default is 48000.</p>
* <p>Command line option: -r</p>
*/
static double samplingRate = 48000.0; // RC-200 default
/** <p>Bits per sample. Default is 32.</p>
* <p>Will be reduced to the value of the Handel-C <i>bps</i>
* macro expr.</p>
* <p>Command line option: -b</p>
*/
static int bitsPerSample = 32;
/** <p>Desired frequency of the generated sine wave, in Hz.</p>
* <p>Default is 440 Hz.</p>
* <p>Command line option: -f</p>
*/
static double desiredFrequency = 440.0;
/** <p>Acceptable maximum deviation of generated frequency from
* desired frequency, in Hz.</p>
* <p>Default value is -1.0, which means no limit.</p>
* <p>Command line option: -d</p>
*/
static double maxDeviation = -1.0;
/** <p>Number of cycles to generate. In general more cycles give
* better accuracy.</p>
* <p>Default value is 1, which is the minimum allowed.</p>
* <p>If maxDeviation is non-negative, the value of this parameter
* is the minimum number of cycles that will be generated.</p>
* <p>Command line option: -c</p>
*/
static int numCycles = 1; // Num cycles!
// For computing radians.
private static final double twoPi = Math.PI + Math.PI;
// Derived parameters
private static double secondsPerSample = 0;
private static double secondsPerCycle = 0;
private static double radiansPerSample = 0;
private static long scaleFactor = 0;
private static long mask = 0;
// Constructor
// ----------------------------------------------------------------
/**
* The no-arg constructor is private. This class is never
* instantiated.
*/
private Generate_PAL_Samples() {}
// decize()
// ----------------------------------------------------------------
/**
* Generate the n-digit decimal representation of an int.
* Assumes the int is positive.
*/
private static String decize( int val, int numDigits )
{
StringBuffer sb = new StringBuffer( Integer.toString( val ) );
while ( sb.length() < numDigits )
{
sb.insert(0, ' ' );
}
return new String( sb );
}
// hexadize()
// ----------------------------------------------------------------
/**
* Generate the n-digit hexadecimal representation of an int.
*/
private static String hexadize( long val, int numDigits )
{
StringBuffer sb =
new StringBuffer( Long.toHexString( val ) );
while (sb.length() < numDigits )
{
sb.insert(0, '0' );
}
if ( sb.length() > numDigits )
{
sb.delete( 0, (sb.length() - numDigits) );
}
for (int i = 0; i < numDigits; i++ )
{
char c = Character.toUpperCase( sb.charAt( i ) );
sb.setCharAt( i, c );
}
sb.insert(0, "0x" );
return new String( sb );
}
// adjust()
// --------------------------------------------------------------
/**
* Adjust for rounding errors -- the last sample has to be
* negative, but not the next one if there were a next one.
*/
private static int adjust( int lastSample,
double radiansPerSample )
{
while ( Math.sin( radiansPerSample * lastSample ) >= 0.0 )
{
lastSample--;
}
while ( Math.sin( radiansPerSample * (1 + lastSample) ) < 0.0 )
{
lastSample++;
}
// Assert correctness
if ( !((Math.sin( radiansPerSample * lastSample) < 0.0 ) &&
(Math.sin( radiansPerSample * (1+lastSample)) >= 0.0 )) )
{
System.err.println( "Error: sine of last sample, ("
+ Math.sin( radiansPerSample * lastSample)
+ ") should be negative, and sin of next sample ("
+ Math.sin( radiansPerSample * (1+lastSample) )
+ ") should be non-negative." );
System.exit( 0 );
}
return lastSample;
}
// isZero()
// --------------------------------------------------------------
/*
* Determines whether a value will be zero when represented as
* an int in the number of bits available.
*/
private static boolean isZero( int sample )
{
double realSampleValue = Math.sin( sample * radiansPerSample );
long longSampleValue = (long)(realSampleValue * scaleFactor);
return (longSampleValue & mask) == 0;
}
// main()
// --------------------------------------------------------------
/**
* Process command line arguments and then generate a list of
* signed integer values suitable for compilation by the Handel-C
* compiler.
*
*<pre>
* Argument Parameter (default)
* -f [frequency] Sine wave frequency in Hz. (440.0)
* -r [rate] Sampling rate. (48000)
* -b [bits] Bits per Sample. (32)
* -d [deviation] Maximum deviation between generated and
* nominal frequencies in Hz. (-1.0)
* -c [cycles] Number of sound wave cycles to generate. (1)
*
*</pre>
*/
public static void main(String[] args)
{
// Process command line options
// --------------------------------------------------------------
for (int i = 0; i < args.length; i++ )
{
try
{
if ( "-f".equals( args[i] ) )
{
desiredFrequency = Double.parseDouble( args[++i] );
// Checked after all options processed.
continue;
}
if ( "-r".equals( args[i] ) )
{
samplingRate = Double.parseDouble( args[ ++i] );
if ( samplingRate <= 0.0 )
{
System.err.println( "Sampling rate must be positive." );
System.exit( 1 );
}
continue;
}
if ( "-b".equals( args[i] ) )
{
bitsPerSample = Integer.decode( args[++i] ).intValue();
if ( bitsPerSample < 1 || bitsPerSample > 32 )
{
System.err.println(
"Bits per sample must be between 1 and 32 " );
System.exit( 1 );
}
continue;
}
if ( "-d".equals( args[i] ) )
{
maxDeviation = Double.parseDouble( args[++i] );
continue;
}
if ( "-c".equals( args[i] ) )
{
numCycles = Integer.decode( args[++i] ).intValue();
if ( numCycles < 1 )
{
System.err.println(
"Number of cycles must be greater than zero." );
System.exit( 1 );
}
continue;
}
}
catch (NumberFormatException e)
{
System.err.println( e + " is not a valid number." );
System.exit( 1 );
}
catch (ArrayIndexOutOfBoundsException e)
{
System.err.println( "Missing option value. ");
System.exit( 1 );
}
System.err.println( args[i] + " is not a valid option." );
System.exit( 1 );
}
// Validate frequency here ... depends on sampling rate, which
// may be specified either before or after -f.
if ( desiredFrequency <= 0.0 || desiredFrequency > samplingRate )
{
System.err.println( "Error: Frequency ("+ desiredFrequency +
") must be positive" );
System.err.println( " and no more than " +
"sampling rate (" + samplingRate + ")." );
System.exit( 1 );
}
// Calculate derived parameters
// --------------------------------------------------------------
secondsPerSample = 1.0 / samplingRate;
secondsPerCycle = 1.0 / desiredFrequency;
radiansPerSample = twoPi * desiredFrequency / samplingRate;
scaleFactor = ((long)1) << (bitsPerSample - 1);
mask = scaleFactor - 1; // Mersenne
// Calculate number of samples.
/* Algorithm:
* The number of samples, n, will be the largest n for which
* n * radiansPerSample * cycles < 2 * pi * cycles, where
* cycles is the number of sine wave cycles to be generated.
*
* With neither -c nor -d specified, cycles is 1. If the
* -c option is specified, cycles is the value of that option.
* If the -d (deviation) option is specified, cycles is
* incremented until the average frequency within +/- deviation
* of the desired frequency.
*
* "Crossing Precision" is how close the final sample value is
* to zero. It is a derived statistic for this version of the
* program.
*/
int samplesPerCycle = (int)(samplingRate / desiredFrequency);
double actualFrequency = 1.0 /
(samplesPerCycle * secondsPerSample);
int lastSample = numCycles * samplesPerCycle - 1;
lastSample = adjust( lastSample, radiansPerSample );
double averageFrequency =
numCycles / ( lastSample * secondsPerSample );
double deviation = averageFrequency - desiredFrequency;
if ( maxDeviation >= 0 )
{
while ( Math.abs(deviation) > maxDeviation )
{
numCycles++;
lastSample = numCycles * samplesPerCycle - 1;
lastSample = adjust( lastSample, radiansPerSample );
averageFrequency =
numCycles / ( lastSample * secondsPerSample );
deviation = averageFrequency - desiredFrequency;
}
}
// A hack: Drop samples from the end if they will come out
// zero in binary, and check there are any samples left when
// finished.
while ( isZero( lastSample ) )
{
if ( --lastSample < 0 )
{
System.err.println( "No non-zero samples generated. " );
System.exit( 1 );
}
}
// Parameters for formatting the output lines.
DecimalFormat df5 = new DecimalFormat( " 0.00000;-0.00000" );
DecimalFormat df1 = new DecimalFormat( "+0.0;-0.0" );
int hexDigits = (bitsPerSample + 3) / 4;
// Print heading information for the list of values to
// follow, including an open brace for beginning a list of
// initializers.
System.out.println(
"// Values generated by Generate_PAL_Samples.java" );
System.out.println( "// Samples per second: " +
samplingRate );
System.out.println( "// Frequency Desired: " +
df5.format( desiredFrequency ) + " Hz" );
System.out.println( "// Number of periods: " + numCycles );
System.out.println( "// Number of samples: " +
( lastSample + 1 ) );
System.out.println( "// Average Frequency: " +
df5.format( averageFrequency ) + " Hz" );
System.out.println( "// Frequency error: " +
df5.format( averageFrequency - desiredFrequency ) + " Hz ("
+ df1.format( 100.0 * (averageFrequency - desiredFrequency)
/ desiredFrequency) + "%)" );
System.out.println( "// Zero precision: " +
df5.format( -Math.sin( lastSample * radiansPerSample ) ) );
System.out.println();
System.out.println(
"// - Signed integer value - Sample# Time " +
" sin(2*pi*t)" );
System.out.println( " {" );
for ( int sampleNum = 0; sampleNum <= lastSample; sampleNum++ )
{
// Get Sine, a value between -1.0 and +1.0
double realSampleValue =
Math.sin( sampleNum * radiansPerSample );
// Convert to signed long between -2^(n-1) and +2^(n-1)-1.
long longSampleValue = (long)(realSampleValue * scaleFactor);
// Display value in format suitable for generating
// Handel-C code, e.g.: " 0x123456 \\ (32 - bps),"
for (int i = 0; i < 8 - ( (3 + bitsPerSample) / 4); i++ )
System.out.print( " " ); // Spacing to match heading.
System.out.println( " "
+ hexadize( longSampleValue, hexDigits ) + " \\\\ ("
+ bitsPerSample + " - bps)"
+ ", /* "
+ decize( sampleNum, 4 ) + ": "
+ df5.format( sampleNum * secondsPerSample ) + " "
+ df5.format( realSampleValue ) + " */" );
}
// End of initialization list
System.out.println( " };" );
System.out.println();
// Tell Handel-C compiler integer values for parameters.
double rate = samplingRate / 1000.0;
int intFrequency = (int) desiredFrequency;
int intSamplingRate = (int) samplingRate;
System.out.println( "// Parameters for a " + desiredFrequency +
" Hz sine wave sampled at a " + rate + "KHz rate " );
int sampleInterval = (int)(1E9 * 1.0 / samplingRate );
System.out.println( "macro expr numSamples_" + intFrequency +
" = " + (1 + lastSample) + ";" );
System.out.println( "macro expr sampleRate_" + intFrequency
+ " = " + intSamplingRate + ";" );
}
}