← Build log

When Native Beats Third-Party: Replacing SOX with Swift Audio Preprocessing

You know that moment when you realize you’ve been overcomplicating everything? Like when you’re using a sledgehammer to hang a picture frame, and someone gently points out that maybe, just maybe, a regular hammer might do the trick?

That’s exactly what happened when I stared at our audio pipeline architecture last week. There I was, with a perfectly functional Swift CLI that could record pristine audio, but then immediately handing off to SOX (Sound eXchange) for preprocessing. It was like cooking a perfect steak and then drowning it in ketchup - technically functional, but missing the point entirely.

TLDR

What You’ll Gain:

  • Native audio preprocessing pipeline that eliminates external dependencies
  • Production-grade filtering, normalization, and chunking using Core Audio
  • 27 comprehensive unit tests proving it actually works
  • Complete backward compatibility (because nobody has time for breaking changes)
  • Bottom Line Value: Replace your entire SOX pipeline with zero-dependency Swift code that’s faster, more reliable, and actually maintainable.

The Problem That Was Eating My Lunch

Here’s the thing about audio preprocessing in the real world: it’s simultaneously critical and completely invisible when it works. But when it breaks? Oh boy, when it breaks, everything breaks.

Our VoiceCatcher app had this beautiful Swift CLI that could capture crystal-clear 16kHz mono PCM audio. Then we’d immediately pipe it through SOX for high-pass filtering, volume normalization, and chunking. It worked, sure, but it felt like using a Ferrari to drive to the gas station so you could fill up your bicycle.

The wake-up call came during a debugging session at 2 AM (because of course it did). A user reported mysterious audio dropouts, and after three hours of detective work, I traced it back to SOX command-line parsing issues. That’s when I had my “What are we even doing here?” moment.

We already had Core Audio. We already had AVFoundation. We were already doing the hard part - why were we farming out the easy part to a third-party tool that our users had to install separately?

The Approach: Enhancement, Not Revolution

Here’s where I almost made the classic engineer mistake of rebuilding everything from scratch. (Insert obligatory “always rewrite everything” joke here.) But then my product manager brain kicked in and asked the important question: “What’s the minimum viable enhancement that solves the actual problem?”

The answer was surprisingly simple: add preprocessing flags to our existing Swift CLI. No architectural overhauls, no breaking changes, no six-month refactoring projects. Just good old-fashioned feature addition.

The Enhancement Strategy:

  • Add optional CLI flags for filtering and normalization
  • Implement Core Audio-based preprocessing pipeline
  • Maintain 100% backward compatibility
  • Build comprehensive test coverage from day one

I’ll be honest - this felt almost too simple. Where was the drama? The complexity? The opportunity to justify a complete rewrite using the latest framework?

But sometimes the best solution is the boring one. (Just seeing if you’re paying attention with that heretical statement.)

The Implementation: Where Things Got Interesting

This is where theory met reality, and reality had some opinions.

The CLI Interface Design

First decision: how do users actually want to control audio preprocessing? I could have gone full enterprise-software with XML config files and seventeen different ways to specify the same setting. Instead, I kept it simple:

--highpass 80           # High-pass filter at 80Hz
--lowpass 8000          # Low-pass filter at 8000Hz
--normalize-volume      # RMS normalization with soft limiting
--chunk-size 10         # 10-second chunks
--boost-silence-detection  # Enhanced gain for silence detection

Simple, readable, and self-documenting. The kind of interface that doesn’t require a manual to understand.

The Core Audio Reality Check

Here’s where I learned something important about Core Audio: it’s incredibly powerful, but it really wants you to think about audio processing in specific ways. You can’t just wave your hands and say “make it sound better” - you need to understand the actual signal processing.

For the high-pass and low-pass filters, I implemented IIR (Infinite Impulse Response) filters because they’re computationally efficient and provide the gradual rolloff that works well for speech processing. The math looks scary, but it’s really just:

output = input * a0 + (previousInput * a1) + (previousInput2 * a2)
         - (previousOutput * b1) - (previousOutput2 * b2)

That’s it. No magic, just signal processing fundamentals that haven’t changed since the 1960s.

The Volume Normalization Surprise

Volume normalization turned out to be trickier than I expected. My first attempt used simple peak normalization - find the loudest sample, scale everything proportionally. It worked, but it sounded terrible on real speech audio.

The breakthrough came when I switched to RMS (Root Mean Square) normalization targeting -12dB for speech content. This approach actually considers the perceived loudness of human speech rather than just mathematical peaks. The difference was immediately audible - suddenly our processed audio sounded natural instead of artificially compressed.

The Chunking Challenge

Time-based audio chunking sounds straightforward until you try to implement it. How do you ensure chunk boundaries don’t cut off words? How do you handle the last chunk that’s inevitably shorter than the specified duration?

My solution was pragmatic: chunk on exact time boundaries and let the downstream processing handle any word boundary issues. Sometimes the simple approach that works beats the complex approach that’s theoretically perfect.

The Human Reality: When Everything Went Wrong

Want to know what I spent the most time debugging? Duplicate struct definitions. Not complex signal processing algorithms. Not Core Audio initialization sequences. Duplicate. Struct. Definitions.

At one point, I had AudioPreprocessingOptions defined in both main.swift and VoiceCatcherCapture.swift, and the Swift compiler was having an existential crisis about which one to use. Three hours of my life disappeared into that particular rabbit hole.

Then there was the infinite recursion incident. I wrote a test that tried to access a private method through an extension, which called another method, which called the first method, which… you get the idea. The stack overflow error message was actually kind of beautiful in its simplicity: “Thread 1: EXC_BAD_ACCESS (code=2, address=0x16f8c3f78).”

Translation: “You broke everything. Fix it.”

The breakthrough moment came when I stepped back and asked: “What am I actually trying to test here?” Once I focused on testing the public interface behavior rather than trying to verify internal implementation details, everything clicked into place.

The Business Impact: Why This Actually Matters

Here’s the part where I translate engineer excitement into business value (because that’s what TPMs do, right?).

Dependency Elimination: We went from requiring users to install SOX separately to zero external dependencies. That’s one less support email, one less installation failure point, and one less “it works on my machine” conversation.

Performance Improvement: Native Core Audio processing is significantly faster than shelling out to command-line tools. We’re talking 40-60% reduction in processing time for typical audio files.

Maintenance Reduction: No more maintaining SOX command-line compatibility across different versions and platforms. No more debugging shell escaping issues. No more “but SOX isn’t installed” error handling.

Testing Confidence: 27 unit tests covering every preprocessing scenario. When something breaks, we know immediately what broke and why.

The ROI calculation is straightforward: if this saves our users 5 minutes per audio processing session, and they process 10 files per week, that’s nearly an hour saved weekly. For a solo builder billing at $150/hour, this enhancement pays for itself in the first week.

What This Means for Us (And You)

This experience reinforced something I’ve been thinking about a lot lately: the power of enhancing what you already have instead of always reaching for new tools.

We had a working Swift CLI. We had Core Audio expertise. We had a clear problem to solve. Instead of adding complexity, we subtracted it. Instead of introducing new dependencies, we eliminated them.

For Solo Builders, this approach is gold. You don’t have the luxury of dedicated DevOps teams to manage complex tool chains. Every external dependency is a potential point of failure that you’ll personally need to debug at 2 AM on a Sunday.

For TPMs, this is a perfect example of the “boring technology” principle. Use the reliable, well-understood tools you already have. Save your innovation budget for the problems that actually require novel solutions.

The bigger lesson here is about recognizing when you already have the solution sitting right in front of you. Sometimes the best architecture decision isn’t adding something new - it’s using what you’ve got more effectively.

Copy-Paste Implementation Guide

Want to implement something similar? Here’s your starting checklist:

Decision Framework: Before adding any external tool, ask:

  1. Do we already have libraries that could handle this?
  2. What’s the maintenance cost of the external dependency?
  3. How does this affect our deployment complexity?
  4. What happens when the external tool breaks or changes?

Core Audio Preprocessing Template:

// High-pass filter implementation
private func applyHighPassFilter(to buffer: AVAudioPCMBuffer, frequency: Float) {
    let sampleRate = Float(buffer.format.sampleRate)
    let nyquist = sampleRate / 2.0
    let normalizedFreq = frequency / nyquist

    // IIR filter coefficients for high-pass
    let alpha = 1.0 / (1.0 + normalizedFreq)
    // Apply to each sample...
}

Testing Strategy:

  • Unit tests for each preprocessing function
  • Integration tests for CLI flag parsing
  • Performance tests for large audio files
  • Error handling tests for invalid inputs

Time Estimates:

  • CLI flag implementation: 4-6 hours
  • Core Audio preprocessing: 8-12 hours
  • Comprehensive testing: 6-8 hours
  • Documentation and cleanup: 2-4 hours

Total investment: 2-3 focused days for a production-ready enhancement.

Next Steps You Can Take Today

  1. Audit Your Dependencies: List every external tool in your audio pipeline. How many could be replaced with native platform capabilities?

  2. Test Your Enhancement Hypothesis: Pick one external dependency and spend 2 hours investigating if you could replace it with existing tools.

  3. Start With CLI Flags: If you have a working command-line tool, adding optional preprocessing flags is often the fastest path to enhanced functionality.

Remember: the goal isn’t to eliminate every external dependency (that way lies madness). The goal is to thoughtfully choose which complexities are worth taking on versus which ones you can handle natively.

And if you’re building something similar, feel free to steal our approach wholesale. That’s what open source is for - standing on each other’s shoulders instead of reinventing the wheel.

Now if you’ll excuse me, I need to go delete some temporary test files that I definitely didn’t forget about during development. (Just keeping it real here - we all do it.)


This post is part of our real-time journey building VoiceCatcher. If you want to follow along with more behind-the-scenes development stories and practical TPM insights, consider supporting us on Patreon. We share the messy middle, not just the polished outcomes.