Nominal Fourier Synthesis

A departure from classic analog synthesizer waveforms.



This is a proposal for a system of assigning a particular harmonic series to a particular word, in such a way that we can talk about, implement, and work with waveforms other than the traditional sine, square, sawtooth, triangle, and pulse waves.

This proposal is not about any new technology; it's merely a method of mapping a word onto a particular harmonic series to be generated through additive synthesis, to be substituted for any other simple waveform in a complex synthesis environment.

Definition: A Nominal Fourier Synthesis specification (NFS) maps a string of text characters onto an array of floating point numbers in the range [0.0..1.0], the array to be used as weighting factors for individual waveform harmonics. The initial element of the array represents the relative amplitude of the fundamental; the next element the relative amplitude of the first harmonic, and so on.

A NFS specification has an "order", which is the number of elements in the array. The order of the NFS specification is a tradeoff between computation time and harmonic complexity. I generally use 40 harmonics in my own projects.

This document describes the Tiny God NFS, implemented in the Tiny God line of products, including the Heartburn software synthesizer. A previous version of this document described a less powerful version of the NFS algorithm, now deprecated. The Tiny God NFS can be used for any number of harmonics. The easiest way to accurately describe it is via code sample — unfortunately, this code has been extracted from a more complex wavetable generation function, modified for context, and so probably is no longer correct, and may not even compile. All just part of the fun. Anyway, here it is:



const int NOMINAL_MASK_EVEN	= 1;
const int NOMINAL_MASK_ODD = 0;
const int NOMINAL_MASK_ALL = 2;
const int NOMINAL_MASK_NONE = 3;

// harmonic_count: input, is the number of harmonic weights to generate
// weights: output, array of (harmonic_count) floats, will be filled
// nom: input, the name of the series to generate; only letters, digits, 
// and the special characters < > * . ^ _ are considered

void NominalSeries( int harmonic_count, float* weights, char* nom )
{
	int i;

	assert( strlen( nom ) > 0 );
	assert( weights != NULL );
	assert( harmonic_count > 0 );

	// harmonic weighting coefficients
	float weight = 1.0f;	// fundamental always gets weight 1.0
	float trend = 0.8f;
	float max = MAX_WEIGHT;
	float min = MIN_WEIGHT;
	int index = 0;
	int mask = NOMINAL_MASK_ALL;

	// for each harmonic presented
	for (int harm = 0; harm < harmonic_count; harm++)
	{
		// This implements the odd/even harmonic selection; 
		// the fundamental is never affected by the odd-even,
		// the low bit of the harmonic index matches the 
		// NOMINAL_MASK_[ODD|EVEN] codes. 

		if ((harm == 0) || (mask == NOMINAL_MASK_ALL) || ((harm&1) == mask))
		{
			*weights++ = weight;
		}
		else
		{
			*weights++ = 0.0f;
		}

		int c; 

		// We keep working our way through the name until we 
		// get to a "data" code - a digit or letter which 
		// informs the next harmonic. The other codes don't 
		// take a harmonic slot of their own, though they 
		// affect subsequent calculations. 

		bool data = false;
		while (!data)
		{
			// if we've gotten to the end of the name, restart
			if (nom[index] == 0)
			{
				index = 0;
			}
			c = tolower(nom[ index++ ]);

			// < yields even harmonics
			if (c == '<')
			{
				mask = NOMINAL_MASK_EVEN;	
			}
			// > yields odd harmonics
			if (c == '>')
			{
				mask = NOMINAL_MASK_ODD;	
			}
			// * yields both even and odd harmonics
			if (c == '*')
			{
				mask = NOMINAL_MASK_ALL;
			}
			// . yields no harmonics
			if (c == '.')
			{
				mask = NOMINAL_MASK_NONE;
			}
			// ^ caps the allowable max weight at the current weight
			if (c == '^')
			{
				max = weight;
			}

			// _ caps the allowable min weight at the current weight
			if (c == '_')
			{
				min = weight;
			}

			if ((c >= 'a') && (c <= 'z'))
			{
				c = c-'a';							// [0..25]
				trend *= 1.0f+((c-12.0f)/20.0f);	// * [0.4 .. 1.65]
				trend = BClampFloat( trend, MIN_TREND, MAX_TREND );

				weight = weight*trend;
				if (weight < min)
				{
					weight = min;
					trend = 1.0f/trend;
				}

				if (weight > max)
				{
					weight = max;
					trend = 1.0f/trend;
				}
				max -= (MAX_WEIGHT/harmonic_count);
				if (max < 0) max = 0;
				data = true;
			}

			if ((c >= '0') && (c <= '9'))
			{
				c = c-'0';	// 0-9
				float new_weight = powf((float(c)/9.0f)+0.05f, float(harm+1) );
				trend = new_weight/weight;
				weight = new_weight;
				data = true;
			}
		}
	}
}



In short summary:

• The name code is used cyclically until all requested harmonics are generated, and case is ignored, so "foo" and "FooFOOfOo" generate the same harmonic series. Only letter and digit characters actually cause harmonics to be generated, though other codes will affect the weights. Due to programmer laziness, while zero-length name codes are detected by the assertions at the top of the function, codes with no alphanumeric characters in them will loop infinitely; fixing this is left as an exercise.

• The letters a-z modify a trend value upward or downward, the weight being multiplied by the trend at each successive harmonic. When the weights go outside the given minimum and maximum weights, the trends are reflected, effectively bouncing off the limits to give sharp peaks or notches in the spectrum.

• The digits 0-9 set the weights directly, according to an exponential function, with '9' actually exceeding the amplitude of the fundamental. The trend that would have been required to set that weight is calculated for future alphabetic trendings.

• The masking codes '<', '>', '*', and '.' select even harmonics only, odd harmonics only, both even and odd harmonics, and no harmonics, respectively, from the point at which they are first encountered until superseded by another masking code. The weights and trends that masked-out harmonics would have had are still calculated according to the letter and digit codes, so the names 'Jehosephat' and 'Jeho.s*ephat' produce identical spectra apart from dropping every tenth harmonic.

• The codes '^' and '_' cap the maximum and minimum allowed weights, respectively, at the current weight. Note that the maximum weight drifts downward slightly by itself with each alphabetic-trend harmonic, and that the maximum takes priority over the minimum, should it become lower than minimum.


I hereby consign the contents of this document and all conceivably-patentable techniques described within to the public domain. Free yourselves from the tyranny of the classic analog waveforms.


Author: Russell Borogove, kaleja@estarcion.com. Last revision: 2003/1/31.

Go to Estarcion...

Go to Tiny God Productions...