openaudiofix 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. openaudiofix-0.1.0/LICENSE +21 -0
  2. openaudiofix-0.1.0/PKG-INFO +168 -0
  3. openaudiofix-0.1.0/README.md +147 -0
  4. openaudiofix-0.1.0/openaudiofix/__init__.py +2 -0
  5. openaudiofix-0.1.0/openaudiofix/__main__.py +5 -0
  6. openaudiofix-0.1.0/openaudiofix/algorithms/__init__.py +0 -0
  7. openaudiofix-0.1.0/openaudiofix/algorithms/align.py +39 -0
  8. openaudiofix-0.1.0/openaudiofix/algorithms/aspade.py +114 -0
  9. openaudiofix-0.1.0/openaudiofix/algorithms/declick.py +164 -0
  10. openaudiofix-0.1.0/openaudiofix/algorithms/declick_detect.py +179 -0
  11. openaudiofix-0.1.0/openaudiofix/algorithms/declip.py +261 -0
  12. openaudiofix-0.1.0/openaudiofix/algorithms/phase_warp.py +312 -0
  13. openaudiofix-0.1.0/openaudiofix/algorithms/v3methods/__init__.py +0 -0
  14. openaudiofix-0.1.0/openaudiofix/algorithms/v3methods/ar48_ls.py +21 -0
  15. openaudiofix-0.1.0/openaudiofix/algorithms/v3methods/ar_bidir_xfade.py +297 -0
  16. openaudiofix-0.1.0/openaudiofix/algorithms/v3methods/ar_bidir_xfade_js.py +3 -0
  17. openaudiofix-0.1.0/openaudiofix/algorithms/v3methods/ar_bidir_xfade_ls.py +26 -0
  18. openaudiofix-0.1.0/openaudiofix/algorithms/v3methods/ar_ls_excite.py +365 -0
  19. openaudiofix-0.1.0/openaudiofix/algorithms/v3methods/ar_ls_excite_js.py +3 -0
  20. openaudiofix-0.1.0/openaudiofix/algorithms/v3methods/ensemble_select.py +74 -0
  21. openaudiofix-0.1.0/openaudiofix/algorithms/v3methods/join_smooth.py +34 -0
  22. openaudiofix-0.1.0/openaudiofix/algorithms/v3methods/pitch_wsola.py +420 -0
  23. openaudiofix-0.1.0/openaudiofix/algorithms/wavio.py +95 -0
  24. openaudiofix-0.1.0/openaudiofix/chain.py +63 -0
  25. openaudiofix-0.1.0/openaudiofix/cli.py +274 -0
  26. openaudiofix-0.1.0/openaudiofix/errors.py +36 -0
  27. openaudiofix-0.1.0/openaudiofix/gpu-requirements.txt +47 -0
  28. openaudiofix-0.1.0/openaudiofix/gpu.py +204 -0
  29. openaudiofix-0.1.0/openaudiofix/io.py +173 -0
  30. openaudiofix-0.1.0/openaudiofix/modules/__init__.py +44 -0
  31. openaudiofix-0.1.0/openaudiofix/modules/base.py +82 -0
  32. openaudiofix-0.1.0/openaudiofix/modules/declip.py +41 -0
  33. openaudiofix-0.1.0/openaudiofix/modules/denoise.py +131 -0
  34. openaudiofix-0.1.0/openaudiofix/modules/glitch.py +43 -0
  35. openaudiofix-0.1.0/openaudiofix/modules/speech.py +81 -0
  36. openaudiofix-0.1.0/openaudiofix/paths.py +60 -0
  37. openaudiofix-0.1.0/openaudiofix/report.py +30 -0
  38. openaudiofix-0.1.0/openaudiofix/speech.py +538 -0
  39. openaudiofix-0.1.0/openaudiofix/synth.py +69 -0
  40. openaudiofix-0.1.0/openaudiofix.egg-info/PKG-INFO +168 -0
  41. openaudiofix-0.1.0/openaudiofix.egg-info/SOURCES.txt +74 -0
  42. openaudiofix-0.1.0/openaudiofix.egg-info/dependency_links.txt +1 -0
  43. openaudiofix-0.1.0/openaudiofix.egg-info/entry_points.txt +2 -0
  44. openaudiofix-0.1.0/openaudiofix.egg-info/requires.txt +13 -0
  45. openaudiofix-0.1.0/openaudiofix.egg-info/top_level.txt +2 -0
  46. openaudiofix-0.1.0/pyproject.toml +37 -0
  47. openaudiofix-0.1.0/server/__init__.py +0 -0
  48. openaudiofix-0.1.0/server/app.py +1020 -0
  49. openaudiofix-0.1.0/server/db.py +276 -0
  50. openaudiofix-0.1.0/server/errors.py +26 -0
  51. openaudiofix-0.1.0/server/fsbrowse.py +100 -0
  52. openaudiofix-0.1.0/server/gpu_manager.py +313 -0
  53. openaudiofix-0.1.0/server/gpu_selftest.py +72 -0
  54. openaudiofix-0.1.0/server/launcher.py +93 -0
  55. openaudiofix-0.1.0/server/locations.py +120 -0
  56. openaudiofix-0.1.0/server/migrate.py +80 -0
  57. openaudiofix-0.1.0/server/models.py +67 -0
  58. openaudiofix-0.1.0/server/previews.py +59 -0
  59. openaudiofix-0.1.0/server/ranges.py +71 -0
  60. openaudiofix-0.1.0/server/storage.py +155 -0
  61. openaudiofix-0.1.0/server/visual.py +106 -0
  62. openaudiofix-0.1.0/server/worker.py +359 -0
  63. openaudiofix-0.1.0/setup.cfg +4 -0
  64. openaudiofix-0.1.0/tests/test_chain.py +117 -0
  65. openaudiofix-0.1.0/tests/test_cli.py +94 -0
  66. openaudiofix-0.1.0/tests/test_declip_module.py +36 -0
  67. openaudiofix-0.1.0/tests/test_denoise_module.py +91 -0
  68. openaudiofix-0.1.0/tests/test_docker.py +75 -0
  69. openaudiofix-0.1.0/tests/test_glitch_module.py +55 -0
  70. openaudiofix-0.1.0/tests/test_gpu_install.py +126 -0
  71. openaudiofix-0.1.0/tests/test_gpu_selftest.py +93 -0
  72. openaudiofix-0.1.0/tests/test_io.py +249 -0
  73. openaudiofix-0.1.0/tests/test_module_base.py +66 -0
  74. openaudiofix-0.1.0/tests/test_speech.py +332 -0
  75. openaudiofix-0.1.0/tests/test_speech_module.py +204 -0
  76. openaudiofix-0.1.0/tests/test_synth.py +32 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 OpenAudioFix contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,168 @@
1
+ Metadata-Version: 2.4
2
+ Name: openaudiofix
3
+ Version: 0.1.0
4
+ Summary: Self-hosted audio repair studio: buffer-dropout glitches, clipping, background noise.
5
+ License: MIT
6
+ Requires-Python: <3.13,>=3.10
7
+ Description-Content-Type: text/markdown
8
+ License-File: LICENSE
9
+ Requires-Dist: numpy==1.26.4
10
+ Requires-Dist: scipy==1.15.3
11
+ Requires-Dist: soundfile==0.13.1
12
+ Requires-Dist: onnxruntime==1.20.1
13
+ Provides-Extra: server
14
+ Requires-Dist: fastapi==0.115.6; extra == "server"
15
+ Requires-Dist: uvicorn==0.34.0; extra == "server"
16
+ Requires-Dist: python-multipart==0.0.20; extra == "server"
17
+ Provides-Extra: dev
18
+ Requires-Dist: pytest==8.3.4; extra == "dev"
19
+ Requires-Dist: httpx==0.28.1; extra == "dev"
20
+ Dynamic: license-file
21
+
22
+ <div align="center">
23
+ <img src="docs/assets/logo.svg" width="84" alt="">
24
+ <h1>OpenAudioFix</h1>
25
+ <p><strong>Repair ruined recordings on your own computer.</strong><br>
26
+ Rebuild clipped peaks, fix dropouts, remove noise and keep only the speech, without touching the rest of your audio.</p>
27
+ <p>
28
+ <a href="https://romanticamaj.github.io/openaudiofix/"><strong>Hear the demo</strong></a> ·
29
+ <a href="#quick-start">Quick start</a> ·
30
+ <a href="docs/how-it-works.md">How it works</a> ·
31
+ <a href="README.zh-TW.md">繁體中文</a>
32
+ </p>
33
+ <p>
34
+ <a href="https://github.com/romanticamaj/openaudiofix/actions/workflows/ci.yml"><img alt="CI" src="https://github.com/romanticamaj/openaudiofix/actions/workflows/ci.yml/badge.svg"></a>
35
+ <a href="LICENSE"><img alt="MIT licence" src="https://img.shields.io/badge/license-MIT-8b7cf6"></a>
36
+ <a href="https://github.com/romanticamaj/openaudiofix/releases"><img alt="Latest release" src="https://img.shields.io/github/v/release/romanticamaj/openaudiofix?color=8b7cf6"></a>
37
+ </p>
38
+ </div>
39
+
40
+ <a href="https://romanticamaj.github.io/openaudiofix/"><img src="docs/screenshots/results.png" alt="The OpenAudioFix results workspace: a waveform with every fix marked, a spectrogram, and the list of repairs"></a>
41
+
42
+ ## Why OpenAudioFix
43
+
44
+ You recorded the perfect take, then found the clipping. Or the ticks from an interface that dropped samples. Most
45
+ tools answer by regenerating the whole file. OpenAudioFix fixes the broken samples and leaves the rest alone.
46
+
47
+ - **Only what is broken changes.** In the demo clip, 94.5% of the samples are exactly as recorded.
48
+ - **You can check every fix.** Each repair is listed and marked on the waveform. Switch between the original, each
49
+ step and the difference on the same word.
50
+ - **Your audio stays with you.** No account, no upload. It runs on your own machine.
51
+
52
+ <img src="docs/assets/peaks.svg" alt="18 milliseconds of a real recording. Before: peaks cut flat at full scale. After: the peaks rebuilt.">
53
+
54
+ **[Listen to it →](https://romanticamaj.github.io/openaudiofix/)** Ten seconds of a real recording with 228 clipped
55
+ peaks and 3 dropouts. Switch between before, after and difference while it plays.
56
+
57
+ ## What it repairs
58
+
59
+ | Module | Fixes | Needs |
60
+ |---|---|---|
61
+ | **Declip** | Peaks flattened by too much input gain | CPU |
62
+ | **Glitch repair** | Ticks and skips from audio-interface buffer dropouts | CPU |
63
+ | **Keep speech only** | Removes knocks, music and silence from a podcast track, and keeps what it removed on a second track | CPU |
64
+ | **Background repair** | Noise and room sound, with MossFormer2 | NVIDIA GPU, optional |
65
+
66
+ ## Quick start
67
+
68
+ Start it with Docker, then open <http://127.0.0.1:8765/>:
69
+
70
+ ```bash
71
+ docker run -p 127.0.0.1:8765:8765 -v openaudiofix-data:/data ghcr.io/romanticamaj/openaudiofix:0.1.0
72
+ ```
73
+
74
+ Drop in a file, choose the repairs, listen, export. Any format ffmpeg reads is accepted, and export gives you a WAV.
75
+
76
+ <details>
77
+ <summary><strong>With an NVIDIA GPU (adds Background repair)</strong></summary>
78
+
79
+ You need the NVIDIA driver and the NVIDIA Container Toolkit. The image is about 9 GB because it carries PyTorch and
80
+ the model.
81
+
82
+ ```bash
83
+ docker run --gpus all -p 127.0.0.1:8765:8765 -v openaudiofix-data:/data ghcr.io/romanticamaj/openaudiofix-gpu:0.1.0
84
+ ```
85
+
86
+ Both images use the same data volume, so switching keeps your projects.
87
+ </details>
88
+
89
+ <details>
90
+ <summary><strong>Without Docker</strong></summary>
91
+
92
+ You need Python 3.10 to 3.12, Node.js and ffmpeg on `PATH`.
93
+
94
+ ```bash
95
+ git clone https://github.com/romanticamaj/openaudiofix && cd openaudiofix
96
+ python -m venv .venv && . .venv/bin/activate # Windows: .venv\Scripts\activate
97
+ pip install -e ".[server]"
98
+ cd web && npm ci && npm run build && cd ..
99
+ AFIX_WEB_DIST=web/dist python -m server.launcher # Windows: set AFIX_WEB_DIST=web\dist first
100
+ ```
101
+
102
+ Open **Modules & models → Background repair** to install the GPU module from the app (about 6 GB).
103
+ </details>
104
+
105
+ <details>
106
+ <summary><strong>Build the images yourself</strong></summary>
107
+
108
+ ```bash
109
+ git clone https://github.com/romanticamaj/openaudiofix && cd openaudiofix
110
+ docker compose up -d --build
111
+ docker compose -f docker-compose.yml -f docker-compose.gpu.yml up -d --build # GPU version
112
+ ```
113
+ </details>
114
+
115
+ ## Command line
116
+
117
+ ```bash
118
+ pip install openaudiofix
119
+ afix scan interview.wav # report what was found, change nothing
120
+ afix repair interview.wav -m glitch,declip # repaired file, difference file and a CSV of every fix
121
+ afix repair interview.wav -m declip --float # 32-bit float, no gain reduction
122
+ afix speech episode.wav # episode_speech.wav, episode_removed.wav, episode_removed.txt
123
+ afix speech host.wav guest.wav --out-dir edit # tracks of one session are split together
124
+ ```
125
+
126
+ You need ffmpeg on `PATH`. `afix --help` lists every option. The web app runs from Docker or a clone.
127
+
128
+ ## Screenshots
129
+
130
+ <table>
131
+ <tr>
132
+ <td><img src="docs/screenshots/modules.png" alt="Detection results and module choice"></td>
133
+ <td><img src="docs/screenshots/export.png" alt="The export dialog"></td>
134
+ </tr>
135
+ <tr>
136
+ <td align="center">Scan first, then choose what to repair</td>
137
+ <td align="center">Export without overwriting anything</td>
138
+ </tr>
139
+ </table>
140
+
141
+ ## How it compares
142
+
143
+ | | OpenAudioFix | Audacity | iZotope RX | Adobe Podcast |
144
+ |---|---|---|---|---|
145
+ | Open source | Yes (MIT) | Yes (GPL) | No | No |
146
+ | Price | Free | Free | Paid | Free tier, paid plans |
147
+ | Audio stays on your computer | Yes | Yes | Yes | No, it is uploaded |
148
+ | Lists every change | Yes | No | Partly | No |
149
+
150
+ RX has far more tools and years of refinement. OpenAudioFix does a few repairs, in the open, and shows its work.
151
+
152
+ ## Good to know
153
+
154
+ - **Peaks above 0 dBFS are expected.** Rebuilt peaks were above it before the clipping. Export offers a 32-bit float
155
+ file that keeps them, or your original bit depth lowered just enough to fit.
156
+ - **Lossy files work**, but repairs are better from WAV or FLAC.
157
+ - **It listens on `127.0.0.1` only.** To use your own hostname, set `AFIX_ALLOWED_HOSTS`. See
158
+ [configuration](docs/configuration.md), which also covers folders and environment variables.
159
+ - **Unrelated names.** This project has no connection to the iOS and macOS app "AudioFix" or to the npm package `afix`.
160
+
161
+ ## Contributing
162
+
163
+ Bug reports with a short sample of the audio help most. New repair modules and UI languages are welcome: see
164
+ [CONTRIBUTING.md](CONTRIBUTING.md). Every bug that gets fixed gets a test.
165
+
166
+ ## License
167
+
168
+ [MIT](LICENSE). Third-party components are listed in [THIRD_PARTY.md](THIRD_PARTY.md).
@@ -0,0 +1,147 @@
1
+ <div align="center">
2
+ <img src="docs/assets/logo.svg" width="84" alt="">
3
+ <h1>OpenAudioFix</h1>
4
+ <p><strong>Repair ruined recordings on your own computer.</strong><br>
5
+ Rebuild clipped peaks, fix dropouts, remove noise and keep only the speech, without touching the rest of your audio.</p>
6
+ <p>
7
+ <a href="https://romanticamaj.github.io/openaudiofix/"><strong>Hear the demo</strong></a> ·
8
+ <a href="#quick-start">Quick start</a> ·
9
+ <a href="docs/how-it-works.md">How it works</a> ·
10
+ <a href="README.zh-TW.md">繁體中文</a>
11
+ </p>
12
+ <p>
13
+ <a href="https://github.com/romanticamaj/openaudiofix/actions/workflows/ci.yml"><img alt="CI" src="https://github.com/romanticamaj/openaudiofix/actions/workflows/ci.yml/badge.svg"></a>
14
+ <a href="LICENSE"><img alt="MIT licence" src="https://img.shields.io/badge/license-MIT-8b7cf6"></a>
15
+ <a href="https://github.com/romanticamaj/openaudiofix/releases"><img alt="Latest release" src="https://img.shields.io/github/v/release/romanticamaj/openaudiofix?color=8b7cf6"></a>
16
+ </p>
17
+ </div>
18
+
19
+ <a href="https://romanticamaj.github.io/openaudiofix/"><img src="docs/screenshots/results.png" alt="The OpenAudioFix results workspace: a waveform with every fix marked, a spectrogram, and the list of repairs"></a>
20
+
21
+ ## Why OpenAudioFix
22
+
23
+ You recorded the perfect take, then found the clipping. Or the ticks from an interface that dropped samples. Most
24
+ tools answer by regenerating the whole file. OpenAudioFix fixes the broken samples and leaves the rest alone.
25
+
26
+ - **Only what is broken changes.** In the demo clip, 94.5% of the samples are exactly as recorded.
27
+ - **You can check every fix.** Each repair is listed and marked on the waveform. Switch between the original, each
28
+ step and the difference on the same word.
29
+ - **Your audio stays with you.** No account, no upload. It runs on your own machine.
30
+
31
+ <img src="docs/assets/peaks.svg" alt="18 milliseconds of a real recording. Before: peaks cut flat at full scale. After: the peaks rebuilt.">
32
+
33
+ **[Listen to it →](https://romanticamaj.github.io/openaudiofix/)** Ten seconds of a real recording with 228 clipped
34
+ peaks and 3 dropouts. Switch between before, after and difference while it plays.
35
+
36
+ ## What it repairs
37
+
38
+ | Module | Fixes | Needs |
39
+ |---|---|---|
40
+ | **Declip** | Peaks flattened by too much input gain | CPU |
41
+ | **Glitch repair** | Ticks and skips from audio-interface buffer dropouts | CPU |
42
+ | **Keep speech only** | Removes knocks, music and silence from a podcast track, and keeps what it removed on a second track | CPU |
43
+ | **Background repair** | Noise and room sound, with MossFormer2 | NVIDIA GPU, optional |
44
+
45
+ ## Quick start
46
+
47
+ Start it with Docker, then open <http://127.0.0.1:8765/>:
48
+
49
+ ```bash
50
+ docker run -p 127.0.0.1:8765:8765 -v openaudiofix-data:/data ghcr.io/romanticamaj/openaudiofix:0.1.0
51
+ ```
52
+
53
+ Drop in a file, choose the repairs, listen, export. Any format ffmpeg reads is accepted, and export gives you a WAV.
54
+
55
+ <details>
56
+ <summary><strong>With an NVIDIA GPU (adds Background repair)</strong></summary>
57
+
58
+ You need the NVIDIA driver and the NVIDIA Container Toolkit. The image is about 9 GB because it carries PyTorch and
59
+ the model.
60
+
61
+ ```bash
62
+ docker run --gpus all -p 127.0.0.1:8765:8765 -v openaudiofix-data:/data ghcr.io/romanticamaj/openaudiofix-gpu:0.1.0
63
+ ```
64
+
65
+ Both images use the same data volume, so switching keeps your projects.
66
+ </details>
67
+
68
+ <details>
69
+ <summary><strong>Without Docker</strong></summary>
70
+
71
+ You need Python 3.10 to 3.12, Node.js and ffmpeg on `PATH`.
72
+
73
+ ```bash
74
+ git clone https://github.com/romanticamaj/openaudiofix && cd openaudiofix
75
+ python -m venv .venv && . .venv/bin/activate # Windows: .venv\Scripts\activate
76
+ pip install -e ".[server]"
77
+ cd web && npm ci && npm run build && cd ..
78
+ AFIX_WEB_DIST=web/dist python -m server.launcher # Windows: set AFIX_WEB_DIST=web\dist first
79
+ ```
80
+
81
+ Open **Modules & models → Background repair** to install the GPU module from the app (about 6 GB).
82
+ </details>
83
+
84
+ <details>
85
+ <summary><strong>Build the images yourself</strong></summary>
86
+
87
+ ```bash
88
+ git clone https://github.com/romanticamaj/openaudiofix && cd openaudiofix
89
+ docker compose up -d --build
90
+ docker compose -f docker-compose.yml -f docker-compose.gpu.yml up -d --build # GPU version
91
+ ```
92
+ </details>
93
+
94
+ ## Command line
95
+
96
+ ```bash
97
+ pip install openaudiofix
98
+ afix scan interview.wav # report what was found, change nothing
99
+ afix repair interview.wav -m glitch,declip # repaired file, difference file and a CSV of every fix
100
+ afix repair interview.wav -m declip --float # 32-bit float, no gain reduction
101
+ afix speech episode.wav # episode_speech.wav, episode_removed.wav, episode_removed.txt
102
+ afix speech host.wav guest.wav --out-dir edit # tracks of one session are split together
103
+ ```
104
+
105
+ You need ffmpeg on `PATH`. `afix --help` lists every option. The web app runs from Docker or a clone.
106
+
107
+ ## Screenshots
108
+
109
+ <table>
110
+ <tr>
111
+ <td><img src="docs/screenshots/modules.png" alt="Detection results and module choice"></td>
112
+ <td><img src="docs/screenshots/export.png" alt="The export dialog"></td>
113
+ </tr>
114
+ <tr>
115
+ <td align="center">Scan first, then choose what to repair</td>
116
+ <td align="center">Export without overwriting anything</td>
117
+ </tr>
118
+ </table>
119
+
120
+ ## How it compares
121
+
122
+ | | OpenAudioFix | Audacity | iZotope RX | Adobe Podcast |
123
+ |---|---|---|---|---|
124
+ | Open source | Yes (MIT) | Yes (GPL) | No | No |
125
+ | Price | Free | Free | Paid | Free tier, paid plans |
126
+ | Audio stays on your computer | Yes | Yes | Yes | No, it is uploaded |
127
+ | Lists every change | Yes | No | Partly | No |
128
+
129
+ RX has far more tools and years of refinement. OpenAudioFix does a few repairs, in the open, and shows its work.
130
+
131
+ ## Good to know
132
+
133
+ - **Peaks above 0 dBFS are expected.** Rebuilt peaks were above it before the clipping. Export offers a 32-bit float
134
+ file that keeps them, or your original bit depth lowered just enough to fit.
135
+ - **Lossy files work**, but repairs are better from WAV or FLAC.
136
+ - **It listens on `127.0.0.1` only.** To use your own hostname, set `AFIX_ALLOWED_HOSTS`. See
137
+ [configuration](docs/configuration.md), which also covers folders and environment variables.
138
+ - **Unrelated names.** This project has no connection to the iOS and macOS app "AudioFix" or to the npm package `afix`.
139
+
140
+ ## Contributing
141
+
142
+ Bug reports with a short sample of the audio help most. New repair modules and UI languages are welcome: see
143
+ [CONTRIBUTING.md](CONTRIBUTING.md). Every bug that gets fixed gets a test.
144
+
145
+ ## License
146
+
147
+ [MIT](LICENSE). Third-party components are listed in [THIRD_PARTY.md](THIRD_PARTY.md).
@@ -0,0 +1,2 @@
1
+ """OpenAudioFix: self-hosted audio repair (glitch, declip, background noise)."""
2
+ __version__ = "0.1.0"
@@ -0,0 +1,5 @@
1
+ import sys
2
+
3
+ from .cli import main
4
+
5
+ sys.exit(main())
File without changes
@@ -0,0 +1,39 @@
1
+ """Resampling and fixed-latency alignment for model outputs (copied from separation/runners/_common.py)."""
2
+ import numpy as np
3
+
4
+ MAX_LAG = 4096 # alignment search range (+-samples); the model delay is usually within a few hundred samples
5
+
6
+
7
+ def resample(x, sr_in, sr_out):
8
+ if sr_in == sr_out:
9
+ return x
10
+ from scipy.signal import resample_poly
11
+ from math import gcd
12
+ g = gcd(sr_in, sr_out)
13
+ return resample_poly(x, sr_out // g, sr_in // g).astype(np.float32)
14
+
15
+
16
+ def align_to(ref, y):
17
+ """Align the model output y to the original ref: compensate a fixed delay and trim to equal length.
18
+ Returns (y_aligned, lag). lag>0 means y is lag samples later than ref. Found by cross-correlating the whole
19
+ signal; speech-enhancement models do not change the time axis, so there should be only one fixed delay, and a
20
+ peak that is not prominent enough is treated as 0 (no spurious shifting)."""
21
+ n = min(len(ref), len(y))
22
+ a = ref[:n].astype(np.float64); b = y[:n].astype(np.float64)
23
+ L = 1 << int(np.ceil(np.log2(2 * n)))
24
+ A = np.fft.rfft(a, L); B = np.fft.rfft(b, L)
25
+ c = np.fft.irfft(np.conj(A) * B, L)
26
+ cand = np.r_[c[:MAX_LAG + 1], c[-MAX_LAG:]]
27
+ lags = np.r_[np.arange(0, MAX_LAG + 1), np.arange(-MAX_LAG, 0)]
28
+ k = int(np.argmax(cand))
29
+ lag = int(lags[k])
30
+ if cand[k] < 3 * np.median(np.abs(cand)):
31
+ lag = 0
32
+ if lag > 0:
33
+ y = np.r_[y[lag:], np.zeros(lag, np.float32)]
34
+ elif lag < 0:
35
+ y = np.r_[np.zeros(-lag, np.float32), y[:lag]]
36
+ if len(y) < len(ref):
37
+ y = np.r_[y, np.zeros(len(ref) - len(y), np.float32)]
38
+ return y[:len(ref)].astype(np.float32), lag
39
+
@@ -0,0 +1,114 @@
1
+ """A-SPADE (analysis sparse audio declipper), for a direct comparison with the whole-frame joint AR
2
+ (declip.declip_joint).
3
+
4
+ Algorithm: Kitić, Bertin, Gribonval 2015; ADMM form and parameters follow Záviška et al. 2019/2021.
5
+ Each frame (after windowing) solves
6
+ min ||A x||_0 s.t. x ∈ Γ
7
+ A = 2x-redundant DFT (Parseval tight frame: A^H A = I), Γ = clipping-consistent set:
8
+ reliable samples x = y; positive clipping x ≥ y; negative clipping x ≤ y (y is the windowed observation).
9
+ ADMM:
10
+ z = H_k(A x + u) hard threshold, keep only the k largest coefficients
11
+ x = P_Γ(A^H (z − u)) project back onto the consistent set
12
+ stop if ||A x − z|| ≤ ε, otherwise u += A x − z, and every r iterations k += s
13
+ The fundamental difference from the AR family: the prior is "time-frequency sparsity", not "linear predictability".
14
+ For periodic speech, harmonics are already sparse in frequency, so in theory it suits dense clipping; the cost is many
15
+ iterations.
16
+
17
+ Frame synthesis: each frame is solved in the windowed domain (x_w ≈ g·x), multiplied by g again when overlap-adding,
18
+ and finally divided by Σg², which is correct for any overlap ratio.
19
+ Only frames containing clipping are processed; regions without clipping are unchanged sample by sample.
20
+ """
21
+ import numpy as np
22
+
23
+ # frame length (43 ms): speech is shorter than the music Záviška tested; start here, sweep later on the benchmark
24
+ W = 2048
25
+ HOP = 512 # 75% overlap
26
+ REDUND = 2 # DFT redundancy factor
27
+ S = 1 # sparsity relaxed at each step
28
+ R = 1 # relax once every R iterations
29
+ EPS = 0.1 # stopping criterion ||Ax − z|| (same as Záviška; relative to the scale of the windowed signal)
30
+ MAX_IT = 1000
31
+
32
+
33
+ def _A(x, n):
34
+ return np.fft.fft(x, n, norm="ortho")
35
+
36
+
37
+ def _AH(z, w):
38
+ return np.real(np.fft.ifft(z, norm="ortho"))[:w]
39
+
40
+
41
+ def _hard(z, k):
42
+ """Keep the k coefficients with the largest magnitude (conjugate pairs counted together: the threshold is decided
43
+ from the first half of the spectrum, which keeps the symmetry of a real signal)."""
44
+ a = np.abs(z)
45
+ if k >= len(a):
46
+ return z
47
+ thr = np.partition(a, len(a) - k)[len(a) - k]
48
+ return np.where(a >= thr, z, 0)
49
+
50
+
51
+ def aspade_frame(y, rel, cp, cn, eps=EPS, s=S, r=R, max_it=MAX_IT):
52
+ """y: windowed frame; rel/cp/cn: reliable / positive-clip / negative-clip masks.
53
+ Returns the reconstructed frame (in the windowed domain)."""
54
+ w = len(y); n = REDUND * w
55
+ x = y.copy(); u = np.zeros(n, dtype=complex)
56
+ k = s
57
+ for it in range(1, max_it + 1):
58
+ Ax = _A(x, n)
59
+ z = _hard(Ax + u, k)
60
+ v = _AH(z - u, w)
61
+ x = v.copy() # projection onto Γ
62
+ x[rel] = y[rel]
63
+ x[cp] = np.maximum(v[cp], y[cp])
64
+ x[cn] = np.minimum(v[cn], y[cn])
65
+ Ax = _A(x, n)
66
+ res = np.linalg.norm(Ax - z)
67
+ if res <= eps:
68
+ break
69
+ u = u + Ax - z
70
+ if it % r == 0:
71
+ k += 2 * s # 2 = conjugate pair; makes the "true" sparsity grow by s each time
72
+ return x, it
73
+
74
+
75
+ def declip_aspade(x, sr, thr_pos=None, thr_neg=None, w=W, hop=HOP, eps=EPS, max_it=MAX_IT, progress=None):
76
+ """Process the whole signal. If thr is not given, it is inferred with declip.estimate_threshold. Returns (y,
77
+ info)."""
78
+ x = np.asarray(x, dtype=np.float64)
79
+ if thr_pos is None:
80
+ from . import declip as _D
81
+ thr_pos, thr_neg, det = _D.estimate_threshold(x)
82
+ if not det:
83
+ return x.copy(), dict(detected=False)
84
+ cp_all = x >= thr_pos; cn_all = x <= thr_neg
85
+ clipped = cp_all | cn_all
86
+ g = np.hanning(w + 2)[1:-1]
87
+ num = np.zeros(len(x)); den = np.zeros(len(x))
88
+ its = []
89
+ starts = range(-w + hop, len(x), hop)
90
+ for st in starts:
91
+ a, b = st, st + w
92
+ a0, b0 = max(a, 0), min(b, len(x))
93
+ if not clipped[a0:b0].any():
94
+ continue
95
+ seg = np.zeros(w); seg[a0 - a:b0 - a] = x[a0:b0]
96
+ cp = np.zeros(w, bool); cn = np.zeros(w, bool)
97
+ cp[a0 - a:b0 - a] = cp_all[a0:b0]; cn[a0 - a:b0 - a] = cn_all[a0:b0]
98
+ rel = ~(cp | cn)
99
+ yw = seg * g
100
+ xr, it = aspade_frame(yw, rel, cp, cn, eps=eps, max_it=max_it)
101
+ its.append(it)
102
+ if progress is not None and len(its) % 25 == 0:
103
+ progress(max(st, 0), len(x))
104
+ num[a0:b0] += (xr * g)[a0 - a:b0 - a]
105
+ den[a0:b0] += (g * g)[a0 - a:b0 - a]
106
+ y = x.copy()
107
+ m = clipped & (den > 1e-9)
108
+ y[m] = num[m] / den[m]
109
+ # Consistency: reconstructed values must not fall back inside the threshold (numerical error at window edges can
110
+ # cause tiny violations)
111
+ y[cp_all] = np.maximum(y[cp_all], thr_pos)
112
+ y[cn_all] = np.minimum(y[cn_all], thr_neg)
113
+ return y, dict(detected=True, n_frames=len(its), iters_median=int(np.median(its)) if its else 0,
114
+ peak=float(np.abs(y).max()))
@@ -0,0 +1,164 @@
1
+ """Repair of phase jumps from dropped samples (single file): detect, then phase-aligned micro-warp / per-hole
2
+ interpolation, then verify the residue.
3
+
4
+ "Track" here corresponds to "channel" (mono=1, stereo=2).
5
+ Uses the same logic as the whole-file pipeline for a long real recording: adjacent/overlapping
6
+ seams are first merged into one discard zone and solved at once; after repair the detector is rerun, residues first
7
+ fall back to the safe path and, if that still fails, are widened, and every round reapplies all patches
8
+ from the original file. The detector is the generic single-file version (`declick_detect.py`).
9
+ """
10
+ import time
11
+ import numpy as np
12
+
13
+ from . import declick_detect as D, phase_warp
14
+
15
+ MAX_PASS = 4
16
+ CLUSTER_MARGIN = 330 # seams closer than this are merged into one discard zone and solved at once
17
+ RESID_NEAR = 200 # residual events within ± this range of an existing group count as the same spot
18
+ MAX_GAP = 960
19
+
20
+
21
+ def merge_groups(seams, margin=CLUSTER_MARGIN):
22
+ """seams: {p: {gap, za, zb, ...}} → merge nearby / overlapping windows [za-gap/2, zb+gap/2) into groups.
23
+ Returns a list of dict(za, zb, subs=[(a,b),...], positions=[p,...], force=False), sorted by position."""
24
+ items = []
25
+ for p, s in sorted(seams.items()):
26
+ half = s["gap"] // 2
27
+ a = min(s["za"] - half, s["za"] - 1)
28
+ b = max(s["zb"] + half, s["zb"] + 1)
29
+ if b - a < s["gap"]:
30
+ b = a + s["gap"]
31
+ items.append(dict(p=p, za=s["za"], zb=s["zb"], a=a, b=b, gap=s["gap"]))
32
+ groups = []
33
+ for it in items:
34
+ if groups and it["a"] <= groups[-1]["zb"] + margin:
35
+ g = groups[-1]
36
+ g["zb"] = max(g["zb"], it["zb"]); g["b"] = max(g["b"], it["b"])
37
+ g["subs"].append((it["a"], it["b"])); g["positions"].append(it["p"])
38
+ else:
39
+ groups.append(dict(za=it["za"], zb=it["zb"], a=it["a"], b=it["b"],
40
+ subs=[(it["a"], it["b"])], positions=[it["p"]]))
41
+ for g in groups:
42
+ g["force"] = any(seams[p].get("force", False) for p in g["positions"])
43
+ return groups
44
+
45
+
46
+ def run_pass(X, groups, sr, on_group=None):
47
+ """Call phase_warp.repair_joint on each group in turn; returns (new channel arrays, per-group repair records).
48
+ on_group(done, total): optional; reports progress after each batch of groups is repaired (does not affect the
49
+ result)."""
50
+ Y = [x.copy() for x in X]
51
+ records = []
52
+ prev_end = -10 ** 9
53
+ for gi, g in enumerate(groups):
54
+ if on_group and gi % 25 == 0:
55
+ on_group(gi, len(groups))
56
+ roomL = g["za"] - max(prev_end, groups[gi - 1]["zb"] if gi else -10 ** 9)
57
+ roomR = (groups[gi + 1]["za"] if gi + 1 < len(groups) else 10 ** 12) - g["zb"]
58
+ WL_av = roomL - 32
59
+ WR_av = roomR // 2 - 32
60
+ # Zero everything when there is not enough room (rather than passing a small non-zero value): a warp window
61
+ # shorter
62
+ # than the pitch period makes phase_warp's internal "extension start continuity correction" read out of bounds.
63
+ # The whole-file pipeline always had this safety margin (its `tight` check); the single-file pipeline initially
64
+ # missed
65
+ # this step, and an end-to-end test on a real clip with dense defects caught it (the stereo test clip triggered
66
+ # a
67
+ # broadcast error in render()), so it was added here too.
68
+ tight = 2 * (phase_warp.W_AVAIL_MIN + 32) + 64
69
+ if roomR < tight:
70
+ WR_av = 0
71
+ if roomL < tight:
72
+ WL_av = 0
73
+ if g["za"] - phase_warp.NEED - phase_warp.W_MAX < 0 or g["zb"] + phase_warp.NEED + phase_warp.W_MAX > len(Y[0]):
74
+ res = None
75
+ else:
76
+ res = phase_warp.repair_joint(Y, g["za"], g["zb"], g["subs"], sr, WL_av=max(WL_av, 0),
77
+ WR_av=max(WR_av, 0), roomL=max(roomL, 0), roomR=max(roomR, 0),
78
+ force_fallback=g["force"])
79
+ if res is None:
80
+ lo0, hi0 = g["a"], g["b"]
81
+ for t in range(len(Y)):
82
+ o = max(lo0 - 16384, 0)
83
+ ctx = Y[t][o:hi0 + 16384].copy(); ctx[lo0 - o:hi0 - o] = np.nan
84
+ from .v3methods import ensemble_select
85
+ yv = ensemble_select.fill(ctx, lo0 - o, hi0 - o, sr)
86
+ Y[t][lo0:hi0] = yv
87
+ info = dict(mode="fallback-edge", lo=lo0, hi=hi0)
88
+ end = hi0
89
+ else:
90
+ end = g["b"]
91
+ for t, (lo, y, info_t) in enumerate(res):
92
+ Y[t][lo:lo + len(y)] = y
93
+ end = max(end, lo + len(y))
94
+ info = res[0][2]
95
+ records.append(dict(group=g, info=info))
96
+ prev_end = end
97
+ return Y, records
98
+
99
+
100
+ def repair_file(channels, sr, job_dir, progress=None, detected=None):
101
+ """channels: list of 1D float64 np.array (equal length). Returns (Y, seams_final, grid_info, history).
102
+ detected: optional (seams, grid), e.g. the result of detecting on another signal (the original, before declip
103
+ repair);
104
+ if given, detection is not rerun on channels."""
105
+ X0 = [x.copy() for x in channels]
106
+ if detected is None:
107
+ seams, grid = D.detect_seams(X0, sr)
108
+ else:
109
+ seams, grid = {p: dict(s) for p, s in detected[0].items()}, detected[1]
110
+ if progress:
111
+ progress(state="repairing", grid=grid, n_seams_initial=len(seams))
112
+ history = []
113
+ Y = X0
114
+ for it in range(1, MAX_PASS + 1):
115
+ groups = merge_groups(seams)
116
+ t0 = time.time()
117
+ Y, records = run_pass(X0, groups, sr, on_group=(lambda i, n, _it=it: progress(
118
+ state="repairing", pass_=_it, done=i, total=n)) if progress else None)
119
+ if progress:
120
+ progress(state="repairing", pass_=it, n_groups=len(groups),
121
+ n_seams=len(seams), pass_seconds=round(time.time() - t0, 1))
122
+ evs, sts = [], []
123
+ for x in Y:
124
+ e, s = D.detect(x)
125
+ evs.append(e); sts.append(s)
126
+ tri = D.coincident_all(evs) if len(Y) > 1 else evs[0]
127
+ grid_tuple = (grid["L"], set(grid["residues"]), grid["coverage"], grid["confidence"])
128
+ resid = tri if len(Y) > 1 else np.array(
129
+ sorted(p for p, ntr, ongrid, strength in D.build_seam_list(evs, sts, grid_tuple) if ongrid or ntr >= 1))
130
+ widened = forced = added = 0
131
+ for t in resid.astype(int):
132
+ hit = [g for g in groups if g["za"] - RESID_NEAR <= t <= g["zb"] + RESID_NEAR]
133
+ if hit:
134
+ g = min(hit, key=lambda g: min(abs(g["za"] - t), abs(g["zb"] - t)))
135
+ if not g["force"]:
136
+ # this group used phase warping in the previous round and the residue is still there -> this round
137
+ # falls back to the safe path (more reliable than widening)
138
+ for p in g["positions"]:
139
+ seams[p]["force"] = True
140
+ forced += 1
141
+ else:
142
+ # already on the safe path and still residual -> widen the window and repair again
143
+ key_p = min(g["positions"], key=lambda p: abs(p - t))
144
+ s = seams[key_p]
145
+ if s["gap"] < MAX_GAP:
146
+ ext = min(s["gap"] // 2, (MAX_GAP - s["gap"]) // 2)
147
+ s["gap"] += 2 * ext; s["za"] -= ext; s["zb"] += ext
148
+ widened += 1
149
+ else:
150
+ Lg, residues, conf = grid["L"], set(grid["residues"]), grid["confidence"]
151
+ ongrid = (t % Lg) in residues if residues else False
152
+ if ongrid and conf >= D.GRID_CONFIDENCE_MIN:
153
+ seams[t] = dict(gap=D.GAP0, za=t, zb=t, ntracks=1, ongrid=True, strength=0.0, force=False)
154
+ added += 1
155
+ history.append(dict(pass_=it, residual=int(len(resid)), forced_fallback=forced, widened=widened, added=added))
156
+ if progress:
157
+ progress(state="repairing", history=history)
158
+ if forced == 0 and widened == 0 and added == 0:
159
+ break
160
+ mode_by_pos = {}
161
+ for rec in records:
162
+ for p in rec["group"]["positions"]:
163
+ mode_by_pos[p] = rec["info"].get("mode", "?")
164
+ return Y, seams, grid, history, mode_by_pos