KudioStudio
English | 繁體中文
A desktop workbench for the kudio audio toolkit.
Open a clip, look at it, change it, measure it, and build datasets from it —
without writing a script first. Every control maps to one documented kudio
function, and the caption under each panel shows the exact call it will make,
so anything you work out here translates straight into code.
Install
pip install kudiostudio
kudiostudio
Requires Python 3.9+. On Linux, playback and recording also need
PortAudio's library (sudo apt install libportaudio2), which most desktop
installs already have. Everything heavy is an opt-in extra:
| Extra | Adds |
|---|---|
pip install "kudiostudio[eval]" |
STOI / SDR metrics |
pip install "kudio[pesq]" |
PESQ (ITU-T P.862). Its own extra because it builds from source — a C compiler, and the MSVC Build Tools on Windows — so it is deliberately left out of [eval] and [all], which would otherwise stop installing |
pip install "kudiostudio[dnsmos]" |
DNSMOS opinion scores (onnxruntime; weights not bundled) |
pip install "kudiostudio[enhance]" |
learned denoising, on PyTorch, through kudioenhance. Without an NVIDIA GPU on Linux, install the CPU-only PyTorch first; kudioenhance's README says how |
pip install "kudiostudio[audio]" |
recording until stopped, on Linux and macOS. It needs PyAudio, which PyPI only has prebuilt for Windows, so elsewhere pip compiles it: install PortAudio's headers first (sudo apt install portaudio19-dev, or brew install portaudio). On Windows the plain install already includes it |
pip install "kudiostudio[all]" |
all of the above except PESQ |
The base install stays light: no PyTorch unless you ask for it, and Qt's essential modules only, not its web engine.
To work on KudioStudio itself, install it from a checkout:
git clone https://github.com/recklight/KudioStudio.git
cd KudioStudio
pip install -e ".[dev]"
kudiostudio # empty session
kudiostudio clip.wav # open a file on start-up
kudiostudio --no-theme # native Qt style
kudiostudio --no-plugins # leave out panels other packages add
kudiostudio --lang zh_TW # interface in Traditional Chinese
kudiostudio --reset # forget saved window geometry
The interface comes in English and Traditional Chinese (繁體中文).
View → Language / 語言 switches between them and offers to restart, since
the change applies from the next start; the choice is remembered. With no
choice saved, the system's language is used when there is a translation for
it. Combo-box values, kudio parameter names and the call captions stay as
the code spells them, so what you read still matches the API.
What each panel does
| Panel | kudio behind it |
What you get |
|---|---|---|
| Analysis (centre) | STFT, melspectrogram, mfcc, audio_report, loudness_over_time, true_peak |
Waveform + log-power STFT, log-mel or MFCC, x-linked, live n_fft / hop_length / n_mels / n_mfcc, a loudness curve on its own LUFS axis, plus a live Levels readout with true peak and loudness range |
| Effects | kudio.effects, kudio.rir, kudio.apply_rir, kudio.rt60, kudio.effects.channel |
Fades, reverse, remove_dc, four Butterworth filters, gain, pitch_shift, time_stretch, reverb, room (a real impulse response, seeded and measurable), measured_room (an impulse response from a file, with its T30, EDT, C50 and DRR read off it), telephone / bit_depth / dropouts (the channel, scattered or in bursts), add_noise_snr, trim_silence, vad_trim, split_on_silence — with a looping Preview that commits nothing |
| Enhance | kudio.spectral_enhance + kudioenhance |
Seven statistical denoisers × four noise estimators, wavelet_low_pass_filter, and learned models from a kudioenhance run |
| Compare | kudio.compare_enhancers |
Every method × every noise estimator over the same clip, ranked, with per-row audition or several rows back to back — and a trained model in the same table |
| Record | kudio.record, kudio.StreamRecorder, kudio.StreamEnhancer, kudio.SpectrogramStream |
Fixed-length capture, or record until you stop with a live level meter, a rolling waveform and spectrogram, and optional live denoised monitoring |
| Synthesize | kudio.Synthesizer |
clean × noise × SNR × room dataset generation, optionally down a link (telephone / VoIP) and with reverberant targets beside the mixtures, seeded and reproducible, writing a manifest that records what came from what |
| Manifest | kudio.load_manifest |
That manifest read back as a sortable table — noisy, clean, noise, SNR, room, link, target — filtered by noise, SNR, room and link, with missing files marked and counted, a noise × SNR coverage grid that shows the holes, and each row's noisy, clean and target files one click from your ears |
| Pitch | kudio.f0 |
pYIN fundamental frequency with the voiced/unvoiced decision — median, range in Hz and semitones, the contour drawn over the spectrogram, and the voiced spans as markers |
| Metrics | kudio.audio_report, kudio.dnsmos, kudio.core.evaluator, kudio.find_delay |
No reference needed: clipping, level, DC, noise floor, real bandwidth, and a predicted DNSMOS opinion. With a reference: SNR / SI-SDR / segmental SNR always, PESQ / STOI / SDR when installed |
| Batch | kudio.convert_folder, kudio.enhance_folder, kudio.loudness, kudio.true_peak, kudio.rir |
Folder-wide resample, bit depth, mono downmix, trim, peak or LUFS normalise, and denoise — mirroring the input's folder structure — with a loudness report naming every file that could not reach the target and why, and a reverberant copy of the tree beside the dry one |
Edits apply to the selected range when there is one, otherwise to the whole
clip — and every edit is undoable. Revert to original drops the lot.
Editing, not just processing
Cut, copy, paste, delete, insert silence and crop, all undoable, all on the selection. Paste lands at the playhead when nothing is selected, and the clipboard remembers its own sample rate — pasting an 8 kHz snippet into a 16 kHz clip resamples it rather than playing it at double speed.
Save selection as… writes just the highlighted range; Export analysis image…
grabs the waveform and spectrogram exactly as you are looking at them, zoom and
all.
Every splice crossfades, 5 ms by default, because a hard cut leaves a step in the waveform and a step is a click. It is on by default and never silent: each edit's status line says how much was applied, including when the answer is "none". Quietly altering audio is worse than a click, so the setting is visible rather than buried.
The length lives on the transport bar, not behind a dialog, because the
right length is per edit — 5 ms inside a word and 200 ms between two takes are
different jobs, and a setting you have to open a dialog to change is one nobody
changes. Edit → Splice crossfade… types an exact value into the same control.
The session API takes it per call too, so session.delete(crossfade_ms=200)
uses 200 ms for that one join and leaves the default where it was.
Applying an effect to a selection is deliberately not crossfaded — it replaces the range with a processed version of itself, and blending the edges back would leave 5 ms at each end un-effected.
Your edits, as a script
File → Export edits as Python script… writes the undo history out as a plain
Python script. It reads the original file the way KudioStudio read it, repeats
every edit in order — each under its undo label, each as the kudio call or
the slicing that made it — and saves the result. Run on the original, it makes
the same samples the session holds, not an approximation of them; the test
suite runs an exported script in a separate interpreter and checks exactly
that.
# 2. highpass
head, y, tail = y[:4800], y[4800:14400], y[14400:] # 0.300–0.900 s
y = kudio.highpass(y, sr, cutoff=120.0, order=4, zero_phase=True)
y = np.concatenate([head, y, tail])
clip = y[8000:11200].copy() # pasted in step 5
# 3. cut
y = kudio.splice([y[:3200], y[6400:]], sr, 0.03) # 0.200–0.400 s
- Undo takes the step back out; Revert, opening another file and a new recording start the history again. An edit too old for the undo stack stays in — it is still in the audio.
- Every splice carries the crossfade it actually used, and an effect on a selection is spliced back without one, as it was applied.
- A paste pastes what was copied, not whatever is at those samples by the
time the script gets there: the
clip = …line goes where the copy was made. A clipboard copied before an edit that was later undone, or from another file, is rebuilt from that file first. One copied from an earlier recording has no file to be rebuilt from — every take is its own, however alike — so the script stops there and the export says why. - The script checks what it is run on. It carries the SHA-256 of the file
the edits were made on and stops if
SOURCEholds anything else.Save Assuggests the file you opened, and once the edited audio has been saved over it, a script that read it back would apply every edit a second time; the export warns about that too, as soon as it can tell. PointSOURCEat a copy of the original, or delete the check to run the same edits on another file. A learned model's files are checked the same way, so a run directory trained again since the edit stops the script rather than denoise with a different model. - An effect left on a random seed gets the seed Apply actually drew written
into its call, so the script repeats that result instead of rolling again.
Denoising is recorded too — a learned model as
Enhancer.load(run_dir).enhance(y)from kudioenhance, resampling included. - Nothing is skipped quietly. An edit that cannot be written as code stops
the script with a
raiseat that step, the script's header lists it, and the export says so in the status bar; a script that leaves out a step writes a different file and nothing about it looks wrong. A recording that was never a file gets a placeholderSOURCEand the same warning. File names and undo labels go into the script's comments escaped, so a newline in a name cannot end a comment and turn the rest of it into code.
From code, session.edit_script() returns the same text and session.history
the steps behind it.
Markers
A selection is single, unnamed and gone the moment you make another one.
Markers are named points and ranges that stay: click to jump, double-click a
range to select it, and Find speech fills the list from kudio.vad.
They are positions in time, so an edit that changes the length moves them — delete two seconds and everything after it shifts left. Undo puts them back where they were, because the marker list rides along on the undo stack rather than being recomputed.
They survive the session. Save writes clip.wav.labels.json beside the
audio, and opening that file again picks them straight back up. Export… writes
an Audacity label track instead, which is three columns of plain text that
most of the audio world already reads.
A/B against the audio you started with
Ctrl+B swaps the whole workbench — playback, waveform and spectrogram —
between your edit and the file as it was loaded. It works while playing:
playback rebases to the same instant on the other buffer, so the switch is
seamless and you are comparing the same moment, not the same button press.
Match loudness is on by default, and it matters more than it sounds.
Between two versions of a clip, the louder one is reliably preferred whatever
its quality — so an unmatched A/B is partly a loudness test. The original is
levelled to the edit with kudio.match_loudness (ITU-R BS.1770), and the
status line always states the gain it applied, so an effect that only changed
the level correctly ends up sounding identical rather than mysteriously better:
A/B — the original, levelled to the edit (-17.6 LU applied)
A/B — the original, +17.6 LU against the edit; turn on Match loudness to compare fairly
Turn it off when the level is what you are judging.
The axes deliberately do not rescale between the two: a denoiser that also halved the level should look half as tall, not be stretched back to fill the plot. Zoom survives the swap for the same reason.
Same clip, same axes, same instant — trad_enhance has thinned the noise bed
and left the chirp.
The tools keep operating on the edit throughout — comparing is monitoring, not a mode you can accidentally process in.
Preview before you commit
Preview auditions an effect and changes nothing: no edit, no undo step.
It loops, so finding the right top_db is a matter of listening rather than
apply-listen-undo-repeat. Preview and Apply run the same code on the same
audio, so what you hear is exactly what Apply will commit.
- Effects re-renders as you turn a knob, debounced at 150 ms.
- Enhance runs on a worker thread — denoising is seconds, not milliseconds — processes the whole clip exactly as Denoise will, then loops whatever is selected.
Anything else claiming playback — Play, Stop, an edit — ends the audition and releases the button.
A room someone measured
room builds a statistical room from an RT60 and a DRR: reproducible, one knob,
and no early reflections, which is what augmentation wants and not what a
particular hall sounds like. measured_room takes an impulse response from
a file instead and applies it with kudio.apply_rir. It is read the way the
recorded call reads it — kudio.file_load at the clip's rate, in mono — and
read again if the clip changes rate, so Preview, Apply and an exported script
all use the same samples:
y = kudio.apply_rir(y, kudio.file_load('/rooms/hall.wav', sr=sr)[0][192:])
The [192:] is start at the direct sound, on by default. A measured
response begins with the time the sound took to reach the microphone, and left
in, that delays the audio — a processed selection comes back late against its
neighbours. It starts the response at its loudest sample, where kudio.rir
puts the direct path.
Under the picker, kudio.rt60 reads the response as it will be applied —
resampled, downmixed, trimmed — so a hall measured at 48 kHz and applied at
8 kHz is described as what is left of it:
0.91 s at 16000 Hz, direct sound at 12.0 ms — applied from there
T30 0.59 s (EDT 0.54 s) · C50 +9.4 dB · DRR +3.0 dB · fit 1.000
A file that is not an impulse response still produces a T30. When the decay is not straight, or its EDT and T30 disagree, the reading says so and turns red.
Is this recording usable at all?
Every metric in the Metrics panel needs a clean reference beside the degraded signal. A synthetic dataset has one. A real recording never does — its "original" is the noisy one, so scoring against it measures how much you changed the audio, not whether you improved it.
kudio.audio_report needs no reference, so the Levels box beside the
analysis view is always live: peak, loudness, bandwidth, and whatever is wrong
with the recording itself. The Metrics panel's Inspect gives the full set.
It catches the things that quietly waste a dataset:
· 4692 samples clipped (9.78% of the clip) — the recording was already
too hot; no processing gets that back
· content stops at 3812 Hz although the file claims 16000 Hz — it was
very likely upsampled from 8000 Hz, and the extra rate is empty
The Record panel runs the same check on every take, while the microphone is still set up and re-recording is still an option.
That says the audio is technically sound, not that it sounds good. For the other half, DNSMOS (ITU-T P.835) predicts what a listener would say:
SIG 2.94 · BAK 4.12 · OVRL 3.01
poor (OVRL 3.01) — the background is cleaner than the speech, which is
what over-aggressive denoising looks like
SIG and BAK move in opposite directions under denoising — suppress harder and
the background gets cleaner while the speech gets eaten — so all three numbers
are shown rather than averaged into one. Needs
pip install -e ".[dnsmos]" and the DNS-Challenge weights, which
neither kudio nor this app bundles; the panel tells you where to get them.
Where is somebody actually talking?
trim_silence and split_on_silence cut on level alone, which works on a
studio take and falls apart on anything with a fan in the background. Above
some noise level no fixed top_db both keeps the quiet consonants and drops
the room.
kudio.vad decides per frame against this recording's own noise floor, and
uses spectral flatness to tell a voice from a hiss. It is in two places: the
vad_trim effect, and Find speech in the markers dock, which turns
every detected span into a marker you can click through.
Analysis views
Log-power STFT, log-mel and MFCC, from the same kudio calls a training
script would use — the caption under the controls is the exact call:
The same pair of plots also carries the two things that are not the clip: the rolling picture of a take as it arrives, and an f0 contour over the spectrogram. Both are below.
Where did the level go?
The Levels box gives one integrated LUFS figure for the whole clip — the number you normalise against, and no help at all in finding the moment somebody leaned away from the microphone. Loudness over time draws the short-term curve (EBU Tech 3341, 3 s window, 100 ms hop) over the waveform on its own LUFS axis, so "-31 LUFS here" is a number you can act on rather than "about a third as tall".
Above, a take that drifts 13 dB and comes back. The Levels box agrees: range 13.0 LU.
It costs no extra measurement. The Levels readout already walks the signal for its report, so the curve, the true peak and the range come out of that same pass — four answers from one worker job, which also means they cannot describe four different moments of an edit.
Two things it is deliberate about:
- The curve is ungated. Gating is what stops pauses dragging the integrated figure down; a curve with holes punched in it where the gate fired would describe a recording that stops existing between words. Real silence still breaks the line, because there the level genuinely is nothing.
- A clip shorter than the 3 s window has no curve, and the checkbox says so rather than ticking and doing nothing. Falling back to the 400 ms window would quietly answer a different question.
True peak is in the Levels box beside the sample peak, and the two
disagree more often than you would think. Samples are points on a curve that
passes between them, and the curve overshoots — so a file with no sample at
full scale can still clip in a converter or a lossy encoder. audio_report
cannot see that (nothing in the samples is wrong), so it is reported
separately, in dBTP, and warns at the usual -1 dBTP delivery ceiling.
Two files that are the same take, eight milliseconds apart
Every metric in the Metrics panel compares sample i of the reference with sample i of the current audio. A clip scored against itself reads +145 dB; one sample of delay costs 134 of them, and a millisecond scores worse than not processing at all. Nothing in the resulting number distinguishes "your processing was bad" from "these two are misaligned", and those mean opposite things.
So the panel measures the offset with kudio.find_delay every time, puts
it in the table, and takes it out before scoring unless you say otherwise:
Alignment lags by 128 samples (8.0 ms), correlation 0.982
SI-SDR +14.210 dB ...instead of -7.890 dB
Untick Line up with the reference first and it still reports the offset,
with a warning that the scores below are mostly measuring it.
When the reference does not look like the same recording at all, it says so and does not line the two up — aligning two unrelated signals produces a confident-looking number about nothing, which is worse than an obviously bad one.
What note is this?
A spectrogram shows the harmonic stack. It does not say which line is the fundamental, and it stops helping the moment that line is weak — a phone band, a high-pass, a low voice under the first few bins.
The Pitch panel runs kudio.f0 (pYIN) and reports the median, the 5–95%
range in Hz and in semitones — the unit pitch is actually heard in — and how
much of the clip was voiced at all. The contour is drawn over the STFT view,
with gaps where there is no pitch: an estimator asked "what is the pitch
here" always answers, including for silence and for an s, and a line joined
through those answers is a picture of nothing.
Two things it is deliberate about:
- The search range is a named choice, not a hidden default. A voice below
fminis not reported as missing — it is reported as something plausible and wrong inside the range, so the presets say out loud what they cover. - On a mel or MFCC image the contour is hidden, because there "180" means band 180. A curve in the wrong place is worse than no curve.
Voiced spans → markers turns the result into marker ranges, which then save
beside the file and export as a label track like any other.
Choosing a denoiser instead of guessing
Applying one method, listening, undoing and trying the next is how you end up with whichever one you happened to try last. The Compare panel runs all seven over identical audio with identical framing, measures each, and lets you audition any row without committing to it.
The seven methods are the same three decisions in different combinations — estimate the noise, estimate the a priori SNR, turn it into a gain — so holding the noise estimator still isolates the gain rule. The estimator is as large a lever as the gain rule: measured across the full grid, 5.8 dB of mean SI-SDR spread from the gain rule against 6.4 dB from the estimator. Neither dominates, which is why both are controls and why the panel can sweep either, or both.
Both axes sweep. Enhancement is two decisions — a gain rule and a noise estimator — so the panel compares gain rules at one estimator, estimators at one gain rule, or the full 7 × 4 grid. It then reports which of the two moved this clip more, or says they were comparable rather than crowning one on a 0.1 dB difference.
Point Learned model at a kudioenhance
run directory and it gets a row too, scored against the same reference on the
same audio — "is the model actually better than log-MMSE" is not a question two
separate printouts answer. The model is loaded, and run once over a short
excerpt, before the clock starts, then kept for the next comparison on the same
run, so its ms is the denoise — not the seconds it takes to import PyTorch
and read the checkpoint. Kept for the run as it is on disk: trained again
into the same directory, it is loaded again.
Select two or more rows and ♪ Audition A→B loops them back to back,
with a third of a second of silence at each seam, so the comparison is not
interrupted by a click and a mouse movement. They play in the table's
order, top to bottom, whichever was clicked first — and re-sorting while they
play re-orders the loop, so "the one above sounds better" is a sentence about
the ranking. Apply this one wants exactly one row that did not fail, and
records it as the code that computes it again from the audio and the range it
was computed on — a comparison whose audio was edited while it ran is
dropped rather than shown, since its rows describe audio that is gone.
Three things the panel says out loud, because all three are easy to get wrong:
-
With no clean reference there is no quality score. All that is left is the residual noise floor, and the easiest way to reach a very low floor is to delete the speech along with the noise. The ranking is a shortlist;
♪ Auditionis the verdict. -
Scoring against the noisy original measures how much you changed the audio, not whether you improved it. When every method comes out worse than doing nothing, that is what the panel tells you rather than crowning a winner that is −1 dB "better".
-
These methods are for genuinely noisy audio. Above roughly 10 dB SNR the distortion they add costs more than the noise they remove, and they all measure worse than leaving the clip alone.
Hearing the denoiser while you record
The Record panel's monitor through runs the take through
kudio.StreamEnhancer as it arrives — a live rolling waveform of the denoised
signal, and a readout of how much is coming off right now. One analysis frame of
latency, 32 ms at the default.
The take itself is committed raw. Monitoring is monitoring, the same rule the A/B follows: what you hear is a view, not an edit. Only the four per-frame methods can stream; the rest need the whole spectrogram and live in Enhance.
The Enhance panel's controls are generated from kudio.METHODS — range,
default, unit and help text all come from the library, so a parameter added
there appears here without anyone editing the GUI.
Watching a take arrive
The waveform monitor was in from the start; the spectrogram was not, because
recomputing an STFT several times a second to draw a window that has already
scrolled away costs more than it shows. kudio.SpectrogramStream fixes that
properly: a ring buffer of columns, each computed once, when the samples
for it arrive.
The Record panel drains the recorder once per tick and hands the new audio over; the view pushes all of it through the transform and redraws a few times a second. Analysis is never skipped — that would lose audio — and drawing always is.
Three details that are easy to get wrong and visible when you do:
drain(), nevertail(). A sliding window overlaps between calls, so every overlapping sample would be drawn twice at two different times.- The colour range is absolute (dBFS per bin), not picked from the window. A range recomputed from something that scrolls twenty times a second is a brightness that means nothing and changes anyway.
- Both plots are right-aligned on now. They share a time axis, so a waveform drawn from zero while the window is still filling would put the same instant in two different places.
Turn on monitor through and the picture is of the denoised stream, while
the take is still committed raw.
Learned denoising
The Enhance panel runs classic DSP out of the box. Install
kudioenhance with
pip install "kudiostudio[enhance]" and the panel also loads the models it
trains: point run_dir at
a run directory and it picks up the model, its normalisation statistics and the
STFT geometry they were trained with — so inference cannot silently disagree
with training.
PyTorch is imported only when that method is actually used, and the panel tells you how to install it rather than failing if it is missing. A run directory trained with its version 0.1 predates the move to PyTorch and cannot be loaded: the panel says it has to be retrained.
Dataset synthesis
Point it at a folder of clean audio and a folder of noise, pick your SNRs, and
it writes the mixture. regular gives one output per clean file; inc writes
every clean × noise × SNR combination. Set a seed and the result is byte-for-byte
reproducible — the same noise offsets, the same pairings.
Reading a corpus back
The Synthesize panel writes manifest.json beside every corpus, and half of
what is in it — the room, the link, the target — is in none of the file names.
File → Open manifest… — or kudiostudio manifest.json on the command line —
reads one with kudio.load_manifest and shows it as rows: noisy, clean, noise,
SNR, RT60, link and target. Sort any column (SNR and
RT60 by value, not as text), filter by noise, SNR, room and link, and select a
row: Open loads its noisy file for editing, as File → Open would, and
♪ noisy / ♪ clean / ♪ target loop one side of it without loading anything.
It answers two questions worth asking before anything is trained on a corpus:
- Is every file still there? Paths in a manifest are often relative — to
wherever the synthesis ran, which nothing records. They are looked for under
the current directory first, then the manifest's own folder and each folder
above it, which is where
Synthesizer(..., out_path='data/mixed')paths start — and the whole manifest is read against one of those folders: the first that holds every file it names, or failing that the one that holds the most. Never file by file: two projects built from the same clean files name their mixtures alike, and a lookup per path would pair one project's mixtures with the other's clean files. The summary names the folder. Rows with a file that is not found there are marked and counted, and one checkbox lists only those — the grid then counts where the damage is, and says nothing about the corpus's coverage. - Is it a manifest at all? kudio reads a manifest without looking at the values; one edited by hand can hold a list where an SNR belongs. That is refused with the row and the field named, and whatever was showing stays.
- Is the noise × SNR grid covered? The grid under the table counts the rows
shown per combination, so a room filter shows that room's coverage. A hole
is a red cell, and the note says which kind it is: fewer rows than
combinations is a
regcorpus that needs more clean files; enough rows and a hole anyway is something no single run writes — rows went missing, or the manifest is two runs merged. Double-click a cell to list its rows. Only SNRs that some row has get a column — the manifest records what was written, not what was asked for.
Reading the manifest and checking the disk run on a worker, and the table only builds the cells on screen, so a corpus of tens of thousands of rows does not freeze the window.
A tree at -23 LUFS, and the files that could not get there
kudio.convert_folder(lufs=-23) applies a gain to every file and says nothing
about what came of it. The job people actually have is "normalise this tree
and tell me which files could not get there", so with a LUFS target the Batch
panel reads every written file back and measures it — kudio.loudness,
then kudio.true_peak — against a stated tolerance (±1 LU, what
kudio loudness --target allows) and a true-peak ceiling (-1 dBTP, EBU R 128's).
Every file that missed is listed, with the reason:
could not get there:
in/b/short.wav — too short to measure — BS.1770 needs 0.4 s, so it was not written
out/a/spiky.wav -35.4 LUFS clipped at full scale — -23 LUFS needs a peak above 0 dBFS; 12.4 LU short
out/b/silent.wav — silent — there is no level to raise
on target, too little headroom:
out/c/plosive.wav -23.0 LUFS true peak +0.5 dBTP, over the -1.0 dBTP ceiling
It is measured from disk rather than from what was handed to save_wave,
because the disk is where it goes wrong: a fixed-point file clips everything
past full scale, so a quiet file levelled up comes back with its peaks
flattened. A clipped file is listed even when its loudness lands near the
target — it got there by clipping. A FLOAT tree keeps the peaks, and says
which files only a float file can hold. With denoising on, the levelling
waits for it: the conversion leaves the level alone, the denoise pass runs,
and every file is levelled after it — a denoiser takes level out along with
the noise, so a tree levelled first lands under the target it was asked for.
Re-room a tree and keep both
Tick Reverberant copy and every converted file is written again in a room,
to a second tree with the same layout — <output>_reverberant by default,
beside the output and never inside it — so a dry file and its wet copy pair by
path: input and target for dereverberation, or two conditions of one corpus.
Each file gets a room of its own: the same RT60 and DRR, and a tail drawn
from the run's seed and the file's path, the way kudio.Synthesizer draws its
per-file rooms — one room repeated over a corpus is one acoustic condition,
and a model trained on it learns that room. The same seed rebuilds the same
rooms, and a random one is drawn at Run and reported. apply_rir keeps the
length and the peak, so wet and dry stay sample-aligned; with a LUFS target the
copies are levelled to it too and read back with the rest.
A batch run can take a while, and nothing in it can be stopped half-way. Quitting while it runs does not cut it short: the window stays open, says what it is waiting for, and closes itself when the job is done — a tree is never left half-written.
Objective metrics
SNR, SI-SDR and segmental SNR are pure numpy and always available. PESQ, STOI and BSS-SDR need the optional back-ends:
pip install "kudio[eval]" # STOI and BSS-SDR
pip install "kudio[pesq]" # PESQ — builds from source, needs a C compiler
PESQ comes on two different scales. kudio reports MOS-LQO by default; enhancement papers mostly quote the raw narrowband P.862 score, and the two are not interchangeable — raw 2.03 is about 1.65 MOS-LQO. The Metrics panel names which one it is showing and lets you switch.
The panel reports which ones are present rather than failing.
Shortcuts
| Key | Action |
|---|---|
Ctrl+O / Ctrl+Shift+S |
Open / Save As |
Ctrl+Shift+E |
Save the selection as its own file |
Ctrl+Z |
Undo the last edit |
Ctrl+X / Ctrl+C / Ctrl+V / Del |
Cut / copy / paste / delete the selection |
Ctrl+T |
Crop to the selection |
Ctrl+A / Esc |
Select all / clear selection |
Space |
Play / stop |
Home |
Playhead back to the start |
Ctrl+L |
Loop what was last played |
Ctrl+B |
A/B against the original |
Ctrl+E / Ctrl+F |
Zoom to selection / zoom out to the whole clip |
Drag on either plot to select, or type an exact range into the Selection boxes. Click anywhere to move the playhead — click while playing and it keeps playing from there. The waveform and the spectrogram share the time axis, the selection, the playhead and the markers, so anything you do to one shows on the other.
Space, Home and the clipboard keys are deliberately not registered as
window shortcuts. Qt resolves shortcuts before the key reaches the focused
widget, so a window-level Ctrl+C would take copy away from every path field
in the tool panels, and Space away from typing a filename with a space in it.
They are handled in keyPressEvent, which only sees them when nothing else
wanted them; the menus show the keys anyway.
Layout
kudiostudio/
├── __main__.py # CLI entry point, argument parsing
├── app.py # main window: docks, tool rail, menus, wiring
├── session.py # AudioSession — audio, undo, selection, clipboard,
│ # markers, splice crossfade, A/B, loudness match,
│ # and the edit history as a script
├── workers.py # QThread helpers, so long kudio calls never block the UI,
│ # and cycle collection kept on the GUI thread
├── theme.py # shared colours
├── style.py # the dark theme: Fusion, palette, stylesheet
├── icons.py # line icons for the toolbar, transport and tool rail
├── plugins.py # panels other packages add, found by entry point
├── assets/logo.png # the window and taskbar icon, the same mark as kudio's
├── widgets/
│ ├── views.py # TimePlot base + waveform, spectrogram/mel/MFCC,
│ │ # the rolling take and the f0 overlay
│ ├── levels.py # reference-free level / loudness / warnings readout
│ ├── markers.py # the marker list, and Find speech
│ └── transport.py # play / stop / playhead / loop / A/B / match / gain,
│ # and the splice crossfade
└── panels/
├── base.py # Panel base class
├── browse.py # file list
├── effects.py ├── enhance.py ├── compare.py
├── record.py ├── synth.py ├── manifest.py
├── pitch.py ├── metrics.py └── batch.py
Panels never talk to each other. They read and mutate the shared
AudioSession; the views repaint on its changed signal. A panel that wants a
file opened asks the window with open_requested, so the file lands in the
Files list like any other. Adding a panel means subclassing Panel and
appending it to PANEL_CLASSES.
A panel can also live in a package of its own, for tools that have no place
in this repository. The package names its Panel subclass under the
kudiostudio.panels entry point group, and the window puts it on the tool
rail, below a rule under the built-in tools:
[project.entry-points."kudiostudio.panels"]
spectra = "mypackage.studio:SpectraPanel"
Two class attributes are read as it loads: icon_svg, its rail icon as an
SVG body on the 24-unit grid icons.py draws on, and messages, its
translations in the form locales/zh_TW.py uses. A plugin that fails to
import or to build is left off the rail and named in the status bar, and the
window opens without it; --no-plugins leaves them all out.
The UI is built in Python — there are no .ui files and no uic step, so what
you read is what runs.
Tests
pytest -q
563 tests covering the session's undo/selection/clipboard/marker arithmetic,
the exported edit script landing on the session's own samples when it is run
in a separate interpreter, a manifest from a real synthesis run finding its
files from wherever the window was started and counting its coverage, a
normalised tree read back with every file that missed the target named for
the right reason, reverberant copies that are exactly their seeded rooms, a
measured room that does not delay the audio, loudness matching, the
playhead's timing, the preview and playback loops, the
shared axes and colour scale, the comparison table's ranking and its refusal
to crown a winner when the reference is not clean, a learned row timed
without the seconds its model took to load, the rolling views' promise
that every column is computed once and that real audio always wins over a live
one, where the pitch contour refuses to draw, the two plots' left and right
gutters staying the same width so a shared time axis actually lines up, each
panel's behaviour, the main window's wiring, another package's panel put on
the rail or left off it with the reason named, a panel let go of the moment its
job ends being freed on the GUI thread, a window built in Traditional
Chinese that still reads its combo boxes back in English, and the whole program quitting
cleanly mid-job — those last two in an interpreter of their own, since what
they guard against is a crash. Qt runs headless (offscreen), so the suite
works in CI. The learned-denoising tests (marked learned) train a one-epoch
model for real, run it on the panels' own worker threads, and skip cleanly
when kudioenhance or PyTorch is not installed or will not import — unless
KUDIOSTUDIO_REQUIRE_LEARNED=1 is set, which makes them fail instead, for a
job that installs both precisely to run them. The open-ended recorder is
driven through a fake device, since CI has no microphone.
tools/smoke.py is the other half: 166 end-to-end checks in one process,
covering what the unit suites cannot — real files, non-default sample rates,
minute-long feeds, the kudio command line, and the window driving a real
worker thread. It exits non-zero on any failure.
The README's screenshots are generated, not hand-captured — rerun
python tools/screenshots.py after changing the window and commit what moved.
Related
- kudio — the audio toolkit underneath: I/O, features, effects, synthesis, enhancement, metrics.
- kudioenhance — train the denoising models this app loads.
Licence
MIT — see LICENSE.txt.
Metadata
Release files for kudiostudio 1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| kudiostudio-1.0.tar.gz | 382.0 kB | With provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| kudiostudio-1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 669.2 kB
Release files / kudiostudio-1.0.tar.gz
| Download URL | kudiostudio-1.0.tar.gz |
|---|---|
| Size | 382.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
44b7804bbf7c1b56c8f19689a5bc34d07a64fd2e3962ab348b9e652930585ba1
|
|
BLAKE2b-256 checksum How to use checksums |
4107d88cf2e7cf0a6444966f9d0c82a3b739ae164b6b86e65225e261e4518e96
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 23, 2026.
Transparency logRelease files / kudiostudio-1.0-py3-none-any.whl
| Download URL | kudiostudio-1.0-py3-none-any.whl |
|---|---|
| Size | 287.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
33245d5aa5dce6734731940d05ed4ec173a30986eee98c296e3a0e971a1067f8
|
|
BLAKE2b-256 checksum How to use checksums |
c96408787cb76dc132a0006977fa355727c143f8e9f49a7805daa41fee05e4b1
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 23, 2026.
Transparency logSecurity for kudiostudio 1.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for kudiostudio-1.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 23, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.