From 3393ba48277e49fe824415af495991b09fad576d Mon Sep 17 00:00:00 2001 From: Etienne Gaudrain Date: Fri, 28 Mar 2025 12:10:48 +0100 Subject: [PATCH] Initial --- MANIFEST.in | 0 README.md | 14 +++++- pyproject.toml | 38 +++++++++++++++ scaled_soundfile/__init__.py | 90 ++++++++++++++++++++++++++++++++++++ 4 files changed, 141 insertions(+), 1 deletion(-) create mode 100644 MANIFEST.in create mode 100644 pyproject.toml create mode 100644 scaled_soundfile/__init__.py diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..e69de29 diff --git a/README.md b/README.md index 226122a..b1e9d89 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,15 @@ # scaled_soundfile -A thin wrapper around the `soundfile` module to allow saving files of any scales. \ No newline at end of file +A thin wrapper around the `soundfile` module to allow saving files of any scales. + +The `write` function rescales the input to normalize it and maximize the number of bits it is saved on. The RMS of the original sound is saved in the 'comment' metadata field as part of a JSON object. The files are saved as 'PCM_24'. + +The `read` function loads the normalized data, and rescales it so that the RMS matches the original by reading the 'comment' metadata field if it exists. + +In the current version of `soundfile` (and `libsndfile`) this only works with WAV files. + +# Installation + +``` +pip install https://git.web.rug.nl/dBSPL/scaled_soundfile +``` diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..7192dfa --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,38 @@ +[project] +name = "scaled_soundfile" +version = "1.0.0" +authors = [ + { name="Etienne Gaudrain", email="etienne.gaudrain@cnrs.fr" }, +] +description = "A thin wrapper around the `soundfile` module to save sound files with arbitrary scale." +readme = "README.md" +requires-python = ">=3.0" +classifiers = [ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: MIT License", + "Operating System :: Microsoft :: Windows", +] +dependencies = [ + "numpy", + "soundfile" +] + +[project.urls] +Homepage = "https://git.web.rug.nl/dBSPL/scaled_soundfile.git" +Issues = "https://git.web.rug.nl/dBSPL/scaled_soundfile/issues" + +[build-system] +requires = [ + "setuptools" +] +build-backend = "setuptools.build_meta" + +# [tool.setuptools] +# include-package-data = true + +[tool.setuptools.packages.find] +# All the following settings are optional: +where = ["."] # ["."] by default +include = ["scaled_soundfile*"] # ["*"] by default +exclude = [] # empty by default +namespaces = false # true by default diff --git a/scaled_soundfile/__init__.py b/scaled_soundfile/__init__.py new file mode 100644 index 0000000..b28cbb4 --- /dev/null +++ b/scaled_soundfile/__init__.py @@ -0,0 +1,90 @@ +import soundfile as sf +import numpy as np +import json + +def read(fname, v=False, **kwargs): + """ + Read a sound file from its filename. If there is no comment field in the file's + metadata, it will silently not scale the file. + + Parameters + ---------- + fname : str + The filename. + v : bool, default=False + Whether to return a boolean indicating whether the file was scaled. + **kwargs : + Keyword arguements as passed to :fun:`sounfile.read`. + + Returns + ------- + x : ndarray + fs : float, int + scaled : bool + If `v` is True, whether the file was scaled or not. + """ + + kwargs_read = {k:v for k,v in kwargs.items() if k in ['frames', 'fill_value', 'atleast_2d', 'dtype']} + kwargs_sf = {k:v for k,v in kwargs.items() if k in ['samplerate', 'channels', 'subtype', 'endian', 'format']} + + if 'out' in kwargs: + raise ValueError("The `out` arguement cannot be used.") + + with sf.SoundFile(fname, *kwargs_sf) as f: + x = f.read() + fs = f.samplerate + try: + cmt = json.loads(f.comment) + r = cmt['rms'] + except: + r = 1 + scaled = False + if r != 1: + x = x/rms(x) * r + scaled = True + if v: + return x, fs, scaled + else: + return x, fs + +def rms(x): + return np.sqrt(np.mean(x**2)) + +def write(fname, x, fs, *args, **kwargs): + """ + Write an array to a sound file with maximum scale. + This only works with WAV files. Note that the extra arguments + `args` and `kwargs` will be ignored. + + Parameters + ---------- + x : ndarray + The sound array. + fs : float, int + The sampling rate. + fname : str + The filename. + """ + r = rms(x) + y = np.atleast_2d(x / np.max(np.abs(x)) * .98) + + if y.shape[1]>y.shape[0]: + y = y.T + + with sf.SoundFile(fname, mode='w', samplerate=fs, channels=y.shape[1], subtype='PCM_24') as f: + f.write(y) + f.comment = json.dumps({'rms': r}) + + +#------------------------------ +if __name__=='__main__': + fs = 44100 + d = 1 + t = np.arange(d*fs)/fs + x = np.sin(2*np.pi*1000*t)*10 + + write(x, fs, "test.wav") + y, fs, _ = read("test.wav") + + print(rms(x)) + print(rms(y))