"""Waveform Generation Functions (:mod:`chaudio.waves`)
====================================================
.. currentmodule:: chaudio.waves
Source for calculating waveform values (like :meth:`chaudio.waves.sin`, :meth:`chaudio.waves.saw`, etc)
In general, all waveform functions ``f`` should take in a time parameter ``t`` that can be either a constant or a numpy array, and `hz` should be able to be a constant or numpy array. Also, they should accept an optional value called ``tweak``, which (if supported) should return a slightly different waveform based on the value of ``tweak``.
Note that ALL waveforms should accept this ``tweak`` value, even if they do nothing. This is for compatability
Some other general rules (these are by no means required, however):
>>> f(0, hz) == 0
>>> f(t, hz, tweak=None) == f(t, hz)
>>> f(t+1.0/hz, hz) == f(t, hz)
The general idea with a waveform is that it repeats every ``1.0/hz`` seconds, and each oscillation (or cycle) is the exact same.
Different frequencies have different pitches (see :meth:`chaudio.util.note`), and different waveforms have different timbres (pronounced ``TAM-BER`` or ``TIM-BER``). In fact, all instruments digital and real-world all are just different waveforms. Even when you play a guitar, it is simply a waveform played at a pitch.
These are generated on a non-continious (which is a synonym of discrete) sample array, each value in the sample array representing a point in time, and the amplitude of the sound at that point in time. These sample arrays, since they aren't continious, have a samplerate, or how many records it has per second. The most common value is 44100, that is, 44100 recordings are held per second of data. And, consequently, if our array is named ``ar``, ``ar[0]`` represents the sound's amplitude at time :math:`t = \\frac{0}{44100} = 0`` seconds. And, at ``ar[25000]``, it holds the amplitude at :math:`t = \\frac{25000}{44100} \\approx .566` seconds.
As we have said, the waveform repeats every :math:`\\frac{1}{hz}`` seconds, which means that it repeats :math:`hz` times per second. Thus, :math:`ar[0]` represents the start of the first waveform, and :math:`ar[\\left \\lfloor \\frac{44100}{hz} \\right \\rfloor]` marks the end of the first oscillation and the beginning of the second.
"""
import chaudio
import numpy as np
[docs]def phase_correction(t, hz):
"""Computes the phase correction factor for time values and frequencies
See `this post <https://stackoverflow.com/questions/3089832/sine-wave-glissando-from-one-pitch-to-another-in-numpy>`_ for more info
Parameters
----------
t : np.ndarray
The time sample values
hz : float, int, np.ndarray
Frequency of wave. If the type of ``hz`` is np.ndarray, it must have the same shape as ``t``, and in that case each corresponding value of ``t``'s wave is given a phase correction value
Returns
-------
np.ndarray
Returns the phase correction vector needed to correctly produce sound with a changing frequency
"""
if not isinstance(hz, np.ndarray):
return 0
else:
return np.add.accumulate(t*np.concatenate((np.zeros(1), (hz[:-1]-hz[1:]))))
[docs]def sin(t, hz, tweak=None):
"""Computes the `sin wave <https://en.wikipedia.org/wiki/Sine_wave>`_ of sample times (``t``), and frequencies (``hz``)
Optionally, if tweak is set, return a slightly modified waveform.
With no tweak, the return value is :math:`sin(2 \pi * hz * t)`, but with the return value, :math:`sin(2 \pi * hz * t) ^ {1 + tweak}` is returned.
Parameters
----------
t : float, int, np.ndarray
If a float or int, return the value of the sin wave at time ``t``, in seconds. If it is a numpy array, return an array of values at the sin wave corresponding to each time value in the array.
hz : float, int, np.ndarray
Frequency of wave. If the type of ``hz`` is np.ndarray, it must have the same shape as ``t``, and in that case each corresponding value of ``t``'s wave is assumed to have ``hz``'s value at the same index as the frequency value.
tweak : float, int, np.ndarray
A value to change the waveform. If the type of ``tweak`` is a numpy array, it must have the same shape as ``t``, and in that case each corresponding value of ``t``'s wave is assumed to have ``tweak``'s value at the same index as the tweak value (see examples below).
Returns
-------
float, np.ndarray
If all ``t``, ``hz``, and ``tweak`` are floats or ints, the function returns a float. Else, all parameters which are np.ndarray's must have the same shape, and the returned value is the same shape.
Examples
--------
>>> t = 0
>>> chaudio.waves.sin(t, 1)
0.0
>>> t = chaudio.times(5)
>>> chaudio.waves.sin(t, 1)
array([ 0. , 0.00014248, 0.00028495, ..., -0.00042743,
-0.00028495, -0.00014248])
See Also
--------
:meth:`chaudio.util.times` : returns sample times, which can be passed to this function as sample times
"""
base_sin = np.sin((2 * np.pi) * (hz * t + phase_correction(t, hz)))
if tweak is None:
return base_sin
else:
signs = np.sign(base_sin)
return signs * (np.abs(base_sin) ** (1 + tweak))
[docs]def saw(t, hz, tweak=None):
"""Computes the `sawtooth wave <https://en.wikipedia.org/wiki/Sawtooth_wave>`_ of sample times (``t``), and frequencies (``hz``)
Optionally, if tweak is set, return a slightly modified waveform.
With no tweak, the return value is :math:`saw(2 \pi * hz * t)`, but with the return value, :math:`saw(2 \pi * hz * t) * (1 + tweak * sin(t, hz, tweak))` is returned.
This has the effect of making the waveform appear "bendy", but still resemble a sawtooth.
Parameters
----------
t : float, int, np.ndarray
If a float or int, return the value of the sawtooth wave at time ``t``, in seconds. If it is a numpy array, return an array of values at the sawtooth wave corresponding to each time value in the array.
hz : float, int, np.ndarray
Frequency of wave. If the type of ``hz`` is np.ndarray, it must have the same shape as ``t``, and in that case each corresponding value of ``t``'s wave is assumed to have ``hz``'s value at the same index as the frequency value.
tweak : float, int, np.ndarray
A value to change the waveform. If the type of ``tweak`` is a numpy array, it must have the same shape as ``t``, and in that case each corresponding value of ``t``'s wave is assumed to have ``tweak``'s value at the same index as the tweak value (see examples below).
Returns
-------
float, np.ndarray
If all ``t``, ``hz``, and ``tweak`` are floats or ints, the function returns a float. Else, all parameters which are np.ndarray's must have the same shape, and the returned value is the same shape.
Examples
--------
>>> t = 0
>>> chaudio.waves.saw(t, 1)
0.0
>>> t = chaudio.times(5)
>>> chaudio.waves.saw(t, 1)
array([ 0.00000000e+00, 4.53514739e-05, 9.07029478e-05, ...,
-1.36054422e-04, -9.07029478e-05, -4.53514739e-05])
See Also
--------
:meth:`chaudio.util.times` : returns sample times, which can be passed to this function as sample times
"""
# have to scale the phase correction
base_saw = 2 * ((t * hz + phase_correction(t, hz) + .5) % 1.0) - 1
if tweak is None:
return base_saw
else:
return base_saw - (chaudio.util.lambda_mask(base_saw, lambda x: x > tweak))
[docs]def square(t, hz, tweak=None):
"""Computes the `square wave <https://en.wikipedia.org/wiki/Square_wave>`_ of sample times (``t``), and frequencies (``hz``)
Optionally, if tweak is set, return a slightly modified waveform.
With no tweak, the return value is :math:`square(2 \pi * hz * t)`, which has a `duty cycle <https://en.wikipedia.org/wiki/Duty_cycle>`_ of ``50%``, or ``.5``. If set, the duty cycle is set to ``tweak``, and if ``tweak==.5``, that results in a normal square wave.
This has similar effects to opening up an envelope
Parameters
----------
t : float, int, np.ndarray
If a float or int, return the value of the square wave at time ``t``, in seconds. If it is a numpy array, return an array of values at the square wave corresponding to each time value in the array.
hz : float, int, np.ndarray
Frequency of wave. If the type of ``hz`` is np.ndarray, it must have the same shape as ``t``, and in that case each corresponding value of ``t``'s wave is assumed to have ``hz``'s value at the same index as the frequency value.
tweak : float, int, np.ndarray
A value to change the waveform. If the type of ``tweak`` is a numpy array, it must have the same shape as ``t``, and in that case each corresponding value of ``t``'s wave is assumed to have ``tweak``'s value at the same index as the tweak value (see examples below).
Returns
-------
float, np.ndarray
If all ``t``, ``hz``, and ``tweak`` are floats or ints, the function returns a float. Else, all parameters which are np.ndarray's must have the same shape, and the returned value is the same shape.
Notes
-----
Unlike most other waveforms, the square wave starts at ``-1``, whereas most start at ``0``. However, since the square wave only has values taking either ``-1`` or ``+1`` (even in modified form), this is done as a compromise
Examples
--------
>>> t = 0
>>> chaudio.waves.square(t, 1)
-1
>>> t = chaudio.times(5)
>>> chaudio.waves.saw(t, 1)
array([-1, -1, -1, ..., 1, 1, 1])
See Also
--------
:meth:`chaudio.util.times` : returns sample times, which can be passed to this function as sample times
`Pulse wave <https://en.wikipedia.org/wiki/Pulse_wave>`_ : with a modified tweak value, the waveform is a Pulse wave with duty cycle equal to ``tweak``
"""
if tweak is None:
return 2 * (((t * hz + phase_correction(t, hz)) % 1.0) > .5) - 1
else:
return 2 * (((t * hz + phase_correction(t, hz)) % 1.0) > tweak) - 1
[docs]def triangle(t, hz, tweak=None):
"""Computes the `triangle wave <https://en.wikipedia.org/wiki/Triangle_wave>`_ of sample times (``t``), and frequencies (``hz``)
Optionally, if tweak is set, return a slightly modified waveform.
With no tweak, the return value is :math:`triangle(2 \pi * hz * t)`, which looks like a sin wave, except it is straight lines.
With a tweak value, :math:`triangle(2 \pi * hz * t) - tweak * saw(2 \pi * hz * t) * square(2 \pi * hz * t, tweak)` is returned, which can generate a lot of different timbres. In the future, I'll add an in depth description of what kind of sounds this creates.
Parameters
----------
t : float, int, np.ndarray
If a float or int, return the value of the triangle wave at time ``t``, in seconds. If it is a numpy array, return an array of values at the triangle wave corresponding to each time value in the array.
hz : float, int, np.ndarray
Frequency of wave. If the type of ``hz`` is np.ndarray, it must have the same shape as ``t``, and in that case each corresponding value of ``t``'s wave is assumed to have ``hz``'s value at the same index as the frequency value.
tweak : float, int, np.ndarray
A value to change the waveform. If the type of ``tweak`` is a numpy array, it must have the same shape as ``t``, and in that case each corresponding value of ``t``'s wave is assumed to have ``tweak``'s value at the same index as the tweak value (see examples below).
Returns
-------
float, np.ndarray
If all ``t``, ``hz``, and ``tweak`` are floats or ints, the function returns a float. Else, all parameters which are np.ndarray's must have the same shape, and the returned value is the same shape.
Notes
-----
Unlike most other waveforms, the square wave starts at ``-1``, whereas most start at ``0``. However, since the square wave only has values taking either ``-1`` or ``+1`` (even in modified form), this is done as a compromise
Examples
--------
>>> t = 0
>>> chaudio.waves.triangle(t, 1)
0.0
>>> t = chaudio.times(5)
>>> chaudio.waves.triangle(t, 1)
array([ 0.00000000e+00, 9.07029478e-05, 1.81405896e-04, ...,
-2.72108844e-04, -1.81405896e-04, -9.07029478e-05])
See Also
--------
:meth:`chaudio.util.times` : returns sample times, which can be passed to this function as sample times
"""
base_triangle = np.abs(4 * ((t * hz + .75 + phase_correction(t, hz)) % 1.0) - 2) - 1
if tweak is None:
return base_triangle
else:
return base_triangle - tweak * saw(t, hz) * square(t, hz, tweak)
# returns random noise (white noise)
[docs]def noise(t, hz=0, tweak=None):
return 2 * np.random.ranf(len(t)) - 1
# returns zeros
[docs]def zero(t, hz=0, tweak=None):
return np.zeros((len(t),), dtype=np.float32)