Making music with Perl

jjohn on 2002-12-15T23:32:21

Wouldn't it be cool to make your own synthesizer in Perl? With a little rudimentary physics and a CPAN module, you can produce the monophonic opus of your dreams without leaving the protective womb of your favorite editor.

Experienced readers of this journal will note that I am interested in music. Not only do I listen to quite a bit of it, but I've been playing, composing and recording music at the hobbist level (read: wanker level) for over ten years now. While you can limp along as an audio engineer without understanding the dynamics of audio waves, this knowledge can save quite a bit of time during tracking and mixing. Knowing which band of frequencies best accentuates those instruments in your mix can help you fill out your sound while reducing unintentional mush ("mush" is a technical term).

Listeners of my recent music will notice a lot of MIDI sequencing. From drums to bass to keyboards, it seems I've discovered the little Gary Neumann inside of me. I use Cakewalk Pro Audio 9 for sequencing the MIDI patches that my SoundBlast Live card has (with additional SoundFonts I bought from a third party). Pro Audio allows me to blend MIDI sequences with "live" audio tracks (vocals, guitars and other assorted noises captured by microphones). These audio tracks can be recorded with Pro Audio or WAV files can be imported into the current project. This is how I work with samples from various movies or CDs. My TV, VCR and CD player are components of my PC (even if they weren't, I have an external Mackie mixer which routes various sound sources through one set of speakers and to my PC, producing the same result for this purpose). I sample by playing back the orginal source while recording the audio signal with Cakewalk. As a WAV file, it is then easy to edit and modify that sample to taste.

Music is fun (but not always profitable) when its experimental. Just as I've recorded spatulas and toolboxes as percussion elements in the past, I found that my experiments with IBM's ViaVoice speech synthesis software to have pleasant musical applications (at least, pleasant by my reckoning). Since Perl is a big part of my life, I have wanted to incorporate some our favorite scripting into my music. When I found the Audio::WAV module on CPAN, I seized upon the opportunity to learn more about WAV files and audio dynamics.

I'll skip the high school physics introduction to sound and waves, since most of the readers here probably remember more of that stuff than I do. However, the important thing to remember is that sound moves in waves. The canonical example of a sound wave is one that takes the form of a sine wave. That is, a wave that smoothly oscillates from peak to valley (there are many other possible wave forms, true sine waves rarely occur naturally). The frequency at which that sine wave propagates is called fundamental or, in musical terms, the tonic note. While very important, sound that only consists of the fundamental frequency can fatigue the ear quickly. Additional frequencies that are even multiples of the fundamental make the final tone more complex and interesting. These additional frequencies are called harmonics and they interfere with the fundamental to produce a more complex wave form.

With this small bit of phyics and the Audio::WAV module, you can produce wave files of any tone you want. By extending the code shown here, your scripts can write out entire songs in glorius 16-bit, 44100hz WAV files. The key is to understand how to use Audio::WAV to write out audio information.

Because this is such a new module (it's only up to 0.2), the documentation is a little underpowered. However the core of what you need is there. The Audio::WAV class has two child classes it uses to read and write WAV files (called Audio::WAV::Read and Audio::WAV::Write respectively). Instead of directly instantiating an Audio::WAV::Write object, Audio::WAV has a write() method that returns a new Audio::WAV::Write object. For instance:

my $wav = Audio::Wav->new;
my $write = $wav->write($outfile, 
			{ 
			 bits_sample => $bits_sample,
			 sample_rate => $sample_rate,
			 channels    => 1,
			}
		       );

Audio::WAV::Write also has a write() method, but it expects to be passed at least one point of wave data to write out to the appropriate file.

  $write->write( sin($pi * $time) * $max_no );

(note: the documentation claims that write() can take an array of samples, but that only produced empty 46 byte WAV files for me.)

Although it seems simple enough to feed write() random numbers, the trick is in understanding how to generate meaning data (isn't that always the way). This discussion is limited to talking about sine waves since that does not exceed my mathematical acumen.

Like the graph of a sine wave made by an eighth-grader, the WAV file consists of points that represent the wave's amplitude at a given point in time (it's a bit more complicated than that, but the Audio::WAV module lets me work at this level). Successive calls to write() place a new point on this imaginary graph at the next available time slot (see below for an explaination of how time is subdivided along this "X-axis" of time). Once the maximum and minimum values for wave's amplitude are know, it's a very simple math problem to determine the appropriate "y value."

y = sin(PI * x) # if you have an X value, find Y

In this case, the X value is going to be a slice of time which is determined by both the frequency of the fundamental and the sampling rate of the WAV file. The higher the sampling rate, the more X values are produced. But, how many time slices (that is divisions of the X-axis) are needed? This is a function of how many seconds you want the sound to last times the sampling rate.

number of X-axis divisions = seconds * sample rate
The value of each X-axis point is:
X-axis value = (X-axis offset/sample rate) * hertz

Now we're getting somewhere! You can approximate PI with (22/7) and now you know your X-axis values. You only need to know the range of allowable amplitudes for this wave file to determine valid Y-values. Recall that sound wave amplitude is perceived as loudness by human ears. It turns out that the amplitude is governed by the bit resolution of the WAV file.

max_amplitude = (2 ** bit resolution) / 2

Why are we raising 2 by the power of the bit resolution? For the same reason that you set your video card to the highest video resolution. The more bits, the more graduation. I assume that WAV files allocate the number of bits designated by the bit resolution for each sample of sound to represent the amplitude of the wave file at that time. The result is divided by two because the wave has positive and negative peaks. In effect, it's like the number is signed (in fact, it may be in the WAV file).

Putting this mess together, the amplitude of the wave at a given sample is found like this:

current amplitude  = sin(PI * x-axis value) * max amplitude

Because sin() produces a number between 1 and -1, the amplitude will either be at the maximum amplitude or smaller. By added a scalar to the maximum amplitude, you can control the volume of the samples too. In Perl code, producing each point on the sine wave is done like this:

for my $pos (0..$len) {
  my $time = ($pos/$sample_rate) * $hertz;
  $write->write( sin($pi * $time) * $max_no );
}

This code produces a sine wave with only the fundamental frequency. If you wanted to add the second harmonic to this wave, simply double the hertz value every other iteration.

for my $pos (0..$len) {
  my $hz = $hertz;

  if ($pos % 2 == 1) {
    $hz *= 2;
  }

  my $time = ($pos/$sample_rate) * $hz;
  $write->write( sin($pi * $time) * $max_no );
}

It's easy enough to generalize this code to support any harmonic. I thought it would be fun to add an arbitrary number of harmonics to the fundamental.

my $next = 0;
for my $pos (0..$len) {
  my $hz = $hertz;

  # throw in some harmonics, but keep the tonic dominate
  if ($pos % 2 == 1) {
    $hz *= $harmonics->[$next++];
  }
  $next = 0 if $next >= @{$harmonics};

  my $time = ($pos/$sample_rate) * $hz;

  $write->write( sin($pi * $time) * $max_no );
}

Notice that the fundamental is represented at least as often as any additional harmonic. The more harmonics are added, the more the fundamental dominates. This may not be entirely what you want, but at least you now have some place to start tinkering.

Wouldn't it be great if someone wrapped this into an easy to use perl script? You bet it would be!

#!/usr/bin/perl
# Create sine wave WAV files
# Based on code found in Audio::WAV::Write POD
# jjohn 12/2002

use strict;
use Audio::Wav;
use Getopt::Std;

my %opts;
getopts('?hb:f:H:s:t:V:z:', \%opts);

if ($opts{h} || $opts{'?'}) {
  print usage();
  exit;
}

my $outfile     = $opts{f} || 'out.wav';
my $hertz       = $opts{z} || 440;
my $seconds     = $opts{t} || 2;
my $harmonics   = $opts{H} || 1;
my $sample_rate = $opts{s} || 44100; # CD quality;
my $bits_sample = $opts{b} || 16;    # 4,8,16 are all good choices
my $volume_scalar = 1;

if ($opts{V} < 1 && $opts{V} > 0) {
  $volume_scalar = $opts{V};
}

my $wav = Audio::Wav->new;
my $write = $wav->write($outfile, 
			{ 
			 bits_sample => $bits_sample,
			 sample_rate => $sample_rate,
			 channels    => 1,
			}
		       );

my $pi     = (22/7); # close enough;
my $len    = $seconds * $sample_rate;
my $max_no = (2 ** $bits_sample) / 2 * $volume_scalar;

# split Harmonics value into an array
$harmonics = [ split /\s*,\s*/, $harmonics ];

my $next = 0;
for my $pos (0..$len) {
  my $hz = $hertz;

  # throw in some harmonics, but keep the tonic dominate
  if ($pos % 2 == 1) {
    $hz *= $harmonics->[$next++];
  }
  $next = 0 if $next >= @{$harmonics};

  my $time = ($pos/$sample_rate) * $hz;

  $write->write( sin($pi * $time) * $max_no );
}

$write->finish;

sub usage {
  return < bit resolution (defaults to 16-bit)
  f  name of the outfile (defaults to 'out.wav') 
  H  Add this harmonic to the base tone. Can be a comma-separated list.
  s  sample rate (defaults to 44100 (CD quality))
  t  number of seconds to make the file (default is 2)
  V  Volume multiplier (decimal values cut the default MAX volume) 
  z  Frequency in hertz of the WAV file (default is 440) 

EOT
}

Next time, I'll look at managing the wave forms better to produced rudimentary FM synthesis. Together with Perl's ability to read MIDI files, you can turn existing MIDI files into "fully realized" WAV files without using a sound card!

Zen thought for the day: If a WAV file is produced on a machine without a sound card, is there any way to tell if the program worked correctly?


Some ideas

ethan on 2002-12-16T03:46:25

Not that I am a particular friend of it, but there have always been some attempts in aleatoric music. Using audio (as opposed to MIDI) for it has some advantages due to its rather analog character: Deduce some metrics from a piece of Perl-code and turn it into a wav file. The resulting sounds (read: noises) let you decide which code is the one with the most musicality in it.

Perhaps some of the outcomes can serve as usable samples (drum samples contain a lot of noise): You sample CGI.pm, Parse::RecDescent and two other suitable modules and thus create a Perl-drumset.

Some fan-boy you are...

rjray on 2002-12-17T00:59:25

That's Gary NUMAN.