Everyone talks about using AI to write code faster. But in open source, speed isn’t necessarily the bottleneck. Trust is.
This is the story of how that trust got built while working on FAAC 2.0 — if you want the technical results of that work rather than the process behind it, that post covers the encoder itself.
I didn’t set out to become deeply involved in FAAC. I came to it because of Thingino, an open-source firmware project for Ingenic-based IP cameras. Thingino replaces the locked-down software that ships on inexpensive cameras with firmware that the community can actually inspect, modify, and improve.
Audio is one of the things Thingino needs to do on the camera itself. That means the encoder has to be small, fast, and easy to distribute. On embedded hardware, every kilobyte and every CPU cycle matters. Licensing matters too: a library with ambiguous licensing isn’t something you want buried in the firmware of an open-source project.
FAAC was an obvious place to look. It was lightweight, dependency-free, and had been around for decades. It also had a lot of room to improve.
My first contribution, back in February, was a small performance patch. It went well — the maintainer was happy, and I kept going, curious how much further the encoder could be pushed.
Getting Faster
FAAC is exactly the kind of project that can turn into a puzzle. An AAC encoder is full of complicated math, historical decisions, and low-level C code where a seemingly innocent optimization can have consequences that aren’t obvious from the diff. Could I make it faster? Could I remove some of the technical debt? Could I improve the encoder without introducing regressions? And, most importantly, how would I know when I’d actually made it better?
AI made that exploration dramatically faster. My workflow became something like the scientific method: profile the encoder, form a hypothesis, ask an LLM for possible approaches, implement one, measure it, reject it or keep it, and repeat. The AI could explore possibilities far faster than I could manually, but I was still the filter — I had to understand what it was proposing, compare it against the AAC specification and the existing architecture, and then prove that it actually worked. There were plenty of bad ideas along the way, which is normal; the point was to experiment quickly enough to find the good ones. That loop wasn’t really mine and one model’s — it would end up running across more team members than just the two of us, human and AI alike.
The problem was that I was getting really fast. My patches became increasingly sophisticated — some involved restructuring quantization loops and optimizing low-level pointer-heavy code, and one optimization combined two passes over an FFT bit-reversal table into one, producing a measurable performance improvement.
That PR was also where the FAAC maintainer, Fabian Greffrath, asked me directly which LLM I was using, and said he thought it was only fair to be upfront about it. It was a reasonable thing to ask. I answered honestly — Gemini 3, via Jules — and after talking it through, I understood the concern behind the question. These weren’t ordinary application-level changes. To review some of them properly, you needed to understand the AAC specification, psychoacoustics, DSP algorithms, and what the C compiler was actually doing with the resulting pointer-heavy code. I could spend hours building that context; a maintainer coming to a large pull request cold had a much harder job. The real risk was never whether the code compiled or passed a unit test — plenty of broken encoders do both — it was whether it preserved properties a compiler can’t check: psychoacoustic masking, bit-exact reproducibility, ABI stability across platforms.
I was producing changes faster than they could comfortably be reviewed. He wasn’t stonewalling anything, and he never once asked me to slow down or turned a PR away — reviewing a change like that properly just takes real time, and PRs were piling up faster than he had uninterrupted stretches of it to give them. That wasn’t a problem with him, or with me — it was a problem with how we were working together.
Building a Shared Definition of Success
So instead of trying to convince the maintainer to trust my code, I started thinking about how I could make it easier for both of us to know whether the code was good. That’s when I invested heavily in building faac-benchmark.
My first attempt at this went straight into the main repo as a pull request, full CI included. The maintainer pushed back — a test suite nearly as large as the library itself, pulling audio samples from an external source under a license he wasn’t sure about, wasn’t something he was comfortable taking on for a project he maintains mostly in his spare time. I got it — I wasn’t asking him to trust my “justice,” I was trying to hand him the math instead. So I pulled the whole suite out of the FAAC repo and into faac-benchmark as its own project, and committed to running and maintaining it myself. That was the actual trade: he never had to own the infrastructure, vet the datasets, or install anything — he just had to read what I posted.
Before that, I was testing things manually. But I realized I needed something more fundamental than a collection of tests — I needed a definition of success. What does “better” actually mean for an AAC encoder? It can’t just mean faster: a faster encoder that produces worse audio isn’t better. It can’t just mean a higher perceptual-quality score, either — stereo imaging can regress without showing up in a conventional MOS measurement, bitrate accuracy matters, decoder compatibility matters, and sometimes you need to know not just that quality changed, but where and why.
So the benchmark measures several dimensions of the encoder: perceptual quality, stereo coherence, encoding throughput, bitrate accuracy, and decode errors, with additional diagnostics for investigating regressions. The methodology and individual metrics are documented in the faac-benchmark documentation.
The important part wasn’t any individual metric — it was that we could agree on them. The maintainer didn’t have to take my word for it. I didn’t have to take the AI’s word for it. The AI didn’t have to understand the maintainer’s concerns. We had a shared measurement of success, and the benchmark became our common language.
On every PR I posted a screenshot of the results straight into the thread, plain-language summary and all, with a link to the full CI run behind it for anyone who wanted to dig into the per-scenario numbers — and over the following weeks he watched the pattern hold, and let that track record do the convincing. That was enough: a change could be submitted, the benchmark would run automatically on my fork, and the summary would land right on the PR as an image he could glance at, with the full run one click away. An AI agent could make a change, push it, see the summary, and iterate. I could do the same thing. The maintainer could look at the result without having to reproduce the entire experiment himself, or run any of it. We didn’t need to agree on every implementation detail — we needed to agree on what success looked like.
The summary I posted on PR #143 — regressions and wins at a glance, a per-scenario breakdown below it, and the full CI run one click further if you wanted every number.
What the Benchmark Found
And then the benchmark started telling me things I wasn’t expecting. While comparing FAAC against other encoders, I found a stereo-coherence problem that had actually existed in FAAC long before I started contributing. The conventional quality measurements didn’t fully explain it, but the stereo metric did.
So I started walking backward through FAAC’s history, running older versions through the benchmark until I could identify when the behavior changed. That investigation eventually led to issue #125, where I traced the problem to an earlier change involving truncated lookahead and window sequencing.
The benchmark wasn’t there to prove that my code was good — it was there to tell us what was true. Sometimes that meant showing that a new optimization was better. Sometimes it meant showing that an idea was worse. And sometimes it meant discovering a bug that had been hiding in the codebase for years. That made the benchmark something we could all trust, not because it was infallible, but because it applied the same rules to everyone. That trust showed up somewhere concrete: how fast PRs started moving through review.
Every merged pull request I sent upstream, February–August 2026 — click a bubble to open that PR. Each is a little pie of lines added (green) versus removed (red); the vertical axis is a log scale of hours from open to merge, and size is lines changed. Watch what happens after the dashed line: the pies get bigger without climbing back up the chart.
The effect on our collaboration was gradual, but obvious. Early on, a few-hundred-line change could sit for the better part of a week — a 414-line quantizer refactor in March took 95 hours to land. By August, PRs ten times that size were routine: a 3,139-line refactor of the encoding engine merged in a little over two hours, and the run of LGPL relicensing PRs in July — several topping 1,000 lines each — mostly cleared in half a day. The benchmark didn’t make big changes rare; it made them fast to trust. As it accumulated more coverage, the maintainer could look at the results and quickly understand whether a change was moving the project in the right direction.
From Contributor to Teammate
His reviews changed too. Instead of spending all of his time digging through implementation details, he could focus on the language, architecture, and design decisions where his experience was most valuable. On the HE-AAC pull request, he said as much directly — there was no way he could seriously review a diff that size line by line — but he still caught something that mattered more than any individual line: introducing HE-AAC was the right moment to break the public API and bump the library’s SONAME. That turned into a real design discussion. I opened two competing approaches, he picked the direction, and I rebuilt HE-AAC on top of it before it merged. He wasn’t reviewing my code anymore so much as he was reviewing where the project was headed.
Eventually, I noticed something even simpler: some of the newer pull requests would just get merged, sometimes with a response as simple as “Thanks, good job.” On one, after I’d started pulling Claude into the workflow, he noticed on his own and wrote back that he didn’t follow all of the changes but trusted me — and Claude — enough to wave it through anyway. That was when I realized the relationship had changed. I wasn’t trying to get my code past a maintainer anymore — we were working on the same project, and the tool I happened to be using that week wasn’t the point.
That became particularly important as the changes got larger. HE-AAC was a good example. I had been discussing it with the maintainer for a long time before implementing it, so it wasn’t a surprise PR appearing out of nowhere. I spent months working through the performance implications, trying different approaches, and eventually finding a way to make the additional functionality effectively neutral in overhead for existing users.
A Multi-Agent Workflow
Partway through, Jules got stuck on the HE-AAC implementation, so I started paying for Claude just to get past the wall — and it finished the job. Later, when Claude got stuck on a performance optimization, Jules turned out to be the stronger of the two at the underlying math and helped work it out. I never split the work between them up front — there was no plan where one owned architecture and the other owned math. I’d just notice one of them going in circles on the same idea for too long, and hand the same problem to the other one cold — there was no separate briefing to write, because the branch and the PR thread were the context. Whichever model picked it up next read the same diff, the same commits, and the same review comments the other one had already left behind. Neither tool was uniformly better; the AI side of this was never one fixed model, just whichever one could actually make progress on the problem in front of it. By then I’d stopped picking one over the other and started running both as a small team of agents, handing each problem to whichever one made headway on it. That communication with the maintainer mattered as much as the code.
The maintainer’s review kept catching real bugs, too, even on tiny PRs the benchmark had no chance of catching. PR #158 was nine lines — a fix for an ABI-compatibility bug in faac_encoder_open(). My agents’ first version hardcoded the struct’s baseline size behind a compile-time assertion, and I didn’t catch the problem either. The maintainer did, in one line: that size wasn’t guaranteed to be portable across platforms at all. He was right — the struct holds a pointer, so its layout shifts with word size, and the hardcoded version would have silently broken on other architectures. We fixed it to derive the baseline instead of assuming it. Nothing in the benchmark would have surfaced that; it runs on one architecture. That’s still what a maintainer who’s spent decades in C is for. It was a reminder that an LLM’s confidence about C is skin-deep — it can hand you code that compiles and passes every test on your machine without ever internalizing that struct layouts, alignment, and pointer widths shift under you the moment the target platform changes.
Even that wasn’t the end of it. Three weeks later, Robert Kausch — the maintainer of fre:ac, an open-source audio converter with no connection to Thingino, just another project integrating FAAC — was adapting his own code to the new API and spotted the flaw just by reading it: faac_params_init() could write past the end of a caller’s struct if that caller was built against an older header. He filed it as an issue directly against FAAC. Exactly the forward-compatibility case the whole redesign existed to protect. Neither Fabian nor I had caught it, and the benchmark never would — it tests FAAC in isolation, feeding it audio and measuring what comes out, with no way to simulate a separately compiled binary linking against an older copy of the header. That kind of bug only exists at the seam between two codebases, and the only way to find it is for someone to actually stand on that seam. I fixed the function and admitted the obvious: it was a new ABI, and I hadn’t designed it right the first time. Fabian’s response wasn’t blame, it was a proposal — bump the SONAME every time the struct grows, so callers rebuild instead of silently corrupting memory. We did both. However carefully a maintainer and a contributor review something together, someone actually depending on the library in production will still find what you missed. The benchmark and the reviews raise the floor; they don’t replace the rest of the ecosystem noticing. It was a clean division of labor in the end: AI gave me speed, the benchmark gave both of us automated confidence, and it still took two people who’d spent years elbow-deep in C — Fabian on one side of the API, Robert on the other — to catch the bugs that mattered most.
The AI was useful because it accelerated the experimentation, but it didn’t replace judgment — every suggestion still had to survive my own understanding of the AAC specification, the architecture, profiling, testing, and measurement. It wasn’t doing the work for me; it was helping me explore the problem faster.
FAAC had been maintained by essentially one active person for a very long time. There was a lot of technical debt, and the project didn’t have an obvious future beyond keeping the existing code working. I had originally come looking for an encoder for another open-source project. Somewhere along the way, the project itself became interesting.
Six months later, the result was FAAC 2.0: a 116 KB encoder running at 544.9× real time in the benchmark, with HE-AAC support, clean LGPL-2.1-or-later licensing, and the best stereo-fidelity score in the comparison I published. The technical details are a story of their own, so I wrote them up separately. Thingino, the project that sent me looking for a better encoder in the first place, pulled FAAC 2.0 in as soon as it shipped.
But those numbers aren’t actually the part I’m most proud of. The interesting part was how we got there: I started as someone submitting patches to an old project, and ended up working with its maintainer as a teammate. By the end, it wasn’t really a two-person effort at all — Jules, Claude, the maintainer, and I were all working off the same benchmark, and none of us had to trust the other three blindly to know whether a change was any good.
And now there’s energy around the project again. We’re not just fixing bugs — we’re talking about where the encoder could go next, and the maintainer has even suggested FAAC as a possible alternative AAC implementation for PipeWire, though that will need its own integration testing before anyone depends on it in production. I posted FAAC 2.0 to Hydrogenaudio, and the same pattern showed up again: a listener named maikmerten noticed the SBR energy in HE-AAC output arriving early relative to the core signal, filed it on the thread, and that became PR #159 — a real payload-timing bug in the SBR delay line that neither the maintainer, the benchmark, nor I had caught, because none of us were listening as closely as he was. That’s the part I didn’t expect when I started. I thought I was contributing code to solve a problem I had. Instead, I found a project that was fun to work on, a maintainer who was willing to work through the challenges with me, and a way of working where AI could make both of us more effective.
What This Means for AI-Assisted Open Source
AI has made code generation cheap. It has not made verification cheap. If your AI-assisted pull request saves you ten hours but makes review much harder for the person on the other side, you haven’t necessarily made open source faster — you’ve just moved the work.
The answer isn’t to stop using AI. It’s to take responsibility for the additional velocity it gives you: build the tests, build the benchmarks, measure your ideas, communicate before making enormous changes, and make it easy for another human to understand why your change should exist and how you know it works. Most importantly, remember that the person reviewing your pull request is probably volunteering their time too. We’re all here because we want to give something back — find a reason to volunteer, find the joy in making something better, and then find other people who want to make it better with you.
In the end, the goal isn’t to make one developer infinitely productive. It’s to make the whole project move faster. That’s the velocity of trust — because the maintainer isn’t a hurdle to jump over. They’re a teammate you haven’t fully met yet.
