Understanding the fundamental principles of metal detection and why smartphones are viable platforms
Metal detection technology has evolved dramatically since Gerhard Fischer's invention of the first portable metal detector in 1925. Early detectors were bulky, consumed significant power, and could only detect large metallic objects at shallow depths. The technology has progressed through several key eras:
Modern smartphones contain powerful processors, high-quality audio hardware, and sophisticated sensors that make them viable platforms for metal detection. The FCMD (Field Coil Metal Detector) project demonstrates that a consumer Android device can perform DSP tasks that once required dedicated hardware costing thousands of dollars.
All metal detectors operate on the same fundamental principle: electromagnetic induction. The process involves three stages:
VLF detectors operate at frequencies between 3-30 kHz and use continuous wave transmission. They excel at discrimination (identifying target type) but can struggle with highly mineralized ground. The FCMD project is a VLF-type detector.
PI detectors send short bursts of current through the transmit coil, then measure the decay time of the induced signal. They handle mineralized ground better but have poor discrimination capabilities.
Modern detectors transmit multiple frequencies simultaneously, combining the benefits of different frequency responses. The FCMD project implements this approach, transmitting 1-24 logarithmically-spaced tones between 1 kHz and 20 kHz.
The ability to distinguish between different types of metals is what separates professional detectors from simple presence-detection devices. Discrimination relies on measuring two key properties:
These properties affect both the amplitude and phase of the received signal. By analyzing these characteristics across multiple frequencies, we can calculate a VDI (Visual Discrimination Indicator) value that classifies the target.
This book provides a complete journey from electromagnetic theory to working code:
By the end, you will understand not just how to build a metal detector, but why each design decision was made and how to optimize performance for your specific use case.
The operation of a metal detector is governed by Maxwell's equations, which describe how electric and magnetic fields interact. While a full derivation is beyond our scope, we'll focus on the equations most relevant to metal detection.
Faraday's law states that a changing magnetic field induces an electric field:
In practical terms: when we send an alternating current through our transmit coil, we create a time-varying magnetic field B. This changing field induces an electric field E in nearby conductors (our metal targets), which causes current to flow.
This tells us that the induced currents (eddy currents) in the target create their own magnetic field, which we detect with our receive coil.
When a conductive object is placed in a time-varying magnetic field, circular currents called eddy currents flow within the conductor. These currents follow closed loops and create their own magnetic field that opposes the original field (Lenz's law).
At higher frequencies, eddy currents concentrate near the surface of conductors. The penetration depth δ is:
This is why multi-frequency detection works: high frequencies give strong signals from shallow/surface targets, while low frequencies penetrate deeper and respond more uniformly to the bulk of the target.
| Material | Skin Depth @ 1 kHz | Skin Depth @ 10 kHz | Skin Depth @ 20 kHz |
|---|---|---|---|
| Copper | 2.1 mm | 0.66 mm | 0.47 mm |
| Aluminum | 2.7 mm | 0.85 mm | 0.60 mm |
| Iron | 0.20 mm | 0.063 mm | 0.045 mm |
One of the most important discriminating factors is the phase relationship between the transmitted and received signals. This phase shift depends on the target's electromagnetic properties.
We can model the target's response as having two components:
The phase angle θ between transmitted and received signals is:
Different metals exhibit different frequency responses due to their electrical and magnetic properties. This is the foundation of multi-frequency discrimination.
The detected signal strength decreases rapidly with distance. For small loop antennas (like our coil), the relationship approximates:
This inverse cube relationship means:
This rapid falloff is why depth estimation is challenging - a small error in measured amplitude translates to significant depth uncertainty.
Naturally occurring minerals in soil, particularly iron oxides and salts, respond to the electromagnetic field just like metal targets. This creates a constant background signal that can overwhelm small target signals.
| Mineral Type | Effect | Common Locations |
|---|---|---|
| Iron Oxides (Magnetite) | Magnetic response, phase shift | Red clay soils, volcanic areas |
| Salts | Conductive response | Beaches, desert areas |
| Clay Minerals | Variable, moisture-dependent | Agricultural areas |
Ground balance algorithms (Chapter 10) address this by measuring the ground's I/Q response and subtracting it from all subsequent readings, effectively "nulling out" the ground signal while preserving target signals.
The key physics principles for metal detection are:
Understanding these principles allows us to design signal processing algorithms that extract maximum information from the received signal.
At first glance, using a smartphone for metal detection might seem unconventional. However, modern Android devices possess several characteristics that make them ideal for this application:
A typical mid-range Android phone (2024) contains:
This is more than sufficient for real-time IQ demodulation of 24 simultaneous frequencies at 44.1 kHz sample rate.
Modern Android audio hardware rivals professional audio interfaces:
| Specification | Typical Android Device | Professional Audio Interface |
|---|---|---|
| Sample Rate | 44.1 - 192 kHz | 44.1 - 192 kHz |
| Bit Depth | 16-24 bit | 16-24 bit |
| THD+N | 0.001% - 0.01% | 0.001% - 0.005% |
| Dynamic Range | 90-120 dB | 100-120 dB |
Beyond audio, Android devices offer:
While powerful, smartphones have constraints we must work within:
Smartphone headphone amplifiers typically deliver 30-100 mW into 16-32Ω loads. This limits our transmit coil design:
Android's audio system determines buffer sizes based on hardware capabilities. We cannot arbitrarily choose small buffers for lower latency. The FCMD project accepts the system-determined buffer size and works within it.
Not all devices support all sample rates. The FCMD project uses 44.1 kHz, which is universally supported. Higher rates (96 kHz, 192 kHz) would allow higher frequency transmission but aren't necessary for our 1-20 kHz frequency range (Nyquist theorem requires minimum 40 kHz for 20 kHz signals).
Sustained CPU usage can cause thermal throttling. The FCMD architecture minimizes CPU load by:
Understanding Android's audio stack helps us optimize performance.
Android audio flows through multiple layers:
Your App (Java/Kotlin)
↓
AudioTrack / AudioRecord (Framework)
↓
Audio Flinger (Native Service)
↓
Audio HAL (Hardware Abstraction)
↓
ALSA / Hardware Drivers
↓
Audio Codec Hardware
Each layer adds latency. The FCMD project uses the lowest-latency path available to regular apps: AudioTrack.MODE_STREAM with PERFORMANCE_MODE_LOW_LATENCY (via buffer size selection).
Buffer size is crucial for balancing latency and reliability:
The FCMD project uses multiplier = 1 for minimum latency. Larger multipliers provide more tolerance for CPU scheduling jitter but increase latency proportionally.
Metal detection requires real-time processing: audio callbacks must complete before the next buffer arrives, or we get dropouts (xruns).
The FCMD project sets audio threads to maximum priority:
android.os.Process.setThreadPriority(
android.os.Process.THREAD_PRIORITY_URGENT_AUDIO
)
This gives our audio processing preferential CPU scheduling, reducing the chance of buffer underruns.
With a buffer size of 1920 samples at 44.1 kHz, we have 43.5 ms to process each buffer. Our IQ demodulation for 24 frequencies takes approximately 5-10 ms on a modern CPU, leaving comfortable margin.
Continuous audio processing consumes significant power. The FCMD project typically draws:
On a 4000 mAh battery at 3.8V (~15 Wh), this yields 7-15 hours of runtime, limited primarily by display power. Using a low-brightness setting or external display can extend this significantly.
Despite limitations, smartphone-based detection offers unique advantages:
| Feature | Dedicated Detector | Smartphone-Based |
|---|---|---|
| Initial Cost | $300-$3000 | $0 (existing device) + coil |
| Software Updates | Rarely, if ever | Continuous improvement possible |
| Display | Small LCD, limited info | Full color, high resolution, flexible UI |
| Data Logging | Limited or none | Unlimited, with GPS, photos, notes |
| Algorithm Changes | Fixed in hardware | Infinitely flexible |
| Connectivity | None or proprietary | WiFi, Bluetooth, cellular, cloud |
The FCMD project embraces a particular design philosophy:
Android devices are viable metal detector platforms because:
The main limitations are transmit power (limiting depth) and audio latency (limiting update rate), but these are acceptable tradeoffs for the advantages gained.
The FCMD metal detector uses the smartphone's audio subsystem as both signal generator and data acquisition system. Audio output drives the transmit coil, while audio input captures the received signal from the receive coil. This approach leverages existing high-quality hardware without requiring custom electronics.
Modern smartphones increasingly lack the traditional 3.5mm headphone jack. The FCMD system can be configured using either a built-in jack (if available) or an external USB audio adapter. The USB adapter approach is actually preferred for several reasons:
For devices with USB-C ports only, a powered USB hub and USB audio dongle provide the interface:
The complete signal path from generation to analysis:
The FCMD architecture uses stereo audio output creatively:
| Channel | Purpose | Signal Content |
|---|---|---|
| Left Output | Transmit Signal | Multi-tone composite (1-24 frequencies) |
| Right Output | Audio Feedback | Target-responsive audio tones for operator |
| Mono Input | Receive Signal | Captured electromagnetic response |
This separation provides several benefits:
The app generates multi-tone signals at adjustable volume (0-100%). Optimal level depends on coil impedance and desired field strength:
Example: 50% volume into 32Ω coil:
Microphone inputs typically have high gain (20-40 dB) and expect 10-100 mV signals. Our coil output may be much smaller (100 μV - 10 mV), requiring attention to noise floor:
| Signal Condition | Expected Amplitude | S/N Ratio |
|---|---|---|
| Strong target (surface) | 1-10 mV | 40-60 dB |
| Medium target (4") | 100-1000 μV | 20-40 dB |
| Weak target (6-8") | 10-100 μV | 0-20 dB |
The IQ demodulation process includes low-pass filtering that improves S/N ratio by approximately 10-15 dB, making weak signals detectable even near the noise floor.
Ground loops occur when multiple ground paths exist between equipment. Symptoms include 50/60 Hz hum and noise. Solutions:
Before field use, verify your hardware setup:
If your phone has a 3.5mm jack, you can connect coils directly:
For larger coils or greater depth, consider adding an audio amplifier:
The audio-based architecture provides a simple yet powerful interface between smartphone and metal detection hardware:
With proper hardware setup, the FCMD system can achieve detection depths of 4-8 inches on coin-sized targets, limited primarily by smartphone transmit power rather than signal processing capability.
Detailed examination of audio signal generation, real-time capture, and initial processing stages
IQ (In-phase/Quadrature) demodulation is the core signal processing technique that allows us to extract both amplitude and phase information from a received signal. It's called "IQ" because we measure two components:
Together, these form a complex number that completely describes the signal's magnitude and phase at a specific frequency.
For a received signal s(t) and a reference frequency f, we calculate I and Q by mixing with sine and cosine:
After low-pass filtering to remove high-frequency components:
Consider a pure tone at frequency f with amplitude A and phase φ:
Mixing with cosine:
After low-pass filtering to remove the 2f term:
Similarly for Q:
Combining these:
Thus we've extracted both amplitude (scaled by 2) and phase!
The FCMD project implements IQ demodulation in IQDemodulator.kt:
class SingleToneDemodulator(
private val frequency: Double,
private val sampleRate: Int
) {
private var phase = 0.0
private val phaseIncrement = 2.0 * PI * frequency / sampleRate
// IIR filter state
private val filterAlpha = 0.01 // ~10 Hz cutoff
private var iFiltered = 0.0
private var qFiltered = 0.0
fun analyze(samples: FloatArray): ToneAnalysis {
for (sample in samples) {
// Quadrature mixing
val i = sample * cos(phase)
val q = -sample * sin(phase)
// IIR low-pass filter
iFiltered = filterAlpha * i + (1.0 - filterAlpha) * iFiltered
qFiltered = filterAlpha * q + (1.0 - filterAlpha) * qFiltered
// Increment phase
phase += phaseIncrement
if (phase >= 2.0 * PI) phase -= 2.0 * PI
}
// Calculate amplitude and phase
val amplitude = sqrt(iFiltered² + qFiltered²) * 2.0
val phaseAngle = atan2(qFiltered, iFiltered)
return ToneAnalysis(frequency, amplitude, phaseAngle,
iFiltered, qFiltered)
}
}
The demodulator maintains a running phase that increments by phaseIncrement each sample. This is equivalent to generating cos(2πft) and sin(2πft) but more efficient than calling trigonometric functions every sample.
The FCMD project uses a single-pole IIR filter rather than a moving average for computational efficiency:
With α = 0.01, the cutoff frequency is approximately:
This is fast enough to track targets moving past the coil while filtering out high-frequency noise.
The FCMD project processes multiple frequencies simultaneously. For each audio buffer of 1920 samples, we run 24 separate IQ demodulators in parallel:
class IQDemodulator(
private val sampleRate: Int,
private val targetFrequencies: List
) {
private val demodulators = targetFrequencies.map { freq ->
SingleToneDemodulator(freq, sampleRate)
}
fun analyze(samples: FloatArray): List {
return demodulators.map { demod ->
demod.analyze(samples)
}
}
}
This parallel processing is efficient on modern multi-core CPUs and provides complete frequency response information every audio callback (23.2 Hz on typical hardware).
The FCMD project uses logarithmically-spaced frequencies from 1 kHz to 20 kHz:
Logarithmic spacing provides:
Several optimizations make real-time multi-tone IQ demodulation practical:
Rather than computing 2π × f × t / sampleRate each sample, we increment a phase accumulator. This replaces expensive multiplication with cheap addition.
As discussed, IIR filters are dramatically more efficient than FIR equivalents for moderate rolloff requirements.
We only calculate sqrt(I² + Q²) once per buffer, not per sample. The IIR filter provides continuous update of I and Q, but amplitude/phase are only needed at the callback rate (23.2 Hz), not sample rate (44.1 kHz).
Why use IQ demodulation instead of FFT (Fast Fourier Transform)?
| Aspect | IQ Demodulation | FFT |
|---|---|---|
| Frequency Selection | Arbitrary | Fixed bins (f_s / N) |
| Time Resolution | Every sample | Block-based (trade with frequency resolution) |
| Computational Cost | O(N × M) for M tones | O(N log N) |
| Memory | Minimal (2 floats/tone) | Full buffer required |
| Latency | IIR filter delay only | Full window length |
For our application with relatively few tones (24) and desire for low latency, IQ demodulation is superior to FFT.
The atan2 function returns phase in the range [-π, +π]. When monitoring phase over time, sudden jumps from +π to -π can occur. For metal detection, we don't typically need to unwrap phase (track continuous rotation) because we're interested in phase differences between frequencies, not absolute phase evolution.
Audio hardware may introduce DC bias (non-zero mean). This appears as a strong component at 0 Hz but doesn't affect our 1-20 kHz tones. Ground balance (Chapter 10) effectively removes DC bias by subtracting the baseline I/Q vector.
The IIR filter has a startup transient. With α = 0.01 and callback rate of 23.2 Hz, the time constant is:
This means full settling takes ~20 seconds. In practice, 95% settling occurs in 3τ ≈ 13 seconds, which is acceptable for startup delay.
When using external USB audio hardware (common with modern smartphones lacking headphone jacks), a critical challenge emerges: clock drift between the phone's digital signal processor and the USB audio dongle's clock.
The root cause: The phone generates the TX signal at its sample clock rate, but the USB dongle captures the RX signal at a slightly different rate. These independent clocks inevitably drift.
The solution is elegant: use the actual transmitted signal as the demodulation reference rather than a synthesized local oscillator.
The coherent demodulation architecture requires a simple Y-cable modification:
| Connection | Signal | Purpose |
|---|---|---|
| L Out → TX Coil | Multi-tone transmit | Generate EM field |
| L Out → R In | TX reference (loopback) | Coherent phase reference |
| L In ← RX Coil | Received signal | Target response |
| R Out → Headphones | Audio feedback | Operator monitoring |
The coherent demodulation algorithm processes both RX and TX_ref signals:
This complex multiplication rotates the RX signal by the conjugate of the reference, yielding the phase and amplitude relative to the actual transmitted signal.
Key insight: Both RX and TX_ref are captured by the same USB audio clock. Any frequency error affects both identically:
The clock error cancels completely in the subtraction, leaving only the phase shift caused by the metal target.
The coherent demodulation is implemented in IQDemodulatorDSP.kt:
override fun processStereo(
leftChannel: FloatArray, // RX signal
rightChannel: FloatArray, // TX reference
sampleRate: Int
): Pair<FloatArray, FloatArray> {
// Demodulate both channels with same local oscillator
val rxAnalysis = demodulator.analyze(leftChannel)
val refAnalysis = referenceDemodulator.analyze(rightChannel)
// Compute coherent phase/amplitude
val coherentAnalysis = rxAnalysis.mapIndexed { i, rx ->
val ref = refAnalysis[i]
if (ref.amplitude > 0.01) {
// Complex multiplication: RX × conj(REF) / |ref|²
val refMagSq = ref.inPhase² + ref.quadrature²
val coherentI = (rx.inPhase * ref.inPhase +
rx.quadrature * ref.quadrature) / refMagSq
val coherentQ = (rx.quadrature * ref.inPhase -
rx.inPhase * ref.quadrature) / refMagSq
ToneAnalysis(
frequency = rx.frequency,
amplitude = sqrt(coherentI² + coherentQ²),
phase = atan2(coherentQ, coherentI),
inPhase = coherentI,
quadrature = coherentQ
)
} else rx // Fallback if no reference
}
// Continue with VDI calculation using coherent analysis
...
}
| Metric | Without Coherent | With Coherent |
|---|---|---|
| Phase stability (20 kHz) | ±90° cycling | ±2-5° stable |
| Confidence score | 3-10% | 85-95% |
| VDI stability | Random 0-99 | Stable ±3 counts |
| Settling time | N/A (never settles) | 10-20 seconds |
| Hardware cost | None | $3 Y-cable |
IQ demodulation provides:
The FCMD implementation achieves this with simple, efficient code that processes up to 24 frequencies in real-time with minimal CPU usage. The coherent demodulation architecture ensures stable, accurate phase measurements even with inexpensive USB audio hardware.
VDI (Visual Discrimination Indicator) is a numerical scale, typically 0-99, that classifies detected targets by their electromagnetic properties. It originated with White's Electronics in the 1990s and has become an industry standard.
The goal: map the complex electromagnetic signature (amplitude and phase at multiple frequencies) to a single number that correlates with target composition.
| VDI Range | Typical Targets |
|---|---|
| 0-30 | Ferrous: iron nails, bottle caps, steel |
| 30-45 | Low conductors: aluminum foil, small rings |
| 45-65 | Mid conductors: brass, zinc pennies, pull tabs |
| 50-70 | Gold range: gold jewelry (overlaps mid) |
| 70-99 | High conductors: copper, silver, large targets |
The FCMD project's VDI calculation is based primarily on phase slope across frequencies. This is more reliable than single-frequency phase because it reveals magnetic properties.
Recall from Chapter 2 that ferrous metals have high magnetic permeability. This causes phase to shift dramatically with frequency. The phase slope is:
Measured in degrees per kHz.
The steep negative slope is a clear signature of ferrous material!
The complete VDI calculation combines multiple factors:
fun calculateVDI(analysis: List): VDIResult { // 1. Calculate phase slope val phaseSlope = calculatePhaseSlope(analysis) // 2. Calculate conductivity index val conductivityIndex = calculateConductivityIndex(analysis) // 3. Measure phase consistency val phaseConsistency = calculatePhaseConsistency(analysis) // 4. Calculate raw VDI val rawVDI = if (phaseSlope < 0) { // Ferrous: use slope steepness val normalized = (phaseSlope / -10.0).coerceIn(0.0, 1.0) (30 * (1.0 - normalized)).toInt() } else { // Non-ferrous: use conductivity (30 + (conductivityIndex * 69)).toInt() } // 5. Adjust for signal strength val amplitude = analysis.map { it.amplitude }.average() val adjustment = when { amplitude > 0.5 -> 5 // Strong signal amplitude < 0.1 -> -5 // Weak signal else -> 0 } val vdi = (rawVDI + adjustment).coerceIn(0, 99) // 6. Classify and return return VDIResult( vdi, confidence, targetType, phaseSlope, conductivityIndex, depthEstimate ) }
For non-ferrous metals, conductivity index separates aluminum from copper/silver:
High conductors maintain amplitude at high frequencies (skin depth still allows penetration). Low conductors attenuate rapidly.
Phase consistency measures how "clean" the target signal is:
A single solid target has consistent phase across frequencies. Multiple targets, junk, or heavy mineralization show poor consistency.
Not all VDI readings are equally reliable. The FCMD project calculates confidence as:
Phase consistency is weighted more heavily because it's the best indicator of a single, solid target versus trash or multiple objects.
| Confidence Range | Interpretation | Action |
|---|---|---|
| 0.8 - 1.0 | High: Clean single target | Trust VDI, worth digging |
| 0.5 - 0.8 | Medium: Decent signal | VDI probably accurate |
| 0.3 - 0.5 | Low: Noisy or multiple targets | VDI uncertain |
| 0.0 - 0.3 | Very Low: Junk or interference | Returns UNKNOWN type |
Based on VDI and phase slope, targets are classified:
fun classifyTarget(vdi: Int, phaseSlope: Double,
consistency: Double): TargetType {
if (consistency < 0.3) return TargetType.UNKNOWN
return when {
vdi <= 30 && phaseSlope < -3.0 -> TargetType.FERROUS
vdi <= 45 -> TargetType.LOW_CONDUCTOR
vdi >= 70 -> TargetType.HIGH_CONDUCTOR
vdi in 50..70 -> TargetType.GOLD_RANGE
vdi in 46..69 -> TargetType.MID_CONDUCTOR
else -> TargetType.UNKNOWN
}
}
VDI cannot distinguish between composition and size. A small copper coin gives similar VDI to a large aluminum can, despite different materials. This is fundamental - we measure electromagnetic response, not material directly.
A coin flat vs. edge-on can show different VDI (±10 points). Phase measurements are somewhat less sensitive to orientation than amplitude, which is why we weight phase slope heavily.
Gold jewelry (VDI 50-70) overlaps with aluminum pull tabs (VDI 45-65). No single-frequency or even multi-frequency detector can perfectly separate these. This is why detectorists learn to dig "iffy" signals in goldfields.
VDI scales benefit from calibration with known targets. The FCMD thresholds (30, 45, 65, 70) are starting points. You can refine them:
Machine learning approaches (Chapter 14) can automatically learn optimal thresholds from labeled data.
Effective VDI discrimination requires:
The FCMD algorithm achieves good discrimination with modest computational cost, suitable for real-time operation on smartphone hardware.
Ground minerals produce constant I/Q vectors that can overwhelm weak target signals. Ground balance algorithms measure and subtract this baseline, effectively "nulling out" the ground while preserving target signatures.
No ground balance applied. Use in air tests or clean beach sand.
User pumps coil over ground 10 times while system captures I/Q baseline. This baseline is then subtracted from all subsequent readings.
Continuously adapts baseline using slow IIR filter (α = 0.0005). Freezes when strong target detected (amplitude > 0.3) to avoid nulling the target.
Time constant ~86 seconds, allowing gradual adaptation to ground changes.
Combines manual preset with auto-tracking for optimal performance in variable ground.
Ground balance subtracts baseline I/Q vector from measured vector. User-adjustable offset (±50) rotates baseline by ±45° for fine-tuning:
// Apply offset rotation
val offsetRadians = (offset / 50.0) * (π/4)
val rotatedI = baseI * cos(offset) - baseQ * sin(offset)
val rotatedQ = baseI * sin(offset) + baseQ * cos(offset)
// Subtract
val newI = currentI - rotatedI
val newQ = currentQ - rotatedQ
// Recalculate amplitude/phase
val amplitude = sqrt(newI² + newQ²)
val phase = atan2(newQ, newI)
Critical feature: when amplitude exceeds threshold (0.3), stop tracking. Otherwise, auto-tracking will try to null out the target itself!
Ground balance is essential for real-world metal detection. The FCMD multi-mode approach provides flexibility for different soils and user skill levels.
Signal strength ∝ 1/r³, but we don't know target size. A large shallow target looks identical to a small deep target. Depth estimation must account for this fundamental ambiguity.
Deep targets attenuate high frequencies more → ratio > 1.5
Use target type to estimate expected size:
Rather than claiming "6.2 inches," FCMD returns honest categories:
With calibration: ±1 category (±2")
Without calibration: ±1-2 categories (±4")
IQ demodulation represents signals as complex numbers:
Relationship between time and frequency domains:
For normally distributed measurements, 95% confidence interval:
audioTrack = AudioTrack.Builder()
.setAudioAttributes(
AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_MEDIA)
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
.build()
)
.setAudioFormat(
AudioFormat.Builder()
.setSampleRate(44100)
.setChannelMask(AudioFormat.CHANNEL_OUT_STEREO)
.setEncoding(AudioFormat.ENCODING_PCM_16BIT)
.build()
)
.setBufferSizeInBytes(minBufferSize)
.setTransferMode(AudioTrack.MODE_STREAM)
.build()
audioRecord = AudioRecord.Builder()
.setAudioSource(MediaRecorder.AudioSource.MIC)
.setAudioFormat(
AudioFormat.Builder()
.setSampleRate(44100)
.setChannelMask(AudioFormat.CHANNEL_IN_MONO)
.setEncoding(AudioFormat.ENCODING_PCM_16BIT)
.build()
)
.setBufferSizeInBytes(minBufferSize)
.build()
android.os.Process.setThreadPriority(
android.os.Process.THREAD_PRIORITY_URGENT_AUDIO
)
val minBufferSize = AudioTrack.getMinBufferSize(
sampleRate,
channelConfig,
audioFormat
)
// Multiply by 1 for lowest latency
// Multiply by 2-4 for more stability
val actualBufferSize = minBufferSize * multiplier
If processing takes too long, audio stutters. Solution: increase buffer size or optimize processing.
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
Not all devices support all rates. Always check with getMinBufferSize() - if it returns ERROR, rate is unsupported.
FCMD/
├── AudioEngine.kt - Audio I/O management
├── IQDemodulator.kt - Core DSP processing
├── VDICalculator.kt - Target discrimination
├── GroundBalanceManager.kt - Ground balance algorithms
├── DepthEstimator.kt - Depth estimation
├── MultiToneGenerator.kt - Transmit signal generation
├── AudioToneGenerator.kt - Audio feedback
└── MainActivity.kt - UI and coordination
Purpose: Manages AudioTrack and AudioRecord, handles playback and record loops.
Key Methods:
start() - Initialize audio hardwarestop() - Cleanup audio resourcessetFrequencyRange() - Configure TX frequenciessetDspProcessor() - Attach signal processingPurpose: Extract amplitude/phase from received signal at each frequency.
Key Methods:
analyze(samples) - Process audio bufferreset() - Clear filter statesPurpose: Calculate VDI and classify targets.
Key Methods:
calculateVDI(analysis) - Main VDI calculationgetTargetDescription() - Human-readable output
AudioRecord → FloatArray samples
↓
IQDemodulator.analyze(samples) → List<ToneAnalysis>
↓
GroundBalanceManager.applyGroundBalance() → balanced List<ToneAnalysis>
↓
VDICalculator.calculateVDI() → VDIResult
↓
DepthEstimator.estimateDepth() → VDIResult with DepthEstimate
↓
MainActivity callback → UI update
| Metric | Typical Value |
|---|---|
| Sample Rate | 44,100 Hz |
| Buffer Size | 1920 samples (device-dependent) |
| Callback Rate | 23.2 Hz |
| Latency | 43.5 ms |
| Update Rate | 30 Hz (configurable, max ~23 Hz) |
| CPU Usage | 15-25% single core |
The Field Coil Metal Detector (FCMD) project demonstrates that professional-grade metal detection is possible using consumer smartphone hardware and open-source software. By leveraging modern Android devices' powerful processors and high-quality audio subsystems, FCMD achieves multi-frequency IQ demodulation, VDI discrimination, ground balance, and depth estimation in real-time.
This book has covered the journey from electromagnetic theory to working code, providing both the "why" (physics and signal processing theory) and the "how" (practical Android implementation). Whether you're building your own detector, learning about DSP, or exploring embedded real-time systems, the principles and techniques presented here are widely applicable.
Experiment with the FCMD codebase. Try different frequency ranges, modify the VDI algorithm, implement new ground balance strategies. The beauty of software-based detection is that experimentation costs nothing but time.
Happy detecting!