A while back, I discovered The Clangers. I was strangely turned on by their voices.

Months later, I fired up Cool Edit and played with the "Ringing A's" filter a while, eventually isolating one 'A' and moving it up to 1000 Hz. It reminded me of Morse code in a heavy echo chamber. I brought out the Morse aspect by gating the signal at approximately the average volume. Then I realized that an easier way to generate the envelope would be to apply a heavy low-pass filter to noise and cut out the negative strokes.

  1. noise(20 s)
  2. sci_filter(butterworth lowpass 10 Hz order 6)
  3. distort(splines, positive place point at -30 dB -> 0 dB, negative to silence)
  4. AM with tones(500 Hz sine)
  5. pitchbender(squirrely)

I felt turned on in the same way.

Hours later, it hit me: that's the voice of The Clangers! I changed a few of the parameters and implemented the filter chain in straight C, using a cheesy way to generate low-pass noise with a cubic filter.


/* clangers.c
Simulate the voice of The Clangers.

Copyright 2001 Damian Yerrick (hereinafter $OWNER)

Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject
to the following conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-
INFRINGEMENT. IN NO EVENT SHALL $OWNER BE LIABLE FOR ANY CLAIM,
DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT
OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

Except as contained in this notice, the name of $OWNER shall not be
used in advertising or otherwise to promote the sale, use or other
dealings in this Software without prior written authorization from
$OWNER.

*/




#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <math.h>

/* This is a template for a Canonical WAVE header.
Byte values that must be replaced are at offset
4: size of wave data + 36
22: number of channels
24: sample rate in Hz
28: avg bytes per second = sample rate * bytes per frame
32: bytes per frame = (bits per sample + 7) / 8 * channels
34: bits per sample
40: number of bytes (must be even)

Documentation at http://www.lightlink.com/tjweber/StripWav/WAVE.html
*/

const unsigned char canonical_wav_header[44] =
{
'R','I','F','F', 0, 0, 0, 0,'W','A','V','E','f','m','t',' ',
16, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0,'d','a','t','a', 0, 0, 0, 0
};

void fill_32(unsigned char *dest, unsigned long src)
{
int i;

for(i = 0; i < 3; i++)
{
*dest++ = src;
src >>= 8;
}
}

typedef struct UPSAMPLE
{
float xnm3, xnm2, xnm1, xn;
float t;
float freq;
float a, b, c, d;
} UPSAMPLE;


/* upsample() **************************
Upsamples a signal using cubic spline interpolation.
Uses new_sample once every time up->t >= 1.
*/
float upsample(UPSAMPLE *up, float new_sample)
{
float tf = up->t;
float ns, omtf;

if(tf >= 1.0)
{
tf -= 1.0;
up->xnm3 = up->xnm2;

up->xnm2 = up->xnm1;
up->xnm1 = up->xn;
up->xn = new_sample;
up->a = up->xnm2;
up->d = up->xnm1;
up->b = 3 * up->a + (up->d - up->xnm3) / 2;
up->c = 3 * up->d + (up->a - up->xn) / 2;
}

omtf = 1 - tf;
ns = ((up->a*omtf+up->b*tf)*omtf*omtf + (up->c*omtf+up->d*tf)*tf*tf);

up->t = tf + up->freq;

return ns;
}


/* init_upsample() *********************
Initialize an upsample buffer.
*/
void init_upsample(UPSAMPLE *up, float freq)
{
up->freq = freq;
up->a = up->b = up->c = up->d = up->xnm2 = up->xnm1 = up->xn = 0;
up->t = 1;
}



#define OUT_FREQ (44100)
#define LEN (2560000)
#define SRC_PERIOD (OUT_FREQ / 65.625)
#define SRC_OFFSET 100
#define BYTES_PER_SAMPLE 2
#define SINMASK 0x3ff

float sintable[SINMASK + 1];

int main(void)
{
unsigned char header[44];
int cur_sample;
FILE *fp;
float yn;
UPSAMPLE keynoise = {0}, prosody = {0};
unsigned int n;

float carrier_t = 0;


/* create wav file */
memcpy(header, canonical_wav_header, 44);
fill_32(header + 4, LEN * BYTES_PER_SAMPLE + 44);

header[22] = 1;
fill_32(header + 24, OUT_FREQ);
header[32] = BYTES_PER_SAMPLE;

fill_32(header + 28, header[32] * OUT_FREQ);
header[34] = 8 * BYTES_PER_SAMPLE;
fill_32(header + 40, LEN * BYTES_PER_SAMPLE);

srand(time(NULL));
fp = fopen("168out.wav", "wb");
if(!fp)
{
perror("could not open 168out.wav");
return 1;
}
fwrite(header, 44, 1, fp);

/* initialize sine table for frequency modulation */
for(n = 0; n <= SINMASK; n++)
sintable[n] = sin(n * (2*M_PI/(SINMASK+1)));

/* initialize 5 Hz lowpass noise generator for keypresses */
init_upsample(&keynoise, 10./44100);
/* initialize 1 Hz lowpass noise generator for inflection */
init_upsample(&prosody, 2./44100);

for(n = 0; n < LEN; n++)
{
/* generate noise */
if(keynoise.t >= 1.0)
yn = upsample(&keynoise, rand() % 240 - 120);
else
yn = upsample(&keynoise, 0);

/* distort into keypresses */
if(yn < 0)
yn = 0;
else if(yn < 8)
yn = yn * yn;
else
yn = 64 + (yn - 8) / 2;

/* modulate with carrier */
yn *= sintable[(int)floor(carrier_t) & SINMASK];

/* generate noise and modulate the carrier's frequency */
if(prosody.t >= 1.0)
carrier_t += upsample(&prosody, rand() % 12 + 10);
else
carrier_t += upsample(&prosody, 0);

if(carrier_t >= 1024)
carrier_t -= 1024;
else if(carrier_t < 0)
carrier_t += 1024;

if(yn > 127)
yn = 127;
else if(yn < -127)
yn = -127;
cur_sample = yn * 256;
fputc(cur_sample & 0xff, fp);
fputc(cur_sample >> 8, fp);
}

fclose(fp);

return 0;
}
A possible improvement would be to simulate a real slide whistle with a resonant filter.