frankl's stereo pages [We support real
    stereo]

[Overview] [Player] [RACE] [LoCo] [DRC]

Utilities for playing and streaming music on Linux

Contents of this page

Aim

These utilities should help to achieve "ultimate" (ok, that's impossible, but we can try to get closer) sound quality when one or several computers running Linux are used for playback of music files. I mainly have in mind playback via a "high end" stereo system, but I have used some of these programs also for improving playback on a notebook.

The aim is not to provide yet another graphical user interface to your music collection. In fact, the programs I provide are meant to be used via the command line or within custom scripts. (Of course, they could be called by fancier user interfaces.)

While developing these programs I was surprised to learn how much influence playback software can have on sound quality. The software is just as important as the computer hardware and the other components of your stereo setup. If the manufacturer of your DAC or soundcard claims it were immune against "jitter", do not believe it!

License

The software here is free software, it comes with full source code, you can change and redistribute it, and it can be downloaded and used without costs. The precise conditions are described in the GNU GPL. Of course, in particular: using the software distributed here is completely on your own risk. As with any new component in your stereo system, keep the volume very low during first tests.

Download

This package is developed on Linux systems and is mainly meant to be run on Linux. (But some code may compile and can be used on other systems as well.)

The software is available via this git-repository. For bug reports, problem reports and suggestions use its issue tracker.

Alternatively, you can download the files of version 0.8 of the package here: frankl_stereo-0.8.tar.gz.

Installation instructions are in the file INSTALL inside the code directory. This includes a description of software packages needed for the installation.

When all needed packages are installed on your Linux system, the installation should just look like:

    git clone https://github.com/frankl-audio/frankl_stereo.git
    cd frankl_stereo
    make REFRESH=X8664    # on x86 machines, or
    make REFRESH=AA64     # on arch64 machines (e.g. Raspberry Pi 4)
The compiled programs are then in the subdirectory ./bin/. Copy or link them somewhere in your standard PATH (e.g., /usr/local/bin/).

Description of the problem

Music stored on a computer is encoded in a sequence of bits, 0's and 1's. To play it these bits need to be sent to a DAC or soundcard which translates them to an analogue wave form which is then amplified and made audible through speakers or headphones. So, do we "just" need to get the correct 0's and 1's quickly enough to the DAC to get an optimal playback on a given system? Unfortunately, it is not that easy. The point is that in real hardware the 0's and 1's are also stored and transfered by analogue (e.g., electrical or magnetic) signals. (With "analogue" I mean here something that can have many states, e.g. a voltage, a charge, a magnetisation, which physically represent one of the two abstract states 0 or 1.) Let me illustrate my vague understanding of this with some (made up) pictures. Say, we want to transfer some bits by an electrical signal, each bit is encoded by a voltage which is for a certain amount of time above or below some threshold. The ideal signal I have in mind would look like this to encode bits 1101000101:

nice square wave

But a real electrical signal cannot have infinitely steep slopes, maybe it looks more like that:

nice not so square wave

Well, probably the power supply for this signal is far from perfect, maybe this is more realistic:

distorted not so square wave

And for some components in a computer even the following heavily distorted looking version is still good enough to correctly(!) recover or interpret the bits from the signal:

heavily distorted wave

There are also other signal depending distortions like reflections in a cable.

Similar remarks apply to the storage of bits on a hard disk by magnetisation of particles along a track on the disk. Or to bits stored on an SSD or USB-stick in charged cells. Or to bits stored in computer memory (RAM) whose cells we can imagine as small caps which are charged above or below some threshold to store a 1 or 0, and the charge is steadily leaking and needs to be refreshed regularly.

The strength of digital storage and processing is that distortions as imagined above do not matter; as long as all components can correctly recognize a 0 or 1, absolutely no information is lost.

A DAC chip will also work "correctly" within a certain specification if the bits in the incoming signal can be correctly recognized. But in practice the audible quality of the outgoing signal does depend on the wave form of the incoming signal, and not just on the bits it encodes! One aspect is the precise time when the DAC chip notices that the incoming signal is switching from above a threshold to below or vice versa. With the perfect square wave such switches occur only at precise equidistant points of time. With a wave form as in the second example above the switches are also recognized clearly, but there is a variation of the time differences between the switches. These timing errors are called jitter and higher jitter leads to worse sound quality of the analogue audio output signal. With the more distorted wave forms also the time when a switch is recognized is less clear. Another aspect is that distortions of the digital signal, say from a not so clean power supply, cannot be filtered out completely by the DAC chip, there will also be some corresponding distortion on the output wave form.

Summary: Bits in a computer and DAC are stored and transferred by various forms of analogue signals. The task of this project is to experiment if and how we can get a cleaner signal to the DAC by player software.

The basic ideas

Starting from the vague and largely simplified model of data storage and transport in computer and DAC as explained above, I experimented with two main ideas for the programs distributed here.

The first idea is to process and transfer audio data in chunks of constant size, each within an interval of time of constant length. For this we use the high resolution timers on modern computer hardware which are made available in Linux by clock_nanosleep and related functions. For example, the program playhrt which sends audio data to a DAC or soundcard has a main loop where in each round a chunk of data is read, then the program sleeps until a certain point of (wall clock) time and after wakeup sends the data to the hardware driver. So, the hardware driver receives (almost) constant sized chunks of input data in very regular time intervals.

The second idea is to refresh the representation of audio data in RAM or on a hard disk by (logically unnecessary) reading and rewriting of data. In the model explained above the hope is to increase the amplitude of the signals (and so also the steepness of slopes and so to reduce the related jitter).

Testing the programs

I'm aware that the model I have explained above is very much simplified. In reality, the processing of data is very complicated. The processed bits are transformed between various physical representations and encodings, data are buffered in various places on hard- and software level (e.g., several levels of caches on a harddisk, network card or in RAM, software buffers in application programs and in hardware drivers, and so on). Having this in mind it seems completely hopeless to produce audible differences between programs which all perform a "bit-perfect" music playback. Fortunately, I started my experiments despite such doubts. (Ok, and encouraged by knowing that others are trying similar things for the Windows or Mac platform, e.g. jplay, MQn, Amarra.)

I have evaluated and compared various versions of my (and other) programs only by listening tests. The starting reference were the standard Linux programs audacious and aplay.

It would be nice to do some sort of technical measurements, which underpin and explain audible differences. But that is probably extremely challenging and certainly outside my technical possibilities.

I was repeatedly surprised how significant audible differences can be in this context. And this not only in my "high-end" stereo setup, but even on a notebook computer or monitor with its lower quality speakers behind the display.

Short description of the programs

Here is an overview of the programs and example scripts provided by this project.

After installation you can run each program with option --help to read its full documentation (or click on the function names in the overview below). The first two, playhrt and bufhrt are the heart of the project.

playhrt
reads an audio stream from the network, stdin or a file and plays it on a sound device
bufhrt
reads data from stdin, a file, shared memory or the network and writes them to stdout, to a network port or to a file
cat64
reads almost any music file (more precisely, those know to the libsndfile library) and outputs its samples in 64-bit floating point format
resample_soxr
resamples input from stdin or music file and outputs result in 64-bit floating point format (a very high quality resampler and proper 64-bit processing; note that sox only uses 32-bit integer encoding internally); this includes the functionality of cat64 and volrace
writeloop and catloop
reads data from stdin and writes it cyclically to some files or to shared memory, and vice versa (e.g., to buffer data in a ramdisk or RAM)
volrace
implements a simple form of the RACE algorithm and can be used for volume control
cptoshm and shmcat
can be used to buffer data in RAM via shared memory
highrestest
a small utility to check if your Linux setup supports high resolution timers (the names playhrt and bufhrt indicate that a high res timer is used)
play_XXXX
various example scripts which call programs mentioned above and which are explained below
improvefile
is an example script which shows how bufhrt can be used to "improve" music files stored on a hard disk

Getting started

Prerequisites

We assume that you are on a Linux system with the programs provided here installed. Furthermore, you should be able to play sounds via a soundcard or external sound device with some installed player program (e.g., audacious). You should also have a few other programs: sox, aplay, flac, and maybe brutefir (if you want to play with convolving), or some version of nc (if you want a two-computer playback setup).

On Debian and related distributions (Debian, Ubuntu, Mint, Raspian, ...) you may use (as root user, or with a sudo in front):

apt-get install sox libsox-fmt-mp3 brutefir flac netcat-openbsd

On other systems there is certainly a similar command.

Furthermore, we assume that your Linux kernel supports the high resolution timers of modern hardware. This is usually the case, you can test it by calling the small utility progam highrestest which is contained in this package. It will print some information.

General comments on example scripts

We provide example scripts which demonstrate the use of the programs in this package. Since a few parameters are system dependent it may be necessary to adjust them before a script works on your system. We comment on a few parameters which can be adjusted close to the top of each script.

CARD
this is the ALSA device name of the sound card you want to use. The default hw:0.0 is often alright. If not, use aplay -l to find out the correct card and device number. We suggest to use the hardware devices directly (and not some virtual or plughw-devices). This is used for the --device option of playhrt.
EXTRABYTESPLAY, EXTRABYTESBUF
these are used for the --extra-bytes-per-second option of playhrt and bufhrt, respectively. This is to synchronize the speed of different computers or of a computer and a DAC or soundcard. It is explained below how to find good values.
VERBOSE
this is used as argument for several programs. In the beginning it can be useful if the programs are a bit chatty for finding good parameters. Once a script runs nicely you may set this to the empty string.

Introduce more such variables yourself if you want to play with other parameters. For some sound cards the default value of --hw-buffer may not be valid.

All scripts discussed here can also be found in the subdirectory scripts of the software distribution.

Warming up with audacious and aplay

Say, you want to play a music file mymusic.flac which is in 44.1/16 format as music from CDs (other formats instead of flac should also work, e.g., mymusic.wav or even mymusic.mp3). First check if your sound device is working with (you may use any other player you know instead of audacious):

audacious mymusic.flac

Now use aplay. But don't let aplay decode the flac file. Do the decoding with sox such that it sends the raw audio data to its standard output (denoted by -). Send this output through a pipe | to the standard input of aplay. Then aplay must be told the format of the input (raw input with samples in S16_LE (16 bit CD) format, two channels and a sample rate of 44100). Adjust the -D parameter if your sound card is not card 0 and device 0, find the correct parameter of your card with aplay -l.

sox mymusic.flac -t raw - | aplay -t raw -f S16_LE -c 2 -r 44100 -D hw:0,0

Stop the playback by pressing Ctrl-C.

Use the manpage of sox and the manpage of aplay if you want to learn more about these programs.

The sox program can also decode many other formats. For flac-files you can alternatively use flac which has --skip and --until options to decode only a part of a file. Here is an example (a backslash before a newline character means that the next line should be appended, this allows to format very long input lines nicely).

flac --totally-silent --force-raw-format --sign=signed --endian=little \
     -d -c mymusic.flac | aplay -t raw -f S16_LE -c 2 -r 44100 -D hw:0,0
Problem with pulseaudio
If your are on a Linux system running the soundserver pulseaudio there can be problem with shown commands if they want to access a hardware device (because the sound server is blocking it). In that case use the player command with pasuspender prepended, e.g., instead of playhrt use pasuspender -- playhrt.
Simple player script play_simple

Now use the program playhrt from this project instead of aplay. Here is the first simple shell script.

#!/bin/bash

#########################################################################
##  frankl (C) 2015              play_simple
##
##  See README or http://frankl.luebecknet.de/stereoutils/player.html
##  for explanations.
#########################################################################

CARD="hw:0,0"
EXTRABYTESPLAY=0
VERBOSE="--verbose --verbose"

sox  "$1" -t raw -r 44100 -c 2 -e signed -b 16 - | playhrt \
      --stdin \
      --device=${CARD} \
      --sample-rate=44100 \
      --sample-format=S16_LE \
      --loops-per-second=1000 \
      --mmap \
      --non-blocking-write \
      ${VERBOSE} \
      --extra-bytes-per-second=${EXTRABYTESPLAY} \
      --hw-buffer=16384

Here $1 is substituted by the first argument of the call of this script, that is mymusic.flac if we call:

play_simple mymusic.flac

Compared to the earlier example we now tell sox that it should generate output with two channels, a sample rate of 44100 and with 16 bit samples. This will also work if the file we want to play is not in 44.1/16 format, sox will resample to 44.1/16 if needed.

The script calls playhrt with the recommended options --mmap and --non-blocking-write. Here playhrt computes from the options how many bytes are needed by the sound device per second (44100 x 2 x 2 = 176400 bytes), and it writes exactly this amount per second to the memory of the driver of your sound device. Chunks of (almost) the same size are written 1000 times per second with quite precisely 1/1000 of a second time distance. In practice it often happens that your computer and your sound device have a slightly different idea about how long a second is. In the next section we describe how to synchronize the speed of playhrt to your sound device.

Customizing the --extra-bytes-per-second parameter for playhrt

If playhrt is called with --mmap and with double --verbose --verbose argument it will print lines like

playhrt: average available buffer: 8495 (1031175 sec 282972195 nsec)

In the beginning the available buffer should be a bit bigger than half of the --hw-buffer-size. When it gets too large or small playhrt will try to adjust its speed once and print a hint about a sensible value for the --extra-bytes-per-second parameter. If at the end of playback the available buffer size is much different compared to the beginning then playhrt will also print a hint for a better value of the --extra-bytes-per-second parameter. This will work more precisely with longer files, and it may be necessary to improve that parameter in several iterations.

Player script with writeloop and catloop

Sometimes it can be useful to decouple two programs (or chains of programs) via a buffer. For example, the first program reads its data from a not so reliable network or the data are processed in a long chain of different programs, but the second program needs to read data regularly.

Here is a variant of the simple player script where the decoded data are first written into a buffer with writeloop and then they are read with catloop and sent to playhrt.

For the buffer there are two possibilities. You can either use two or more regular files, usually on a ramdisk; or you can use blocks of shared memory which has the advantage that it avoids file system overhead and additional buffering by the operating system. We use the second variant, the convention is to specify names for these shared memory blocks starting with a slash /. The first command is sent into background with &, such that the second is also started.

#!/bin/bash

#########################################################################
##  frankl (C) 2015              play_writecatloop
##
##  See README or http://frankl.luebecknet.de/stereoutils/player.html
##  for explanations.
#########################################################################

CARD="hw:0,0"
EXTRABYTESPLAY=0
VERBOSE="--verbose --verbose"

trap "trap - SIGTERM && kill -- -$$ " SIGINT SIGTERM EXIT

rm -f /dev/shm/bl{1,2,3} /dev/shm/sem.bl{1,2,3}*
sox  "$1" -t raw -r 44100 -c 2 -e signed -b 16 - | \
      writeloop --block-size=1024 --file-size=4096 --shared /bl1 /bl2 /bl3 &

# maybe the start of the background command takes a little time
sleep 0.1

catloop --block-size=1024 --shared /bl1 /bl2 /bl3 | \
      playhrt \
           --stdin \
           --device=${CARD} \
           --sample-rate=44100 \
           --sample-format=S16_LE \
           --loops-per-second=1000 \
           --mmap \
           --non-blocking-write \
           ${VERBOSE} \
           --extra-bytes-per-second=${EXTRABYTESPLAY} \
           --hw-buffer=16384

The shared memory blocks can be seen as virtual files in the directory /dev/shm. There are also files sem.bl1* which are used for synchronization between the two programs.

The script can be interrupted with Ctrl-C. Without the trap line the background processes would remain running.

Player script with volrace

In this example we introduce volume control during playback and the RACE algorithm. The parameters are given in the file /tmp/VOLRACE which is created when it does not exist. Once the music is running you can edit that file (or change or overwrite it by some script) to change the parameters on the fly (and volrace will fade to the new values). If the file contains only one parameter then RACE is disabled. The first parameter for the volume can be negative in which case the phase of the music will be inverted.

The default parameters may be interesting on a notebook with builtin speakers, it creates a 3D-image as in Ambiophonics. In a standard stereo setup try 0.6 13 0.22, for headphones use only the first volume parameter or set the third parameter to 0.0.

This script also demonstrates how sox can convert the music to 64-bit floating point samples, this is needed by volrace, and would also be useful for other filters (e.g., a convolver for room correction). And vice versa it shows how to cut the samples to 16-bit integers with the application of dithering and noise shaping.

Alternatively, instead of sox one could also use cat64 or resample_soxr from this package.

#!/bin/bash 

#########################################################################
##  frankl (C) 2015              play_volrace
##
##  See README or http://frankl.luebecknet.de/stereoutils/player.html
##  for explanations.
#########################################################################

CARD="hw:0,0"
EXTRABYTESPLAY=0
VERBOSE="--verbose --verbose"

trap "trap - SIGTERM && kill -- -$$ " SIGINT SIGTERM EXIT

rm -f /dev/shm/bl{1,2,3} /dev/shm/sem.bl{1,2,3}*

if ! test -e /tmp/VOLRACE ; then
  echo "0.6 4 0.75" > /tmp/VOLRACE
fi

sox  "$1" -t raw -r 44100 -c 2 -e float -b 64 - | \
      volrace --param-file=/tmp/VOLRACE | \
      sox -t raw -r 44100 -c 2 -e float -b 64 - \
          -t raw -e signed -b 16 - dither -f high-shibata | \
      writeloop --block-size=1024 --file-size=4096 --shared /bl1 /bl2 /bl3 &

# maybe the start of the background command takes a little time
sleep 0.1

catloop --block-size=1024 --shared /bl1 /bl2 /bl3 | \
      playhrt \
           --stdin \
           --device=${CARD} \
           --sample-rate=44100 \
           --sample-format=S16_LE \
           --loops-per-second=1000 \
           --mmap \
           --non-blocking-write \
           ${VERBOSE} \
           --extra-bytes-per-second=${EXTRABYTESPLAY} \
           --hw-buffer=16384
Player script with convolver, upsampling and other tricks

In this example we apply further processing to the music signal before playing it. First we resample any input to a specified sample rate (192000) and then we use a convolver, which may be fed with a room correction filter and/or a LoCo filter, or which may implement a crossover in the digital domain (in the latter case you would need a multi channel sound card for the output).

Resampling should be done with high quality, in our example script we use resample_soxr from this package (this is true 64-bit processing, while for example sox would only use 32-bit integers internally). Adjust the value of the variable SAMPLERATE if you want other output than 192000.

The convolving is done with brutefir which is called with a configuration file. You can find an example of such a configuration file on the LoCo page. Specify your config file in the variable BRUTEFIRCONFIG in the script. In the example we assume that brutefir produces output samples in S32_LE format.

Of course, if you just want to try the upsampling and no convolving then delete or comment the line with brutefir in the script.

A further change compared to the previous example scripts is that we first copy the music file into RAM and play it from there. Of course this could be done with simple copying into a ramdisk, like (assuming /tmp is a ramdisk):

FILENAME=`basename $1`
cp $1 /tmp/${FILENAME}

and then playing /tmp/${FILENAME} instead of $1.

But in our example we use the programs cptoshm and shmcat provided by this package (and assuming that we want to play a flac-file). This avoids some operating system and file system overhead.

#!/bin/bash 

#########################################################################
##  frankl (C) 2015              play_upsample_convolve
##
##  See README or http://frank_l.bitbucket.org/stereoutils/player.html
##  for explanations.
#########################################################################

CARD="hw:0,0"
EXTRABYTESPLAY=0
SAMPLERATE=192000
# configure brutefir with output format needed by your DAC
# (here we assume S32_LE)
BRUTEFIRCONFIG=
VERBOSE="--verbose --verbose"

# find input sample rate - only needed when resample_soxr is not used below
ORIGRATE=`sox --i "$1" | grep "Sample Rate" | cut -d: -f2 | sed -e "s/ //g"`

trap "trap - SIGTERM && kill -- -$$ " SIGINT SIGTERM EXIT

cptoshm --file="$1" --shmname=/play.flac

rm -f /dev/shm/bl{1,2,3} /dev/shm/sem.bl{1,2,3}* 

if ! test -e /tmp/VOLRACE ; then
  echo "0.6 4 0.75" > /tmp/VOLRACE
fi

# old version, without resample_soxr
#shmcat --shmname=/play.flac | \
#      sox -t flac - -t raw -r ${ORIGRATE} -c 2 -e float -b 64 - | \
#      sox -t raw -r ${ORIGRATE} -c 2 -e float -b 64 - \
#          -t raw - vol 0.8 rate -v -I ${SAMPLERATE} | \
#      volrace --param-file=/tmp/VOLRACE | \
#      brutefir ${BRUTEFIRCONFIG} -quiet | \
#      writeloop --block-size=1024 --file-size=4096 --shared /bl1 /bl2 /bl3 &

resample_soxr --shmname=/play.flac --param-file=/cache/VOLRACE \
           --outrate=${SAMPLERATE}  --fading-length=100000  | \
      brutefir ${BRUTEFIRCONFIG} -quiet | \
      writeloop --block-size=1024 --file-size=4096 --shared /bl1 /bl2 /bl3 &

# allow some startup time for background command
sleep 0.5

# if this is started from an environment that uses the pulseaudio
# sound server, you can substitute 'playhrt' below by 
# 'pasuspender -- playhrt'
catloop --block-size=1024 --shared /bl1 /bl2 /bl3 | \
      playhrt \
           --stdin \
           --device=${CARD} \
           --sample-rate=${SAMPLERATE} \
           --sample-format=S32_LE \
           --loops-per-second=1000 \
           --mmap \
           --non-blocking-write \
           ${VERBOSE} \
           --extra-bytes-per-second=${EXTRABYTESPLAY} \
           --hw-buffer=8192

Player script for two computer setup (with insecure listen_loop script)

In my experience it can be good for the sound quality to do the processing of music data on one sufficiently powerful machine (call this the convolver machine) and to do the actual playing on another dedicated machine (call this the audio machine). The data are transfered between the machines via the (preferably Ethernet) network.

We demonstrate how to achieve this using the program bufhrt from this package. It buffers incoming data and can send them over the network to another machine. Similarly to what playhrt does, the data are sent in chunks of (almost) constant size within short time intervals of constant length. If parameters are chosen carefully it can be avoided that the network buffers on the two machines are filled up, thereby avoiding that writing or reading data is blocking processes.

The first variant works by starting once a simple script listen_loop on the audio machine that runs a loop which listens on the network for some input and then executes this input. (Warning: do not use this on a computer which can be connected from untrusted machines because everyone with network connection to the audio machine can start arbitrary commands on it.) The communication between the two computers is done with the UNIX utility nc (also called netcat). The script looks as follows:

#!/bin/bash 

#########################################################################
##  frankl (C) 2015              listen_loop
##
##  See README or http://frankl.luebecknet.de/stereoutils/player.html
##  for explanations.
#########################################################################

while true; do
  command=`nc -l -p 5501` 
  eval "$command"
done 

Login to the audio machine and start this script in the background. You may do this in a tmux or screen session, then you can logout and login again and still check the output of the remote commands.

listen_loop &

A corresponding player script on the convolver machine sends an appropriate call of playhrt to be executed on the audio machine. We modify play_upsample_convolve from above to get an example. Instead of playhrt we now call bufhrt on the convolver machine.
The other example scripts from above can be adjusted similarly to play on a remote machine.

HINT: there is an error in this script: change shmcat /play.flac 32768 to shmcat --shmname=/play.flac

#!/bin/bash 

#########################################################################
##  frankl (C) 2015              play_remote
##
##  See README or http://frankl.luebecknet.de/stereoutils/player.html
##  for explanations.
#########################################################################

CARD="hw:0,0"
VERBOSE="--verbose --verbose"
EXTRABYTESBUF=10
EXTRABYTESPLAY=0
AUDIOCOMPUTER="<put here name of your audio machine>"
SAMPLERATE=192000
SAMPLEFORMAT=S32_LE
# this is 192000 * 4 * 2 (32 bit samples, 2 channels)
BYTESPERSECOND=1536000
BRUTEFIRCONFIG="<path to your brutefir config>"
HOST=`hostname`
PORT=5570

# find input sample rate
ORIGRATE=`sox --i "$1" | grep "Sample Rate" | cut -d: -f2 | sed -e "s/ //g"`

trap "trap - SIGTERM && kill -- -$$ " SIGINT SIGTERM EXIT

cptoshm --file="$1" --shmname=/play.flac

rm -f /dev/shm/bl{1,2,3} /dev/shm/sem.bl{1,2,3}* 

if ! test -e /tmp/VOLRACE ; then
  echo "0.6 4 0.75" > /tmp/VOLRACE
fi

shmcat /play.flac 32768 |
      sox  -t flac -  -t raw -r ${ORIGRATE} -c 2 -e float -b 64 - | \
      sox -t raw -r ${ORIGRATE} -c 2 -e float -b 64 - \
          -t raw - vol 0.8 rate -v -I ${SAMPLERATE} | \
      volrace --param-file=/tmp/VOLRACE | \
      brutefir ${BRUTEFIRCONFIG} -quiet | \
      writeloop --block-size=1536 --file-size=9216 --shared /bl1 /bl2 /bl3 &

sleep 0.5

# tell audio computer what to do, short sleep as startup time for sender
REMOTE="sleep 0.3;\
        playhrt ${VERBOSE} --host=${HOST} --port=${PORT} \
                     --sample-rate=${SAMPLERATE} \
                     --sample-format=${SAMPLEFORMAT} \
                     --loops-per-second=1000 \
                     --device=${CARD} --hw-buffer=7680 \
                     --extra-bytes-per-second=${EXTRABYTESPLAY} \
                     --non-blocking-write --mmap "

echo "${REMOTE}" |  nc -q 0 ${AUDIOCOMPUTER} 5501 
# instead of
#            catloop --block-size=1024 --shared /bl1 /bl2 /bl3 | \
# we use the --shared option of bufhrt.
bufhrt --bytes-per-second=${BYTESPERSECOND} \
       --loops-per-second=2000 --port=${PORT} \
       --extra-bytes-per-second=${EXTRABYTESBUF} \
       ${VERBOSE} \
       --shared /bl1 /bl2 /bl3 

A few more comments on the play_remote script. If bufhrt is called with --shared option then the size of the shared memory chunks should be divisible by the number of bytes to be sent per loop (in our example we send 1536 bytes per loop and the size of the shared memory blocks is 6*1536 = 9216 bytes).

Player script for two computer setup (using ssh)

In my setup the audio computer is only connected to the convolver computer, and the listen_loop approach above is no security risk. For a more secure access to the audio computer one can use the secure shell ssh. There are several possibilities to configure ssh such that no password is needed for login. Consult the web if you do not know how to do this.

The remote player script becomes a little more tricky in this case, because remote background processes started via ssh are by default terminated when the shell terminates. To avoid this the remote programs must be called with nohup and standard input and output must be redirected. Here is the play_remote_ssh script as alternative to play_remote above.

HINT: there is an error in this script: change shmcat /play.flac 32768 to shmcat --shmname=/play.flac

#!/bin/bash 

#########################################################################
##  frankl (C) 2015              play_remote_ssh
##
##  See README or http://frankl.luebecknet.de/stereoutils/player.html
##  for explanations.
#########################################################################

CARD="hw:0,0"
EXTRABYTESBUF=200
EXTRABYTESPLAY=0
VERBOSE="--verbose --verbose"
AUDIOCOMPUTER=<name of your audio computer>
SAMPLERATE=192000
SAMPLEFORMAT=S32_LE
# this is 192000 * 4 * 2 (32 bit samples, 2 channels)
BYTESPERSECOND=1536000
BRUTEFIRCONFIG=<path to your brutefir configuration>
HOST=`hostname`
PORT=5570
PLAYHRTLOGFILE=/tmp/playhrt.log

# find input sample rate
ORIGRATE=`sox --i "$1" | grep "Sample Rate" | cut -d: -f2 | sed -e "s/ //g"`

trap "trap - SIGTERM && kill -- -$$ " SIGINT SIGTERM EXIT

cptoshm --file="$1" --shmname=/play.flac

rm -f /dev/shm/bl{1,2,3} /dev/shm/sem.bl{1,2,3}* 

if ! test -e /tmp/VOLRACE ; then
  echo "0.6 4 0.75" > /tmp/VOLRACE
fi

shmcat /play.flac 32768 |
      sox  -t flac -  -t raw -r ${ORIGRATE} -c 2 -e float -b 64 - | \
      sox -t raw -r ${ORIGRATE} -c 2 -e float -b 64 - \
          -t raw - vol 0.8 rate -v -I ${SAMPLERATE} | \
      volrace --param-file=/tmp/VOLRACE | \
      brutefir ${BRUTEFIRCONFIG} -quiet | \
      writeloop --block-size=1536 --file-size=9216 --shared /bl1 /bl2 /bl3 &

sleep 0.5

# tell audio computer what to do 
REMOTE="nohup sleep 0.8 >/dev/null 2>/dev/null </dev/null ;\
        nohup playhrt ${VERBOSE} --host=${HOST} --port=${PORT} \
                     --sample-rate=${SAMPLERATE} \
                     --sample-format=${SAMPLEFORMAT} \
                     --loops-per-second=1000 \
                     --device=${CARD} --hw-buffer=7680 \
                     --extra-bytes-per-second=${EXTRABYTESPLAY} \
                     --non-blocking-write --mmap \
                     >/dev/null 2>${PLAYHRTLOGFILE} </dev/null "

#echo "${REMOTE}" |  nc -q 0 ${AUDIOCOMPUTER} 5501 

REMOTE='sh -c "( (  '${REMOTE}' ) & )"'

echo "${REMOTE}"

ssh ${AUDIOCOMPUTER} "${REMOTE}"

bufhrt --bytes-per-second=${BYTESPERSECOND} \
       --loops-per-second=2000 --port-to-write=${PORT} \
       --extra-bytes-per-second=${EXTRABYTESBUF} \
       ${VERBOSE} \
       --shared /bl1 /bl2 /bl3 
Customizing the --extra-bytes-per-second parameter for bufhrt

The parameters of the bufhrt utility should be chosen such that the program writes out exactly the amount of data needed by the receiving side. In the remote player examples above we can use the UNIX command netstat -tpn on the convolver and on the audio machine repeatedly while the music is playing (on the convolver machine we look at the entry of the Send-Q column for bufhrt and on the audio machine at the entry of the Recv-Q column for playhrt. If these values remain roughly constant while the music is playing all settings are fine.

If not, here is an example what to do: Say, the value on the audio computer decreases (respectively increases) by about 3600 bytes during 5 minutes. Then we can add (respectively subtract) 12 to the current value of the --extra-bytes-per-second parameter of bufhrt (5 minutes are 300 seconds and 300 * 12 = 3600).

Further finetuning is possible by playing around with explicitly setting input and output buffer sizes, see parameters --in-net-buffer-size of playhrt and --out-net-buffer-size of bufhrt. It could be good to use small buffers, but keep in mind that an uninterrupted data flow is more important.

Network buffer with bufhrt

When I used a convolver machine further away from the stereo system I have made good experience with using a small computer like a Raspberry Pi as buffer between the convolver and audio machine. The buffering was done by running bufhrt with input and output from and to the network.

(In my current setup everything is close to the stereo system and only one short high quality network cable is used between convolver and audio machine.)

Improving music files on a hard disk with improvefile

In some experiments I noticed that two bit-identical copies of the same music file on the same SSD, harddisk or other medium can sound differently(!). To my surprise this can still be true when the files are heavily processed (upsampled, convolved, ...) for playback. My guess is that on a hard disk there is a huge bandwidth of possibilities how the same bit pattern can be represented by magnetized particles on a harddisk (similarly for voltages in memory cells of SSD, USB-stick or memory card) and that different representations influence the timing and amplitude of the output signal of the harddisk when the data are read. (I do not think that fragmentation of the file system or error correction plays a role here). There is a longer thread on this topic in this forum (in German).

After a lot of testing I can improve the sound quality of music files on the SSD in my stereo system quite significantly with the help of bufhrt. This is done as in the following script improvefile.

#!/bin/bash

#########################################################################
##  frankl (C) 2015
##  
##  USAGE:
##    improvefile orig new
##  This script generates a (bit-identical) copy of a file 'orig' to
##  the file 'new'.
##  
#########################################################################

if test -e "$2" ; then
  echo "the file $2 already exists, please delete it first"
  exit
fi

bufhrt --file="$1" --outfile="$2" --buffer-size=50000000 \
       --loops-per-second=2000 --bytes-per-second=6144000 --interval 

The script needs to be used only once (not before every playback). The effect on sound quality will probably decrease when the file is copied (with some system program) to another disk or sent over the network to another computer.

The script uses the --interval option of bufhrt. This causes that the program in intervals only reads data until the (large) buffer is filled and then writes out the buffer (without reading more input) in well timed blocks which are refreshed in RAM before writing. If you have a lot of RAM you can increase the --buffer-size and you can experiment with the --bytes-per-second parameter (which should be somewhat lower than the maximal write speed of your disk).

I use bufhrt with similar arguments to store pre-convolved files on my SSD (that is, in a script like play_upsample_convolve above I substitute the final call of playhrt with a call to bufhrt with output file on the harddisk).

Some general tuning tips

In general it can be advantageous to run as few processes as possible on your convolver and/or audio computer. Stop all jobs and processes which are not needed. If possible, use your audio computer remotely with no graphical user interface running or monitor connected.

It can be useful to give playhrt and bufhrt a high process priority with chrt. (If you are not allowed to do this on your system, start your player program with sudo or change the setuid bit of chrt once with sudo chmod 4755 `which chrt`.) For example, you could change in one of the scripts above the call playhrt ... to chrt -f 70 playhrt ....

If you want to copy many music files into RAM you either need a huge ramdisk or a large space for shared memory blocks. To create a large ramdisk add (as root or with sudo) a line like

none /ramdisk tmpfs defaults,size=800M,mode=1777,exec,nosuid 0 0

in your file /etc/fstab. If not yet done create the directory with sudo mkdir /ramdisk and mount with sudo mount /ramdisk (the latter is automatic on reboot). Adjust the size to some value which is somewhat smaller than the amount of physical RAM.

To enlarge the space that can be used for shared memory, use a command like

sysctl -w "kernel.shmmax=1782579200"

or some even higher value if you have more than 2GB of RAM. It depends on your system setup how to set this automatically after reboot. (On my Debian systems one can put a line kernel.shmmax=1782579200 in a file /etc/sysctl.d/shmmax.conf to achieve this.

On a multicore processor it is possible to prescribe the core on which a process is running, using the taskset utility. For example, on a 4-core processor (cores 0,1,2,3) one could put brutefir and bufhrt or playhrt on different cores (with taskset -a -c 1 brutefir ... or chrt -r 99 taskset -c 3 bufhrt ... and so on.

The speed of modern processors (or the separate cores) is usually adjusted dynamically by the hardware depending on the load. On some systems this dynamic change of the speed can be avoided and the speed can be prescribed by the user. For example, on my convolver machine I delegate the speed setting to userspace by sudo cpufreq-set -g userspace. In the beginning of my player scripts I push up the speed with cpufreq-set -f 1.2GHz and at the end I set it back to 0.25GHz (use cpufreq-info to find out the possible values).

A very useful trick is to make some CPU cores unavailable to system programs with the isolcpus parameter to the Linux kernel. For example, on my convolver computer, a Raspberry Pi 4 with four CPU cores numbered 0, 1, 2, 3, I use the kernel parameter isolcpus=1,2,3. Then the system only uses core 0 for all processes, except when they are started explicitly with taskset on one of the cores 1, 2 or 3. This way I can reserve cores 1, 2, 3 exclusively for the audio programs.