Re: [LAU] Online resources/books for programming sound synthesis software?

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

 



On Thu, 2007-04-12 at 12:59 +0200, Thomas Janu wrote:
> > The Jack examples are a great place to start looking at code.  Also, I
> > wrote a howto for people writing Jack apps for the first time: 
> > 
> > http://www.dis-dot-dat.net/index.cgi?item=/jacktuts/starting/
> > 
> 
> That's already great, thanks a lot! ;)
> 
> > I hope you get more sources in this thread, because I would like to
> > read more myself.
> 
> More specifically I'm looking for commented code examples/tutorials on how to
> emulate sound synthesis, so writing oscillators, filters and so on as well as
> nice examples of the ``big picture'' so that i can see how it's all put together
> to form a synth. That'd be really nice.

I've been learning the same stuff recently as well.

I've attached my first attempt at a very basic synth using ALSA MIDI for
input and JACK for output. (Note that I'm not an audio expert so there
may be a few errors in it. If anyone spots an error please let me know.)

Wikipedia seems to have quite a few articles on synthesis:
  http://en.wikipedia.org/wiki/Audio_synthesis

I just read "The Computer Music Tutorial" by Curtis Roads, which was
recommended by a few Linux audio developers. It's a fairly good
introduction, though it seems slightly dated. I'm also reading "Elements
of Computer Music" by F.Richard Moore, but I wouldn't recommend it as a
first book as the maths is pretty difficult.

Damon

/*
 * A very simple synthesizer using ALSA for MIDI input and JACK for output.
 * By Damon Chaplin <damon@xxxxxxxxx>
 *
 * To try this out start JACK with qjackctl, then run this application and
 * connect your MIDI keyboard up using the "Connect" dialog in qjackctl.
 * (If you don't have a MIDI keyboard you could use vkeybd.)
 *
 * This is just example code - do what you like with it.
 *
 * Compile with:
 *   gcc `pkg-config --cflags --libs jack` -lasound generatorx.c
 */

#include <stdio.h>
#include <errno.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>

#include <alsa/asoundlib.h>
#include <jack/jack.h>

/* A client name we use to identify ourselves to JACK and ALSA. */
const char *ClientName = "GeneratorX";

/* The priority to use for the thread that handles MIDI events.
   FIXME: I'm not sure yet what I should use for this. */
#define MIDI_THREAD_PRIORITY	15

/* The maximum number of notes we can play at once. */
#define MAX_POLYPHONY		16

/* The size of our oscillator table. With 4096 samples we get about 132dB
   signal-to-noise ratio for a simple sine wave. But less with added
   harmonics. */
#define OSCILLATOR_TABLE_SIZE	4096

/* ALSA MIDI stuff. */
snd_seq_t *seq_handle;
pthread_t alsa_midi_thread;

/* JACK stuff. */
jack_port_t *output_port;
jack_client_t *client;
int sample_rate;

/* The table of oscillator data used to create the sounds. Note that we add
   an extra sample on the end to help with the linear interpolation code. */
jack_default_audio_sample_t oscillator_table[OSCILLATOR_TABLE_SIZE + 1];

/* An array of multipliers for each MIDI note used to calculate frequency
   stuff. A at 440Hz (note number 69) will be 1.0. An octave above is 2.0.
   An octave below is 0.5. */
double FrequencyMultipliers[128];

/* From -8192 to 8191. We adjust the pitch by up to 2 semi-tones. */
int pitchbend = 0;

/* From 0 to 127. FIXME: Unused at present. */
int aftertouch = 0;

/* From 0 to 127. FIXME: Unused at present. */
int modulation = 0;

/* This holds the data for one note that is currently playing. */
typedef struct _Note Note;
struct _Note
{
  /* The MIDI note number (0 to 127), or -1 if no note is playing. A at 440Hz
     is 69. The notes go up or down in semitones (12 is an octave). */
  int note;

  /* The note velocity (0 to 127), converted to the range 0 to 1. */
  double velocity;

  /* If the note has been released. */
  int in_release;

  /* The fraction of total amplitude, for the release. */
  double release;

  /* The amount to deduct from release at each step. When this reaches 0 or
     below, the note is stopped. */
  double release_step;

  /* The current position in the oscillator table. */
  double oscillator_offset;

  /* The step to add to oscillator_offset between each sample. */
  double oscillator_step;
};

/* This holds the data for all notes we are playing. */
Note Notes[MAX_POLYPHONY];


/* Set this to 1 to get some debugging output. */
#if 0
#define DEBUG(x) x
#else
#define DEBUG(x)
#endif


/***************************************************************************
 * MIDI INPUT
 ***************************************************************************/

static void* alsa_midi_thread_func (void *unused);

static void
init_alsa_midi (void)
{
  snd_seq_port_info_t * port_info = NULL;
  pthread_attr_t attr;
  struct sched_param rtparam;
  int status;

  status = snd_seq_open (&seq_handle, "default", SND_SEQ_OPEN_INPUT, 0);
  if (status < 0)
    {
      fprintf (stderr, "Cannot open sequencer: %s\n", snd_strerror (status));
      exit (1);
    }

  snd_seq_set_client_name (seq_handle, ClientName);

  snd_seq_port_info_alloca (&port_info);
  snd_seq_port_info_set_capability (port_info, 
				    SND_SEQ_PORT_CAP_WRITE
				    | SND_SEQ_PORT_CAP_SUBS_WRITE);
  snd_seq_port_info_set_type (port_info, SND_SEQ_PORT_TYPE_APPLICATION);
  snd_seq_port_info_set_midi_channels (port_info, 16);
  snd_seq_port_info_set_port_specified (port_info, 1);
  snd_seq_port_info_set_name (port_info, "Midi In");
  snd_seq_port_info_set_port (port_info, 0);

  status = snd_seq_create_port (seq_handle, port_info);
  if (status < 0)
    {
      fprintf (stderr, "Error creating ALSA sequencer port: %s\n",
	       snd_strerror (status));
      exit (1);
    }

  pthread_attr_init (&attr);
  status = pthread_attr_setinheritsched (&attr, PTHREAD_EXPLICIT_SCHED);
  if (status)
    {
      fprintf (stderr, "Error requesting explicit scheduling for thread: %s",
	       strerror (status));
      exit (1);
    }
  status = pthread_attr_setdetachstate (&attr, PTHREAD_CREATE_JOINABLE);
  if (status)
    {
      fprintf (stderr, "Error setting detach state for thread: %s",
	       strerror (status));
      exit (1);
    }
  status = pthread_attr_setscope (&attr, PTHREAD_SCOPE_SYSTEM);
  if (status)
    {
      fprintf (stderr, "Error requesting system scheduling scope for thread: %s",
	       strerror (status));
      exit (1);
    }

  status = pthread_create (&alsa_midi_thread, &attr, alsa_midi_thread_func,
			   NULL);
  if (status != 0)
    {
      fprintf (stderr, "Error creating ALSA MIDI thread: %s\n",
	       strerror (status));
      exit (1);
    }

  rtparam.sched_priority = MIDI_THREAD_PRIORITY;
  status = pthread_setschedparam (alsa_midi_thread, SCHED_FIFO, &rtparam);
  if (status != 0)
    {
      fprintf (stderr, "Cannot use real-time scheduling for ALSA MIDI thread: %s\n",
	       strerror (status));
    }
}


static void*
alsa_midi_thread_func (void *unused)
{
  int npfd, status;
  struct pollfd *pfd;
  snd_seq_event_t *ev;
  double frequency;
  int note, i;

  /* Get ready to poll the ALSA MIDI file descriptors to handle MIDI input. */
  npfd = snd_seq_poll_descriptors_count (seq_handle, POLLIN);
  pfd = (struct pollfd *) alloca (npfd * sizeof (struct pollfd));
  snd_seq_poll_descriptors (seq_handle, pfd, npfd, POLLIN);

  /* Loop round polling the ALSA MIDI file descriptions. */
  for (;;)
    {
      status = poll (pfd, npfd, -1);
      if (status <= 0)
	{
	  fprintf (stderr, "Poll failed: %s\n",
		   strerror (errno));
	  exit (1);
	}

      do
	{
	  snd_seq_event_input (seq_handle, &ev);

	  note = ev->data.note.note;

	  if (ev->type == SND_SEQ_EVENT_NOTEON)
	    {
	      for (i = 0; i < MAX_POLYPHONY; i++)
		{
		  if (Notes[i].note == -1)
		    {
		      Notes[i].note = note;
		      Notes[i].velocity = (double) ev->data.note.velocity / 128.0;
		      Notes[i].in_release = 0;
		      Notes[i].oscillator_offset = 0;
		      frequency = 440.0 * FrequencyMultipliers[note];
		      Notes[i].oscillator_step = (frequency * OSCILLATOR_TABLE_SIZE) / sample_rate;

		      DEBUG (printf ("NOTEON received: %i velocity: %g frequency: %g step: %.12g\n",
				     note, Notes[i].velocity, frequency, Notes[i].oscillator_step));
		      break;
		    }
		}
	    }
	  else if (ev->type == SND_SEQ_EVENT_NOTEOFF)
	    {
	      for (i = 0; i < MAX_POLYPHONY; i++)
		{
		  if (Notes[i].note == note)
		    {
		      Notes[i].in_release = 1;
		      Notes[i].release = 1.0;
		      Notes[i].release_step = 0.001;
		      break;
		    }
		}

	      DEBUG (printf ("NOTEOFF received\n"));
	    }
	  else if (ev->type == SND_SEQ_EVENT_PITCHBEND)
	    {
	      pitchbend = ev->data.control.value;
	      DEBUG (printf ("PITCHBEND received: %i\n", pitchbend));
	    }
	  else if (ev->type == SND_SEQ_EVENT_CHANPRESS)
	    {
	      aftertouch = ev->data.control.value;
	      DEBUG (printf ("CHANPRESS received: %i\n", aftertouch));
	    }
	  else if (ev->type == SND_SEQ_EVENT_CONTROLLER
		   && ev->data.control.param == MIDI_CTL_MSB_MODWHEEL)
	    {
	      modulation = ev->data.control.value;
	      DEBUG (printf ("CONTROLLER MODWHEEL received: %i\n", modulation));
	    }
	}
      while (snd_seq_event_input_pending (seq_handle, 0) > 0);
    }
}


/***************************************************************************
 * JACK OUTPUT
 ***************************************************************************/

static int jack_process (jack_nframes_t nframes, void *arg);
static void jack_shutdown (void *arg);

static void
init_jack (void)
{
  jack_status_t status;
  const char **ports;

  /* Open a client connection to the JACK server. */
  client = jack_client_open (ClientName, JackNullOption, &status, NULL);
  if (client == NULL)
    {
      fprintf (stderr, "jack_client_open() failed, status = 0x%2.0x\n",
	       status);
      if (status & JackServerFailed)
	fprintf (stderr, "Unable to connect to JACK server\n");
      exit (1);
    }
  if (status & JackServerStarted)
      fprintf (stderr, "JACK server started\n");
  if (status & JackNameNotUnique)
    {
      char *client_name = jack_get_client_name(client);
      fprintf (stderr, "unique name `%s' assigned\n", client_name);
    }

  jack_set_process_callback (client, jack_process, 0);
  jack_on_shutdown (client, jack_shutdown, 0);

  /* Display the current sample rate. */
  sample_rate = jack_get_sample_rate (client);
  DEBUG (printf ("JACK sample rate: %i\n", sample_rate));

  /* Create a single output port. */
  output_port = jack_port_register (client, "output", JACK_DEFAULT_AUDIO_TYPE,
				    JackPortIsOutput, 0);
  if (output_port == NULL)
    {
      fprintf(stderr, "no more JACK ports available\n");
      exit (1);
    }

  /* Activate our client.  Our jack_process() callback will be called now. */
  if (jack_activate (client))
    {
      fprintf (stderr, "cannot activate client");
      exit (1);
    }

  /* Connect to the first 2 physical output ports (hopefully for the left
     and right speakers). Must be done after jack_activate(). */
  ports = jack_get_ports (client, NULL, NULL,
			  JackPortIsPhysical | JackPortIsInput);
  if (ports == NULL)
    {
      fprintf(stderr, "no physical playback ports\n");
      exit (1);
    }

  if (jack_connect (client, jack_port_name (output_port), ports[0]))
    {
      fprintf (stderr, "cannot connect output ports\n");
    }
  if (ports[1]
      && jack_connect (client, jack_port_name (output_port), ports[1]))
    {
      fprintf (stderr, "cannot connect output ports\n");
    }
  free (ports);
}


/*
 * The process callback for this JACK application is called in a
 * special realtime thread once for each audio cycle.
 */
static int
jack_process (jack_nframes_t nframes, void *arg)
{
  jack_default_audio_sample_t *out, total_sample, sample, next_sample_offset;
  int frame, i, table_index;

  out = jack_port_get_buffer (output_port, nframes);

  for (frame = 0; frame < nframes; frame++)
    {
      total_sample = 0;

      for (i = 0; i < MAX_POLYPHONY; i++)
	{
	  if (Notes[i].note >= 0)
	    {
	      table_index = (int) Notes[i].oscillator_offset;

	      /* Use linear interpolation to calculate the sample value,
		 i.e. somewhere between two values in the oscillator table. */
	      sample = oscillator_table[table_index];
	      next_sample_offset = oscillator_table[table_index + 1] - sample;

	      sample += next_sample_offset * (Notes[i].oscillator_offset - table_index);

	      /* We use the note's velocity to simply scale the amplitude. */
	      sample *= Notes[i].velocity;

	      Notes[i].oscillator_offset += Notes[i].oscillator_step;
	      if (pitchbend)
		{
		  /* The constant here was calculated as the offset change
		     needed for A at 440Hz. We then convert that for other
		     frequencies just as we calculated the frequency before. */
		  Notes[i].oscillator_offset += 1.370322266e-7 * OSCILLATOR_TABLE_SIZE * FrequencyMultipliers[Notes[i].note] * pitchbend;
		}

	      if (Notes[i].oscillator_offset >= OSCILLATOR_TABLE_SIZE)
		Notes[i].oscillator_offset -= OSCILLATOR_TABLE_SIZE;

	      /* We have to fade out the note gradually to avoid a click in
		 the audio output. */
	      if (Notes[i].in_release)
		{
		  sample *= Notes[i].release;
		  Notes[i].release -= Notes[i].release_step;
		  if (Notes[i].release <= 0)
		    Notes[i].note = -1;
		}

	      total_sample += sample;
	    }
	}

      out[frame] = total_sample;
    }

  return 0;      
}

/*
 * JACK calls this shutdown_callback if the server ever shuts down or
 * decides to disconnect the client.
 */
static void
jack_shutdown (void *arg)
{
  exit (1);
}


/***************************************************************************
 * GENERAL
 ***************************************************************************/

/*
 * This creates a table of oscillator data that is used to calculate the
 * output of all notes. It should hold one complete period of the required
 * waveform. The waveform could be a simple sine wave, or could have some
 * harmonics added to it (resulting in a nicer sound).
 */
static void
create_oscillator_table (void)
{
  int sample;

  for (sample = 0; sample <= OSCILLATOR_TABLE_SIZE; sample++)
    {
      double phase = 2 * M_PI * sample / OSCILLATOR_TABLE_SIZE;

      /* Calculate the fundamental tone. */
      oscillator_table[sample] = sin (phase);

      /* Add a few harmonics for a nicer sound. */
      oscillator_table[sample] += 0.2 * sin (2 * phase);
      oscillator_table[sample] += 0.15 * sin (3 * phase);

      /* I think that 1.0 represents the maximum amplitude of JACK samples,
	 so we use a smaller value here to allow for polyphony. */
      oscillator_table[sample] *= 0.2;
    }
}


int
main (int argc, char *argv[])
{
  int i;

  /* Initialize the notes array - turn all notes off. */
  for (i = 0; i < MAX_POLYPHONY; i++)
    Notes[i].note = -1;

  /* Compute the multipliers needed for frequency calculations. */
  for (i = 0; i < 128; i++)
    FrequencyMultipliers[i] = pow (2.0, (i - 69) / 12.0);

  /* Create the oscillator table - a basic sine wave with a few harmonics. */
  create_oscillator_table ();

  /* Initialize the ALSA MIDI input. */
  init_alsa_midi ();

  /* Initialize the JACK output. */
  init_jack ();

  /* Sleep forever. Let the JACK and MIDI threads do the work.
     In a normal app we'd use the main thread for the GUI code. */
  for (;;)
    sleep (1000000);
}
_______________________________________________
Linux-audio-user mailing list
Linux-audio-user@xxxxxxxxxxxxxxxxxxxx
http://lists.linuxaudio.org/mailman/listinfo.cgi/linux-audio-user

[Index of Archives]     [Linux Sound]     [ALSA Users]     [Pulse Audio]     [ALSA Devel]     [Sox Users]     [Linux Media]     [Kernel]     [Photo Sharing]     [Gimp]     [Yosemite News]     [Linux Media]

  Powered by Linux