:py:mod:`soundit`
=================
.. py:module:: soundit
.. autoapi-nested-parse::
Make audio
This module uses iterators to represent audio streams. These iterators return
float values between [-1.0, 1.0) and can be chained, averaged, and precomputed
to your heart's content to create music. We call these iterators "sounds".
Note that all sounds are in 48kHz.
There is also a kinda sus music parser which can aid in creating longer music.
More info on that can be found in the `music_to_notes`'s docstring.
Sound generators:
`sine`
`square`
`sawtooth`
`triangle`
`silence`
`piano` (requires `init_piano` to be called)
Sound creation utilities:
`passed`
Sound effects:
`fade`
`volume`
`cut`
`pad`
`exact`
`delay`
Frequency utilities:
`make_frequencies_dict`
`make_indices_dict`
Music functions:
`split_music`
`music_to_notes`
`notes_to_sine`
`_notes_to_sound` (unfinalized API)
Audio source utilities:
`chunked`
`unchunked`
We also provide some utility functions with other tools such as converting
chunks into discord.py AudioSources or decompressing audio on the fly with
FFmpeg. These only work when their required library is installed.
discord.py utilities:
`wrap_discord_source`
`unwrap_discord_source`
`play_discord_source`
FFmpeg utilities:
`file_chunks`
`make_ffmpeg_section_args`
`create_ffmpeg_process`
`chunked_ffmpeg_process`
sounddevice utilities:
`play_output_chunks`
`create_input_chunks`
There is also some built-in music that are prefixed with \MUSIC_, such as
MUSIC_DIGITIZED, provided for testing purposes.
Classes
-------
.. autoapisummary::
:nosignatures:
soundit.LRUCache
soundit.LRUIterableCache
soundit.DiscordIteratorSource
Functions
---------
.. autoapisummary::
:nosignatures:
soundit.silence
soundit.sine
soundit.square
soundit.sawtooth
soundit.triangle
soundit.init_piano
soundit.piano
soundit.file_chunks
soundit.lru_iter_cache
soundit.create_ffmpeg_process
soundit.chunked_ffmpeg_process
soundit.make_ffmpeg_section_args
soundit.loop_stream
soundit.equal_chunk_stream
soundit.passed
soundit.fade
soundit.both
soundit.single
soundit.volume
soundit.cut
soundit.pad
soundit._cut_cross
soundit.exact
soundit.delay
soundit._resample_linear
soundit.play_discord_source
soundit.wrap_discord_source
soundit.chunked
soundit.unwrap_discord_source
soundit.unchunked
soundit.make_frequencies_dict
soundit.make_indices_dict
soundit.music_to_notes
soundit.split_music
soundit.notes_to_sine
soundit._notes_to_sound
soundit.play_output_chunks
soundit.create_input_chunks
soundit.reload
Attributes
----------
.. autoapisummary::
soundit.has_discord
soundit.has_sounddevice
soundit.has_av
soundit.has_numpy
soundit.RATE
soundit.A4_FREQUENCY
soundit.A4_INDEX
soundit.NOTE_NAMES
soundit.piano_data
soundit.MUSIC_DIGITIZED
soundit.MUSIC_MEGALOVANIA
soundit.MUSIC_DIGITIZED_DUAL
Documentation
-------------
.. py:data:: has_discord
:value: False
.. py:data:: has_sounddevice
:value: False
.. py:data:: has_av
:value: False
.. py:data:: has_numpy
:value: False
.. py:data:: RATE
:value: 48000
.. py:data:: A4_FREQUENCY
:value: 440
.. py:data:: A4_INDEX
:value: 57
.. py:data:: NOTE_NAMES
.. py:function:: silence()
Returns silence
.. py:function:: sine(freq=A4_FREQUENCY)
Returns a sine wave at freq
.. py:function:: square(freq=A4_FREQUENCY)
Returns a square wave at freq
.. py:function:: sawtooth(freq=A4_FREQUENCY)
Returns a sawtooth wave at freq
.. py:function:: triangle(freq=A4_FREQUENCY)
Returns a triangle wave at freq
.. py:data:: piano_data
.. py:function:: init_piano()
Loads the piano sound for use
The raw file 0.raw was generated from Online Sequencer's Electric Piano
instrument (from https://onlinesequencer.net/app/instruments/0.ogg?v=12)
and FFmpeg was then used to convert it into a raw mono 48kHz signed 16-bit
little endian file (using ffmpeg -i 0.ogg -f s16le -acodec pcm_s16le -ac 1
-ar 48000 0.raw).
.. py:function:: piano(index=A4_INDEX)
Returns a piano sound at index
.. py:function:: file_chunks(filename: str, start: float = 0)
Returns a sound from an audio file using FFmpeg
:param filename: path to the audio file
:param start: seconds into the audio to start at
:returns: stream of two-tuples of floats decoded from the audio file
.. py:class:: LRUCache(*, maxsize: Optional[int] = 128)
An LRU cache
:param maxsize: the maximum size of the cache (None means unbounded)
.. py:attribute:: maxsize
The maximum size of the cache
0 means that the cache will remain empty. Specifying None means the
cache will grow without bound.
Note that changes to maxsize won't take effect until the next `get()`
call with a key not in the cache. It is not recommended, but you can
call ``._ensure_size()`` to force it to resize the cache.
.. py:attribute:: hits
:value: 0
The number of cache hits
.. py:attribute:: misses
:value: 0
The number of cache misses
.. py:attribute:: results
:type: dict
The dictionary between keys and values
Checking and modifying the cache manually isn't recommended.
.. py:method:: get(key, value_func)
Return the value for this key, calling value_func if needed
:param key: unique key for a value
:param value_func: a zero-arg function returning a value for this key
.. py:method:: clear()
Clears the cache and the hits / misses counters
.. py:class:: LRUIterableCache(*, maxsize: Optional[int] = 128)
Bases: :py:obj:`LRUCache`
An LRU cache for iterables
This class internally stores `itertools.tee` objects wrapped around the
original values and returns a copy of the tees in the cache.
We use tee objects for a few reasons:
- They can be iterated at different speeds.
- They are iterators (lazily evaluated).
- They can be copied (major orz for this one).
- They are fast (implemented in C).
See `LRUCache` for more info on caching. See `itertools.tee` for more info on
tee objects.
.. py:method:: get(key, iterable_func)
Return the iterator for this key, calling iterable_func if needed
If a matching tee is cached, return a copy. If no tee is found, create
a new one using the iterable_func and make a copy of it.
.. py:function:: lru_iter_cache(func=None, *, maxsize: Optional[int] = 128, cache: Optional[LRUIterableCache] = None)
Decorator to wrap a function returning iterables
:param maxsize: the maximum size of the cache (None means unbounded)
:param cache: the cache to use
See `LRUIterableCache` for more info.
.. py:function:: create_ffmpeg_process(*args, executable='ffmpeg', pipe_stdin=False, pipe_stdout=True, pipe_stderr=False, **kwargs)
Creates a process that run FFmpeg with the given arguments
This assumes that ``ffmpeg.exe`` is on your PATH environment variable. If not,
you can specify its location using the executable argument.
For the ``pipe_*`` arguments, if it is True, `subprocess.PIPE` will be passed to
`subprocess.Popen`'s constructor. Otherwise, None will be passed.
All other keyword arguments are passed directly to `subprocess.Popen`.
.. py:function:: chunked_ffmpeg_process(process: Popen, *, close: Optional[bool] = True) -> Iterator[bytes]
Returns an iterator of chunks from the given process
:param process: the subprocess to stream stdout from
:param close: whether to terminate the process when finished
This function is hardcoded to take PCM 16-bit stereo audio, same as the
chunked function. See that function for more info.
.. py:function:: make_ffmpeg_section_args(filename, start, length, *, before_options=(), options=())
Returns a list of arguments to FFmpeg
It will take the required amount of audio starting from the specified start
time and convert them into PCM 16-bit stereo audio to be piped to stdout.
The before_options argument will be passed after ``-ss`` and before ``-i``,
and the options argument will be passed after ``-t`` and before ``pipe:1``.
If length is None, the audio will play to the end of the file.
The returned args are of this form::
-ss {start}
-t {length}
{before_options}
-i {filename}
-f s16le
-ar 48000
-ac 2
-loglevel warning
-nostdin
{options}
pipe:1
.. py:function:: loop_stream(data_iterable: Iterable[bytes], *, copy: Optional[bool] = True, when_empty: Optional[Literal['ignore', 'error']] = 'error') -> Iterator[bytes]
Consumes a stream of buffers and loops them forever
:param data_iterable: the iterable of buffers
:param copy: whether or not to copy the buffers
:param when_empty: what to do when data is empty (ignore or error)
:returns: stream of buffers
The buffers are reused upon looping. If the buffers are known to be unused
after being yielded, you can set copy to False to save some time copying.
When sum(len(b) for b in buffers) == 0, a RuntimeError will be raised.
Otherwise, this function can end up in an infinite loop, or it can cause
other functions to never yield (such as equal_chunk_stream). This behaviour
is almost never useful, though if necessary, pass when_empty="ignore" to
suppress the error.
.. admonition:: Example
>>> from itertools import islice
>>> parts = [b"abc", b"def", b"ghi"]
>>> looped = list(islice(loop_stream(parts), 9))
>>> looped[::3]
[b'abc', b'abc', b'abc']
>>> looped[1::3]
[b'def', b'def', b'def']
>>> looped[2::3]
[b'ghi', b'ghi', b'ghi']
.. py:function:: equal_chunk_stream(data_iterable: Iterable[bytes], buffer_len: int, *, copy: Optional[bool] = True) -> Iterator[bytes]
Normalizes a stream of buffers into ones of length buffer_len
:param data_iterable: the iterable of buffers
:param buffer_len: the size to normalize buffers to
:param copy: return copies of the internal buffer. If ``False``, the yielded
buffer may be reused to reduce object creation and collection.
:returns: stream of buffers with len(buffer) == buffer_len except the last one
The last buffer yielded is always smaller than buffer_len. Other code can
fill it with zeros, drop it, or execute clean up code.
.. admonition:: Example
>>> list(equal_chunk_stream([b"abcd", b"efghi"], 3))
[b'abc', b'def', b'ghi', b'']
>>> list(equal_chunk_stream([b"abcd", b"efghijk"], 3))
[b'abc', b'def', b'ghi', b'jk']
>>> list(equal_chunk_stream([b"a", b"b", b"c", b"d"], 3))
[b'abc', b'd']
>>> list(equal_chunk_stream([], 3))
[b'']
>>> list(equal_chunk_stream([b"", b""], 3))
[b'']
>>> list(equal_chunk_stream([b"", b"", b"a", b""], 3))
[b'a']
.. py:function:: passed(seconds=1)
Returns a sound lasting the specified time yielding the seconds passed
This abstracts away the use of RATE to calculate the number of points.
If seconds is None, the returned sound will be unbounded.
.. admonition:: Example
>>> x = list(passed(0.25))
>>> x[0] * RATE
0.0
>>> x[1] * RATE
1.0
>>> len(x) / RATE
0.25
.. py:function:: fade(iterator, *, fadein=0.005, fadeout=0.005)
Fades in and out of the sound
If the sound is less than fadein + fadeout seconds, the time between fading
in and fading out is split proportionally.
.. py:function:: both(iterator)
Deprecated. sound.chunked accepts floats
.. py:function:: single(iterator)
Merge stereo sounds into mono
.. py:function:: volume(factor, sound)
Multiplies each point by the specified factor
.. py:function:: cut(seconds, sound)
Ends the sound after the specified time
.. py:function:: pad(seconds, sound)
Pads the sound with silence if shorter than the specified time
.. py:function:: _cut_cross(seconds: float, sound)
End sound at the first sign flip after the specified time
.. // :meta public:
.. py:function:: exact(seconds, sound)
Cuts or pads the sound to make it exactly the specified time
.. py:function:: delay(seconds: float, sound)
Add silence before the sound
.. py:function:: _resample_linear(factor: float, sound)
Resample the sound using linear interpolation
The returned sound will be shorter/longer by 1 over the specified factor.
Resampling a sound created by ``cut(1, sine(440))`` would give a sound
similar to one created by ``cut(1/factor, 440*factor)``.
.. // :meta public:
.. py:function:: play_discord_source(voice_client, source)
:async:
Plays and waits until the source finishes playing
.. py:class:: DiscordIteratorSource(iterator, *, is_opus=False)
Bases: :py:obj:`discord.AudioSource`
Internal subclass of discord.py's AudioSource for iterators
See wrap_discord_source for more info.
.. py:method:: is_opus()
.. py:method:: cleanup()
.. py:method:: read()
.. py:function:: wrap_discord_source(iterator, *, is_opus=False)
Wraps an iterator of bytes into an audio source
If is_opus is False (the default), the iterator must yield 20ms of signed
16-bit little endian stereo 48kHz audio each iteration. If is_opus is True,
the iterator should yield 20ms of Opus encoded audio each iteration.
.. admonition:: Example
::
# source implements discord.AudioSource
source = wrap_discord_source(chunked(cut(1, sine(440))))
ctx.voice_client.play(source, after=lambda _: print("finished"))
.. py:function:: chunked(sound)
Converts a stream of floats or two-tuples of floats in [-1, 1) to bytes
This is hardcoded to return 20ms chunks of signed 16-bit little endian
stereo 48kHz audio.
If the sound yield float instead of two-tuples, it will have both sides
play the same point.
If the sound doesn't complete on a chunk border, null bytes will be added
until it reaches the required length, which should be 3840 bytes.
Note that floats not in the range [-1, 1) will be silently truncated to
fall inside the range. For example, 1.5 will be processed as 1 and -1.5
will be processed as -1.
.. py:function:: unwrap_discord_source(source)
Converts an audio source into a stream of bytes
This basically does the opposite of wrap_discord_source. See that
function's documentation for more info.
.. py:function:: unchunked(chunks)
Converts a stream of bytes to two-tuples of floats in [-1, 1)
This basically does the opposite of chunked. See that function's
documentation for more info.
.. py:function:: make_frequencies_dict(*, a4=A4_FREQUENCY, offset=0)
Makes a dictionary containing frequencies for each note
- a4 is the frequency for the A above middle C
- offset is the number of semitones to offset each note by
.. py:function:: make_indices_dict(names=NOTE_NAMES, *, a4=57, offset=0)
Makes a dictionary containing note indices of common note names
- a4 is the note index for the A above middle C
- names is a list of note names
- offset is the number of semitones to offset each note by
.. py:function:: music_to_notes(music, *, line_length=1)
Converts music into notes (two tuples of note name and length)
This function returns a list of two-tuples of a string/None and a float.
The first item is the note name (or a break if it is a None). The second
item is its length.
Note that there is a break between notes by default.
A music string is first divided into lines with one line being the
specified length, defaulting to 1. Each line is then split by whitespace
into parts with the length divided evenly between them. Each part is then
split by commas "," into notes with the length again divided evenly between
them.
Empty lines or lines starting with a hash "#" are skipped.
Note names can be almost anything. A note name of a dash "-" continues the
previous note without a break between them. A suffix of a tilde "~" removes
the break after the note, whereas an exclamation point "!" adds one.
.. py:function:: split_music(music)
Splits music into individual sequences
Lines starting with a slash "/" will be added to a new sequence. All other
lines (including blanks and comments) will be part of the main sequence.
>>> assert split_music("1\n1") == ["1\n1"]
>>> assert split_music("1\n/2\n1") == ["1\n1", "2"]
>>> assert split_music("1\n/2\n/3\n1\n/2") == ["1\n1", "2\n2", "3"]
.. py:function:: notes_to_sine(notes, frequencies, *, line_length=1)
Converts notes into sine waves
- notes is an iterator of two-tuples of note names/None and lengths
- frequencies is a dict to look up the frequency for each note name
- line_length is how much to scale the note by
.. py:function:: _notes_to_sound(notes, func)
Converts notes to a sound using the provided func
The provided func is called with the note to get its sound. When there are
no more notes to add nor sounds to play, this stops.
.. // :meta public:
.. py:function:: play_output_chunks(chunks: Iterable[bytes], **kwargs: Any)
Plays chunks to the default audio output device
This is hardcoded to take PCM 16-bit 48kHz stereo audio, preferably in 20ms
blocks.
Keyword arguments are passed to sounddevice.RawOutputStream.
Note that the sounddevice library is required for this function.
.. py:function:: create_input_chunks(**kwargs)
Returns chunks from the default audio input device
This is hardcoded to yield 20ms blocks of PCM 16-bit 48kHz stereo audio.
Keyword arguments are passed to sounddevice.RawInputStream.
Note that the sounddevice library is required for this function.
.. py:function:: reload()
Reloads this module. Helper function
.. py:data:: MUSIC_DIGITIZED
:value: Multiline-String
.. raw:: html
Show Value
.. code-block:: text
# names="do di re ri mi fa fi so si la li ti".split()
# offset=1
# line_length=1.15
. mi mi mi
fa do . do
. so mi do
re mi,re - mi
la3 mi mi mi
fa do . do
. so mi do
re mi,re - mi
do mi mi mi
fa do . do
. so mi do
re mi,re - mi
la3 la3 la3 fa3
fa3 fa3 fa3 fa3
do do do so3
so3 so3 si3 si3
la3 la3 la3 fa3
fa3 fa3 fa3 fa3
do do do so3
la3,do,mi,so la,do5,mi5,so5 la5 .
do so mi do
re re,mi so mi
do so mi do
re re,mi so re
do so mi do
re re,mi so mi
do so mi do
re re,mi so re
- do . la3
. la3 do re
so3 do mi do
re do,re - do
- do . la3
. la3 do re
so3 do mi do
re do,re - do
do la3 - so3
do re,mi - re
. . do so3
re mi re do
. . do so3
fa mi,re - do
- so3 re do
mi so re do
la3 mi mi mi
fa do . do
. so mi do
re mi,re - mi
do mi mi mi
fa do . do
. so mi do
re mi,re - mi
la3 la3 la3 fa3
fa3 fa3 fa3 fa3
do do do so3
so3 so3 si3 si3
la3 la3 la3 fa3
fa3 fa3 fa3 fa3
do do do so3
la3,do,mi,so la,do5,mi5,so5 la5 .
do so mi do
re re,mi so mi
do so mi do
re re,mi so re
do so mi do
re re,mi so mi
do so mi do
re re,mi so re
- do . la3
. la3 do re
so3 do mi do
re do,re - do
- do . la3
. la3 do re
so3 do mi do
re do,re - -
do
.. raw:: html
.. py:data:: MUSIC_MEGALOVANIA
:value: Multiline-String
.. raw:: html
Show Value
.. code-block:: text
# names="do di re ri mi fa fi so si la li ti".split()
# offset=6
# line_length=2.2
la3 la3 la - mi - - ri - re - do - la3 do re
so3 so3 la - mi - - ri - re - do - la3 do re
fi3 fi3 la - mi - - ri - re - do - la3 do re
fa3 fa3 la - mi - - ri - re - do - la3 do re
la3 la3 la - mi - - ri - re - do - la3 do re
so3 so3 la - mi - - ri - re - do - la3 do re
fi3 fi3 la - mi - - ri - re - do - la3 do re
fa3 fa3 la - mi - - ri - re - do - la3 do re
do - do do - do - do - la3 - la3 - - - -
do - do do - re - ri - re do la3 do re - -
do - do do - re - ri - mi - so - mi - -
la - la - la mi la so - - - - - - - -
mi - mi mi - mi - mi - re - re - - - -
mi - mi mi - mi - re - mi - so - mi re -
la do mi do so do mi do re do re mi so mi re do
la3 - ti3 - do la3 do so - - - - - - - -
la3 - - - - - - - do la3 do re ri re do la3
do la3 do - re - - - - - - - - - re mi
la - re mi re do ti3 la3 do - re - mi - so -
la - la - la mi la so - - - - - - - -
do - re - mi - do5 - ti - - - si - - -
ti - - - do5 - - - re5 - - - ti - - -
mi5 - - - - - - - mi5 ti so re do ti3 la3 si3
so3 - - - - - - - si3 - - - - - - -
mi3 - - - - - - - - - - - do - - -
ti3 - - - - - - - si3 - - - - - - -
la3
-
.. raw:: html
.. py:data:: MUSIC_DIGITIZED_DUAL
:value: Multiline-String
.. raw:: html
Show Value
.. code-block:: text
# names="do di re ri mi fa fi so si la li ti".split()
# line_length=4.36
# offset=13
/ # offset=1
. mi mi mi fa do . do . so mi do re mi,re - mi
/ .
la3 mi mi mi fa do . do . so mi do re mi,re - mi
/ la3 la3 la3 fa3 fa3 fa3 fa3 fa3 do do do so3 so3 so3 si3 si3
do mi mi mi fa do . do . so mi do re mi,re - mi
/ la3 la3 la3 fa3 fa3 fa3 fa3 fa3 do do do so3 so3 so3 si3 si3
do mi mi mi fa do . do . so mi do re mi,re - mi
/ la3 la3 la3 la3 la3 la3 la3 la3 la3 la3 la3 la3 la3 la3 la3 la3
do mi mi mi fa do . do . so mi do . . . .
/ la3 la3 la3 la3 la3 la3 la3 la3 la3 la3 la3 la3 la3,do,mi,so la,do5,mi5,so5 la5 .
do so mi do re re,mi so mi do so mi do re re,mi so re
/ la3 la3 la3 fa3 fa3 fa3 fa3 fa3 do do do so3 so3 so3 si3 si3
do so mi do re re,mi so mi do so mi do re re,mi so re
/ la3 la3 la3 fa3 fa3 fa3 fa3 fa3 do do do so3 so3 so3 si3 si3
- do . la3 . la3 do re so3 do mi do re do,re - do
/ la3 la3 la3 fa3 fa3 fa3 fa3 fa3 do do do so3 so3 so3 si3 si3
- do . la3 . la3 do re so3 do mi do re do,re - do
/ la3 la3 la3 fa3 fa3 fa3 fa3 fa3 do do do so3 so3 so3 si3 si3
do la3 - so3 do re,mi - re . . do so3 re mi re do
/ .
. . do so3 fa mi,re - do - so3 re do mi so re do
/ .
la3 mi mi mi fa do . do . so mi do re mi,re - mi
/ la3 . la3 . fa3 . fa3 . do . do . so3 . so3 .
do mi mi mi fa do . do . so mi do re mi,re - mi
/ la3 . la3 . fa3 . fa3 . do . do . so3 . so3 .
do mi mi mi fa do . do . so mi do re mi,re - mi
/ la3 la3 la3 fa3 fa3 fa3 fa3 fa3 do do do so3 so3 so3 si3 si3
do mi mi mi fa do . do . so mi do . . . .
/ la3 la3 la3 fa3 fa3 fa3 fa3 fa3 do do do so3 la3,do,mi,so la,do5,mi5,so5 la5 .
do so mi do re re,mi so mi do so mi do re re,mi so re
/ la3 la3 la3 fa3 fa3 fa3 fa3 fa3 do do do so3 so3 so3 si3 si3
do so mi do re re,mi so mi do so mi do re re,mi so re
/ la3 la3 la3 fa3 fa3 fa3 fa3 fa3 do do do so3 so3 so3 si3 si3
- do . la3 . la3 do re so3 do mi do re do,re - do
/ la3 la3 la3 fa3 fa3 fa3 fa3 fa3 do do do so3 so3 so3 si3 si3
- do . la3 . la3 do re so3 do mi do re do,re - -
/ la3 la3 la3 fa3 fa3 fa3 fa3 fa3 do do do so3 so3 so3 si3 si3
do . . .
/ la3 . . .
.. raw:: html