Skip to content

scikit-learn, PyTorch and batch extraction

scikit-learn

afx.sklearn.AFXFeatureExtractor (extra sklearn) takes file paths or 1-D signals and returns an (n_samples, n_features) array, so it fits in Pipeline, GridSearchCV and cross_val_score. n_jobs parallelizes over samples with joblib.

from afx_audio.sklearn import AFXFeatureExtractor
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.svm import SVC

pipe = make_pipeline(
    AFXFeatureExtractor(features=['mfcc', 'spectral_centroid', 'zcr'],
                        sample_rate=16000, aggregation='summary', n_jobs=-1),
    StandardScaler(),
    SVC(),
)
pipe.fit(train_paths, y_train)
pipe[0].get_feature_names_out()   # ['mfcc_mean_0', ..., 'zcr_std']

PyTorch

afx.torch (extra torch) reimplements 20 features with torch operations and the same conventions as the NumPy path: mel spectrogram, MFCC, MFCC deltas, GFCC, chroma_stft, the twelve spectral descriptors (centroid, bandwidth, rolloff, entropy, flatness, flux, skewness, kurtosis, slope, crest, decrease, contrast), zero-crossing rate and RMS. afx.torch.supported_features() lists them, and the tests hold every one to rtol 1e-9 of the NumPy extractor in float64 at two framings (afx.feature_info(name)['torch'] gives each tolerance). Inputs are (time,) or (batch, time) tensors on any device. Gradients reach the waveform except through the counting in zcr and the bin selection in spectral_rolloff.

import torch
from afx_audio.torch import AFXFeatureModule, functional as F_afx

mfcc = F_afx.mfcc(waveform, sr=16000, n_mfcc=13)

frontend = AFXFeatureModule(
    features={'mfcc': {'n_mfcc': 13}, 'spectral_centroid': {}},
    sample_rate=16000, aggregation='mean_std', output='vector',
)
model = torch.nn.Sequential(frontend, torch.nn.Linear(28, 2))

With output='vector', frontend.get_feature_names_out() names the columns as the NumPy path does, and the vector equals flatten_features(extract_all_features(...)) with aggregation='summary' for 'mean_std'.

AFXFeatureModule.from_config(config) builds the module from a config dict, a JSON path or a preset id, with the config's per-feature parameters, sample_rate, aggregation and coverage (preserve_shape becomes aggregation='none'). Aggregations other than mean, std and summary, and normalize, raise ValueError. So do features without a torch implementation, unless skip_unsupported=True drops them with an AFXWarning. Of the presets only bioacoustics is fully on the torch path; speech loses pitch, HNR and formants.

frontend = AFXFeatureModule.from_config('speech@1', output='vector', skip_unsupported=True)

Windows, filterbanks and DCT matrices are buffers, so they follow .to(device). They are stored in float64 and cast to the input dtype at each forward: float64 input gives float64 output at the precision of the functions, and float32 input gives float32 output. .float() or .half() on the module lowers the stored constants as well. Parameters the torch path does not implement raise in the constructor.

Folders and the CLI

afx.extract_features_from_folder(folder, config, output) walks a folder recursively, reads every file whose extension (any case) belongs to a format the installed soundfile reads (.wav, .flac, .ogg, .opus, .mp3, .aiff, .au, .caf, .w64, ...; see afx_audio.batch_extractor.default_extensions()), and writes one row per file as npy (a pickled dict), json, csv or parquet (extra parquet). Vector features expand to name_0, name_1, ... columns, except egemaps and egemaps_lld, whose columns take openSMILE's names (see eGeMAPS). n_jobs runs files in worker processes with output identical to a serial run. The workers never fork (fork deadlocks when the parent has threads): they start with forkserver on Linux and spawn on Windows and macOS, or with the method set by multiprocessing.set_start_method. Each worker imports the calling script, so a script calling it with n_jobs > 1 must do so under if __name__ == '__main__':, and registered custom features must be importable (not defined in a notebook).

afx dataset/ --preset speech --output features.parquet --jobs 8
afx dataset/ --config my.json --features mfcc,zcr --aggregation summary --output features.csv
afx dataset/ --preset speech --ext wav,flac --output features.csv   # only .wav and .flac files
afx --list-features
afx --describe spectral_rolloff

Without --output, afx writes features.csv in the current folder.

Custom features

afx.register_feature(name, func, units=..., rows=...) adds a function func(signal, sr, **params) -> {name: array} to FEATURE_MAP, so configs, validate_config, feature_info, the scikit-learn transformer and the batch extractor accept it. Wrap a per-frame function with afx.utils.framewise.framewise_extractor to get frame_size, hop_length, center and frame times for free (it calls Python once per frame). The batch extractor and AFXFeatureExtractor repeat the registrations in their worker processes, which requires the function to be picklable: defined at module level, not a lambda or a nested function.

import numpy as np
import afx_audio as afx
from afx_audio.utils.framewise import framewise_extractor

@framewise_extractor
def peak_amplitude(signal, sr=None, *, scale=1.0):
    return {'peak_amplitude': np.array([scale * np.max(np.abs(signal))])}

afx.register_feature('peak_amplitude', peak_amplitude, units='amplitude')
afx.extract_all_features(x, sr, {'features': {'peak_amplitude': {'frame_size': 1024, 'hop_length': 512}}})