Source code for cutcutcodec.core.generation.audio.noise

"""Generate an audio noise signal."""

import math
import numbers
import typing
from fractions import Fraction

import numpy as np

from cutcutcodec.core.classes.container import ContainerInput
from cutcutcodec.core.classes.frame_audio import FrameAudio
from cutcutcodec.core.classes.layout import Layout
from cutcutcodec.core.classes.stream import Stream
from cutcutcodec.core.classes.stream_audio import StreamAudio
from cutcutcodec.core.exceptions import OutOfTimeRange
from cutcutcodec.core.interfaces.seedable import Seedable


[docs] class GeneratorAudioNoise(ContainerInput, Seedable): """Generate a pure noise audio signal. Attributes ---------- layout : cutcutcodec.core.classes.layout.Layout The signification of each channels (readonly). Examples -------- >>> from fractions import Fraction >>> from cutcutcodec.core.generation.audio.noise import GeneratorAudioNoise >>> (stream,) = GeneratorAudioNoise(0).out_streams >>> stream.snapshot(Fraction(2, 48000), 48000, 5).numpy(force=True) array([[-0.25397146, -0.1199106 , -0.12052107, -0.16986334, -0.50950706], [ 0.17409873, 0.42185044, -0.7231959 , 0.39764726, -0.25968206]], dtype=float32) >>> stream.snapshot(0, 24000, 5).numpy(force=True) array([[ 0.44649088, -0.25397146, -0.12052107, -0.50950706, 0.5112531 ], [-0.8036704 , 0.17409873, -0.7231959 , -0.25968206, -0.7944578 ]], dtype=float32) >>> frame = stream.snapshot(0, 48000, 48000*60) # test uniform >>> abs(round(frame.mean().item(), 3)) # theory 0 0.0 >>> round(frame.var().item(), 3) # theory 1/3 0.333 >>> """ def __init__( self, seed: numbers.Real | None = None, layout: Layout | str | numbers.Integral | None = "stereo", ): """Initialise and create the class. Parameters ---------- seed : numbers.Real, optional Transmitted to ``cutcutcodec.core.interfaces.seedable.Seedable``. layout: cutcutcodec.core.classes.layout.Layout or str or int, optional The audio layout to associate to each equation, let see ``cutcutcodec.core.classes.layout.Layout`` for more details. By default, the layout is stereo, two channels. """ assert layout is None or isinstance(layout, (Layout, str, numbers.Integral)), \ layout.__class__.__name__ Seedable.__init__(self, seed) super().__init__([_StreamAudioNoiseUniform(self)]) self._layout = Layout(layout) def _getstate(self) -> dict: return {**self._getstate_seed(), "layout": self._layout.name} def _setstate(self, in_streams: typing.Iterable[Stream], state: dict) -> None: assert state.keys() == {"seed", "layout"}, set(state) self._setstate_seed(state) self._layout = Layout(state["layout"]) ContainerInput.__init__(self, [_StreamAudioNoiseUniform(self)]) @property def layout(self) -> Layout: """Return the signification of each channels.""" return self._layout
class _StreamAudioNoiseUniform(StreamAudio): """Random audio stream where each sample follows a uniform law. Based on the md5 hash algorithm on the timestamps. """ def __init__(self, node: GeneratorAudioNoise): assert isinstance(node, GeneratorAudioNoise), node.__class__.__name__ super().__init__(node) self._seed = np.frombuffer(self.node.seed_bytes, dtype=np.uint32).reshape(8, 1) def _snapshot(self, timestamp: Fraction, rate: int, samples: int) -> FrameAudio: if timestamp < 0: raise OutOfTimeRange(f"there is no audio frame at timestamp {timestamp} (need >= 0)") # initialisation message, depend to the seed and the timestamps string = np.empty((16, samples), dtype=np.uint32) # ti = t0 + i/rate num = timestamp.numerator*rate + np.arange(samples, dtype=np.uint64)*timestamp.denominator gcds = np.gcd(num, timestamp.denominator*rate) num //= gcds den = rate*timestamp.denominator // gcds string[0, :] = num >> 32 string[1, :] = num # & 0x00000000ffffffff is implicite string[2, :] = den >> 32 string[3, :] = den # & 0x00000000ffffffff is implicite string[4:8, :] = 0 # because unitializated string[8:16, :] = self._seed # compute hash pseudo md5 hashes = [] while True: hashes.extend(md5(string)) # +4 items if len(hashes) >= len(self.node.layout): break string[5, :] = hashes[-1] # conversion uint32 to float32 uniform [0, 1] n_channels = len(self.node.layout) for i in range(n_channels): sign = hashes[i] & 0b1_00000000_00000000000000000000000 hashes[i] &= 0b0_00000000_11111111111111111111111 # exponant reset and sign > 0 hashes[i] ^= 0b0_01111111_00000000000000000000000 # exponant set for 2**0, range [1, 2] hashes[i] = hashes[i].view(np.float32) # reinterpret cast for substraction operation hashes[i] -= 1.0 # range [0, 1] hashes[i] = hashes[i].view(np.uint32) # reinterpret cast for bit operation hashes[i] |= sign # add random sign, range [-1, 1] hashes[i] = hashes[i].view(np.float32) # reinterpret cast for final dtype return FrameAudio( timestamp, rate, self.node.layout, np.vstack(hashes[:n_channels]), ) @property def beginning(self) -> Fraction: return Fraction(0) @property def duration(self) -> Fraction | float: return math.inf @property def layout(self) -> Layout: """Return the signification of each channels.""" return self.node.layout
[docs] def md5(string: np.ndarray) -> np.ndarray: """Vectorised version of the md5 algorithm. Parameters ---------- string : np.ndarray The values to hash, of shape (16, n) and dtype uint32. Returns ------- hash : tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray] The hashed value of the input string, of shape (n,). Fours differents 32 bits values ares yields, total 128 bits. Examples -------- >>> import numpy as np >>> from cutcutcodec.core.generation.audio.noise import md5 >>> string = np.zeros((16, 1), dtype=np.uint32) >>> np.vstack(md5(string)) array([[1543640769], [2049272632], [1899532917], [2915357108]], dtype=uint32) >>> """ # verifs assert isinstance(string, np.ndarray), string.__class__.__name__ assert string.dtype == np.uint32, string.dtype assert string.ndim == 2, string.shape assert string.shape[0] == 16, string.shape # initialisation _a = np.full((string.shape[1],), 0x67452301, dtype=np.uint32) _b = np.full((string.shape[1],), 0xefcdab89, dtype=np.uint32) _c = np.full((string.shape[1],), 0x98badcfe, dtype=np.uint32) _d = np.full((string.shape[1],), 0x10325476, dtype=np.uint32) _f = np.empty(string.shape[1], dtype=np.uint32) # compute md5 on each elements for i, (const, shift) in enumerate(zip( [ 0xd76aa478, 0xe8c7b756, 0x242070db, 0xc1bdceee, 0xf57c0faf, 0x4787c62a, 0xa8304613, 0xfd469501, 0x698098d8, 0x8b44f7af, 0xffff5bb1, 0x895cd7be, 0x6b901122, 0xfd987193, 0xa679438e, 0x49b40821, 0xf61e2562, 0xc040b340, 0x265e5a51, 0xe9b6c7aa, 0xd62f105d, 0x02441453, 0xd8a1e681, 0xe7d3fbc8, 0x21e1cde6, 0xc33707d6, 0xf4d50d87, 0x455a14ed, 0xa9e3e905, 0xfcefa3f8, 0x676f02d9, 0x8d2a4c8a, 0xfffa3942, 0x8771f681, 0x6d9d6122, 0xfde5380c, 0xa4beea44, 0x4bdecfa9, 0xf6bb4b60, 0xbebfbc70, 0x289b7ec6, 0xeaa127fa, 0xd4ef3085, 0x04881d05, 0xd9d4d039, 0xe6db99e5, 0x1fa27cf8, 0xc4ac5665, 0xf4292244, 0x432aff97, 0xab9423a7, 0xfc93a039, 0x655b59c3, 0x8f0ccc92, 0xffeff47d, 0x85845dd1, 0x6fa87e4f, 0xfe2ce6e0, 0xa3014314, 0x4e0811a1, 0xf7537e82, 0xbd3af235, 0x2ad7d2bb, 0xeb86d391, ], [ 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21, ], )): if i <= 15: _f = (_b & _c) | ((~_b) & _d) index = i elif i <= 31: _f = (_d & _b) | ((~_d) & _c) index = (5*i + 1) % 16 elif i <= 47: _f = _b ^ _c ^ _d index = (3*i + 5) % 16 else: _f = _c ^ (_b | (~_d)) index = (7*i) % 16 _f += const + _a + string[index] _f = ((_f << shift) | (_f >> (32-shift))) _a, _b, _c, _d = _d, _f, _b, _c return _a, _b, _c, _d