diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
index b3a5c2e1..2ecb33d2 100644
--- a/.github/workflows/codeql.yml
+++ b/.github/workflows/codeql.yml
@@ -2,7 +2,7 @@ name: CodeQL Analysis
on:
push:
- branches: [ main ]
+ branches: [ main, dev ]
pull_request:
branches: [ main ]
schedule:
diff --git a/AUDIO_FX.md b/AUDIO_FX.md
new file mode 100644
index 00000000..09fcd07e
--- /dev/null
+++ b/AUDIO_FX.md
@@ -0,0 +1,184 @@
+# FRecorder Audio FX & Noise Reduction
+
+All audio processing in FRecorder is applied to **WAV recordings only**. Effects are applied in real-time during recording (filters, gate, boost) or as post-processing after the recording stops (noise reduction).
+
+---
+
+## Signal Chain (recording order)
+
+```
+Mic → Gain Boost → High-Pass Filter → Low-Pass Filter → Noise Gate → WAV file → Noise Reduction
+```
+
+Real-time effects are applied sample-by-sample as audio is captured. Noise reduction runs on the finished WAV file.
+
+---
+
+## 1. Input Gain Boost
+
+Amplifies the raw microphone signal before any other processing.
+
+| Setting | Multiplier | Use case |
+|---------|-----------|----------|
+| Off | 1× | Default — no amplification |
+| +6 dB | 2× | Quiet sources at moderate distance |
+| +12 dB | 4× | Very quiet sources, distant speakers |
+
+**How it works:** Each PCM sample is multiplied by the gain factor and clamped to 16-bit range (±32767) to prevent digital clipping.
+
+**Recommendations:**
+- Start with Off. Only increase if the waveform is visibly too small.
+- +12 dB amplifies noise by 4× as well — combine with the noise gate or noise reduction to compensate.
+- If you hear distortion or see the waveform hitting the rails, reduce the boost.
+
+---
+
+## 2. High-Pass Filter (HPF)
+
+Removes low-frequency content below a cutoff frequency. Implemented as a second-order Butterworth (biquad) filter applied in real-time.
+
+| Setting | Cutoff | Slope |
+|---------|--------|-------|
+| Off | — | — |
+| HPF 80 Hz | 80 Hz | 12 dB/octave |
+| HPF 120 Hz | 120 Hz | 12 dB/octave |
+
+**How it works:** A biquad high-pass filter (Q = 0.7071, Butterworth) attenuates frequencies below the cutoff at 12 dB per octave. The filter state is maintained across audio buffers for seamless operation.
+
+**Recommendations:**
+- **80 Hz** — Removes deep rumble (HVAC, traffic, wind) while preserving bass in music and speech.
+- **120 Hz** — More aggressive. Good for speech-only recordings where bass content is unwanted. Removes handling noise and proximity effect from close-miked vocals.
+- Use HPF in almost all field recording situations. Low-frequency rumble is rarely useful and wastes dynamic range.
+
+---
+
+## 3. Low-Pass Filter (LPF)
+
+Removes high-frequency content above a cutoff frequency. Same biquad implementation as the HPF.
+
+| Setting | Cutoff | Slope |
+|---------|--------|-------|
+| Off | — | — |
+| LPF 9.5 kHz | 9,500 Hz | 12 dB/octave |
+| LPF 15 kHz | 15,000 Hz | 12 dB/octave |
+
+**How it works:** A biquad low-pass filter (Q = 0.7071, Butterworth) attenuates frequencies above the cutoff.
+
+**Recommendations:**
+- **15 kHz** — Gentle rolloff. Removes ultrasonic noise and aliasing artifacts from cheap microphones while keeping all audible content.
+- **9.5 kHz** — Aggressive. Useful for speech-only recordings (intelligibility lives below 8 kHz). Removes hiss, high-frequency interference, and sibilance.
+- Leave Off for music or any recording where high-frequency detail matters.
+
+---
+
+## 4. Noise Gate
+
+Silences the audio when the signal level drops below a threshold. Prevents recording room tone, hiss, or background noise during pauses in speech.
+
+| Parameter | Value | Description |
+|-----------|-------|-------------|
+| Threshold | 400 RMS | Signal level that opens the gate |
+| Hysteresis | 50% of threshold | Lower level that triggers gate closing (prevents chatter) |
+| Attack | 0.1 ms | Time to fully open (nearly instant) |
+| Hold | 300 ms | Time the gate stays open after signal drops below threshold |
+| Release | 500 ms | Fade-out time from open to closed |
+
+**How it works:** The gate operates as a state machine: CLOSED → ATTACK → OPEN → HOLD → RELEASE → CLOSED. When the RMS level of an audio chunk exceeds the threshold, the gate opens. When it drops below the hysteresis level, the gate enters a hold period before fading out. The envelope multiplier (0.0–1.0) is applied to every sample.
+
+**Recommendations:**
+- Best for **speech recordings** in quiet environments where you want dead silence between phrases.
+- **Not recommended** for music, ambient recordings, or environments with continuous background sound — the gate will chop the audio unnaturally.
+- Works well combined with HPF (remove rumble first, then gate on the cleaner signal).
+- The fixed threshold (400 RMS) is tuned for typical phone microphone levels. Very quiet sources may never open the gate — use gain boost to compensate.
+
+---
+
+## 5. Noise Reduction (post-processing)
+
+Spectral-gating noise reduction inspired by Audacity's Noise Reduction effect. Applied **after recording stops**, processing the entire WAV file. A progress dialog is shown during processing.
+
+### How it works
+
+1. **Noise profile** — The first N seconds of the recording are analyzed to build a per-frequency-bin noise profile (mean + standard deviation of spectral magnitude).
+2. **Threshold** — For each frequency bin, a threshold is computed: `mean + scale × std`. The sensitivity parameter controls the scale factor.
+3. **Spectral subtraction** — For each FFT frame of the full recording, bins whose magnitude is below the noise threshold are attenuated. The reduction amount is controlled by the reduction dB parameter.
+4. **Frequency smoothing** — The gain mask is averaged across neighboring frequency bins to avoid musical noise (isolated tonal artifacts).
+5. **Temporal smoothing** — The gain mask is smoothed over time with attack (20 ms) and release (100 ms) constants to prevent abrupt transitions.
+6. **Overlap-add reconstruction** — Processed frames (2048-sample Hann-windowed, 50% overlap) are combined back into a continuous signal and written to the WAV file in-place.
+
+### Configurable parameters
+
+| Parameter | Range | Default | Description |
+|-----------|-------|---------|-------------|
+| Reduction (dB) | 0–24 | 12.0 | How much noise to remove. Higher = more aggressive removal. |
+| Sensitivity | 0–24 | 12.0 | How aggressively bins are classified as noise. Higher = more bins treated as noise. |
+| Freq smoothing | 0–6 bands | 3 | Number of neighboring frequency bands averaged. Reduces musical noise artifacts. |
+| Noise profile | 0.5–5.0 s | 1.0 | Duration of audio from the start used to learn the noise signature. |
+
+### Parameter details
+
+**Reduction (dB):**
+- 0 dB — No reduction (pass-through).
+- 6 dB — Light cleanup. Subtle hiss removal.
+- 12 dB — Moderate reduction. Good general-purpose setting.
+- 18–24 dB — Heavy reduction. May introduce artifacts on transients.
+
+The value maps to a reduction strength multiplier: `strength = dB / 12.0`. At 12 dB, the full estimated noise floor is subtracted. At 24 dB, twice the noise floor is subtracted.
+
+**Sensitivity:**
+- 0 — Conservative. Only the most obvious noise bins are affected. Threshold = `mean + 3×std`.
+- 12 — Balanced. Good default.
+- 24 — Aggressive. Everything near the noise floor is treated as noise. Threshold = `mean + 0×std`. Risk of removing quiet signal content.
+
+**Frequency smoothing:**
+- 0 — No smoothing. Each bin is treated independently. May produce "musical noise" (random tonal blips).
+- 3 — Default. Averages the gain mask across 3 neighboring bins on each side. Good balance.
+- 6 — Maximum smoothing. Very smooth but may blur frequency detail.
+
+**Noise profile duration:**
+- The algorithm assumes the **first N seconds of the recording contain only noise** (no speech or signal). Record a moment of silence at the start.
+- 1.0 s — Default. Sufficient for stationary noise (fan, hiss, hum).
+- 2.0–5.0 s — Better for non-stationary noise. Captures more variation in the noise floor.
+- 0.5 s — Minimum useful. Use when you can't afford a long silence at the start.
+
+### Recommendations
+
+- **Always record 1–2 seconds of silence** at the beginning before speaking. This gives the algorithm a clean noise profile.
+- Start with defaults (12 dB / 12 sensitivity / 3 bands / 1.0 s). Adjust only if results are unsatisfactory.
+- If you hear "musical noise" (watery, bubbly artifacts), increase frequency smoothing or decrease sensitivity.
+- If speech sounds muffled or thin, reduce the reduction dB or sensitivity.
+- Noise reduction works best on **stationary noise** (constant hiss, fan, hum). It struggles with intermittent noise (traffic, people talking nearby).
+- Combine with HPF: remove low-frequency rumble with the filter during recording, then clean up remaining hiss with noise reduction after.
+- Currently only supports **16-bit WAV** files. 24-bit recordings are not processed.
+
+---
+
+## Recommended setups
+
+### Speech / interview in a quiet room
+- HPF: 120 Hz
+- LPF: Off
+- Noise gate: On
+- Gain boost: Off
+- Noise reduction: Off (or 6 dB if there's audible hiss)
+
+### Speech in a noisy environment (street, café)
+- HPF: 80 Hz
+- LPF: 15 kHz
+- Noise gate: Off (continuous noise would cause choppy gating)
+- Gain boost: Off or +6 dB
+- Noise reduction: On (12 dB / 12 sensitivity / 3 bands / 1.0 s)
+
+### Quiet source at distance
+- HPF: 80 Hz
+- LPF: Off
+- Noise gate: Off
+- Gain boost: +6 dB or +12 dB
+- Noise reduction: On (12–18 dB / 12 sensitivity / 3 bands / 1.0 s)
+
+### Music / ambient recording
+- HPF: Off (or 80 Hz if there's rumble)
+- LPF: Off
+- Noise gate: Off
+- Gain boost: Off
+- Noise reduction: Off (or very light: 6 dB / 6 sensitivity)
diff --git a/README.md b/README.md
index b1ba71d2..8a272dd0 100644
--- a/README.md
+++ b/README.md
@@ -15,9 +15,9 @@ Fork of [Dimowner/AudioRecorder](https://github.com/Dimowner/AudioRecorder) with
- **USB Audio Input** — Record from external USB microphones, audio interfaces, and other USB audio devices. Automatically detects connected devices and lets you select them as the recording source. Use a USB audio interface like the Rode AI-Micro or a TRS-to-USB-C adapter like the BOYA BY-K4 to plug in contact mics, lavaliers, or any standard 3.5mm audio source.
- **Live Monitoring** — Listen to what's being recorded in real-time through Bluetooth headphones or the built-in speaker. Toggle on/off before or during recording.
- **Gain Boost** — Adjustable input gain (+6 dB / +12 dB) to amplify quiet sources. Applied in real-time with clipping protection.
-- **Noise Reduction** — Optional spectral noise reduction applied on save (WAV only).
+- **Noise Reduction** — Optional spectral noise reduction applied on save (WAV only). Configurable parameters.
- **High/Low-Pass Filters** — Configurable HPF (80/120 Hz) and LPF (9.5/15 kHz) for cleaning up recordings.
-- **Noise Gate** — Monitor-only noise gate to cut background noise during live monitoring.
+- **Noise Gate** — Noise gate to cut background noise during silence.
- **Save Formats** — WAV (16-bit or 24-bit), MP3 (320 kbps via LAME), and FLAC (lossless). Recording is always done internally in WAV for maximum quality; conversion happens on save.
- **Bit Depth** — Selectable 16-bit or 24-bit WAV output.
- **Configurable Audio** — Sample rate (8–48 kHz), mono/stereo, audio input device selection.
@@ -27,6 +27,8 @@ Fork of [Dimowner/AudioRecorder](https://github.com/Dimowner/AudioRecorder) with
- **File Management** — Rename, share, import, bookmark, trash/restore recordings. Built-in file browser.
- **Themes** — Multiple color themes to personalize the app.
+See **[Audio FX & Noise Reduction](AUDIO_FX.md)** for a detailed explanation of all audio effects, how they work, and recommended setups.
+
## Format Changes from Upstream
Removed support for **3GP** and **M4A** recording formats. These were low-quality legacy formats not suited for field recording. All recording is now done in WAV internally, with the user choosing the output/save format (WAV, MP3, FLAC).
diff --git a/app/src/main/java/com/vdo/frecorder/app/RecordingService.java b/app/src/main/java/com/vdo/frecorder/app/RecordingService.java
index d8ff1d8d..f14d9ab6 100644
--- a/app/src/main/java/com/vdo/frecorder/app/RecordingService.java
+++ b/app/src/main/java/com/vdo/frecorder/app/RecordingService.java
@@ -453,6 +453,10 @@ private void startRecording(String path) {
// Set noise reduction and filter preferences on WavRecorder
if (recorder instanceof WavRecorder) {
((WavRecorder) recorder).setNoiseReductionEnabled(prefs.isNoiseReductionEnabled());
+ ((WavRecorder) recorder).setNoiseReductionDb(prefs.getNoiseReductionDb());
+ ((WavRecorder) recorder).setNoiseReductionSensitivity(prefs.getNoiseReductionSensitivity());
+ ((WavRecorder) recorder).setNoiseReductionFreqSmoothing(prefs.getNoiseReductionFreqSmoothing());
+ ((WavRecorder) recorder).setNoiseProfileSeconds(prefs.getNoiseProfileSeconds());
((WavRecorder) recorder).setHpfMode(prefs.getHpfMode());
((WavRecorder) recorder).setLpfMode(prefs.getLpfMode());
((WavRecorder) recorder).setNoiseGateEnabled(prefs.isNoiseGateEnabled());
diff --git a/app/src/main/java/com/vdo/frecorder/app/main/MainPresenter.java b/app/src/main/java/com/vdo/frecorder/app/main/MainPresenter.java
index 4e61fa30..cc340c7d 100644
--- a/app/src/main/java/com/vdo/frecorder/app/main/MainPresenter.java
+++ b/app/src/main/java/com/vdo/frecorder/app/main/MainPresenter.java
@@ -776,9 +776,15 @@ public void importAudioFile(final Context context, final Uri uri) {
@Override
public void run() {
try {
+ if (uri.getScheme() == null || !uri.getScheme().equals("content")) {
+ throw new SecurityException("Only content:// URIs are supported for import");
+ }
ParcelFileDescriptor parcelFileDescriptor = context.getContentResolver().openFileDescriptor(uri, "r");
FileDescriptor fileDescriptor = parcelFileDescriptor.getFileDescriptor();
- String name = extractFileName(context, uri);
+ String name = FileUtil.sanitizeFileName(extractFileName(context, uri));
+ if (name == null) {
+ throw new IOException("Invalid file name extracted from URI");
+ }
File newFile = fileRepository.provideRecordFile(name);
if (FileUtil.copyFile(fileDescriptor, newFile)) {
diff --git a/app/src/main/java/com/vdo/frecorder/app/settings/SettingsActivity.java b/app/src/main/java/com/vdo/frecorder/app/settings/SettingsActivity.java
index 415661dd..90f21e6a 100644
--- a/app/src/main/java/com/vdo/frecorder/app/settings/SettingsActivity.java
+++ b/app/src/main/java/com/vdo/frecorder/app/settings/SettingsActivity.java
@@ -34,11 +34,14 @@
import android.widget.Button;
import android.widget.CompoundButton;
import android.widget.LinearLayout;
+import android.widget.SeekBar;
import android.widget.Spinner;
import android.widget.Switch;
import android.widget.TextView;
import android.widget.Toast;
+import android.app.AlertDialog;
+
import com.vdo.frecorder.ARApplication;
import com.vdo.frecorder.AppConstants;
import com.vdo.frecorder.ColorMap;
@@ -48,6 +51,7 @@
import com.vdo.frecorder.app.trash.TrashActivity;
import com.vdo.frecorder.app.widget.SettingView;
import com.vdo.frecorder.audio.AudioDeviceManager;
+import com.vdo.frecorder.data.Prefs;
import com.vdo.frecorder.util.AndroidUtils;
import com.vdo.frecorder.util.FileUtil;
import com.vdo.frecorder.util.RippleUtils;
@@ -78,6 +82,7 @@ public class SettingsActivity extends Activity implements SettingsContract.View,
private Switch swKeepScreenOn;
private Switch swAskToRename;
private Switch swNoiseReduction;
+ private TextView btnNoiseReductionConfigure;
private Spinner nameFormatSelector;
private Spinner audioSourceSelector;
@@ -262,6 +267,9 @@ protected void onCreate(Bundle savedInstanceState) {
swNoiseReduction = findViewById(R.id.swNoiseReduction);
swNoiseReduction.setOnCheckedChangeListener((btn, isChecked) -> presenter.setNoiseReductionEnabled(isChecked));
+ btnNoiseReductionConfigure = findViewById(R.id.btnNoiseReductionConfigure);
+ btnNoiseReductionConfigure.setOnClickListener(v -> showNoiseReductionSettingsDialog());
+
audioSourceSelector = findViewById(R.id.audio_source_selector);
audioSourceSelector.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
@@ -654,6 +662,96 @@ public void disableAudioSettings() {
gainBoostSetting.setEnabled(false);
}
+ private void showNoiseReductionSettingsDialog() {
+ Prefs prefs = ARApplication.getInjector().providePrefs(getApplicationContext());
+
+ float currentDb = prefs.getNoiseReductionDb();
+ float currentSensitivity = prefs.getNoiseReductionSensitivity();
+ int currentFreqSmoothing = prefs.getNoiseReductionFreqSmoothing();
+ float currentProfileSeconds = prefs.getNoiseProfileSeconds();
+
+ LinearLayout layout = new LinearLayout(this);
+ layout.setOrientation(LinearLayout.VERTICAL);
+ int pad = (int) getResources().getDimension(R.dimen.spacing_normal);
+ layout.setPadding(pad, pad, pad, 0);
+
+ // Reduction dB slider (0-24, step 0.5)
+ TextView txtDb = new TextView(this);
+ txtDb.setText(getString(R.string.noise_reduction_db, currentDb));
+ layout.addView(txtDb);
+ SeekBar seekDb = new SeekBar(this);
+ seekDb.setMax(48); // 0-24 in 0.5 steps
+ seekDb.setProgress((int) (currentDb * 2));
+ seekDb.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
+ @Override public void onProgressChanged(SeekBar sb, int progress, boolean fromUser) {
+ txtDb.setText(getString(R.string.noise_reduction_db, progress / 2.0f));
+ }
+ @Override public void onStartTrackingTouch(SeekBar sb) {}
+ @Override public void onStopTrackingTouch(SeekBar sb) {}
+ });
+ layout.addView(seekDb);
+
+ // Sensitivity slider (0-24, step 0.5)
+ TextView txtSensitivity = new TextView(this);
+ txtSensitivity.setText(getString(R.string.noise_reduction_sensitivity, currentSensitivity));
+ layout.addView(txtSensitivity);
+ SeekBar seekSensitivity = new SeekBar(this);
+ seekSensitivity.setMax(48); // 0-24 in 0.5 steps
+ seekSensitivity.setProgress((int) (currentSensitivity * 2));
+ seekSensitivity.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
+ @Override public void onProgressChanged(SeekBar sb, int progress, boolean fromUser) {
+ txtSensitivity.setText(getString(R.string.noise_reduction_sensitivity, progress / 2.0f));
+ }
+ @Override public void onStartTrackingTouch(SeekBar sb) {}
+ @Override public void onStopTrackingTouch(SeekBar sb) {}
+ });
+ layout.addView(seekSensitivity);
+
+ // Frequency smoothing slider (0-6, step 1)
+ TextView txtFreqSmoothing = new TextView(this);
+ txtFreqSmoothing.setText(getString(R.string.noise_reduction_freq_smoothing, currentFreqSmoothing));
+ layout.addView(txtFreqSmoothing);
+ SeekBar seekFreqSmoothing = new SeekBar(this);
+ seekFreqSmoothing.setMax(6);
+ seekFreqSmoothing.setProgress(currentFreqSmoothing);
+ seekFreqSmoothing.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
+ @Override public void onProgressChanged(SeekBar sb, int progress, boolean fromUser) {
+ txtFreqSmoothing.setText(getString(R.string.noise_reduction_freq_smoothing, progress));
+ }
+ @Override public void onStartTrackingTouch(SeekBar sb) {}
+ @Override public void onStopTrackingTouch(SeekBar sb) {}
+ });
+ layout.addView(seekFreqSmoothing);
+
+ // Noise profile seconds slider (0.5-5.0, step 0.5)
+ TextView txtProfile = new TextView(this);
+ txtProfile.setText(getString(R.string.noise_profile_seconds, currentProfileSeconds));
+ layout.addView(txtProfile);
+ SeekBar seekProfile = new SeekBar(this);
+ seekProfile.setMax(9); // 0.5-5.0 in 0.5 steps → 0..9 maps to 0.5..5.0
+ seekProfile.setProgress((int) ((currentProfileSeconds - 0.5f) * 2));
+ seekProfile.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
+ @Override public void onProgressChanged(SeekBar sb, int progress, boolean fromUser) {
+ txtProfile.setText(getString(R.string.noise_profile_seconds, (progress + 1) / 2.0f));
+ }
+ @Override public void onStartTrackingTouch(SeekBar sb) {}
+ @Override public void onStopTrackingTouch(SeekBar sb) {}
+ });
+ layout.addView(seekProfile);
+
+ new AlertDialog.Builder(this)
+ .setTitle(R.string.noise_reduction_settings)
+ .setView(layout)
+ .setPositiveButton(R.string.btn_save, (dialog, which) -> {
+ prefs.setNoiseReductionDb(seekDb.getProgress() / 2.0f);
+ prefs.setNoiseReductionSensitivity(seekSensitivity.getProgress() / 2.0f);
+ prefs.setNoiseReductionFreqSmoothing(seekFreqSmoothing.getProgress());
+ prefs.setNoiseProfileSeconds((seekProfile.getProgress() + 1) / 2.0f);
+ })
+ .setNegativeButton(R.string.btn_cancel, null)
+ .show();
+ }
+
@Override
public void showProgress() {
// TODO: showProgress
diff --git a/app/src/main/java/com/vdo/frecorder/audio/AudioDecoder.java b/app/src/main/java/com/vdo/frecorder/audio/AudioDecoder.java
index 389421ae..0527fbb9 100644
--- a/app/src/main/java/com/vdo/frecorder/audio/AudioDecoder.java
+++ b/app/src/main/java/com/vdo/frecorder/audio/AudioDecoder.java
@@ -261,8 +261,9 @@ public static RecordInfo readRecordInfo(@NonNull final File inputFile)
boolean isInTrash = false;
try {
- if (!inputFile.exists()) {
- throw new java.io.FileNotFoundException(inputFile.getAbsolutePath());
+ File canonicalFile = inputFile.getCanonicalFile();
+ if (!canonicalFile.exists()) {
+ throw new java.io.FileNotFoundException(canonicalFile.getAbsolutePath());
}
String name = inputFile.getName().toLowerCase();
String[] components = name.split("\\.");
@@ -278,7 +279,7 @@ public static RecordInfo readRecordInfo(@NonNull final File inputFile)
MediaFormat format = null;
int i;
- extractor.setDataSource(inputFile.getPath());
+ extractor.setDataSource(canonicalFile.getPath());
int numTracks = extractor.getTrackCount();
// find and select the first audio track present in the file.
for (i = 0; i < numTracks; i++) {
diff --git a/app/src/main/java/com/vdo/frecorder/audio/recorder/WavRecorder.java b/app/src/main/java/com/vdo/frecorder/audio/recorder/WavRecorder.java
index 34f541ef..15f4bc51 100644
--- a/app/src/main/java/com/vdo/frecorder/audio/recorder/WavRecorder.java
+++ b/app/src/main/java/com/vdo/frecorder/audio/recorder/WavRecorder.java
@@ -67,6 +67,10 @@ public class WavRecorder implements RecorderContract.Recorder {
private int gainBoostLevel = AppConstants.GAIN_BOOST_OFF;
private volatile boolean monitoringEnabled = false;
private volatile boolean noiseReductionEnabled = false;
+ private volatile float noiseReductionDb = AppConstants.DEFAULT_NOISE_REDUCTION_DB;
+ private volatile float noiseReductionSensitivity = AppConstants.DEFAULT_NOISE_REDUCTION_SENSITIVITY;
+ private volatile int noiseReductionFreqSmoothing = AppConstants.DEFAULT_NOISE_REDUCTION_FREQ_SMOOTHING;
+ private volatile float noiseProfileSeconds = AppConstants.DEFAULT_NOISE_PROFILE_SECONDS;
private volatile int hpfMode = AppConstants.HPF_OFF;
private volatile int lpfMode = AppConstants.LPF_OFF;
@@ -292,10 +296,10 @@ public void stopRecording() {
new Thread(() -> {
boolean success = NoiseReducer.process(
fileToProcess,
- AppConstants.DEFAULT_NOISE_PROFILE_SECONDS,
- AppConstants.DEFAULT_NOISE_REDUCTION_DB,
- AppConstants.DEFAULT_NOISE_REDUCTION_SENSITIVITY,
- AppConstants.DEFAULT_NOISE_REDUCTION_FREQ_SMOOTHING,
+ noiseProfileSeconds,
+ noiseReductionDb,
+ noiseReductionSensitivity,
+ noiseReductionFreqSmoothing,
progress -> {
if (noiseReductionListener != null) {
AndroidUtils.runOnUIThread(() -> noiseReductionListener.onNoiseReductionProgress(progress));
@@ -612,6 +616,22 @@ public boolean isNoiseReductionEnabled() {
return noiseReductionEnabled;
}
+ public void setNoiseReductionDb(float db) {
+ this.noiseReductionDb = db;
+ }
+
+ public void setNoiseReductionSensitivity(float sensitivity) {
+ this.noiseReductionSensitivity = sensitivity;
+ }
+
+ public void setNoiseReductionFreqSmoothing(int bands) {
+ this.noiseReductionFreqSmoothing = bands;
+ }
+
+ public void setNoiseProfileSeconds(float seconds) {
+ this.noiseProfileSeconds = seconds;
+ }
+
public void setHpfMode(int mode) {
this.hpfMode = mode;
}
diff --git a/app/src/main/java/com/vdo/frecorder/data/Prefs.java b/app/src/main/java/com/vdo/frecorder/data/Prefs.java
index f8766755..57fc4f24 100644
--- a/app/src/main/java/com/vdo/frecorder/data/Prefs.java
+++ b/app/src/main/java/com/vdo/frecorder/data/Prefs.java
@@ -87,6 +87,18 @@ public interface Prefs {
void setNoiseReductionEnabled(boolean enabled);
boolean isNoiseReductionEnabled();
+ void setNoiseReductionDb(float db);
+ float getNoiseReductionDb();
+
+ void setNoiseReductionSensitivity(float sensitivity);
+ float getNoiseReductionSensitivity();
+
+ void setNoiseReductionFreqSmoothing(int bands);
+ int getNoiseReductionFreqSmoothing();
+
+ void setNoiseProfileSeconds(float seconds);
+ float getNoiseProfileSeconds();
+
void setHpfMode(int mode);
int getHpfMode();
diff --git a/app/src/main/java/com/vdo/frecorder/data/PrefsImpl.java b/app/src/main/java/com/vdo/frecorder/data/PrefsImpl.java
index f26628f8..74380ad0 100644
--- a/app/src/main/java/com/vdo/frecorder/data/PrefsImpl.java
+++ b/app/src/main/java/com/vdo/frecorder/data/PrefsImpl.java
@@ -59,6 +59,10 @@ public class PrefsImpl implements Prefs {
private static final String PREF_KEY_SETTING_AUDIO_SOURCE = "setting_audio_source";
private static final String PREF_KEY_GAIN_BOOST_LEVEL = "gain_boost_level";
private static final String PREF_KEY_NOISE_REDUCTION_ENABLED = "noise_reduction_enabled";
+ private static final String PREF_KEY_NOISE_REDUCTION_DB = "noise_reduction_db";
+ private static final String PREF_KEY_NOISE_REDUCTION_SENSITIVITY = "noise_reduction_sensitivity";
+ private static final String PREF_KEY_NOISE_REDUCTION_FREQ_SMOOTHING = "noise_reduction_freq_smoothing";
+ private static final String PREF_KEY_NOISE_PROFILE_SECONDS = "noise_profile_seconds";
private static final String PREF_KEY_HPF_MODE = "hpf_mode";
private static final String PREF_KEY_LPF_MODE = "lpf_mode";
private static final String PREF_KEY_NOISE_GATE_ENABLED = "noise_gate_enabled";
@@ -460,6 +464,54 @@ public boolean isNoiseReductionEnabled() {
return sharedPreferences.getBoolean(PREF_KEY_NOISE_REDUCTION_ENABLED, AppConstants.DEFAULT_NOISE_REDUCTION_ENABLED);
}
+ @Override
+ public void setNoiseReductionDb(float db) {
+ SharedPreferences.Editor editor = sharedPreferences.edit();
+ editor.putFloat(PREF_KEY_NOISE_REDUCTION_DB, db);
+ editor.apply();
+ }
+
+ @Override
+ public float getNoiseReductionDb() {
+ return sharedPreferences.getFloat(PREF_KEY_NOISE_REDUCTION_DB, AppConstants.DEFAULT_NOISE_REDUCTION_DB);
+ }
+
+ @Override
+ public void setNoiseReductionSensitivity(float sensitivity) {
+ SharedPreferences.Editor editor = sharedPreferences.edit();
+ editor.putFloat(PREF_KEY_NOISE_REDUCTION_SENSITIVITY, sensitivity);
+ editor.apply();
+ }
+
+ @Override
+ public float getNoiseReductionSensitivity() {
+ return sharedPreferences.getFloat(PREF_KEY_NOISE_REDUCTION_SENSITIVITY, AppConstants.DEFAULT_NOISE_REDUCTION_SENSITIVITY);
+ }
+
+ @Override
+ public void setNoiseReductionFreqSmoothing(int bands) {
+ SharedPreferences.Editor editor = sharedPreferences.edit();
+ editor.putInt(PREF_KEY_NOISE_REDUCTION_FREQ_SMOOTHING, bands);
+ editor.apply();
+ }
+
+ @Override
+ public int getNoiseReductionFreqSmoothing() {
+ return sharedPreferences.getInt(PREF_KEY_NOISE_REDUCTION_FREQ_SMOOTHING, AppConstants.DEFAULT_NOISE_REDUCTION_FREQ_SMOOTHING);
+ }
+
+ @Override
+ public void setNoiseProfileSeconds(float seconds) {
+ SharedPreferences.Editor editor = sharedPreferences.edit();
+ editor.putFloat(PREF_KEY_NOISE_PROFILE_SECONDS, seconds);
+ editor.apply();
+ }
+
+ @Override
+ public float getNoiseProfileSeconds() {
+ return sharedPreferences.getFloat(PREF_KEY_NOISE_PROFILE_SECONDS, AppConstants.DEFAULT_NOISE_PROFILE_SECONDS);
+ }
+
@Override
public void setHpfMode(int mode) {
SharedPreferences.Editor editor = sharedPreferences.edit();
@@ -508,6 +560,10 @@ public void resetSettings() {
editor.putInt(PREF_KEY_SETTING_AUDIO_SOURCE, AppConstants.DEFAULT_AUDIO_SOURCE);
editor.putInt(PREF_KEY_GAIN_BOOST_LEVEL, AppConstants.DEFAULT_GAIN_BOOST_LEVEL);
editor.putBoolean(PREF_KEY_NOISE_REDUCTION_ENABLED, AppConstants.DEFAULT_NOISE_REDUCTION_ENABLED);
+ editor.putFloat(PREF_KEY_NOISE_REDUCTION_DB, AppConstants.DEFAULT_NOISE_REDUCTION_DB);
+ editor.putFloat(PREF_KEY_NOISE_REDUCTION_SENSITIVITY, AppConstants.DEFAULT_NOISE_REDUCTION_SENSITIVITY);
+ editor.putInt(PREF_KEY_NOISE_REDUCTION_FREQ_SMOOTHING, AppConstants.DEFAULT_NOISE_REDUCTION_FREQ_SMOOTHING);
+ editor.putFloat(PREF_KEY_NOISE_PROFILE_SECONDS, AppConstants.DEFAULT_NOISE_PROFILE_SECONDS);
editor.putInt(PREF_KEY_HPF_MODE, AppConstants.DEFAULT_HPF_MODE);
editor.putInt(PREF_KEY_LPF_MODE, AppConstants.DEFAULT_LPF_MODE);
editor.putBoolean(PREF_KEY_NOISE_GATE_ENABLED, AppConstants.DEFAULT_NOISE_GATE_ENABLED);
diff --git a/app/src/main/java/com/vdo/frecorder/util/FileUtil.java b/app/src/main/java/com/vdo/frecorder/util/FileUtil.java
index 65b2ff85..49a101bd 100755
--- a/app/src/main/java/com/vdo/frecorder/util/FileUtil.java
+++ b/app/src/main/java/com/vdo/frecorder/util/FileUtil.java
@@ -60,6 +60,40 @@ public class FileUtil {
private FileUtil() {
}
+ /**
+ * Sanitize a file name by removing path separators and traversal sequences.
+ * This prevents path traversal attacks when the name comes from uncontrolled input.
+ */
+ public static String sanitizeFileName(String name) {
+ if (name == null) return null;
+ // Remove any path separators and parent-directory references
+ String sanitized = name.replace("..", "")
+ .replace(File.separator, "")
+ .replace("/", "")
+ .replace("\\", "")
+ .replace("\0", "");
+ if (sanitized.isEmpty()) {
+ return null;
+ }
+ return sanitized;
+ }
+
+ /**
+ * Validate that the given file's canonical path is under the expected parent directory.
+ * This prevents path traversal attacks.
+ * @return true if the file is safely within parentDir, false otherwise.
+ */
+ public static boolean isPathWithinDir(File file, File parentDir) {
+ try {
+ String canonicalFile = file.getCanonicalPath();
+ String canonicalParent = parentDir.getCanonicalPath() + File.separator;
+ return canonicalFile.startsWith(canonicalParent);
+ } catch (IOException e) {
+ Timber.e(e, "Failed to resolve canonical path");
+ return false;
+ }
+ }
+
public static File getAppDir() {
return getStorageDir(AppConstants.APPLICATION_NAME);
}
@@ -240,11 +274,15 @@ public static boolean copyFile(File fileToCopy, File newFile, FileOnCopyListener
* @return true if copy succeed, otherwise - false.
*/
public static boolean copyFile(FileDescriptor fileToCopy, File newFile) throws IOException {
+ if (newFile.getParentFile() != null && !isPathWithinDir(newFile, newFile.getParentFile())) {
+ Log.e(LOG_TAG, "Path traversal detected in copyFile, rejecting: " + newFile.getPath());
+ return false;
+ }
FileInputStream in = null;
FileOutputStream out = null;
try {
in = new FileInputStream(fileToCopy);
- out = new FileOutputStream(newFile);
+ out = new FileOutputStream(newFile.getCanonicalFile());
if (copyLarge(in, out) > 0) {
return true;
@@ -377,9 +415,18 @@ public static boolean externalMemoryAvailable() {
*/
public static File createFile(File path, String fileName) {
if (path != null) {
+ String safeName = sanitizeFileName(fileName);
+ if (safeName == null) {
+ Log.e(LOG_TAG, "Invalid file name after sanitization: " + fileName);
+ return null;
+ }
createDir(path);
- Log.d(LOG_TAG, "createFile path = " + path.getAbsolutePath() + " fileName = " + fileName);
- File file = new File(path, fileName);
+ Log.d(LOG_TAG, "createFile path = " + path.getAbsolutePath() + " fileName = " + safeName);
+ File file = new File(path, safeName);
+ if (!isPathWithinDir(file, path)) {
+ Log.e(LOG_TAG, "Path traversal detected, rejecting file: " + safeName);
+ return null;
+ }
//Create file if need.
if (!file.exists()) {
try {
@@ -396,7 +443,7 @@ public static File createFile(File path, String fileName) {
Log.e(LOG_TAG, "File already exists!! Please rename file!");
Log.i(LOG_TAG, "Renaming file");
// TODO: Find better way to rename file.
- return createFile(path, "1" + fileName);
+ return createFile(path, "1" + safeName);
}
if (!file.canWrite()) {
Log.e(LOG_TAG, "The file can not be written.");
diff --git a/app/src/main/res/layout/activity_settings.xml b/app/src/main/res/layout/activity_settings.xml
index 630be93b..255ef814 100644
--- a/app/src/main/res/layout/activity_settings.xml
+++ b/app/src/main/res/layout/activity_settings.xml
@@ -384,6 +384,20 @@
android:layout_weight="1"
android:text="@string/noise_reduction_on_save" />
+
+
Feedback warning
The microphone input and speaker output are the same device. This will cause audio feedback.\n\nConnect headphones or an external audio device to use monitoring safely.
Noise reduction on save (WAV)
+ Noise reduction settings
+ Configure
+ Reduction (dB): %.1f
+ Sensitivity: %.1f
+ Freq smoothing (bands): %d
+ Noise profile (seconds): %.1f
Noise reduction on
Noise reduction off
Applying noise reduction…