Module musx.audio
A module providing support for reading and writing audio files using Todd Ingall's pysndlib package:
% pip install pysndlib
Example:
from musx import Seq, Score, between
import pysndlib.clm as CLM
# Define a simple audio instrument:
def simp(start, dur, freq, amp=.5):
start = CLM.seconds2samples(start)
end = start + CLM.seconds2samples(dur)
osc = CLM.make_oscil(freq)
for i in range(start, end):
CLM.outa(i, amp * CLM.oscil(osc))
# Create an audio note class for it:
SimpNote = AudioNote(simp)
# Define a part composer that will add instances of
# SimpNote to a score:
def playsimp(score, num, rate, dur, low, high, amp):
for _ in range(num):
freq = between(low, high)
# add SimpNotes to the score
score.add(SimpNote(score.now, dur, freq, amp))
yield rate
# Compose the score:
score = Score(out=Seq())
score.compose(playsimp(score, 10, .3, .3, 200, 440, .2))
# Print the score:
score.out.print()
# Write the score to a sound file and play it:
file = AudioFile("test.wav", score.out).write(play=True)
Expand source code
"""
A module providing support for reading and writing audio files using
Todd Ingall's [pysndlib package](https://pypi.org/project/pysndlib/):
```bash
% pip install pysndlib
```
### Example:
```python
from musx import Seq, Score, between
import pysndlib.clm as CLM
# Define a simple audio instrument:
def simp(start, dur, freq, amp=.5):
start = CLM.seconds2samples(start)
end = start + CLM.seconds2samples(dur)
osc = CLM.make_oscil(freq)
for i in range(start, end):
CLM.outa(i, amp * CLM.oscil(osc))
# Create an audio note class for it:
SimpNote = AudioNote(simp)
# Define a part composer that will add instances of
# SimpNote to a score:
def playsimp(score, num, rate, dur, low, high, amp):
for _ in range(num):
freq = between(low, high)
# add SimpNotes to the score
score.add(SimpNote(score.now, dur, freq, amp))
yield rate
# Compose the score:
score = Score(out=Seq())
score.compose(playsimp(score, 10, .3, .3, 200, 440, .2))
# Print the score:
score.out.print()
# Write the score to a sound file and play it:
file = AudioFile("test.wav", score.out).write(play=True)
```
"""
try:
import pysndlib.clm as CLM
except ModuleNotFoundError as err:
pad=' '
print("****Error while loading musx.audio:")
print(f"{pad*2}{repr(err)}")
print(f"{pad}musx.audio requires the pysndlib package.")
print(f"{pad}Type 'pip install pysndlib' in your terminal to install it.")
exit()
import inspect
from .seq import Seq
class AudioFile:
'''
A class for reading and writing audio files using pysndlib.
Parameters
----------
path : string
The pathname of the audio file to be written, e.g. '/tmp/sweet.wav'.
seq : Seq
A sequence containing the instrument notes to render.
'''
def __init__(self, path, seq):
if not isinstance(path, str) or len(path) == 0:
raise TypeError(f"SoundFile(): '{path}' is not a valid pathname string.")
self.pathname = path
if not isinstance(seq, Seq):
raise TypeError(f"SoundFile(): '{seq}' is not a sequence.")
self.seq = seq
def read(self):
"""Implement me!"""
return self
def write(self, **kwargs):
'''
Render the notes in the SoundFile to an audio file on the disk.
Parameters
----------
kwargs : keyword args
Any keywords that are supported by pysndlib's
[Sound Context](https://testcase.github.io/pysndlib/with_sound.html)
'''
with CLM.Sound(self.pathname, **kwargs):
for note in self.seq:
note._write()
return self
def __str__(self):
name = self.__class__.__name__
seq = f'<Seq: len={len(self.seq)}, endtime={self.seq.endtime()}>'
return f'<{name}: path="{self.pathname}", seq={seq}>'
__repr__ = __str__
def AudioNote(ins, classname=''):
"""
Defines a note class given a pysndlib instrument (function). Once defined,
instances of the new note class can be added to Score and Seq objects and
will generate audio when rendered by AudioFile.write().
Parameters
----------
ins : function
The CLM instrument (function) to create the new note class for. The first
parameter to the instrument must contain the start time of the instrument call.
classname : string
An optional argument providing the full name for the new Note class.
Defaults to '', in which case the name will be the capitalized instrument name
with all "_" removed and appended with "Note". For example, if the instrument is
hi_ho() the default name for the new note class will be HiHoNote.
Return
------
The function returns a Python class whose instances hold parameter values
for the instument to render when a sound file is written, e.g.:
```python
# Immediate instrument call inside a Sound context:
with Sound():
simp(1,2,3,4)
# SimpNote equivalent that can be added to a score, editied,
# and rendered later, when an audio file is written:
SimpNote(1,2,3,4)
```
"""
if not callable(ins): raise TypeError(f"Not a function: {ins}")
if not classname:
for n in f"{ins.__name__}_Note".split('_'):
if n == '': continue
classname += n.capitalize()
# dictionary of function parameters with default values or None.
params = {n: inspect.Parameter.empty if v.default==inspect._empty else v.default
for n, v in inspect.signature(ins).parameters.items()}
# define the init method for the new note class.
def __init__(self, *args, **kwargs):
###print(f"args: {args}, kwargs: {kwargs}")
###print(f"params: {self.params}")
# cache the list of function parameter names
self._params = list(params)
# define an attribute for each instrument parameter and initialize
# it to the parameter's default value or None
for p in params.items():
setattr(self, p[0], p[1])
# keep a record of how many times each arg receives a value
counts = {p:0 for p in params}
###print(f'counts: {counts}')
# collect ordered list of insturment's parameter names (strings)
names = list(params)
###print(f'names: {names}')
# parse the positional arguments, signal error if more arguments than parameters,
# create the attribute and increment arg's count.
for index, value in enumerate(args):
if index == len(params):
raise TypeError(f"{classname}() takes {len(params)} positional arguments but {len(args)} were given.")
name = names[index]
setattr(self, name, value)
counts[name] += 1
# parse the keyword arguments, signal error if the keyword parameter
# was already set by a positional argument, or keyword is not value.
# set the attribute and increment arg's count.
for name, value in kwargs.items():
if name in names:
if counts[name] > 0:
raise TypeError(f"{classname}() parameter '{name}' assigned more than once.")
setattr(self, name, value)
counts[name] += 1
else:
raise TypeError(f"{classname}() got an unexpected keyword argument '{name}'.")
# for any parameter that was not assigned set to default value or None.
unassigned = []
for name, count in counts.items():
if count == 0:
if params[name] is not inspect.Parameter.empty:
setattr(self, name, params[name])
counts[name] += 1
else:
unassigned.append(name)
elif count > 1:
raise TypeError(f"{classname}() parameter '{name}' assigned more that once.")
# one or more parameters do not have values.
if unassigned:
l = len(unassigned)
p = "parameters" if l > 1 else "parameter"
unassigned = ", ".join(unassigned)
raise TypeError(f"{classname}() has {l} unassigned {p}: {unassigned}.")
# First argument to note must be the start time value....
# Event.__init__(self, getattr(self, params[0][0]))
self.time = getattr(self, names[0])
# repr() and str() will print like an instrument call, floats will be
# trucated to 3 places.
def __repr__(self):
"""
Both repr() and str() print like an instrument call. To save space floats are
rounded to three places and lists of more than 10 elements are elided. To see
exact values in the object use it's self.params() method.
"""
def paramstr(val):
if isinstance(val, float):
#return f"{val:.3f}"
return f"{round(val,3)}"
if isinstance(val, list):
val = [paramstr(v) for v in val]
if len(val) > 10:
val = val[:4] + ["..."] + val[-4:]
return "[" + ", ".join(val) + "]"
return f"{val}"
text = self.__class__.__name__
#return text + '(' + ", ".join(f"{getattr(self,p)}" for p in self.params) + ')'
return text + '(' + ", ".join(f"{paramstr(getattr(self,p))}"
for p in self._params) + ')'
def __eq__(self, other):
'''
Returns True if parameter values in two instances are all equal (==).
'''
if type(self) is not type(other):
return False
for p in self._params:
if getattr(self, p) != getattr(other, p):
return False
return True
def _write(self):
'''
Internal function called by AudioFile().write() to render
audio samples inside a 'with Sound:' construct.
'''
args = [getattr(self, p) for p in self._params]
#print(f"{ins} writing args: {args}")
ins(*args)
def parameters(self):
"""Returns an ordered dictionary of the note's attributes and their values."""
return {p: getattr(self, p) for p in self._params}
return type(classname, (), {"__init__": __init__,
"__repr__": __repr__,
"__str__": __repr__,
# ARRRG caching instrument in an instance attr doesn't work
#"ins": ins,
"__eq__": __eq__,
"_write": _write,
"parameters": parameters
})
if __name__ == '__main__':
pass
# from musx import Seq, Score, between
# def simp(start, dur, freq, amp=.5):
# #print(f"simp: start={start}, dur={dur}, freq={freq}, amp={amp}")
# start = CLM.seconds2samples(start)
# end = start + CLM.seconds2samples(dur)
# osc = CLM.make_oscil(freq)
# for i in range(start, end):
# CLM.outa(i, amp * CLM.oscil(osc))
# SimpNote = AudioNote(simp)
# def playsimp(score, num, rate, dur, low, high, amp):
# for _ in range(num):
# freq = between(low, high)
# score.add(SimpNote(score.now, dur, freq, amp))
# yield rate
# score = Score(out=Seq())
# score.compose(playsimp(score, 10, .3, .3, 200, 440, .2))
# score.out.print()
# file = AudioFile("test.wav", score.out) #.write(play=True)
# print(f"Writing {file}")
# file.write(play=True)
#========================================================
# def testnote(*args, **kwargs):
# def printargs(*args, **kwargs):
# a = ', '.join(str(a) for a in args)
# k = ', '.join(f"{k}={v}" for k,v in kwargs.items())
# if a and k: sig = a + ", " + k
# else: sig = a or k
# print(f'simp({sig})')
# printargs(*args, **kwargs)
# try:
# n = SimpNote(*args,**kwargs)
# print(n)
# except Exception as e:
# print(f'{type(e).__name__}: {e.args[0]}')
# print("--------------------------------------------------")
# testnote(1,2,3,4)
# print("--------------------------------------------------")
# testnote(amp=3,freq=2,dur=2,start=0)
# print("--------------------------------------------------")
# testnote(1,2,freq=2,amp=4)
# print("--------------------------------------------------")
Functions
def AudioNote(ins, classname='')
-
Defines a note class given a pysndlib instrument (function). Once defined, instances of the new note class can be added to Score and Seq objects and will generate audio when rendered by AudioFile.write().
Parameters
ins
:function
- The CLM instrument (function) to create the new note class for. The first parameter to the instrument must contain the start time of the instrument call.
classname
:string
- An optional argument providing the full name for the new Note class. Defaults to '', in which case the name will be the capitalized instrument name with all "_" removed and appended with "Note". For example, if the instrument is hi_ho() the default name for the new note class will be HiHoNote.
Return
The function returns a Python class whose instances hold parameter values for the instument to render when a sound file is written, e.g.:
# Immediate instrument call inside a Sound context: with Sound(): simp(1,2,3,4) # SimpNote equivalent that can be added to a score, editied, # and rendered later, when an audio file is written: SimpNote(1,2,3,4)
Expand source code
def AudioNote(ins, classname=''): """ Defines a note class given a pysndlib instrument (function). Once defined, instances of the new note class can be added to Score and Seq objects and will generate audio when rendered by AudioFile.write(). Parameters ---------- ins : function The CLM instrument (function) to create the new note class for. The first parameter to the instrument must contain the start time of the instrument call. classname : string An optional argument providing the full name for the new Note class. Defaults to '', in which case the name will be the capitalized instrument name with all "_" removed and appended with "Note". For example, if the instrument is hi_ho() the default name for the new note class will be HiHoNote. Return ------ The function returns a Python class whose instances hold parameter values for the instument to render when a sound file is written, e.g.: ```python # Immediate instrument call inside a Sound context: with Sound(): simp(1,2,3,4) # SimpNote equivalent that can be added to a score, editied, # and rendered later, when an audio file is written: SimpNote(1,2,3,4) ``` """ if not callable(ins): raise TypeError(f"Not a function: {ins}") if not classname: for n in f"{ins.__name__}_Note".split('_'): if n == '': continue classname += n.capitalize() # dictionary of function parameters with default values or None. params = {n: inspect.Parameter.empty if v.default==inspect._empty else v.default for n, v in inspect.signature(ins).parameters.items()} # define the init method for the new note class. def __init__(self, *args, **kwargs): ###print(f"args: {args}, kwargs: {kwargs}") ###print(f"params: {self.params}") # cache the list of function parameter names self._params = list(params) # define an attribute for each instrument parameter and initialize # it to the parameter's default value or None for p in params.items(): setattr(self, p[0], p[1]) # keep a record of how many times each arg receives a value counts = {p:0 for p in params} ###print(f'counts: {counts}') # collect ordered list of insturment's parameter names (strings) names = list(params) ###print(f'names: {names}') # parse the positional arguments, signal error if more arguments than parameters, # create the attribute and increment arg's count. for index, value in enumerate(args): if index == len(params): raise TypeError(f"{classname}() takes {len(params)} positional arguments but {len(args)} were given.") name = names[index] setattr(self, name, value) counts[name] += 1 # parse the keyword arguments, signal error if the keyword parameter # was already set by a positional argument, or keyword is not value. # set the attribute and increment arg's count. for name, value in kwargs.items(): if name in names: if counts[name] > 0: raise TypeError(f"{classname}() parameter '{name}' assigned more than once.") setattr(self, name, value) counts[name] += 1 else: raise TypeError(f"{classname}() got an unexpected keyword argument '{name}'.") # for any parameter that was not assigned set to default value or None. unassigned = [] for name, count in counts.items(): if count == 0: if params[name] is not inspect.Parameter.empty: setattr(self, name, params[name]) counts[name] += 1 else: unassigned.append(name) elif count > 1: raise TypeError(f"{classname}() parameter '{name}' assigned more that once.") # one or more parameters do not have values. if unassigned: l = len(unassigned) p = "parameters" if l > 1 else "parameter" unassigned = ", ".join(unassigned) raise TypeError(f"{classname}() has {l} unassigned {p}: {unassigned}.") # First argument to note must be the start time value.... # Event.__init__(self, getattr(self, params[0][0])) self.time = getattr(self, names[0]) # repr() and str() will print like an instrument call, floats will be # trucated to 3 places. def __repr__(self): """ Both repr() and str() print like an instrument call. To save space floats are rounded to three places and lists of more than 10 elements are elided. To see exact values in the object use it's self.params() method. """ def paramstr(val): if isinstance(val, float): #return f"{val:.3f}" return f"{round(val,3)}" if isinstance(val, list): val = [paramstr(v) for v in val] if len(val) > 10: val = val[:4] + ["..."] + val[-4:] return "[" + ", ".join(val) + "]" return f"{val}" text = self.__class__.__name__ #return text + '(' + ", ".join(f"{getattr(self,p)}" for p in self.params) + ')' return text + '(' + ", ".join(f"{paramstr(getattr(self,p))}" for p in self._params) + ')' def __eq__(self, other): ''' Returns True if parameter values in two instances are all equal (==). ''' if type(self) is not type(other): return False for p in self._params: if getattr(self, p) != getattr(other, p): return False return True def _write(self): ''' Internal function called by AudioFile().write() to render audio samples inside a 'with Sound:' construct. ''' args = [getattr(self, p) for p in self._params] #print(f"{ins} writing args: {args}") ins(*args) def parameters(self): """Returns an ordered dictionary of the note's attributes and their values.""" return {p: getattr(self, p) for p in self._params} return type(classname, (), {"__init__": __init__, "__repr__": __repr__, "__str__": __repr__, # ARRRG caching instrument in an instance attr doesn't work #"ins": ins, "__eq__": __eq__, "_write": _write, "parameters": parameters })
Classes
class AudioFile (path, seq)
-
A class for reading and writing audio files using pysndlib.
Parameters
path
:string
- The pathname of the audio file to be written, e.g. '/tmp/sweet.wav'.
seq
:Seq
- A sequence containing the instrument notes to render.
Expand source code
class AudioFile: ''' A class for reading and writing audio files using pysndlib. Parameters ---------- path : string The pathname of the audio file to be written, e.g. '/tmp/sweet.wav'. seq : Seq A sequence containing the instrument notes to render. ''' def __init__(self, path, seq): if not isinstance(path, str) or len(path) == 0: raise TypeError(f"SoundFile(): '{path}' is not a valid pathname string.") self.pathname = path if not isinstance(seq, Seq): raise TypeError(f"SoundFile(): '{seq}' is not a sequence.") self.seq = seq def read(self): """Implement me!""" return self def write(self, **kwargs): ''' Render the notes in the SoundFile to an audio file on the disk. Parameters ---------- kwargs : keyword args Any keywords that are supported by pysndlib's [Sound Context](https://testcase.github.io/pysndlib/with_sound.html) ''' with CLM.Sound(self.pathname, **kwargs): for note in self.seq: note._write() return self def __str__(self): name = self.__class__.__name__ seq = f'<Seq: len={len(self.seq)}, endtime={self.seq.endtime()}>' return f'<{name}: path="{self.pathname}", seq={seq}>' __repr__ = __str__
Methods
def read(self)
-
Implement me!
Expand source code
def read(self): """Implement me!""" return self
def write(self, **kwargs)
-
Render the notes in the SoundFile to an audio file on the disk.
Parameters
kwargs
:keyword args
- Any keywords that are supported by pysndlib's Sound Context
Expand source code
def write(self, **kwargs): ''' Render the notes in the SoundFile to an audio file on the disk. Parameters ---------- kwargs : keyword args Any keywords that are supported by pysndlib's [Sound Context](https://testcase.github.io/pysndlib/with_sound.html) ''' with CLM.Sound(self.pathname, **kwargs): for note in self.seq: note._write() return self