Tutorial

There is one idea to absorb, and the rest follows from it: every routine in this package returns a numpy array of PCM samples. A note is an array. An envelope is an array. A whole piece is an array. Nothing is a handle to an audio engine, nothing has to be started or torn down, and there is no graph to wire up. You make arrays, you combine them with arithmetic, and at the end you write one to a file.

This page walks from a single note to a short stereo piece. Every block below runs as written.

A first sound

import music

a = music.note(freq=440, duration=1.5)
music.write_wav_mono(a, "first.wav")

a is 66,150 float64 samples — 1.5 seconds at 44.1 kHz — in the range [-1, 1]. Frequency is in hertz, duration in seconds. To hear it without writing a file, use music.play_audio().

Should you prefer to think in samples rather than seconds, every synthesis routine takes number_of_samples instead, and it wins over duration:

exactly_one_period = music.note(440, number_of_samples=round(44100 / 440))

It is all numpy

Because the return value is an ordinary array, the operations you already know are the mixing desk:

quiet = music.note(440, 1) * 0.5           # halve the amplitude
inverted = -music.note(440, 1)             # flip the phase
both = music.note(440, 1) + music.note(660, 1)   # sum two tones

The package’s own helpers exist for the cases where plain arithmetic is not enough — where lengths differ, or where a stereo array has to be kept distinct from a mono one. music.mix() sums two vectors, padding the shorter; music.horizontal_stack() concatenates them end to end.

Notes in sequence

A melody is notes concatenated:

freqs = [261.63, 293.66, 329.63, 349.23, 392.0]   # C D E F G
notes = [music.note(f, 0.35) for f in freqs]
melody = music.horizontal_stack(*[music.adsr(sonic_vector=n)
                                  for n in notes])

Each note is shaped before the notes are stacked, and that is not a flourish. A note ends wherever its phase happens to land, and the next one opens at the bottom of its wavetable, so two raw notes joined end to end step from one to the other – by up to the full scale, which is a click. Whether it happens at all depends on the frequency and the duration multiplying out to a whole number of cycles, so a melody can click on some notes and not others, which makes it sound like an articulation rather than a defect. music.adsr(), the next section, runs each note to silence at both ends and the joins go quiet. tests/test_artifacts.py measures both.

If you would rather write rhythm in beat units than in seconds, music.rhythm_to_durations() converts them. Its durations are relative counts of the beat unit. With bpm=120, one unit lasts half a second:

>>> music.rhythm_to_durations([1, 2, 2, 1], bpm=120)
[0.5, 1.0, 1.0, 0.5]

Pass duration instead when you already know the duration of one beat.

Shaping a note

A bare note starts and stops abruptly, which is audible as a click. An ADSR envelope fixes that and is most of what makes a sound feel like an instrument:

raw = music.note(440, 1)
shaped = music.adsr(sonic_vector=raw, attack_duration=50,
                    decay_duration=100, sustain_level=-9,
                    release_duration=300)

shaped is the same length as raw: the envelope is fitted to the sound it is given, not appended to it. Called without a sonic_vector, the same function returns the bare envelope, which you can inspect, plot, or multiply into something yourself.

Note the units. They are not uniform, and deliberately so — each parameter is expressed in the unit that parameter is normally thought about in:

Kind of quantity

Unit

Examples

Duration of a sound

seconds

duration, envelope_duration

Duration of an envelope stage

milliseconds

attack_duration, release_duration

Level

decibels

sustain_level, max_db_dev, trans_dev

Pitch deviation

semitones

max_pitch_dev

Frequency

hertz

freq, vibrato_freq, fm

Angle

degrees

theta, theta1, theta2

The other shapers follow the same convention — pass sonic_vector to apply, omit it to get the envelope. music.fade() for a fade in or out, music.tremolo() for periodic loudness, music.am() for amplitude modulation, music.loud() for a single loudness transition, music.reverb() for a room.

What “sample by sample” buys you

This is the claim the package is built on, and it is worth seeing rather than taking on trust. A vibrato is a periodic deviation in pitch. The common way to build one is to render a steady tone and modulate it afterwards; this package instead folds the deviation into the wavetable lookup, accumulating phase sample by sample, so the note genuinely has a different instantaneous frequency at every one of its samples.

You can measure that from the output alone. Render two semitones of vibrato around 440 Hz, then recover the frequency cycle by cycle from the interpolated zero crossings:

import numpy as np

v = music.note_with_vibrato(freq=440, duration=2, vibrato_freq=5,
                            max_pitch_dev=2)

negative = np.signbit(v)
crossings = np.where(~negative[1:] & negative[:-1])[0]
exact = crossings + v[crossings] / (v[crossings] - v[crossings + 1])
inst = 44100 / np.diff(exact)

print(f"{inst.min():.1f} .. {inst.max():.1f} Hz")

which prints 392.0 .. 493.9 Hz. The model says the extremes should be 440·2±2/12, which is 392.0 and 493.9 Hz. The rendered audio agrees to the last digit shown, and takes 617 distinct values along the way — it is not stepping between a handful of pitches, it is a continuous curve sampled at the audio rate.

The same is true of a glissando. An exponential sweep from 220 Hz to 880 Hz should pass through the geometric mean at its midpoint:

g = music.note_with_glissando(220, 880, duration=2, method="exp")

Measured at the halfway sample, it is at 441 Hz; the geometric mean of 220 and 880 is 440.

music.note_with_fm() (frequency modulation) and music.note_with_doppler() (a source moving past the listener, which produces the pitch shift and the stereo image) are built the same way.

Layering

music.mix() sums two vectors and pads the shorter one, so a chord is a fold:

chord = music.note(261.63, 2)
for f in (329.63, 392.0):
    chord = music.mix(chord, music.note(f, 2))

music.mix_with_offset() starts the second sound a given number of seconds after the first, which is enough for an arpeggio or a canon:

canon = music.mix_with_offset(melody, melody, duration=0.7)

A negative duration places the second sound that many seconds before the first one ends.

Scales, chords and intervals

music.theory counts everything in semitones from a tonic of zero, which is what music.pitch_to_freq() takes, so a scale or a chord becomes frequencies, and then sound, in two steps:

dorian = music.pitch_to_freq(220.0, music.scale("dorian"))
ascent = music.horizontal_stack(
    *[music.adsr(sonic_vector=music.note(f, 0.3)) for f in dorian])

seventh = music.pitch_to_freq(220.0, music.chord("minor seventh"))
chord = music.mix_many([music.note(f, 1.5) for f in seventh])

The seven modes are one step pattern read from seven places, which is how music.mode_by_rotation() reaches them, and intervals are written the way a musician writes them:

>>> music.scale("dorian"), music.chord("minor seventh")
((0, 2, 3, 5, 7, 9, 10), (0, 3, 7, 10))
>>> music.interval("M3"), music.interval("P11")
(4, 17)
>>> music.interval_names(6)
('aug4', 'dim5', 'TT')

music.harmonic_series() gives the partials over a fundamental, where the octaves land on the tempered scale and nothing else does.

The sequencer

Once there are more than a handful of overlapping events, give them absolute start times instead:

seq = music.Sequencer()
for i, freq in enumerate([440, 550, 660]):
    seq.add_note(freq, start=i * 0.25, duration=1.0,
                 adsr_params={"attack_duration": 20,
                              "release_duration": 400})
seq.write("chord.wav")

seq.render() returns the array instead of writing it, so the result goes back into everything above.

Stereo, and where a sound is

A stereo signal is a (2, n) array — channel first. The localization routines take a mono vector and return one of these, computing the interaural time and intensity differences from a geometry you describe.

music.localize() fixes the source at an angle and distance:

fixed = music.localize(music.note(440, 1), theta=45, distance=2)

Angles are measured from the ear axis: 0 is the right ear’s side, 90 straight ahead and 180 the left. music.localize2() is the exception, measuring from straight ahead, as its documentation says.

music.localize_linear() moves it in a straight line, recomputing the position — and from it the two delays and the two gains — at every sample:

passing = music.localize_linear(music.note(330, 3),
                                theta1=150, theta2=30, dist=0.6)
music.write_wav_stereo(passing, "passing.wav", fades=(100, 500))

Angles are in degrees, and x is the lateral axis: 0° is the right ear, 90° is straight ahead, 180° is the left ear. So the call above sweeps from left to right.

The delays are fractional — a source 0.3 samples further from one ear than the other is rendered as 0.3 of a sample, by cubic interpolation, not rounded to the nearest one. At 44.1 kHz a whole sample is already 8 mm of head, so rounding would be audible as a coarse, stepping image.

fades above is a shorthand every writer takes: (in, out) in milliseconds, applied on the way out to the file.

Noise

Six colours, each defined by its gain per octave — brown at −6 dB, pink at −3, white at 0, blue at +3, violet at +6, black at −12:

colours = music.horizontal_stack(
    *[music.noise(kind, 0.5)
      for kind in ("brown", "pink", "white", "blue", "violet")])

Any number works in place of a name, and means exactly that many decibels per octave, so music.noise(-1.5, 0.5) is halfway between pink and white. Noise is also what music.reverb() convolves with to make a tail.

For a response you design yourself, music.fir() takes a magnitude spectrum — one absolute value per frequency bin, the last being Nyquist — and applies it:

dull = music.fir(np.linspace(1, 0, 64), music.noise("white", 0.5))

Filters

music.iir() applies a filter given its coefficients, and music.low_pass(), music.high_pass(), music.band_pass() and music.band_reject() compute them, from the four designs the article specifies. A cutoff is a fraction of the sample rate, which music.fraction_of() converts a frequency in Hertz into, so the same coefficients make the same filter at any rate:

a, b = music.low_pass(music.fraction_of(1000))
muffled = music.iir(music.noise("white", 2), a, b)

Noise is the thing to hear a filter on, having every frequency there is for it to take away.

Structure: change ringing as melody

The part of the package with the least competition elsewhere is the music.structures module: permutation groups, and the peals of English change ringing. A peal is a sequence of permutations in which each step swaps only adjacent pairs, and which visits every permutation exactly once. Applied to bells it is campanology; applied to frequencies the peal is the melody, and its symmetry is audible:

peal = music.PlainChanges(4)
rows = peal.act([220, 275, 330, 440])
notes = [music.note(freq, 0.2) for row in rows for freq in row]
music.write_wav_mono(music.horizontal_stack(*notes), "campanology.wav")

rows holds all 24 permutations of the four frequencies, in ringing order — [220, 275, 330, 440], [275, 220, 330, 440], [275, 330, 220, 440], and so on. act will permute any sequence you hand it, so the same structure can order durations, amplitudes, or stereo positions rather than pitches.

music.InterestingPermutations gives the other groups — rotations, reflections, alternating and dihedral subgroups — with the same interface.

Sensory stimulation

music.stimulation renders the stimuli of sensory-stimulation work, each named for the technique it implements in SSTIM, the Sensory Stimulation Vocabulary. A binaural beat is two steady tones a few hertz apart, one in each ear: the beat is in the listener rather than in the file, and is heard over headphones:

beat = music.binaural_beats(carrier_freq=220, beat_freq=7, duration=8)
music.write_wav_stereo(beat, "binaural.wav")

Such stimuli are delivered as protocols rather than one at a time, and music.StimulationSession renders one: phases in order, crossfaded rather than cut, lasting exactly the sum of their durations:

session = music.StimulationSession(end_ramp=2.0)
session.add(music.binaural_beats, duration=8, ramp=1.0,
            carrier_freq=220, beat_freq=7)
session.add(music.isochronic_tones, duration=8, ramp=2.0,
            carrier_freq=220, pulse_rate=4.0, ramp_duration=0.005)
session.write("session.wav")

Monaural beats, isochronic tones, amplitude and frequency modulation, modulated noise and a source circling the head are the others, and the sensory_stimulation example writes one of each.

Singing

music.sing() sings a lyric to a melody, one syllable a note. The syllables of a word are joined by hyphens and the words parted by spaces; each note is a pitch, in semitones above reference, middle C, and a duration, in units of L at the tempo Q, as ABC notation writes them: a quarter note, at 120 a minute, by default.

twinkle = music.sing(text="Twin-kle, twin-kle, lit-tle star",
                     notes=(0, 0, 7, 7, 9, 9, 7),
                     durs=(1, 1, 1, 1, 1, 1, 2))
music.write_wav_mono(twinkle, "twinkle.wav")

The singer is the package’s own. espeak-ng says each syllable, and Praat’s pitch-synchronous overlap-add, PSOLA, holds it at its note’s pitch and makes it the note’s length. A syllable said in less time than its note is said again, slower; then only the middle of its vowel is lengthened, so that its consonants keep the length they were said with, and a long note is sung with a vibrato. The notes on the strong beats of the meter, M, are sung a little louder. It needs espeak-ng (sudo apt install espeak-ng, or brew install espeak-ng) and pip install 'music[singing]', and says which is missing.

lang names the espeak voice, and with it the language the lyric is sung in, and effect sings with one of three voices: "tremolo", which trembles and rings in a room, in stereo; "melt", the same with a voice whose formants sink with the pitch; and "flite", flite’s voice in espeak’s place.

frere = music.sing(text="Frè-re Jac-ques, dor-mez vous?",
                   notes=(0, 2, 4, 0, 4, 5, 7),
                   durs=(1, 1, 1, 1, 1, 1, 2),
                   lang="fr", effect="tremolo")
music.write_wav_stereo(frere, "frere.wav")

backend="ecantorix" sings the same score with eCantorix, the engine the package sang with before it had its own, and keeps as the reference: a Perl program, which music.setup_engine() clones once, and which needs Perl and four of its modules, abc2midi and sox. The two sing the same pitches for the same lengths, so what differs is the singing, and the examples singing_backends.py and singing_effects.py sing songs in three languages, and the effects, with each, side by side.

Putting it together

Nothing here is new; it is the pieces above composed:

import music
import numpy as np

# a melodic line, each note given a vibrato and an envelope
voice = music.horizontal_stack(*[
    music.adsr(sonic_vector=music.note_with_vibrato(
                   freq, 0.5, vibrato_freq=6, max_pitch_dev=0.3),
               sustain_level=-6)
    for freq in (392, 440, 493.88, 587.33)])

# a quiet bed of pink noise underneath it, of exactly the same length
bed = music.loud(sonic_vector=music.noise("pink",
                                          len(voice) / 44100)) * 0.05

# mix them, then sweep the result across the stereo field
piece = music.localize_linear(music.mix(voice, bed),
                              theta1=120, theta2=60)
music.write_wav_stereo(piece, "piece.wav", fades=(100, 500))

Where to go next

  • The API reference reference. Every routine’s docstring carries the equation it implements, an example, and the article section it comes from.

  • The examples folder, which holds each of the above as a runnable script, plus the Being and IteratorSynth classes for generating material algorithmically, and the two singers singing the same songs side by side.

  • What this page leaves out: music.setup_hrtf() and music.hrir(), which place a source with measured ears, above or behind you, where music.localize() can only place it to a side; and music.Bonds, which ties a note’s vibrato and tremolo to its pitch, once, for a whole piece.

  • Musical elements in the discrete-time representation of sound, the article the whole package implements. If you use this package, please cite it.

  • If music is useful to you or your institution, sponsoring its development helps keep it maintained and open. Commissioned features and sponsored development are available too.