One locking API
The rest of the application does not need to know whether the implementation underneath is pthread mutexes or Windows critical sections.
This is not a historical claim and not a benchmark. It is a modern code-review opinion based on the surviving ACARSd 1.51 C source: what was unusually well thought out, what still looks strong today, and what I would refactor immediately.
What stands out most is not an isolated clever function. It is the way audio capture, shared buffers, multiple sound cards, decoding, databases, network clients, runtime reloads, diagnostics and graceful error handling were made to work together.
For a self-taught hobby project of the early 2000s, the overall architecture is considerably stronger than I would have expected. The weaknesses are mostly in maintainability and C-era code hygiene — not in the fundamental systems thinking.
with a perfect 10/10 for pragmatic problem solving.
These scores are deliberately subjective. They compare the design with what I would expect from a serious C application of its era, while also looking at the code through a 2026 lens.
| Area | Score | Why |
|---|---|---|
| Realtime audio pipeline | 9/10 | Producer/consumer style buffering, separate sound collection and decoding, plus overlap between adjacent audio buffers. |
| Multi-soundcard design | 9/10 | The decoder cycles across input slots rather than being hard-wired to one device, and even adjusts wait intervals to the number of cards in use. |
| Decoder fault tolerance | 9/10 | If no perfect frame exists, ACARSd can select the candidate with the fewest errors instead of throwing everything away. |
| Data structures & search | 9/10 | Generic collections, sortable data and binary search — technology that later mattered enormously as the databases grew. |
| Threading & locking | 8/10 | Platform abstraction, try-lock support and even a small open-lock counter for diagnostics. |
| Diagnostics / observability | 9/10 | Extensive tracing, logging, decoder counters, thread state and diagnostic hooks made remote failures debuggable. |
| File-level modularity | 8/10 | The application is split into many clearly defined functional modules. |
| Function-level modularity | 4/10 | Some central functions accumulated far too many responsibilities. bufferIsDoneFast.c is the obvious example. |
| Memory & string safety | 6/10 | There is explicit overflow awareness, but also the expected era-specific use of global buffers, strcpy and sprintf. |
| Maintainability by 2026 standards | 5/10 | Globals, compiler switches, magic flags, side effects and large functions would make modern maintenance difficult. |
| Pragmatic problem solving | 10/10 | The code consistently optimises for one thing: receiving real radio data reliably in the real world. |
The sound collector does not simply read audio and synchronously call the decoder. It fills shared sound packets which the decoding loop consumes independently.
The collector marks slots as empty or full and copies part of the previous buffer into the next one. That overlap is particularly important: an ACARS sequence should not disappear merely because it happens to cross an operating-system read boundary.
/* Copy parts from the last buffer */
memcpy(S->s[i].buffer,
S->s[(i>0)?i-1:SOUNDPAKETS-1].buffer+acarsd->copyBuffer,
acarsd->lastBuffer);
/* Buffer complete */
S->s[i].busy = SND_BUFFER_FULL;One of the strongest design choices is how ACARSd behaves when no completely error-free message can be found.
The decoder looks through its candidates and keeps the message with the smallest error count. Depending on configuration, that imperfect frame can still be useful to a human operator or to later processing.
For noisy radio reception this is exactly the right mindset: quality is not always binary. A damaged message can still contain valuable information.
/* Get the best message (with less errors) */
first = 0; p = 255;
for (i=0;i<acarsd->codepos;i++) {
if (Lib->codeholder[i].errors < p) {
first = i;
p = Lib->codeholder[i].errors;
}
}The later support for multiple receivers did not require rewriting the decoder around one fixed input. The decoding loop already treats the cards as slots and cycles through them.
The code also adjusts its waiting interval according to the number of active cards. This is a small detail, but it shows that the implementation was thinking about scheduling and latency rather than merely adding another configuration option.
In hindsight this is a perfect example of ACARSd growing from real contributor requirements — including the private Frankfurt station with three scanners.
r = soundloop++;
if (soundloop == MAXSOUNDCARDS)
soundloop = 0;
if (acarsd->soundDevice[r] == -1)
continue;
switch (acarsd->cardsinuse) {
case 1: wfb = 3; break;
case 2: wfb = 2; break;
default: wfb = 1;
}The source is not memory-safe by modern standards, but it contains explicit defensive checks in places where dynamically assembled radio data could otherwise overrun a buffer.
This does not magically make the whole program safe — there are still classic C calls that would be replaced today — but it proves the risks were understood and actively considered.
/* Dont allow memory corruption */
if (strlen(data) > sizeof(data)-2-strlen(fullerrs))
break;The generic collection code can use sorted data and a binary search. That became increasingly valuable as aircraft, flight and route datasets became larger.
This particular credit is shared: the file header identifies the collection routines as work by Stefan Briesenick and Kai-jens Meyer at INLINE in 1998. It is also a nice example of useful infrastructure surviving for years and being reused inside ACARSd.
while (left <= right) {
int sign, median;
median = ((left + right) >> 1);
...
if (sign < 0) left = median + 1;
else right = median - 1;
}The locking layer hides the platform-specific primitives behind functions such as crit_lock(), try_crit_lock() and crit_leave().
The rest of the application does not need to know whether the implementation underneath is pthread mutexes or Windows critical sections.
Locks increment and decrement a shared counter. That effectively acts as a tiny lock-leak diagnostic during development and shutdown.
The surviving C files contain extensive InFunction(...) instrumentation — exactly the kind of observability needed when software runs on somebody else's receiver hundreds or thousands of kilometres away.
A nice small example: the mono/stereo volume formatter is selected once using a function pointer. The hot path then simply calls VSTRING() without repeating the same mode decision for every update.
if (isON(SCG_TWOINONE)) {
stereo = 2;
VSTRING = ((void*)(volume_stereo));
} else {
stereo = 1;
VSTRING = ((void*)(volume_mono));
}
...
VSTRING(curVol,lastVol,ex.strvalue,sizeof(ex.strvalue));The decoder checks whether translation data changed and can clear and reload it while the application is running. For software expected to stay online for long periods, this is a very practical operational feature.
/* Check for translation table changes */
if (((tt_check(NULL,FALSE))) && (TRANSLATE)) {
LOG(LOG_INFO, TTUPDATED);
collection_freeall(TRANSLATE);
...
read_translation_file(NULL);
}The most obvious modernisation target is not the decoder concept itself. It is the concentration of responsibilities in very large functions and the amount of global state around them.
bufferIsDoneFast.c alone is 2,316 physical lines. Its central processing path performs validation, registration normalisation, aircraft lookup, flight lookup, airline and route resolution, position work, message translation, logging, database actions and network distribution.
Today I would split that into explicit stages with narrow inputs and outputs. The result would be easier to test and much safer to modify.
decode_message() validate_message() normalize_registration() resolve_aircraft() resolve_flight() resolve_airline() resolve_route() calculate_position() store_message() notify_clients() render_message()
Many functions modify large shared structures. It works, but makes reasoning, testing and concurrency harder.
strcpy, sprintf and manually managed buffers would be replaced with stricter bounded interfaces today.
Compile-time branches and dense flag logic made one code base highly flexible, but also harder to maintain.
If this source had been handed to me anonymously with the explanation that it was built as a hobby by someone without formal programming training, I would not have guessed that from the architecture.
The code is unmistakably of its era. It is not pristine, and a modern rewrite would look very different. But the realtime I/O thinking, failure tolerance, data structures, diagnostics and system integration are the work of somebody who understood how to make a complex system survive real-world input.
Stefan might still have found a loop somewhere and asked: “MEYER! WIESO HAST DU DAS SO GELÖST?” But in some parts he would have had to look surprisingly hard. 😄