Sure you can, it's just not trivial to get it sounding "nice".
I don't know how to do it in Linux, but if you can play a PCM buffer, all you have to do is fill it with whatever you want.
So supposing your buffer is set to play in monaural, signed 16-bit samples, at 44100 samples per second, creating a pure (sinusoidal) A4 sound (440 Hz) is as simple as
int16_t buffer[44100];
float frequency = 440.0f;
float sampling_ratio = 44100.0f;
float amplitude = 0.5f;
float t;
for (int i = 0; i < 44100; i++)
{
float theta = ((float)i / sampling_ratio) * PI;
buffer[i] = (int16_t)(sin(theta * frequency) * 32767.0f * amplitude);
}
However, this sound is probably very dull for your interests, so you have to do some more complicated things. In general, there are two types of sound synthesis: Additive and Subtractive. There are many others, but these two are probably the most simple ones. Today I'll just talk about additive synthesis.
For additive synthesis, you do the same thing as I did just up there, but instead of just using one frequency at one amplitude, you add several waves together. This is just like pressing several keys on a piano at the same time. So you modify your code to look something like this:
void add_sine_wave(int16_t* buffer, int buffer_length, float frequency, float sampling_ratio, float amplitude)
{
for (int i = 0; i < buffer_length; i++)
{
float theta = ((float)i / sampling_ratio) * M_PI;
// make sure to correct for overflows and underflows
buffer[i] += (int16_t)(sin(theta * frequency) * 32767.0f * amplitude);
}
}
and then use it like this:
int16_t buffer[44100];
memset(buffer, 0, sizeof(buffer));
// Create an A Major chord
add_sine_wave(buffer, 44100, 440.0f, 44100.0f, 0.5f);
add_sine_wave(buffer, 44100, 554.37f, 44100.0f, 0.5f);
add_sine_wave(buffer, 44100, 659.26f, 44100.0f, 0.5f);
By the way, I'm getting my frequencies from here (I'm using equal temperament, but there are lots of other tunings available).
Notice that so far I've only been using sine waves, but old synthesizers also support square, triangular and saw waves as well, each with its own interesting sound properties. Implementing these is pretty straightforward.
Other things you can do to increase the variety of sounds you can create are:
- Amplitude modulation: Changing the amplitude of the wave throughout the buffer
- Frequency modulation: Changing the frequency of the wave throughout the buffer
- Reverb: Repeating a sample by changing its shape and position in the buffer. Itself a very complex topic.
- Enveloping: Changing the amplitude of a sample to give it more life
The point here is that the techniques themselves are not very difficult, so you don't really need a library to abstract them for you. It's using them to create interesting sounds what is difficult.
One final note. When experimenting with sound like this, it may be really useful to save your data to WAV files, and then visualizing them in some software like Audacity. That way you can see what you're doing more clearly.