summaryrefslogtreecommitdiff
path: root/posts/making-your-own-music-player.org
blob: f96d4a555bded638c3d7233ab88bcfc1c877ea31 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
#+TITLE: Making Your Own Music Player: A Gentle Introduction to Audio Programming
#+DATE: <2017-07-15 Sat 00:00>
#+TAGS: tutorial programming audio c

To start off, I'd like to say that I know very little about audio programming
and digital audio in general. I've never formally studied signal processing,
and hell, I haven't even started high school physics yet. This post merely
documents what I've learned while trying to get sound working in my game,
because there aren't really any other learning resources about this out there.

In this tutorial, we'll write a basic music player for Ogg Vorbis in C using
two awesome libraries from Xiph.Org. The first, libao, will provide us with a
means to play sound through our speakers, or headphones. or whatever, and we'll
use libvorbisfile to decode the Ogg Vorbis files.

libao, like most other audio libraries, works by giving us a *PCM buffer* that
we write sound data to, and that gets played back. *PCM* stands for Pulse-Code
Modulation, and it's the basis of digital audio programming. You might have
heard people talk about how analog audio is so much better than digital, and I
think that learning the difference between the two helps to better understand
digial audio. Historically, sound was recorded in terms of analog signals,
which were easy to store as something like field strength on a magnetic medium.
However, digitizing audio requires the signal to be either sampled or
quantized. Both techniques are fairly similar, basically getting an
instantaneous representation of the signal some number of times a second. The
image below does a good job of explaining it, I think.

#+CAPTION: Diagram showing the difference between an analog sine wave and a quantization of that wave.
[[./making-your-own-music-player-analog-vs-digital.png]]

The rate at which the signal is sampled or quantized is the *frequency*. 44.1
kHz is typically the standard - meanining that 441,000 samples are taken every
second. The number of *channels* is essentially how many speakers the sound is
meant for. Stereo sound is the standard, so that is typically 2. And finally,
the audio can be 8, 16, 24, or 32 bit, representing the size of the integer
used to represent the sample.

Before we get into the code; you might need to configure libao if you're using
PulseAudio. Just open it up in your favorite editor and change it as shown
below.

#+BEGIN_SRC
$ sudo $EDITOR /etc/libao.conf
# Change from
default_driver=alsa
dev=default
# To
default_driver=pulse
# Make sure to remove the dev=default line
#+END_SRC

Now we're ready to get into the code. We'll include the headers for libao and
libvorbisfile, as well as some standard library headers and the size of the PCM
buffer, which I'll explain soon.

#+BEGIN_SRC c :hl_lines 0
#include <stdio.h>
#include <stdlib.h>

#include <ao/ao.h>
#include <vorbis/vorbisfile.h>

#define BUF_SIZE 256
#+END_SRC

The program is actually simple enough that we can do everything in main. For
clarity, I'll be using C99 variable declaration. Our program will take the file
to play as a command-line argument, so the first thing we need to do is check
argc.

#+BEGIN_SRC c :hl_lines 0
if (argc != 2) {
    fprintf(stderr, "Usage: %s [PATH]\n", argv[0]);
    return 1;
}
#+END_SRC

Next, we'll initialize libao. We'll also get the ID of the default sound driver
for when we open an audio device later.

#+BEGIN_SRC c :hl_lines 0
ao_initialize();
int default_driver = ao_default_driver_id();
#+END_SRC

Now, we'll specify the output format we want. This is what we were talking about
earlier, about frequency and channels and such. The only part of this that
wasn't mentioned was =format.byte_format=, which is just the byte order of the
PCM buffer. The Vorbis decoder will work with either big or little endian, but
we'll just stick with little endian for simplicity.

#+BEGIN_SRC c :hl_lines 0
ao_sample_format format = {0};
format.bits        = 16;
format.channels    = 2;
format.rate        = 44100;
format.byte_format = AO_FMT_LITTLE;
#+END_SRC

We'll use this format structure to open an audio device with the default sound
driver we figured out earlier.

#+BEGIN_SRC c :hl_lines 0
ao_device *device = ao_open_live(default_driver, &format, NULL);
if (device == NULL) {
    fprintf(stderr, "Error opening device\n");
    return 1;
}
#+END_SRC

And now, we'll get our PCM buffer. Some audio libraries have a routine to give
you a a buffer, but libao is alright with us using pretty much anything, so
we'll allocate it with =malloc(3)=. At this point, maybe you're wondering why we
use a buffer. While we /could/ read and play one byte at a time, that can be
very inefficient. It's better to read it into a buffer, and then play that
buffer. You don't want it to be too large, though, as there will be a longer
pause every time the buffer has to be read into. You also don't want it to be
too small. I find that 256 is good enough, but you can tweak that to your needs.
The size should be a power of two.

#+BEGIN_SRC c :hl_lines 0
char *buf = malloc(BUF_SIZE);
if (buf == NULL) {
    fprintf(stderr, "Error allocating PCM buffer.\n");
    return 1;
}
#+END_SRC

Now, we'll initialize libvorbisfile, which is done by opening the file we want
to play. This huge switch statement isn't necessary, it's just there to show all
the possible status codes of =ov_fopen=. Checking for a status code of 0 would
be just fine here.

#+BEGIN_SRC c :hl_lines 0
OggVorbis_File vf;
switch (ov_fopen(argv[1], &vf)) {
case OV_EREAD:
    fprintf(stderr, "Couldn't open %s.\n", argv[1]);
    return 1;

case OV_ENOTVORBIS:
    fprintf(stderr, "File contains no vorbis data.\n");
    return 1;

case OV_EVERSION:
    fprintf(stderr, "Vorbis version mismatch.\n");
    return 1;

case OV_EBADHEADER:
    fprintf(stderr, "File contains a bad bitstream header.\n");
    return 1;

case OV_EFAULT:
    fprintf(stderr, "Failure induced by heap/stack corruption.\n");
    return 1;
}
#+END_SRC

The real meat and potatoes of the program comes next. A loop that continually
reads data into our PCM buffer and plays it, until there's no more data to
play.

#+BEGIN_SRC c :hl_lines 0
int read, bitstream;
do {
    read = ov_read(&vf, buf, BUF_SIZE, 0, 2, 1, &bitstream);
    ao_play(device, buf, BUF_SIZE);
} while (read > 0);
#+END_SRC

The random integer constants in the call to =ov_read= might be a bit
intimidating, but it's really nothing to worry about. The first parameter is
whether or not the PCM buffer is big endian (which it is not, so we pass 0), the
second is the sample size, where 2 represents 16-bit, and the third is whether
or not the data is signed. You can read more about it in [[https://xiph.org/vorbis/doc/vorbisfile/ov_read.html][the documentation]].

Hopefully, things are starting to click around now. Any sound that comes out of
your speakers is just a bunch of numbers, and file formats like Ogg and MP3 are
just a means of compressing those numbers.

And finally, we'll finish up with some cleanup.

#+BEGIN_SRC c :hl_lines 0
free(buf);
ov_clear(&vf);
ao_close(device);
ao_shutdown();
return 0;
#+END_SRC

Compilation is pretty easy, too.

#+BEGIN_SRC
$ gcc -o oggplay oggplay.c -lvorbisfile -lao
#+END_SRC

Pretty painless, right? Without error handling, this is about 21 lines of code.

Go ahead, try it out! If you don't save your music as Ogg Vorbis, you can
convert songs with ffmpeg:

#+BEGIN_SRC
$ ffmpeg -i [file] -c:a libvorbis song.ogg
#+END_SRC

Here are some exercises if you want to play with this more:
- Get the frequency from the file being played, rather than hardcoding it at
  44.1 kHz. Check out the [[https://xiph.org/vorbis/doc/vorbisfile/reference.html][file Information section of the documentation]].
- Add a status line showing the current timestamp.
- Watch [[https://www.youtube.com/watch?v=pFgui9uGmr4][this talk from SIGINT13]].
- Play two sounds at once by adding their PCM values. Keep in mind that 8-bit
  and 16-bit integers overflow quite easily.
- Learn the library for another audio codec/container, like libopenmpt for
  classic tracker music.
- If you're feeling particularly up to a challenge, try rewriting the player
  using just libvorbis and libogg, rather than libvorbisfile.