Real-time audio on a mobile phone is a scheduling problem with human consequences. The operating system calls your callback every N samples — at 48 kHz with a 512-sample buffer, that is every 10.67 milliseconds — and expects a full buffer of PCM floats back before the deadline. Miss it once and the user hears a click or pop. Miss it repeatedly and Estua's sleep sound becomes unusable at 3 AM, which becomes a one-star review, which becomes a real problem for a studio with five apps and no venture capital cushion. Sonarish's microphone capture path lives under the same constraints: the audio HAL delivers samples on a high-priority thread, and anything you do in that callback determines whether the noise meter reads truth or garbage.
What the audio thread is and why it is sacred
On iOS, Estua uses AVAudioEngine with a manual source node callback. On Android, it uses AudioTrack with ENCODING_PCM_FLOAT in low-latency mode when the OEM supports it. Both platforms invoke your render function on a real-time priority thread that bypasses the normal UI run loop. This thread must never wait on contended locks, never allocate memory from the heap, never perform I/O, and never call into code paths that might do any of those things transitively. The reason is scheduling unpredictability: a malloc on a fragmented heap might take 200 microseconds on a good day and 5 milliseconds on a bad day. Your budget is 10.67 milliseconds total for all channels, all DSP, all mixing — and the OS needs part of that for its own overhead. One slow malloc and you glitch.
Rules we enforce in code review
These are not guidelines — they are review blockers. No heap allocation in the callback: no Swift Array append, no Kotlin list growth, no std::vector push_back, no NSString formatting. Preallocate all buffers at session start on the UI thread and pass pointers into the callback context. No locks that can block — use lock-free single-producer single-consumer ring buffers for passing data between the audio thread and the UI thread. No logging, file writes, or network calls, even guarded by debug flags, because debug builds ship to ktuyen's overnight soak tests and a forgotten print statement that allocates a string has caused more clicks than any DSP bug. On iOS specifically, avoid Objective-C message sends on the hot path; keep the callback in C or Swift struct code that the compiler can inline. Biquad filter coefficients, LFO rates, and scene parameters are computed when the user changes a setting on the UI thread; the callback reads only const structs that were written once and never mutated concurrently.
How Estua generates sound under these constraints
Estua's synthesis chain — described in detail in our non-repeating audio post — generates pink noise via Paul Kellet's economical recursive filter, a handful of multiplies and adds per sample with no FFT per block. Slow amplitude modulation uses one-pole low-pass filters on the absolute value of the signal envelope, running at the same sample rate with precomputed coefficients. Multiple uncorrelated LFOs modulate filter cutoff and gain at 0.02–0.15 Hz so the output never repeats on human-perceptible timescales. All of this fits comfortably within the 10.67 ms budget on modern phones at roughly 3–8% CPU, leaving headroom for Bluetooth audio output and background app suspension scares. The product context for why synthesis instead of loops is in our sleep and waves post.
Buffer size adaptation and Sonarish capture
Not every Android device wants 512 samples. Some OEMs request 192 or 256 for their DSP path. Estua adapts the requested buffer size at session start but keeps internal DSP state continuous across size changes so a resize does not produce a discontinuity click. Sonarish's capture side feeds a ring buffer consumed by the FFT engine on a background queue — the FFT itself never runs on the audio thread, only the PCM copy into the ring buffer does. That separation is why Sonarish can run a 4096-point transform while the meter updates at 30 Hz without glitches; see our FFT post for the transform details. The architectural principle is the same across both apps: the audio thread copies samples and returns; everything else happens elsewhere.
Debugging the clicks that still happen
When a click appears, we profile the audio thread directly — Instruments Time Profiler with the audio thread marked on iOS, Systrace with Trace.beginSection on Android. In our experience, 99% of production clicks trace to debug-only code that leaked into the hot path: a String format for logging, a guard assertion that allocates, an accidental Swift optional unwrap that triggers a slow path. We guard all debug instrumentation with compile-time flags that are verified off in release builds, and ktuyen's overnight Estua soak test — eight hours of continuous playback in airplane mode — is the final gate before any release tagged by atuan. The audio thread is unforgiving. Treat it accordingly.
Audio real-time trên điện thoại là bài toán scheduling với hậu quả con người. Hệ điều hành gọi callback mỗi N sample — ở 48 kHz với buffer 512 sample, đó là mỗi 10,67 millisecond — và kỳ vọng buffer PCM float đầy trả về trước deadline. Trễ một lần user nghe click hoặc pop. Trễ liên tục thì âm ngủ Estua không dùng được lúc 3 giờ sáng, thành review một sao, thành vấn đề thật cho studio năm app không có đệm gọi vốn. Đường capture micro Sonarish sống dưới cùng ràng buộc: HAL audio giao sample trên thread ưu tiên cao, và mọi thứ bạn làm trong callback quyết định đồng hồ đo ồn đọc sự thật hay rác.
Audio thread là gì và vì sao thiêng
Trên iOS, Estua dùng AVAudioEngine với source node callback thủ công. Trên Android, AudioTrack với ENCODING_PCM_FLOAT chế độ low-latency khi OEM hỗ trợ. Cả hai nền gọi hàm render trên thread ưu tiên real-time, bypass run loop UI bình thường. Thread này không bao giờ chờ lock tranh chấp, không cấp phát heap, không I/O, và không gọi code path có thể làm những việc đó theo chuỗi. Lý do là unpredictability scheduling: malloc trên heap phân mảnh có thể 200 microsecond ngày tốt và 5 millisecond ngày xấu. Ngân sách bạn 10,67 millisecond cho mọi kênh, mọi DSP, mọi mix — và OS cần phần cho overhead riêng. Một malloc chậm là glitch.
Luật team enforce trong code review
Đây không phải guideline — là blocker review. Không cấp phát heap trong callback: không Swift Array append, không Kotlin list grow, không std::vector push_back, không format NSString. Preallocate mọi buffer lúc bắt đầu session trên UI thread và truyền pointer vào context callback. Không lock có thể block — dùng ring buffer lock-free single-producer single-consumer để truyền data giữa audio thread và UI thread. Không log, ghi file, hay network, kể cả guard bằng debug flag, vì bản debug ship tới soak test qua đêm của ktuyen và câu print quên allocate string đã gây click nhiều hơn bug DSP. Trên iOS riêng, tránh Objective-C message send trên hot path; giữ callback trong C hoặc Swift struct code compiler inline được. Hệ số biquad, tốc LFO, và tham số scene tính khi user đổi setting trên UI thread; callback chỉ đọc struct const ghi một lần không mutate đồng thời.
Estua tạo âm dưới ràng buộc này thế nào
Chuỗi synthesis Estua — mô tả chi tiết trong bài audio không loop — tạo pink noise qua filter đệ quy tiết kiệm Paul Kellet, vài phép nhân cộng mỗi sample không FFT mỗi block. Modulation biên độ chậm dùng filter one-pole low-pass trên giá trị tuyệt đối envelope, chạy cùng sample rate với hệ số precompute. Nhiều LFO không tương quan modulate cutoff và gain ở 0,02–0,15 Hz để output không lặp trên scale cảm nhận con người. Tất cả nằm gọn trong ngân sách 10,67 ms trên máy hiện đại ~3–8% CPU, chừa headroom cho output Bluetooth và cơn hoảng suspend app nền. Bối cảnh sản phẩm vì sao synthesis thay loop ở bài giấc ngủ và sóng.
Thích ứng buffer size và capture Sonarish
Không phải mọi thiết bị Android muốn 512 sample. Một số OEM yêu cầu 192 hoặc 256 cho DSP path. Estua adapt kích thước buffer lúc bắt đầu session nhưng giữ state DSP nội bộ liên tục qua thay đổi size để resize không tạo click discontinuity. Phía capture Sonarish feed ring buffer tiêu thụ bởi engine FFT trên background queue — FFT không bao giờ chạy trên audio thread, chỉ copy PCM vào ring buffer. Tách đó là lý do Sonarish chạy transform 4096 điểm trong khi meter cập nhật 30 Hz không glitch; xem bài FFT cho chi tiết transform. Nguyên tắc kiến trúc giống nhau cả hai app: audio thread copy sample và return; mọi thứ khác xảy ra chỗ khác.
Debug click vẫn xảy ra
Khi click xuất hiện, team profile trực tiếp audio thread — Instruments Time Profiler đánh dấu audio thread trên iOS, Systrace với Trace.beginSection trên Android. Theo kinh nghiệm, 99% click production truy về code chỉ debug rò vào hot path: String format log, guard assertion allocate, optional unwrap Swift vô tình kích slow path. Team guard mọi instrumentation debug bằng compile flag xác minh tắt ở release build, và soak test Estua qua đêm của ktuyen — tám giờ playback liên tục airplane mode — là cổng cuối trước mọi release atuan tag. Audio thread không tha thứ. Treat accordingly.