/* * A very simple synthesizer using ALSA for MIDI input and JACK for output. * By Damon Chaplin * * 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 #include #include #include #include #include #include #include /* 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); }