Linux aplay command and ALSA

The Linux aplay command of ALSA is pretty cool as it can be used as a tool to play any kind of raw data as sound. This data can be piped into the standard input of the aplay command, or a file can be passed as a positional argument. Any kind of data can be used as sample data, but to really start using aplay by one way or another it would be best to fine ways to generate sample data.

1 - Pipe in random data by using cat

Any kind of data can be piped into the standard input of aplay, this includes random data generated by making use of cat, and dev random. When doing something like this I can use the -d option of aplay to limit the amount of time that this will play to 3 seconds.

1
2
$ cat /dev/random | aplay -d 3
Playing raw data 'stdin' : Unsigned 8 bit, Rate 8000 Hz, Mono

I can set -d option to 0, or just not give any -d option at all sense that is the default which will result in aplay just continuing to play random data as sound until I do a control+c or end the process by whatever means.

2 - Formats

By default the format that is used is U8 which is unsigned 8 bit audio. There are a number of other formats that often can be used, however the full list of formats will change from one sound card to another. For the most part thus far I have just been sticking to unsigned 8 bit mono audio

1
2
$ cat /dev/random | aplay -d 5 -f U8
$ cat /dev/random | aplay -d 5 -f S32_LE

3 - Sample Rates

The sample rate can also be adjusted by way of the -r option. The main page says that valid values are 2000 through 192000 Hertz, and the default as we can see is 8000 Hertz. The man page also says that if I give a value less than 300 that will be used as a kilohertz which would be that I should be able to give a value like 41 for for 41000 Hertz.

1
2
$ cat /dev/random | aplay -d 5 -f U8 -r 2000
$ cat /dev/random | aplay -d 5 -f U8 -r 88000

4 - Using cat and dd to generate 8K bytes of random data for just one second of noise

So far I have been piping in data to aplay by way of using the linux cat command with the use of /dev/random. However another major command I could pipe the random data into would be the dd command which has the -bs option that can be used to set, say 8000 bytes blocks, and I can do 60 blocks. This would then result in 60 seconds worth of audio data if it is still 8 Bit Sample Size, and 8000 hertz. However when doing so I will want to make sure that I set the iflag option to fullblock else the resulting file will end up being a bit light because of a partial read error.

1
2
3
4
5
6
$ cat /dev/random | dd bs=8000 count=60 iflag=fullblock > audiodata
60+0 records in
60+0 records out
480000 bytes (480 kB, 469 KiB) copied, 0.0826348 s, 5.8 MB/s
$ aplay -f U8 -r 8000 audiodata
Playing raw data 'audiodata' : Unsigned 8 bit, Rate 8000 Hz, Mono

5 - Start Using nodejs to create data to then use with aplay

Piping random data into aplay is a nice start, there is also looking into other ways to pipe in all kinds of other not so random data as well. However there is also getting into at least a little programming in order to start getting into the full, fine grain control when it comes to the idea of generating sample data. For this section then I will be doing just that by making use of nodejs and a little javaScript code.

5.1 - The waves.js file to use with nodejs

If I am to use nodejs to create some sample data I will want to use process.stdout.write over that of console.log so that I have control over having an End Of Line or not in the output. I then also make use of a buffer as a way to create an output mode that will be the binary data rather than text data that I will want to then pipe or redirect into aplay. I have found that thus far I mostly want to redirect into a file and then use aplay until I figure out how to address an issue with data that is lost when piping. More on that when we get into the actually usage in the bash prompt.

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
// POSITIONAL ARGUMENTS
const sample_rate = process.argv[2] || 8000;
const sample_secs = process.argv[3] || 1;
const wave_count = process.argv[4] || 80;
const amplitude = parseFloat( process.argv[5] || 0.45 );
const output_mode = process.argv[6] || 'binary';
// CREATE DATA
const COUNT_SAMPLES = sample_rate * sample_secs;
const SAMPLE_SIZE = 1;
let buff = Buffer.alloc(SAMPLE_SIZE);
let i_sample = 0;
while(i_sample < COUNT_SAMPLES){
// figure current sample value
const a_sample = i_sample / COUNT_SAMPLES;
const a_waves = a_sample * wave_count % 1;
const n = Math.round( 127.5 - Math.sin( Math.PI * 2 * a_waves ) * (128 * amplitude) );
// write to buffer, and then to standard output if 'binary' output
if(output_mode === 'binary'){
buff.write( n.toString(16), 0, 'hex');
process.stdout.write( buff );
}
// also have a plain 'dec' output for debugging as I write more of these
if(output_mode === 'dec'){
process.stdout.write( n.toString(10) + '\n' );
}
i_sample += 1;
}

5.2 - Pipe into aplay with default settings

One way to get started with this is to just pipe the data that this script generates with default settings into aplay with default settings.

1
$ node wave | aplay

This kind of thing will work okay for small use case examples that are about a second or so, but I loose data if I make the tone too long. Not a big deal with this script, but if I really get into this it will become a problem. I am sure that there is a way to address this, must have something to do with the chuck size with the nodejs script maybe. However in any case regardless if I address this or not there are other ways of doing this.

5.3 - Redirection

Thus far I have found that if I want to make a long noise I will want to create a large file, and then play that file with aplay. One way to do this would be to make use of redirection.

1
2
$ node wave 8000 10 800 0.5 binary > adata
$ aplay adata -f U8 -r 8000

6 - Live stream of sample data

The above script that I started out with will work just fine when it comes to using redirection rather than piping. However it would be nice to work out a script that will work okay when it comes to generating data in real time that will then in turn be consumed by aplay. The problem with this is that doing so is a little tricky. I managed to work out something that seems to hold up okay, but this is very much a kind of place holder script that I have in place for now until I lean more about how to work with streams.

The tricky part with this is making sure that the rate at which the script is generating sample data is on target with the rate at which aplay is consuming this data. Doing so in a way that will not result in underrun, or memory leakage is indeed the tricky part. If the script does not generate data fast enough that will result in an underrun condition in which aplay runs out of data to play resulting in an interuption, however if the rate is to high that will result in problems with eating up memory and drain event related issues.

6.a - A high low rate script

A script that I have worked out that seems to work okay so far addresses this problem by setting a fast, or slow rate that is used when calling the setTimeout method. Yes this is indeed crude, and over the log run I still run into the occasional problem where the stream needs to drain and I loose audio for a bit. However if I set the fast rate high enough it takes a long time for this to happen, while still avoiding the under run problem at least for the most part.

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
// write a single sample to the given buffer
const writeSample = (buff, a_sample = 0.5, wave_count = 1, amplitude = 0.3) => {
const a_waves = a_sample * wave_count % 1;
const n = Math.round( 127.5 - Math.sin( Math.PI * 2 * a_waves ) * (128 * amplitude) );
buff.write( n.toString(16), 0, 'hex');
return buff;
};
//-------- ----------
// LOOP
//-------- ----------
const buff = Buffer.alloc(1);
const frame_count = 60;
let i_sample = 0;
let i_frame = 0;
const count_sample = 8000;
let to_high = false;
let last_time = new Date();
const ms_fast = 999;
const ms_slow = 5000;
const loop = () => {
const t = setTimeout(loop, to_high ? ms_slow: ms_fast);
while( i_sample < count_sample){
// alphas
const a_sample = i_sample / count_sample;
const a_framecount = (i_frame / frame_count);
const a3 = a_framecount;
const a_wavecount = Math.sin( Math.PI * a3 );
// write sample to buffer
const wave_count = Math.floor(75 + 25 * a_wavecount);
writeSample(buff, a_sample, wave_count, 0.6);
to_high = !process.stdout.write(buff);
if(to_high){
break;
}
i_sample += 1;
}
i_sample %= count_sample;
i_frame += 1;
i_frame %= frame_count;
};
// drain event
process.stdout.on('drain', () => {
const now = new Date();
const time = (now - last_time) / 1000 / 60;
last_time = now;
process.stderr.write('\nNeeded to drain.\n');
process.stderr.write('Went ' + time.toFixed(2) + ' Minutes.\n\n');
});
process.stderr.write('\nScript started: ' + last_time + ' .\n\n');
loop();

6.1 - Starting the script

For the most part it would seem that this works okay thus far. When the script first starts I do get an underrun from aplay, and I also also sure that if I let this run long enough it will still need to drain. However until I get a better idea of how to manage this, I would have to go with some code like this.

1
$ node live_highlow | aplay -f U8 -r 8000

At least I do understand what the problem is, and it is just the nature of streams which is what I am dealing with. What would be nice is to have a way to monitor what the current state of the standard output stream is, and throttle the rate at which I am generating sample data up and down as needed long before the high water mark is reached.

7 - Converting JSON data into sample data for aplay

I am thinking that one way that I might want to get into creating sample data for aplay would involve writing scripts that generate JSON data in the form of arguments that will be used to call a funciton on a frame by frame basis. So then I would have code that generates this JSON data, and then I would also have code that will convert this JSON data to binary data that will then be used to create files that in turn can be played by way of the aplay command.

7.a - JSON data example

An exmaple of some json data that will create one second of sound would then look like this:

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
{
"bytes_per_frame" : 1400,
"frames": [
[1, 40, 10],
[2, 40, 10],
[3, 40, 10],
[4, 40, 10],
[5, 40, 10],
[6, 40, 10],
[7, 40, 10],
[8, 40, 10],
[9, 40, 10],
[10, 40, 10],
[11, 40, 10],
[12, 40, 10],
[13, 39, 9],
[14, 38, 9],
[15, 37, 9],
[16, 36, 8],
[17, 35, 8],
[18, 34, 8],
[19, 33, 7],
[20, 32, 7],
[21, 31, 7],
[22, 30, 6],
[23, 29, 6],
[24, 28, 6],
[25, 25, 5],
[26, 22, 5],
[27, 18, 5],
[28, 16, 2],
[29, 10, 3],
[30, 8, 2]
]
}

7.b - Script to convert JSON data to binary data

The script that I then use to convert this json data to binary data will then maybe look somehting like this:

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
const fs = require('fs');
const path = require('path');
//-------- ----------
// file uri, data object, buffer
//-------- ----------
const file_uri = path.resolve( process.argv[2] );
const data = require(file_uri);
const buff = Buffer.alloc(1);
//-------- ----------
// BYTES PER FRAME
//-------- ----------
// Sample rate / Frame Rate = Bytes Per Frame
// 2,010 / 30 = 67
// 2,100 / 30 = 70
// 8,100 / 30 = 270
// 16,200 / 30 = 540
// 42,000 / 30 = 1,400
const bytes_per_frame = data.bytes_per_frame || 270;
//-------- ----------
// FOR EACH FRAME in data.frames...
//-------- ----------
data.frames.forEach( ( frameData ) => {
let i_byte = 0;
const count_wave = frameData[0];
const byte_bias = frameData[1];
const count_bias = frameData[2];
while(i_byte < bytes_per_frame){
const a_bytes = i_byte / bytes_per_frame;
const a2 = (a_bytes * count_bias % 1);
const a3 = Math.sin( Math.PI * a2 )
const a = i_byte / count_wave % 1;
const n = Math.round( ( 128 + Math.cos( Math.PI * a ) * ( byte_bias * a3 ) ) );
buff.write( n.toString(16), 0, 'hex');
process.stdout.write( buff );
i_byte += 1;
}
});

Using the JSON to binart script

I would then call the script and redirect the output into a file. I can then play the file with aplay.

1
2
$ node sin2-frames.js fd1.json > adata
$ aplay -f U8 -r 42000 adata

Conclusion

The aplay command is then pretty cool as it allows for me to make noise with any kind of binary data, and there might be forms of data that will result in interesting sounds when it comes to just going ahead and using it that way. However if you do know a thing or two about a programming language or two there is doing all kinds of things with everything and anything that there is to work with in that programming environment to generate binary data to then pipe into aplay.