This commit is contained in:
Etienne Gaudrain 2025-03-28 12:10:48 +01:00
parent 1b7b8b6c6f
commit 3393ba4827
4 changed files with 141 additions and 1 deletions

0
MANIFEST.in Normal file
View File

View File

@ -1,3 +1,15 @@
# scaled_soundfile
A thin wrapper around the `soundfile` module to allow saving files of any scales.
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
```

38
pyproject.toml Normal file
View File

@ -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

View File

@ -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))