: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