Changelog¶
0.3.1 (2026-10-11)¶
Fixed¶
plot_spectrogramdrew the rows of a mel spectrogram at linearly spaced STFT bin frequencies, so the frequency axis ofextract_melspectrogramoutput was wrong.y_axis='mel', withfminandfmaxas given to the extractor, places row m at filter centermel_frequencies(n_mels, fmin, fmax, sr)[m + 1]on a mel-scaled axis.plot_spectrogramraisesValueErrorfor ay_axisother than'log','linear'or'mel'; any other value used to give a linear axis.
0.3.0 (2026-10-10)¶
Breaking changes¶
- The package is
afx-audioon PyPI and imports asafx_audio(import afx_audio as afx);import AFXno longer works. The PyPI projectafxis unrelated, and itsafx/package collides withAFX/on case-insensitive file systems. - Python 3.10 or newer is required.
- The core install is numpy, scipy and soundfile. scikit-learn, pandas and matplotlib moved to the
sklearn,parquetandplotextras;allinstalls them with torch. - Extractor arguments after
signalandsrare keyword-only, as in librosa, in bothafx.extractorsandafx.torch.functional:extract_mfcc(y, sr, n_mfcc=20)works,extract_mfcc(y, sr, 20)raises. Several signatures gained parameters in the middle, so positional calls would otherwise have changed meaning silently. - Unknown feature parameters raise instead of being ignored, both at config validation and when calling an extractor directly (
TypeError). Non-positive frame sizes and counts are rejected, and so are unknown top-level config keys (agregation) andsliding_windowwithout its window sizes. - An unknown feature name raises
ValueError(it wasKeyErrorfromextract_all_features), with the closest valid name in the message. - scikit-learn 1.3 or newer for the
sklearnextra. - Signals must be 1-D floating point and finite. Integer PCM, complex, empty, multichannel and NaN/inf input raise with a message saying what to do, including through
AFXFeatureExtractor. spectral_rolloffsums magnitudes and returns the bin frequency, as librosa does, instead of interpolating on the power spectrum (power=2gives an energy roll-off).spectral_contrastfollows librosa:power_to_dbon the peak and valley magnitudes (10 log10, top_db 80) and the quantile size taken from the untrimmed band. Values are half the previous ones.- MFCC deltas use the Savitzky-Golay filter with fitted edges (
librosa.feature.delta);order=2is the second derivative, not a delta of the delta. zcrdivides by the frame length instead of frame length minus one and treats |x| <= 1e-10 as zero.chroma_stftuses librosa's chroma filterbank on the power spectrogram with max normalization.f_refis replaced bytuning.- Aggregation skips NaN frames: a statistic is NaN only when the feature is undefined on every frame.
- Sample entropy uses the same N - m templates for both lengths (Richman and Moorman), which changes values slightly.
extract_features_from_folderinfers the output format from the file extension (save_format=None), and rejects an unknown extension instead of writing a.npyfile next to it. JSON output writesnullfor NaN.afxexits with status 2 and anafx: error:message for usage and configuration errors (argument errors print argparse's usage block first), and exits with 1 when no features were extracted.--aggregationno longer offerssliding_window, which needs window sizes from a config file.framewise_extractortakesframe_size,hop_length,centerandreturn_metadataas keyword-only arguments and no longer forwards them to the wrapped function.pitchis NaN on unvoiced frames instead of 0, so pitch statistics cover voiced frames only and a clip with no voiced frame gives NaNpitch_*.afx_audio.utils.pitch.yinstill returns 0.- Every framed feature except
onset_strength, which is always centered, defaults tocenter=True(librosa's convention) on the NumPy and torch paths, so features with the sameframe_sizeandhop_lengthshare frame count and frame times. Passcenter=Falsefor the previous framing of MFCC, GFCC, deltas, the spectral shape, time-domain, autocorrelation, THD, LPC, formant and chroma_cqt features.pitchandhnrgainedcenter. - On an all-zero frame these features are NaN instead of 0 or 1:
crest_factor,energy_ratio,mobility,complexity(any constant frame),autocorr_snr,autocorr_peak_width,autocorr_decay_rate,spectral_entropy,spectral_skewness,spectral_slope,spectral_crest,spectral_decrease. Features with a librosa equivalent keep librosa's value on silence.feature_info(name)['undefined']states each condition. - Every extractor returns float64, including for float32 input and for the counts
autocorr_zero_crossingsandautocorr_peak_width. Torch functions return the input tensor's dtype. - Presets are versioned files
afx_audio/presets/<name>@<version>.jsonwith a"preset": {"name": ..., "version": ...}block instead of the top-level"name", and spell out every parameter.speechandmusicframerms_energylike their other features (it was one value per clip), and thespeechpitch and HNR search stops at 600 Hz. afx_audio.utils.shape_utilsandafx_audio.utils.torch_utilsare removed; nothing used them.afxwritesfeatures.csvwhen--outputis not given (was a pickledfeatures.npy), andextract_features_from_folderreads every extension of the formats the installed soundfile reads, case-insensitively (afx_audio.batch_extractor.default_extensions()), instead of wav, mp3, flac and ogg.afx --ext wav,flacrestricts them.extract_features_from_folderdefaults toverbose=False;afxstill prints progress unless-q.- eGeMAPS columns in tabular outputs, dataset statistics,
feature_names/flatten_featuresandAFXFeatureExtractor.get_feature_names_outuse openSMILE's names (EGEMAPS_NAMES,EGEMAPS_LLD_NAMES) instead ofegemaps_0, ...; statistics are appended (<name>_mean) and several values per row get an index. hnrsearches lags up toframe_size // 2, as YIN does, so its effective lowest pitch ismax(fmin, 2 sr / frame_size);speech@1sets HNRfminto 80 Hz accordingly.- Importing
afx_audio.torchwithout PyTorch raises the install hint at import time. from afx_audio import *no longer exportsAFXFeatureExtractor, so it works without scikit-learn; usefrom afx_audio.sklearn import AFXFeatureExtractororafx.AFXFeatureExtractor.AFXFeatureExtractor(features=[...])orders output columns asfeatures, and itssample_rateandaggregationdefault to None, meaning the config's values, instead of overriding a preset with 22050 Hz andmean.
Fixed¶
hnrread white noise as up to +100 dB whensr / fminwas close toframe_size, and scored signals below about -80 dBFS as noise; it is now level-independent and its noise floor varies by under 2 dB between 16 and 96 kHz.- The gammatone filterbank (
gfcc, NumPy and torch) used bin frequencies off by up to 20 Hz for oddframe_size. read_provenancerecognizes Parquet by content, so an output whose name does not match its format reads back, andafxno longer ends a successful run with a traceback in that case. Without pyarrow it names theparquetextra.AFXFeatureExtractor(normalize=None)keeps the config'snormalizeinstead of clearing it, and itsprovenance_lists custom features like the batch record.validate_configstores an integral floatsample_rateas an int, so16000.0and16000hash alike, andpreset_modifiedignores parameters written out at their default value.- A rerun to the same output removes
.errors.json,.stats.jsonand stale.provenance.jsonsidecars it does not write, and parallel progress reports failed files as failed. AFXFeatureModuleoutput has the input's dtype for every feature: float64 input failed for the mel, MFCC, GFCC and chroma features. Its windows and filterbanks are kept in float64 and cast at use.- The YIN parity test passes with librosa 0.10.1, the declared minimum.
plot_mfccandplot_spectrogramplaced frame i at its start instead of at the frame time inmetadata['times'].- Float32 input to the spectral and cepstral extractors went through a complex64 STFT; it is now computed in double precision.
afx -j Non Linux could print Python's "use of fork() may lead to deadlocks" warning. Batch workers never fork: forkserver on Linux, spawn on Windows and macOS, or the method set withmultiprocessing.set_start_method. A custom feature defined in a notebook raises a ValueError before workers start instead ofBrokenProcessPool, and registered features that the config does not use are not sent to workers.- HNR used the biased autocorrelation, so r_max could not exceed (N - lag)/N: a pure tone read about 13 dB at 2048/22050 and about 2 dB with the speech preset framing. It now uses the normalized cross-correlation of the overlapping segments (NCCF) and tracks SNR within 1 dB on tone-plus-noise tests.
spectral_flatnessreturned 0 or 1 for silent frames depending on rounding; it is 1, as in librosa.- On the torch path, silent frames gave NaN gradients through
spectral_bandwidthandspectral_flux(the derivative of sqrt at 0). AFXFeatureModule(aggregation='mean_std')concatenated along the batch axis for per-frame features (shape(2B,)instead of(B, 2)) and raised on unbatched input.- Frame-time metadata added
frame_size // 2forcenter=Trueand nothing forcenter=False, the reverse of the librosa convention. All extractors now report frame centers throughafx.utils.frame_utils.frame_times, so features with the same framing share one time axis. aggregation: "sliding_window"could not be used from a config becausewindow_sizeandhop_lengthwere never forwarded; pass them underaggregation_params.- Frame-wise time-domain features on a clip shorter than one frame were computed on the zero-padded frame. The clip is now analyzed as one unpadded frame.
- FFT round-off in the autocorrelation made
autocorr_zero_crossingscount random sign flips where the exact value is 0. - Float32 input lost precision in kurtosis and energy ratios; time-domain moments are accumulated in float64.
spectral_fluxon a single frame returned an empty array; it returns[0.0].- The STFT cache was not safe to use from several threads.
examples/example_extract_and_plot.pyplotted the aggregated MFCC vector and always raised.- The STFT result cache keyed entries on a full copy of the signal bytes; it now stores a BLAKE2b digest.
AFXFeatureExtractordeclares__sklearn_tags__for scikit-learn 1.6 and later.
Added¶
- eGeMAPS v02 (Eyben et al., 2016) under MIT:
afx.egemaps(signal, sr)returns the 88 functionals with openSMILE's names and order,FEATURE_MAPgainsegemaps(88 values per clip) andegemaps_lld(25 descriptors per 10 ms frame), and there is anegemapspreset. Agreement with openSMILE per functional is indocs/egemaps.md, written bybenchmarks/validate_egemaps.py. Jitter and shimmer come from periods marked on the LPC residual and follow synthesized values with slopes of 0.92 to 0.99 on steady vowels; openSMILE's follow them less closely, so the two disagree by about 20 % on synthetic voices. - Provenance in batch outputs: config,
config_hash, library versions, sample rate and resampling, and the sha256 of every input file. Parquet keeps it in the schema metadata keyafx; other formats write<output>.provenance.json.afx.config_hash,afx.read_provenance,AFXFeatureExtractor.provenance_,afx --provenance FILEand--no-hash. - Dataset statistics:
extract_features_from_folder(dataset_stats=True)andafx --statswrite per-column count, mean, variance, min, max and NaN count to<output>.stats.jsonin one streaming pass (Welford, with Chan's merge for workers);afx.load_statsandafx_audio.sklearn.scaler_from_statsturn it into a fittedStandardScaler. - Coverage:
"coverage": true(alsocoverage=onextract_features_from_folder,AFXFeatureExtractor,AFXFeatureModule, andafx --coverage) adds<feature>_valid_frac, the fraction of frames a feature's statistics are based on.afx.valid_fraction,metadata['voiced']fromextract_pitch, andfeature_info(name)['undefined'], which says when each feature is NaN. - Numbered notebooks
01_quickstartto08_egemapsthat run on synthetic audio without a dataset,notebooks/demo_data.py(synthetic clips, and voices with known F0, jitter and shimmer), andtests/test_notebooks.py(runs them withAFX_RUN_NOTEBOOKS=1). benchmarks/bench_features.py --librosa --torch-batch N --markdownand thedocs/benchmarks.mdpage;docs/reproducibility.md.afx_audio.methods.lpc.levinson.benchmarks/baselines.pyanddocs/baselines.md: ESC-50 accuracy over the 5 official folds for every preset andconfig.json, with a fixed logistic regression and random forest, config hashes, extraction time and versions.config_hashhashesafx_audio.provenance.computed_config(config): every default written out, labels andflattendropped. A preset, its copy in a file and the config the scikit-learn transformer builds from it hash alike.- Versioned preset ids:
load_preset('speech@1'),afx --preset speech@1,AFXFeatureExtractor(config='speech@1'),list_presets(versions=True),resolve_preset. Provenance records carrypreset_idandpreset_modified, and a test pins the config hash of every released preset version. feature_info(name)['torch']: the torch function and the rtol and atol of its float64 parity test against NumPy (also inafx --describeanddocs/features.md). The parity tests read the same table and were tightened to rtol 1e-9.AFXFeatureModule.from_config(config, output=..., skip_unsupported=False)builds the torch module from a config dict, JSON path or preset id.- The release workflow refuses a tag whose version has no dated heading in this changelog.
afx.AFXWarning, the category of every warning AFX emits, andafx.AFXDeprecationWarning.frame_sizeandcenterforplot_mfccandplot_spectrogram.- A
slowpytest marker (pytest -m "not slow"). CONTRIBUTING.md,CODE_OF_CONDUCT.md,SECURITY.md, issue and pull request templates, a pre-commit configuration, Dependabot, and coverage upload in CI.- The sdist contains the tests, examples, docs sources, benchmarks and
CITATION.cff, and the test suite passes from it. Releases install the built sdist and wheel in clean environments and run the tests before publishing. centerfor the time-domain, autocorrelation and THD extractors, giving the same frames and times as a centered STFT; centeredzcrandrms_energymatch librosa.afx.list_features(group),afx.feature_info(name)(group, units, rows, parameters, definition) andafx --describe FEATURE.tonnetzandonset_strength, equal to librosa'sfeature.tonnetz(on chroma_stft) andonset.onset_strength.AFXFeatureExtractoraccepts a preset name asconfigand a dict of per-feature parameters asfeatures.AFXFeatureModuleacceptsaggregation='summary', rejects integer tensors, and names its vector columns withget_feature_names_out().afx.extract_segmentsreturns one aggregated vector per fixed-length segment of a long signal.afx.register_featureandunregister_featureadd custom extractors that configs, validation,feature_infoand every front end accept.afx.feature_namesandafx.flatten_featuresfor thename/name_icolumns used by every tabular output.load_audio(path, sr, offset, duration)reads part of a file and accepts file objects.- A config's
featuresmay be a list of names. - Parameters that were hidden in
**kwargsare named:mfcctop_db;mfcc_deltawidth,n_mels,fmin,fmax;pitchthreshold;thdframe_size,hop_length,n_harmonics;chroma_stfttuning,normalize;cqtandchroma_cqtn_bins,bins_per_octave,fmin. afx.methods.chroma_filterbankandchroma_from_power.spectral_kurtosis,spectral_crestandspectral_decrease(Peeters, 2004), on both the NumPy and torch paths.lpc(autocorrelation method, Levinson-Durbin over all frames at once) andformants(LPC root solving with bandwidth and frequency limits), withafx.methods.lpcandlpc_roots. The speech preset includes F1 to F3.- The torch path covers 20 features: MFCC deltas, GFCC, chroma_stft, spectral skewness, kurtosis, slope, crest, decrease and contrast were added,
rms_energyandzcrtake framing andcenter, andafx.torch.functional.deltareproducescompute_delta. tests/test_librosa_parity.py(fourteen features against librosa), edge-case tests for every feature, a documentation site (mkdocs,docs/), a mypy job,py.typed, andbenchmarks/bench_features.py.melspectrogram,chroma_stftandcqtacceptcenter;melspectrogramacceptsfminandfmaxas named parameters.validate_config(also run byload_config,extract_all_features, the batch extractor and the scikit-learn transformer) rejects unknown feature names and parameters, badaggregationornormalizevalues and non-object feature parameters.extract_features_from_folderaccepts a config dict,sample_rateandverbose.- The batch extractor checks the output folder and the Parquet dependencies before extracting, and
--featuresmay name features that are not in the config (they get default parameters). - CLI:
--preset,-o,--version,--sample-rate,--quiet,--list-features,--list-presets,--describe; the output format follows the--outputextension when--formatis omitted. afx.methods.autocorrelation(FFT autocorrelation shared by the time-domain, pitch and HNR code),afx.__version__, ruff configuration, Windows and macOS CI jobs, wheel build check.
Changed¶
- Documentation moved to https://danilopena.com/AFX/.
- Workflow actions are pinned to commit SHAs, with read-only default permissions.
- Extractor docstrings cite Davis and Mermelstein (MFCC), Brown (CQT bin layout), Boersma (HNR), Böck and Widmer (SuperFlux onset strength) and Shao and Wang (GFCC).
- The demo notebooks were replaced by the numbered set;
feature_benchmark_validation.ipynbis nowbenchmark_validation.ipynb. - Time-domain extractors, YIN, HNR, THD, chroma folding and sliding-window aggregation are vectorized over frames. On 10 s at 22.05 kHz: pitch 88 to 50 ms, hnr 77 to 45 ms, entropy 88 to 40 ms, autocorr_variance 66 to 20 ms, chroma_stft 48 to 21 ms.
- Sample entropy checks the candidate template pairs found by sorting directly when there are few of them (frames up to about 1500 samples of noise-like audio) and keeps the KD-tree count otherwise; results are the same.
import afx_audio as afxtakes about 0.4 s instead of 1.3 s (scipy submodules are imported where used).- CSV output no longer needs pandas. Batch output keys use forward slashes on every OS, and failed or missing files are reported with warnings instead of prints.
- Whole-clip spectral features use the periodic Hann window, like the STFT.
extract_all_featuresrejects non-1-D signals with a clear error.frame_signalis vectorized (stride tricks instead of a Python loop).- Removed the unused window cache helpers and
pseudo_cqt(usecqt_approximation), and the requirements files (pyproject.toml lists the dependencies).
0.2.0¶
Domain presets, scikit-learn transformer, PyTorch path, parallel batch extraction with Parquet output.