synths 0.1.0b1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
synths/__init__.py
ADDED
|
@@ -0,0 +1,3827 @@
|
|
|
1
|
+
"""synths: sounds and music in Python. The sound sibling of Pyctures.
|
|
2
|
+
|
|
3
|
+
from synths import *
|
|
4
|
+
|
|
5
|
+
play_sound("laser") # a ready-made game sound: coin, jump, laser, explosion, hit, powerup, ...
|
|
6
|
+
play_note("C4") # a note on a pleasant synth: a name or a number (60 is middle C),
|
|
7
|
+
play_note("C4", 0.5, 0.8) # then how long in seconds and how loud (0 to 1). A list is a chord
|
|
8
|
+
play_notes("C4 E4 G4 - C5:2", 0.25) # a tune, a step every quarter of a second; "-" is a rest
|
|
9
|
+
lead = make_synth("lead", cutoff=1200) # a synth of your own, from a ready-made one (bass, lead, pad, pluck, ...)
|
|
10
|
+
lead.play("E4", 0.5) # play a note; lead.play(["C4", "E4", "G4"]) is a chord
|
|
11
|
+
lead.cutoff = 800 # every setting is a plain attribute, and changes notes already sounding
|
|
12
|
+
held = lead.start("C4") # a note that stays until held.stop(); held.slide_to("G4", 0.5)
|
|
13
|
+
note("C4", "quarter"), rest("eighth") # a tune a line at a time: each note begins where the last ended
|
|
14
|
+
lead.arp("Cm7") # a chord from one voice, as a fast arpeggio (a C64 chord)
|
|
15
|
+
arp = lead.start_arp("Am") # one that stays: arp.change("F"), arp.stop()
|
|
16
|
+
drum = load_sample("kick.wav") # a recorded sound from a file; drum.play()
|
|
17
|
+
song = play_song("tune.mod") # a tracker song (.mod, .xm) or one of synths' own: song.pause(), song.stop()
|
|
18
|
+
set_volume(0.5), mute_sound(), unmute_sound(), stop_sound()
|
|
19
|
+
set_effects(echo_time=0.3, reverb_size=0.8)
|
|
20
|
+
set_tempo(120), beats(2) # beats(2) is how many seconds two beats last
|
|
21
|
+
lead.play_from_midi() # a MIDI keyboard plays a synth; lead.learn("cutoff") ties a knob to a setting
|
|
22
|
+
send_midi_note("C4", 0.8, length="quarter") # a note out to a synth on the desk; midi_devices()
|
|
23
|
+
mic = use_microphone() # the microphone is a synth whose sound comes in: mic.voice = "robot",
|
|
24
|
+
mic.listen() # mic.pitch = 7, mic.echo = 0.4. Nothing is heard until listen()
|
|
25
|
+
take = record_sound(3) # three seconds of it, as a sample, at once: take.play("E4"), take.done
|
|
26
|
+
save_sound(take, "hello.wav") # a recording or a sample as a WAV file
|
|
27
|
+
lead.voice = "robot" # a voice changer on any synth; lead.pitch_shift, lead.ring_mod, lead.comb
|
|
28
|
+
lead.samples(), lead.level(), lead.notes() # what it is doing, for your own scope, meter and lit keys
|
|
29
|
+
|
|
30
|
+
def on_midi_note(note, velocity, channel): # in a Pyctures scene: called for every key of a MIDI keyboard
|
|
31
|
+
...
|
|
32
|
+
|
|
33
|
+
A length is seconds as a number, or a note length by name, which follows the tempo: "whole", "half",
|
|
34
|
+
"quarter", "eighth", "sixteenth" (also "1/4", "q", "quarter." for a dotted one, "eighth triplet").
|
|
35
|
+
|
|
36
|
+
When something starts: everything that makes a sound takes after=seconds, at=a time read from
|
|
37
|
+
sound_time(), or beat=a beat number.
|
|
38
|
+
|
|
39
|
+
HOW IT IS BUILT. No sound is worked out in Python. Every function here only WRITES A COMMAND:
|
|
40
|
+
plain data such as {"op": "note", "id": "s1", "note": [60], "len": 0.5, "t": 12.25}, in the format
|
|
41
|
+
of synths.js (its docs/commands.md is the contract). Every id is chosen here, and nothing is ever
|
|
42
|
+
answered, so no line of a program waits for the sound. A PLAYER somewhere else (synths.js, in a
|
|
43
|
+
web page) owns the sound card and carries the commands out.
|
|
44
|
+
|
|
45
|
+
TIME. Each command carries the time it was asked for on Python's own clock
|
|
46
|
+
(time.perf_counter(); the video's own time in a scene that is being recorded a frame at a time), and
|
|
47
|
+
each batch the clock when it was sent. The player lines that clock up
|
|
48
|
+
with the sound card's and plays everything a moment late on the exact sample, so commands that
|
|
49
|
+
were called at uneven moments and sent together once a frame keep exactly the spacing they had.
|
|
50
|
+
|
|
51
|
+
HOW THE COMMANDS TRAVEL (see _sound):
|
|
52
|
+
|
|
53
|
+
- In a Pyctures scene they ride the frame's own message: the first sound of a frame adds ONE
|
|
54
|
+
command to the frame, a call of `send(batch)` on the page's sound object (the fixed handle
|
|
55
|
+
H_SOUND, 10), and the rest of that frame's sounds join the same batch. This is so wherever
|
|
56
|
+
the scene's Python runs: in a browser's worker, in your own Python behind `pyctures run`, or
|
|
57
|
+
on another computer. Sound asked for outside draw() and update() (at the top of the file, in
|
|
58
|
+
setup(), in on_key_down()) is inside the same call from the page, so it goes with that
|
|
59
|
+
call's reply and never waits for a frame of its own.
|
|
60
|
+
- A host that plays the commands some other way calls connect(send), and each command is
|
|
61
|
+
handed to it at once.
|
|
62
|
+
- MIDI goes the same way: its functions write `midi` commands into the same batches, and the page, where the
|
|
63
|
+
devices are, plays a synth from a keyboard itself. What was received is handed back as plain data with the
|
|
64
|
+
next frame (see _Midi), or by a host with receive_midi(events).
|
|
65
|
+
- WHAT COMES BACK. Commands are never answered. A few things are said by the player all the same: the
|
|
66
|
+
microphone is on or is not, a recording is over, a file is saved, and what a synth is doing now (its wave,
|
|
67
|
+
its level, its notes) and where a song is, for a program that draws them. In a Pyctures scene the page
|
|
68
|
+
hands them over with the NEXT FRAME, so each is a frame late: take.done turns True the frame after the
|
|
69
|
+
recording ended, and lead.samples() is the wave as it was a frame ago. A host hands them over with
|
|
70
|
+
receive_sound_events(events).
|
|
71
|
+
- A program started with plain Python (`python tune.py`) has no player. If Pyctures is
|
|
72
|
+
installed the program is started again with `pyctures run`, which opens it in a browser;
|
|
73
|
+
otherwise it says how to, once, and carries on in silence.
|
|
74
|
+
|
|
75
|
+
MIT licence.
|
|
76
|
+
"""
|
|
77
|
+
import base64
|
|
78
|
+
import json
|
|
79
|
+
import math
|
|
80
|
+
import os
|
|
81
|
+
import re
|
|
82
|
+
import sys
|
|
83
|
+
import time
|
|
84
|
+
|
|
85
|
+
__version__ = "0.1.0"
|
|
86
|
+
|
|
87
|
+
__all__ = ["play_sound", "play_note", "play_notes", "make_synth", "load_sample", "load_samples", "note_in_name", "play_song", "stop_song",
|
|
88
|
+
"stop_sound", "set_volume", "mute_sound", "unmute_sound", "set_effects", "set_tempo", "beats", "beat", "set_sound_buffer", "sound_buffer",
|
|
89
|
+
"sound_time", "sound_names", "synth_names", "setting_names", "note_number", "chord_notes", "play_arp",
|
|
90
|
+
"start_arp", "main_synth", "sound_output", "note", "chord", "rest", "start_lines", "loop_notes",
|
|
91
|
+
"length_beats",
|
|
92
|
+
"on_midi", "on_midi_note", "on_midi_note_off", "on_midi_control", "on_midi_devices", "use_midi_hooks",
|
|
93
|
+
"send_midi", "send_midi_note", "send_midi_note_off", "send_midi_control", "send_midi_bend",
|
|
94
|
+
"send_midi_program", "send_midi_clock", "midi_devices", "use_midi_input", "use_midi_output", "midi_input", "midi_output",
|
|
95
|
+
"receive_midi", "stop_midi",
|
|
96
|
+
"use_microphone", "stop_microphone", "record_sound", "start_recording", "stop_recording",
|
|
97
|
+
"record_output", "save_sound", "receive_sound_events", "voice_names"]
|
|
98
|
+
|
|
99
|
+
# The version of the command format: `synths` in every batch.
|
|
100
|
+
COMMANDS = 1
|
|
101
|
+
# The page's sound object in a Pyctures scene, unless Pyctures says otherwise (its H_SOUND).
|
|
102
|
+
H_SOUND = 10
|
|
103
|
+
|
|
104
|
+
# Every sound, ready-made synth and setting, as synths.js names them. Written here by
|
|
105
|
+
# scripts/sync_names.py from synths.js's synths-names.json. Do not edit by hand.
|
|
106
|
+
# NAMES:BEGIN
|
|
107
|
+
_NAMES = json.loads(r'''
|
|
108
|
+
{
|
|
109
|
+
"version": "0.1.0",
|
|
110
|
+
"commands": 1,
|
|
111
|
+
"engine": {"abi": 1, "version": "0.0.0", "parts": 64, "voices": 64, "samples": 1024, "zones": 512, "zoneLayers": 4, "events": ["note_on", "note_off", "all_notes_off", "param", "pitch_bend", "voice_pitch", "voice_freq", "voice_volume", "voice_pan", "voice_position", "voice_kill", "macro_tick", "arp", "arp_more", "song", "record", "arp_style", "arp_beat", "arp_notes", "arp_step", "arp_pattern", "clock", "voice_vibrato"]},
|
|
112
|
+
"settings": [
|
|
113
|
+
{"id": 0, "name": "volume", "kind": "float", "min": 0, "max": 2, "default": 1, "unit": "gain", "scope": "synth"},
|
|
114
|
+
{"id": 1, "name": "pan", "kind": "float", "min": -1, "max": 1, "default": 0, "unit": "pan", "scope": "synth"},
|
|
115
|
+
{"id": 2, "name": "transpose", "kind": "float", "min": -48, "max": 48, "default": 0, "unit": "semitones", "scope": "synth"},
|
|
116
|
+
{"id": 3, "name": "tune", "kind": "float", "min": -100, "max": 100, "default": 0, "unit": "cents", "scope": "synth"},
|
|
117
|
+
{"id": 4, "name": "bend", "kind": "float", "min": -1, "max": 1, "default": 0, "unit": "", "scope": "synth"},
|
|
118
|
+
{"id": 5, "name": "bend_range", "kind": "float", "min": 0, "max": 48, "default": 2, "unit": "semitones", "scope": "synth"},
|
|
119
|
+
{"id": 6, "name": "glide", "kind": "float", "min": 0, "max": 10, "default": 0, "unit": "seconds", "scope": "synth"},
|
|
120
|
+
{"id": 7, "name": "mode", "kind": "choice", "min": 0, "max": 2, "default": 0, "unit": "", "choices": ["poly", "mono", "legato"], "scope": "synth"},
|
|
121
|
+
{"id": 8, "name": "unison.voices", "kind": "int", "min": 1, "max": 8, "default": 1, "unit": "voices", "scope": "synth"},
|
|
122
|
+
{"id": 9, "name": "unison.detune", "kind": "float", "min": 0, "max": 100, "default": 20, "unit": "cents", "scope": "synth"},
|
|
123
|
+
{"id": 10, "name": "unison.spread", "kind": "float", "min": 0, "max": 1, "default": 0.5, "unit": "", "scope": "synth"},
|
|
124
|
+
{"id": 11, "name": "velocity", "kind": "float", "min": 0, "max": 1, "default": 1, "unit": "", "scope": "synth"},
|
|
125
|
+
{"id": 12, "name": "pan.law", "kind": "choice", "min": 0, "max": 1, "default": 0, "unit": "", "choices": ["equal_power", "square_root"], "scope": "synth"},
|
|
126
|
+
{"id": 16, "name": "osc1.wave", "kind": "choice", "min": 0, "max": 4, "default": 1, "unit": "", "choices": ["sine", "triangle", "saw", "pulse", "sample"], "scope": "synth"},
|
|
127
|
+
{"id": 17, "name": "osc1.level", "kind": "float", "min": 0, "max": 1, "default": 1, "unit": "gain", "scope": "synth"},
|
|
128
|
+
{"id": 18, "name": "osc1.coarse", "kind": "float", "min": -48, "max": 48, "default": 0, "unit": "semitones", "scope": "synth"},
|
|
129
|
+
{"id": 19, "name": "osc1.fine", "kind": "float", "min": -100, "max": 100, "default": 0, "unit": "cents", "scope": "synth"},
|
|
130
|
+
{"id": 20, "name": "osc1.pulse_width", "kind": "float", "min": 0, "max": 1, "default": 0.5, "unit": "", "scope": "synth"},
|
|
131
|
+
{"id": 21, "name": "osc1.sample", "kind": "int", "min": 0, "max": 1023, "default": 0, "unit": "slot", "scope": "synth"},
|
|
132
|
+
{"id": 22, "name": "osc1.start", "kind": "float", "min": 0, "max": 1, "default": 0, "unit": "", "scope": "synth"},
|
|
133
|
+
{"id": 23, "name": "osc1.keytrack", "kind": "bool", "min": 0, "max": 1, "default": 1, "unit": "", "scope": "synth"},
|
|
134
|
+
{"id": 24, "name": "osc2.wave", "kind": "choice", "min": 0, "max": 4, "default": 2, "unit": "", "choices": ["sine", "triangle", "saw", "pulse", "sample"], "scope": "synth"},
|
|
135
|
+
{"id": 25, "name": "osc2.level", "kind": "float", "min": 0, "max": 1, "default": 0, "unit": "gain", "scope": "synth"},
|
|
136
|
+
{"id": 26, "name": "osc2.coarse", "kind": "float", "min": -48, "max": 48, "default": 0, "unit": "semitones", "scope": "synth"},
|
|
137
|
+
{"id": 27, "name": "osc2.fine", "kind": "float", "min": -100, "max": 100, "default": 0, "unit": "cents", "scope": "synth"},
|
|
138
|
+
{"id": 28, "name": "osc2.pulse_width", "kind": "float", "min": 0, "max": 1, "default": 0.5, "unit": "", "scope": "synth"},
|
|
139
|
+
{"id": 29, "name": "osc2.sample", "kind": "int", "min": 0, "max": 1023, "default": 0, "unit": "slot", "scope": "synth"},
|
|
140
|
+
{"id": 30, "name": "osc2.start", "kind": "float", "min": 0, "max": 1, "default": 0, "unit": "", "scope": "synth"},
|
|
141
|
+
{"id": 31, "name": "osc2.keytrack", "kind": "bool", "min": 0, "max": 1, "default": 1, "unit": "", "scope": "synth"},
|
|
142
|
+
{"id": 32, "name": "osc2.sync", "kind": "bool", "min": 0, "max": 1, "default": 0, "unit": "", "scope": "synth"},
|
|
143
|
+
{"id": 33, "name": "osc2.ring", "kind": "float", "min": 0, "max": 1, "default": 0, "unit": "", "scope": "synth"},
|
|
144
|
+
{"id": 34, "name": "osc1.reverse", "kind": "bool", "min": 0, "max": 1, "default": 0, "unit": "", "scope": "synth"},
|
|
145
|
+
{"id": 35, "name": "osc2.reverse", "kind": "bool", "min": 0, "max": 1, "default": 0, "unit": "", "scope": "synth"},
|
|
146
|
+
{"id": 36, "name": "sub.level", "kind": "float", "min": 0, "max": 1, "default": 0, "unit": "gain", "scope": "synth"},
|
|
147
|
+
{"id": 37, "name": "sub.wave", "kind": "choice", "min": 0, "max": 2, "default": 0, "unit": "", "choices": ["sine", "square", "triangle"], "scope": "synth"},
|
|
148
|
+
{"id": 38, "name": "sub.octave", "kind": "int", "min": -2, "max": -1, "default": -1, "unit": "octaves", "scope": "synth"},
|
|
149
|
+
{"id": 39, "name": "sample.antialias", "kind": "bool", "min": 0, "max": 1, "default": 1, "unit": "", "scope": "synth"},
|
|
150
|
+
{"id": 40, "name": "noise.level", "kind": "float", "min": 0, "max": 1, "default": 0, "unit": "gain", "scope": "synth"},
|
|
151
|
+
{"id": 41, "name": "noise.type", "kind": "choice", "min": 0, "max": 3, "default": 0, "unit": "", "choices": ["white", "pink", "lfsr", "lfsr_short"], "scope": "synth"},
|
|
152
|
+
{"id": 42, "name": "noise.pitch", "kind": "float", "min": -48, "max": 48, "default": 0, "unit": "semitones", "scope": "synth"},
|
|
153
|
+
{"id": 48, "name": "filter.type", "kind": "choice", "min": 0, "max": 7, "default": 0, "unit": "", "choices": ["off", "lowpass", "highpass", "bandpass", "notch", "low_band", "band_high", "all"], "scope": "synth"},
|
|
154
|
+
{"id": 49, "name": "filter.slope", "kind": "choice", "min": 0, "max": 1, "default": 0, "unit": "", "choices": ["12db", "24db"], "scope": "synth"},
|
|
155
|
+
{"id": 50, "name": "filter.cutoff", "kind": "float", "min": 20, "max": 20000, "default": 2000, "unit": "hz", "scope": "synth"},
|
|
156
|
+
{"id": 51, "name": "filter.resonance", "kind": "float", "min": 0, "max": 1, "default": 0.15, "unit": "", "scope": "synth"},
|
|
157
|
+
{"id": 52, "name": "filter.keytrack", "kind": "float", "min": 0, "max": 1, "default": 0, "unit": "", "scope": "synth"},
|
|
158
|
+
{"id": 53, "name": "filter.env", "kind": "float", "min": -96, "max": 96, "default": 0, "unit": "semitones", "scope": "synth"},
|
|
159
|
+
{"id": 54, "name": "filter.velocity", "kind": "float", "min": -96, "max": 96, "default": 0, "unit": "semitones", "scope": "synth"},
|
|
160
|
+
{"id": 56, "name": "amp.attack", "kind": "float", "min": 0, "max": 20, "default": 0.005, "unit": "seconds", "scope": "synth"},
|
|
161
|
+
{"id": 57, "name": "amp.decay", "kind": "float", "min": 0, "max": 20, "default": 0.1, "unit": "seconds", "scope": "synth"},
|
|
162
|
+
{"id": 58, "name": "amp.sustain", "kind": "float", "min": 0, "max": 1, "default": 0.8, "unit": "", "scope": "synth"},
|
|
163
|
+
{"id": 59, "name": "amp.release", "kind": "float", "min": 0, "max": 20, "default": 0.15, "unit": "seconds", "scope": "synth"},
|
|
164
|
+
{"id": 60, "name": "amp.curve", "kind": "float", "min": 0, "max": 1, "default": 0.5, "unit": "", "scope": "synth"},
|
|
165
|
+
{"id": 64, "name": "filter_env.attack", "kind": "float", "min": 0, "max": 20, "default": 0.005, "unit": "seconds", "scope": "synth"},
|
|
166
|
+
{"id": 65, "name": "filter_env.decay", "kind": "float", "min": 0, "max": 20, "default": 0.2, "unit": "seconds", "scope": "synth"},
|
|
167
|
+
{"id": 66, "name": "filter_env.sustain", "kind": "float", "min": 0, "max": 1, "default": 0.3, "unit": "", "scope": "synth"},
|
|
168
|
+
{"id": 67, "name": "filter_env.release", "kind": "float", "min": 0, "max": 20, "default": 0.2, "unit": "seconds", "scope": "synth"},
|
|
169
|
+
{"id": 68, "name": "filter_env.curve", "kind": "float", "min": 0, "max": 1, "default": 0.5, "unit": "", "scope": "synth"},
|
|
170
|
+
{"id": 72, "name": "mod_env.attack", "kind": "float", "min": 0, "max": 20, "default": 0.005, "unit": "seconds", "scope": "synth"},
|
|
171
|
+
{"id": 73, "name": "mod_env.decay", "kind": "float", "min": 0, "max": 20, "default": 0.2, "unit": "seconds", "scope": "synth"},
|
|
172
|
+
{"id": 74, "name": "mod_env.sustain", "kind": "float", "min": 0, "max": 1, "default": 0, "unit": "", "scope": "synth"},
|
|
173
|
+
{"id": 75, "name": "mod_env.release", "kind": "float", "min": 0, "max": 20, "default": 0.2, "unit": "seconds", "scope": "synth"},
|
|
174
|
+
{"id": 76, "name": "mod_env.curve", "kind": "float", "min": 0, "max": 1, "default": 0.5, "unit": "", "scope": "synth"},
|
|
175
|
+
{"id": 77, "name": "mod_env.pitch", "kind": "float", "min": -48, "max": 48, "default": 0, "unit": "semitones", "scope": "synth"},
|
|
176
|
+
{"id": 78, "name": "mod_env.osc2_pitch", "kind": "float", "min": -48, "max": 48, "default": 0, "unit": "semitones", "scope": "synth"},
|
|
177
|
+
{"id": 79, "name": "mod_env.pulse_width", "kind": "float", "min": -1, "max": 1, "default": 0, "unit": "", "scope": "synth"},
|
|
178
|
+
{"id": 80, "name": "lfo1.wave", "kind": "choice", "min": 0, "max": 4, "default": 0, "unit": "", "choices": ["sine", "triangle", "saw", "square", "random"], "scope": "synth"},
|
|
179
|
+
{"id": 81, "name": "lfo1.rate", "kind": "float", "min": 0.01, "max": 50, "default": 5, "unit": "hz", "scope": "synth"},
|
|
180
|
+
{"id": 82, "name": "lfo1.delay", "kind": "float", "min": 0, "max": 10, "default": 0, "unit": "seconds", "scope": "synth"},
|
|
181
|
+
{"id": 83, "name": "lfo1.fade", "kind": "float", "min": 0, "max": 10, "default": 0, "unit": "seconds", "scope": "synth"},
|
|
182
|
+
{"id": 84, "name": "lfo1.sync", "kind": "bool", "min": 0, "max": 1, "default": 1, "unit": "", "scope": "synth"},
|
|
183
|
+
{"id": 85, "name": "lfo1.pitch", "kind": "float", "min": -48, "max": 48, "default": 0, "unit": "semitones", "scope": "synth"},
|
|
184
|
+
{"id": 86, "name": "lfo1.pulse_width", "kind": "float", "min": -1, "max": 1, "default": 0, "unit": "", "scope": "synth"},
|
|
185
|
+
{"id": 87, "name": "lfo1.cutoff", "kind": "float", "min": -96, "max": 96, "default": 0, "unit": "semitones", "scope": "synth"},
|
|
186
|
+
{"id": 88, "name": "lfo1.amp", "kind": "float", "min": 0, "max": 1, "default": 0, "unit": "", "scope": "synth"},
|
|
187
|
+
{"id": 89, "name": "lfo1.pan", "kind": "float", "min": 0, "max": 1, "default": 0, "unit": "", "scope": "synth"},
|
|
188
|
+
{"id": 96, "name": "lfo2.wave", "kind": "choice", "min": 0, "max": 4, "default": 1, "unit": "", "choices": ["sine", "triangle", "saw", "square", "random"], "scope": "synth"},
|
|
189
|
+
{"id": 97, "name": "lfo2.rate", "kind": "float", "min": 0.01, "max": 50, "default": 0.5, "unit": "hz", "scope": "synth"},
|
|
190
|
+
{"id": 98, "name": "lfo2.delay", "kind": "float", "min": 0, "max": 10, "default": 0, "unit": "seconds", "scope": "synth"},
|
|
191
|
+
{"id": 99, "name": "lfo2.fade", "kind": "float", "min": 0, "max": 10, "default": 0, "unit": "seconds", "scope": "synth"},
|
|
192
|
+
{"id": 100, "name": "lfo2.sync", "kind": "bool", "min": 0, "max": 1, "default": 0, "unit": "", "scope": "synth"},
|
|
193
|
+
{"id": 101, "name": "lfo2.pitch", "kind": "float", "min": -48, "max": 48, "default": 0, "unit": "semitones", "scope": "synth"},
|
|
194
|
+
{"id": 102, "name": "lfo2.pulse_width", "kind": "float", "min": -1, "max": 1, "default": 0, "unit": "", "scope": "synth"},
|
|
195
|
+
{"id": 103, "name": "lfo2.cutoff", "kind": "float", "min": -96, "max": 96, "default": 0, "unit": "semitones", "scope": "synth"},
|
|
196
|
+
{"id": 104, "name": "lfo2.amp", "kind": "float", "min": 0, "max": 1, "default": 0, "unit": "", "scope": "synth"},
|
|
197
|
+
{"id": 105, "name": "lfo2.pan", "kind": "float", "min": 0, "max": 1, "default": 0, "unit": "", "scope": "synth"},
|
|
198
|
+
{"id": 106, "name": "fx.pitch", "kind": "float", "min": -12, "max": 12, "default": 0, "unit": "semitones", "scope": "synth"},
|
|
199
|
+
{"id": 107, "name": "fx.ring", "kind": "float", "min": 0, "max": 1, "default": 0, "unit": "", "scope": "synth"},
|
|
200
|
+
{"id": 108, "name": "fx.ring_freq", "kind": "float", "min": 1, "max": 5000, "default": 30, "unit": "hz", "scope": "synth"},
|
|
201
|
+
{"id": 109, "name": "fx.ring_wave", "kind": "choice", "min": 0, "max": 1, "default": 0, "unit": "", "choices": ["sine", "noise"], "scope": "synth"},
|
|
202
|
+
{"id": 110, "name": "fx.comb", "kind": "float", "min": 0, "max": 1, "default": 0, "unit": "", "scope": "synth"},
|
|
203
|
+
{"id": 111, "name": "fx.comb_freq", "kind": "float", "min": 50, "max": 5000, "default": 200, "unit": "hz", "scope": "synth"},
|
|
204
|
+
{"id": 112, "name": "fx.drive", "kind": "float", "min": 0, "max": 1, "default": 0, "unit": "", "scope": "synth"},
|
|
205
|
+
{"id": 113, "name": "fx.bits", "kind": "float", "min": 1, "max": 16, "default": 16, "unit": "bits", "scope": "synth"},
|
|
206
|
+
{"id": 114, "name": "fx.downsample", "kind": "int", "min": 1, "max": 64, "default": 1, "unit": "", "scope": "synth"},
|
|
207
|
+
{"id": 115, "name": "send.delay", "kind": "float", "min": 0, "max": 1, "default": 0, "unit": "gain", "scope": "synth"},
|
|
208
|
+
{"id": 116, "name": "send.reverb", "kind": "float", "min": 0, "max": 1, "default": 0, "unit": "gain", "scope": "synth"},
|
|
209
|
+
{"id": 117, "name": "send.chorus", "kind": "float", "min": 0, "max": 1, "default": 0, "unit": "gain", "scope": "synth"},
|
|
210
|
+
{"id": 120, "name": "macro.rate", "kind": "float", "min": 1, "max": 1000, "default": 50, "unit": "hz", "scope": "synth"},
|
|
211
|
+
{"id": 121, "name": "macro.sync", "kind": "bool", "min": 0, "max": 1, "default": 0, "unit": "", "scope": "synth"},
|
|
212
|
+
{"id": 122, "name": "fx.comb_feedback", "kind": "float", "min": -0.97, "max": 0.97, "default": 0.7, "unit": "", "scope": "synth"},
|
|
213
|
+
{"id": 123, "name": "fx.comb_rate", "kind": "float", "min": 0.01, "max": 10, "default": 0.3, "unit": "hz", "scope": "synth"},
|
|
214
|
+
{"id": 124, "name": "fx.comb_depth", "kind": "float", "min": 0, "max": 1, "default": 0, "unit": "", "scope": "synth"},
|
|
215
|
+
{"id": 90, "name": "gate.depth", "kind": "float", "min": 0, "max": 1, "default": 0, "unit": "", "scope": "synth"},
|
|
216
|
+
{"id": 91, "name": "gate.rate", "kind": "float", "min": 0.25, "max": 16, "default": 4, "unit": "steps", "scope": "synth"},
|
|
217
|
+
{"id": 92, "name": "gate.smooth", "kind": "float", "min": 0.001, "max": 0.5, "default": 0.008, "unit": "seconds", "scope": "synth"},
|
|
218
|
+
{"id": 93, "name": "gate.swing", "kind": "float", "min": 0, "max": 0.75, "default": 0, "unit": "", "scope": "synth"},
|
|
219
|
+
{"id": 94, "name": "gate.sends", "kind": "bool", "min": 0, "max": 1, "default": 1, "unit": "", "scope": "synth"},
|
|
220
|
+
{"id": 95, "name": "gate.stereo", "kind": "bool", "min": 0, "max": 1, "default": 0, "unit": "", "scope": "synth"},
|
|
221
|
+
{"id": 200, "name": "master.volume", "kind": "float", "min": 0, "max": 2, "default": 0.35, "unit": "gain", "scope": "all"},
|
|
222
|
+
{"id": 201, "name": "master.limiter", "kind": "bool", "min": 0, "max": 1, "default": 1, "unit": "", "scope": "all"},
|
|
223
|
+
{"id": 202, "name": "delay.time", "kind": "float", "min": 0.001, "max": 2, "default": 0.3, "unit": "seconds", "scope": "all"},
|
|
224
|
+
{"id": 203, "name": "delay.feedback", "kind": "float", "min": 0, "max": 0.95, "default": 0.4, "unit": "", "scope": "all"},
|
|
225
|
+
{"id": 204, "name": "delay.tone", "kind": "float", "min": 200, "max": 20000, "default": 6000, "unit": "hz", "scope": "all"},
|
|
226
|
+
{"id": 205, "name": "delay.pingpong", "kind": "bool", "min": 0, "max": 1, "default": 0, "unit": "", "scope": "all"},
|
|
227
|
+
{"id": 208, "name": "reverb.size", "kind": "float", "min": 0, "max": 1, "default": 0.5, "unit": "", "scope": "all"},
|
|
228
|
+
{"id": 209, "name": "reverb.damping", "kind": "float", "min": 0, "max": 1, "default": 0.4, "unit": "", "scope": "all"},
|
|
229
|
+
{"id": 210, "name": "reverb.type", "kind": "choice", "min": 0, "max": 4, "default": 0, "unit": "", "choices": ["room", "hall", "plate", "cathedral", "spring"], "scope": "all"},
|
|
230
|
+
{"id": 211, "name": "reverb.decay", "kind": "float", "min": 0, "max": 30, "default": 0, "unit": "seconds", "labels": {"0": "auto"}, "scope": "all"},
|
|
231
|
+
{"id": 212, "name": "chorus.rate", "kind": "float", "min": 0.05, "max": 10, "default": 0.8, "unit": "hz", "scope": "all"},
|
|
232
|
+
{"id": 213, "name": "chorus.depth", "kind": "float", "min": 0, "max": 1, "default": 0.5, "unit": "", "scope": "all"},
|
|
233
|
+
{"id": 216, "name": "voices", "kind": "int", "min": 1, "max": 64, "default": 64, "unit": "voices", "scope": "all"},
|
|
234
|
+
{"id": 217, "name": "input.part", "kind": "int", "min": 0, "max": 63, "default": 31, "unit": "part", "scope": "all"},
|
|
235
|
+
{"id": 218, "name": "input.gain", "kind": "float", "min": 0, "max": 8, "default": 1, "unit": "gain", "scope": "all"},
|
|
236
|
+
{"id": 219, "name": "input.monitor", "kind": "bool", "min": 0, "max": 1, "default": 1, "unit": "", "scope": "all"},
|
|
237
|
+
{"id": 220, "name": "input.gate", "kind": "float", "min": -100, "max": 0, "default": -100, "unit": "db", "labels": {"-100": "off"}, "scope": "all"},
|
|
238
|
+
{"id": 221, "name": "input.gate_release", "kind": "float", "min": 0.005, "max": 2, "default": 0.15, "unit": "seconds", "scope": "all"},
|
|
239
|
+
{"id": 222, "name": "reverb.predelay", "kind": "float", "min": 0, "max": 0.2, "default": 0, "unit": "seconds", "scope": "all"},
|
|
240
|
+
{"id": 223, "name": "reverb.low_cut", "kind": "float", "min": 20, "max": 1000, "default": 120, "unit": "hz", "scope": "all"},
|
|
241
|
+
{"id": 224, "name": "tempo", "kind": "float", "min": 20, "max": 999, "default": 120, "unit": "bpm", "scope": "all"},
|
|
242
|
+
{"id": 225, "name": "reverb.level", "kind": "float", "min": 0, "max": 2, "default": 1, "unit": "gain", "scope": "all"},
|
|
243
|
+
{"id": 226, "name": "delay.level", "kind": "float", "min": 0, "max": 2, "default": 1, "unit": "gain", "scope": "all"}
|
|
244
|
+
],
|
|
245
|
+
"names": {"wave": {"setting": "osc1.wave", "scale": 1}, "level1": {"setting": "osc1.level", "scale": 1}, "width": {"setting": "osc1.pulse_width", "scale": 1}, "pulseWidth": {"setting": "osc1.pulse_width", "scale": 1}, "sample": {"setting": "osc1.sample", "scale": 1}, "octave": {"setting": "transpose", "scale": 12}, "pitch": {"setting": "transpose", "scale": 1}, "maxVoices": {"setting": "voices", "scale": 1}, "voices": {"setting": "unison.voices", "scale": 1}, "detune": {"setting": "unison.detune", "scale": 1}, "spread": {"setting": "unison.spread", "scale": 1}, "wave2": {"setting": "osc2.wave", "scale": 1}, "level2": {"setting": "osc2.level", "scale": 1}, "pitch2": {"setting": "osc2.coarse", "scale": 1}, "detune2": {"setting": "osc2.fine", "scale": 1}, "width2": {"setting": "osc2.pulse_width", "scale": 1}, "sync": {"setting": "osc2.sync", "scale": 1}, "ring": {"setting": "osc2.ring", "scale": 1}, "sub": {"setting": "sub.level", "scale": 1}, "noise": {"setting": "noise.level", "scale": 1}, "macroRate": {"setting": "macro.rate", "scale": 1}, "macroSync": {"setting": "macro.sync", "scale": 1}, "filter": {"setting": "filter.type", "scale": 1}, "slope": {"setting": "filter.slope", "scale": 1}, "cutoff": {"setting": "filter.cutoff", "scale": 1}, "resonance": {"setting": "filter.resonance", "scale": 1}, "keytrack": {"setting": "filter.keytrack", "scale": 1}, "sweep": {"setting": "filter.env", "scale": 1}, "sweepAttack": {"setting": "filter_env.attack", "scale": 1}, "sweepDecay": {"setting": "filter_env.decay", "scale": 1}, "sweepSustain": {"setting": "filter_env.sustain", "scale": 1}, "sweepRelease": {"setting": "filter_env.release", "scale": 1}, "attack": {"setting": "amp.attack", "scale": 1}, "decay": {"setting": "amp.decay", "scale": 1}, "sustain": {"setting": "amp.sustain", "scale": 1}, "release": {"setting": "amp.release", "scale": 1}, "curve": {"setting": "amp.curve", "scale": 1}, "pitchSweep": {"setting": "mod_env.pitch", "scale": 1}, "pitchSweepAttack": {"setting": "mod_env.attack", "scale": 1}, "pitchSweepDecay": {"setting": "mod_env.decay", "scale": 1}, "vibrato": {"setting": "lfo1.pitch", "scale": 1}, "vibratoRate": {"setting": "lfo1.rate", "scale": 1}, "vibratoDelay": {"setting": "lfo1.delay", "scale": 1}, "tremolo": {"setting": "lfo1.amp", "scale": 1}, "wobble": {"setting": "lfo2.cutoff", "scale": 1}, "wobbleRate": {"setting": "lfo2.rate", "scale": 1}, "autopan": {"setting": "lfo2.pan", "scale": 1}, "pwm": {"setting": "lfo2.pulse_width", "scale": 1}, "pwmRate": {"setting": "lfo2.rate", "scale": 1}, "distortion": {"setting": "fx.drive", "scale": 1}, "drive": {"setting": "fx.drive", "scale": 1}, "crush": {"setting": "fx.bits", "scale": 1}, "bits": {"setting": "fx.bits", "scale": 1}, "downsample": {"setting": "fx.downsample", "scale": 1}, "pitchShift": {"setting": "fx.pitch", "scale": 1}, "ringMod": {"setting": "fx.ring", "scale": 1}, "ringSpeed": {"setting": "fx.ring_freq", "scale": 1}, "ringWave": {"setting": "fx.ring_wave", "scale": 1}, "comb": {"setting": "fx.comb", "scale": 1}, "combTone": {"setting": "fx.comb_freq", "scale": 1}, "combFeedback": {"setting": "fx.comb_feedback", "scale": 1}, "flanger": {"setting": "fx.comb_depth", "scale": 1}, "flangerRate": {"setting": "fx.comb_rate", "scale": 1}, "reverse": {"setting": "osc1.reverse", "scale": 1}, "echo": {"setting": "send.delay", "scale": 1}, "delay": {"setting": "send.delay", "scale": 1}, "reverb": {"setting": "send.reverb", "scale": 1}, "chorus": {"setting": "send.chorus", "scale": 1}, "masterVolume": {"setting": "master.volume", "scale": 1}, "limiter": {"setting": "master.limiter", "scale": 1}, "echoTime": {"setting": "delay.time", "scale": 1}, "echoFeedback": {"setting": "delay.feedback", "scale": 1}, "echoTone": {"setting": "delay.tone", "scale": 1}, "echoBounce": {"setting": "delay.pingpong", "scale": 1}, "reverbSize": {"setting": "reverb.size", "scale": 1}, "reverbDamping": {"setting": "reverb.damping", "scale": 1}, "reverbType": {"setting": "reverb.type", "scale": 1}, "reverbDecay": {"setting": "reverb.decay", "scale": 1}, "reverbPredelay": {"setting": "reverb.predelay", "scale": 1}, "reverbLowCut": {"setting": "reverb.low_cut", "scale": 1}, "reverbLevel": {"setting": "reverb.level", "scale": 1}, "echoLevel": {"setting": "delay.level", "scale": 1}, "tranceGateDepth": {"setting": "gate.depth", "scale": 1}, "tranceGateRate": {"setting": "gate.rate", "scale": 1}, "tranceGateSmooth": {"setting": "gate.smooth", "scale": 1}, "tranceGateSwing": {"setting": "gate.swing", "scale": 1}, "tranceGateSends": {"setting": "gate.sends", "scale": 1}, "tranceGateStereo": {"setting": "gate.stereo", "scale": 1}, "chorusRate": {"setting": "chorus.rate", "scale": 1}, "chorusDepth": {"setting": "chorus.depth", "scale": 1}, "micGain": {"setting": "input.gain", "scale": 1}, "micGate": {"setting": "input.gate", "scale": 1}, "micGateRelease": {"setting": "input.gate_release", "scale": 1}},
|
|
246
|
+
"moves": {"words": ["glide", "bend", "slide", "vibrato"], "shapes": ["scoop", "fall", "dip"], "bend": ["to", "over", "shape"], "vibrato": ["depth", "rate", "delay"]},
|
|
247
|
+
"arp": {"words": ["speed", "step", "order", "style", "gate", "octaves", "swing", "accent", "pattern"], "styles": ["slide", "strike"], "patterns": ["bounce", "octaves", "root-fifth", "thumb", "converge", "diverge", "offbeat", "gallop", "trance", "acid"], "patternSteps": {"bounce": "1 2 3 2", "octaves": "1 1' 1 1'", "root-fifth": "1 3 1' 3", "thumb": "1 2 1 3 1 4 1 3", "converge": "1 4 2 3", "diverge": "2 3 1 4", "offbeat": "- 1 - 2 - 3 - 2", "gallop": "1^ - 1 1 2^ - 2 2", "trance": "1^ - 3 2 - 1^ 3 -", "acid": "1^ 1 ~3 - 1' ~1 2^ -"}, "setting": "arpPattern", "step": "arpStep", "fields": ["offsets", "rate", "beats", "order", "octaves", "gate", "style", "swing", "accent", "pattern"]},
|
|
248
|
+
"tranceGate": {"functions": ["setTranceGate", "tranceGateStep"], "patterns": {"eighths": "x.x.x.x.x.x.x.x.", "offbeat": "..x...x...x...x.", "gallop": "x.xxx.xxx.xxx.xx", "trance": "x.x.xx.xx.x.xx.x", "stutter": "xxx.xx.xx.xxx.x.", "pump": "2468246824682468", "triplets": "x..x..x..x..x..x..x..x..", "dotted": "x..x..x.x..x..x."}, "setting": "tranceGate", "settings": ["tranceGateDepth", "tranceGateRate", "tranceGateSmooth", "tranceGateSwing", "tranceGateSends", "tranceGateStereo"], "words": ["rate", "depth", "smooth", "swing", "stereo", "sends"], "step": "tranceGateStep"},
|
|
249
|
+
"choiceWords": {"square": "pulse", "sawtooth": "saw", "tri": "triangle", "sin": "sine", "low": "lowpass", "high": "highpass", "band": "bandpass", "none": "off", "hiss": "white", "chip": "lfsr", "buzz": "lfsr_short"},
|
|
250
|
+
"plain": {"filter": "lowpass", "cutoff": 20000, "resonance": 0.1},
|
|
251
|
+
"synths": {
|
|
252
|
+
"keys": {"wave": "triangle", "wave2": "sine", "level2": 0.35, "pitch2": 12, "cutoff": 3500, "sweep": 18, "sweepDecay": 0.3, "sweepSustain": 0.2, "keytrack": 0.5, "attack": 0.003, "decay": 0.45, "sustain": 0.3, "release": 0.35, "reverb": 0.12},
|
|
253
|
+
"bass": {"wave": "saw", "sub": 0.6, "cutoff": 320, "resonance": 0.3, "sweep": 30, "sweepDecay": 0.18, "sweepSustain": 0.15, "attack": 0.003, "decay": 0.2, "sustain": 0.75, "release": 0.12, "note": 36},
|
|
254
|
+
"lead": {"wave": "saw", "wave2": "square", "level2": 0.6, "detune2": 9, "cutoff": 2800, "resonance": 0.25, "keytrack": 0.5, "attack": 0.01, "decay": 0.15, "sustain": 0.7, "release": 0.2, "vibrato": 0.15, "vibratoDelay": 0.25, "vibratoRate": 5.5, "echo": 0.2},
|
|
255
|
+
"pad": {"wave": "saw", "voices": 4, "detune": 22, "spread": 0.8, "cutoff": 1600, "attack": 0.5, "decay": 0.5, "sustain": 0.8, "release": 1.2, "chorus": 0.4, "reverb": 0.4},
|
|
256
|
+
"pluck": {"wave": "saw", "cutoff": 500, "resonance": 0.2, "sweep": 48, "sweepDecay": 0.12, "sweepSustain": 0, "keytrack": 0.5, "attack": 0.001, "decay": 0.3, "sustain": 0, "release": 0.25},
|
|
257
|
+
"organ": {"wave": "sine", "wave2": "sine", "level2": 0.6, "pitch2": 12, "sub": 0.5, "attack": 0.008, "decay": 0.01, "sustain": 1, "release": 0.06, "chorus": 0.3},
|
|
258
|
+
"strings": {"wave": "saw", "voices": 3, "detune": 14, "spread": 0.6, "cutoff": 3200, "attack": 0.25, "decay": 0.3, "sustain": 0.85, "release": 0.6, "vibrato": 0.08, "vibratoDelay": 0.4, "reverb": 0.3},
|
|
259
|
+
"bell": {"wave": "sine", "wave2": "sine", "level2": 0.45, "pitch2": 19, "detune2": 2, "attack": 0.001, "decay": 1.4, "sustain": 0, "release": 1.2, "reverb": 0.3},
|
|
260
|
+
"flute": {"wave": "triangle", "noise": 0.03, "cutoff": 3000, "attack": 0.06, "decay": 0.1, "sustain": 0.9, "release": 0.15, "vibrato": 0.1, "vibratoDelay": 0.3},
|
|
261
|
+
"chip": {"wave": "square", "filter": "off", "attack": 0, "decay": 0, "sustain": 1, "release": 0.02},
|
|
262
|
+
"chipLead": {"wave": "pulse", "width": 0.25, "filter": "off", "attack": 0, "decay": 0, "sustain": 1, "release": 0.02, "vibrato": 0.2, "vibratoDelay": 0.15, "vibratoRate": 6},
|
|
263
|
+
"chipBass": {"wave": "pulse", "filter": "off", "attack": 0, "decay": 0, "sustain": 1, "release": 0.02, "note": 36, "macroRate": 4, "macro": {"steps": [{"width": 0.06}, {"width": 0.2}, {"width": 0.5}, {"width": 0.8}, {"width": 0.94}, {"width": 0.8}, {"width": 0.5}, {"width": 0.2}], "loop": true, "slide": true}},
|
|
264
|
+
"chipNoise": {"level1": 0, "noise": 1, "noise.type": "lfsr", "filter": "off", "attack": 0, "decay": 0.12, "sustain": 0, "release": 0.05},
|
|
265
|
+
"chipKick": {"wave": "triangle", "filter": "off", "attack": 0, "decay": 0.16, "sustain": 0, "release": 0.05, "note": 48, "macro": [{"wave": "square", "pitch": 12}, {"wave": "triangle", "pitch": 5}, {"wave": "triangle", "pitch": 0}, {"wave": "triangle", "pitch": -5}, {"wave": "triangle", "pitch": -10, "volume": 0.8}, {"wave": "triangle", "pitch": -14, "volume": 0.6}]},
|
|
266
|
+
"chipSnare": {"wave": "pulse", "noise.type": "lfsr", "filter": "off", "attack": 0, "decay": 0.16, "sustain": 0, "release": 0.05, "note": 57, "macro": [{"wave": "noise", "pitch": 24}, {"wave": "square", "pitch": 0}, {"wave": "square", "pitch": -5, "volume": 0.9}, {"wave": "noise", "pitch": 18, "volume": 0.8}, {"wave": "noise", "pitch": 14, "volume": 0.6}, {"wave": "noise", "pitch": 12, "volume": 0.45}]},
|
|
267
|
+
"chipHat": {"wave": "pulse", "noise.type": "lfsr", "filter": "off", "attack": 0, "decay": 0.05, "sustain": 0, "release": 0.02, "note": 72, "macro": [{"wave": "noise", "pitch": 30}, {"wave": "noise", "pitch": 26, "volume": 0.6}, {"wave": "noise", "pitch": 24, "volume": 0.3}]},
|
|
268
|
+
"kick": {"wave": "sine", "filter": "off", "pitchSweep": 30, "pitchSweepDecay": 0.06, "pitchSweepAttack": 0, "attack": 0, "decay": 0.3, "sustain": 0, "release": 0.1, "note": 36},
|
|
269
|
+
"snare": {"wave": "triangle", "level1": 0.6, "noise": 1, "filter": "highpass", "cutoff": 500, "resonance": 0.05, "pitchSweep": 12, "pitchSweepDecay": 0.04, "pitchSweepAttack": 0, "attack": 0, "decay": 0.17, "sustain": 0, "release": 0.08, "note": 52},
|
|
270
|
+
"hat": {"level1": 0, "noise": 1, "filter": "highpass", "cutoff": 7000, "attack": 0, "decay": 0.05, "sustain": 0, "release": 0.03, "note": 60},
|
|
271
|
+
"sample": {"wave": "sample", "filter": "off", "attack": 0, "decay": 0, "sustain": 1, "release": 0.03}
|
|
272
|
+
},
|
|
273
|
+
"sounds": {
|
|
274
|
+
"coin": {"patch": {"wave": "square", "filter": "off", "attack": 0, "decay": 0.3, "sustain": 0, "release": 0.2}, "notes": [[0, 83, 0.07, 0.8], [0.07, 88, 0.4, 0.8]]},
|
|
275
|
+
"jump": {"patch": {"wave": "square", "width": 0.3, "filter": "off", "attack": 0, "decay": 0.25, "sustain": 0, "release": 0.05}, "notes": [[0, 57, 0.22, 0.8, 78, 0.2]]},
|
|
276
|
+
"laser": {"patch": {"wave": "saw", "cutoff": 6000, "attack": 0, "decay": 0.24, "sustain": 0, "release": 0.05}, "notes": [[0, 98, 0.24, 0.8, 58, 0.22]]},
|
|
277
|
+
"explosion": {"patch": {"wave": "sine", "level1": 0.9, "noise": 1, "cutoff": 180, "sweep": 50, "sweepAttack": 0, "sweepDecay": 0.55, "sweepSustain": 0, "pitchSweep": 24, "pitchSweepDecay": 0.25, "pitchSweepAttack": 0, "distortion": 0.5, "attack": 0, "decay": 0.9, "sustain": 0, "release": 0.3}, "notes": [[0, 30, 0.9, 1]]},
|
|
278
|
+
"hit": {"patch": {"wave": "square", "noise.type": "lfsr", "filter": "off", "attack": 0, "decay": 0.12, "sustain": 0, "release": 0.05, "macro": [{"wave": "noise", "pitch": 12}, {"wave": "square", "pitch": 0}, {"wave": "square", "pitch": -5}, {"wave": "noise", "pitch": 4, "volume": 0.7}, {"wave": "noise", "pitch": 0, "volume": 0.5}, {"wave": "noise", "pitch": -4, "volume": 0.3}]}, "notes": [[0, 50, 0.12, 0.9]]},
|
|
279
|
+
"shoot": {"patch": {"wave": "pulse", "width": 0.25, "filter": "off", "attack": 0, "decay": 0.16, "sustain": 0, "release": 0.03, "macroRate": 60, "macro": [{"wave": "noise", "pitch": 30}, {"wave": "pulse", "pitch": 19}, {"pitch": 14}, {"pitch": 9}, {"pitch": 4}, {"pitch": 0}, {"pitch": -5, "volume": 0.8}, {"pitch": -10, "volume": 0.6}, {"pitch": -15, "volume": 0.4}]}, "notes": [[0, 76, 0.16, 0.8]]},
|
|
280
|
+
"powerup": {"patch": {"wave": "square", "filter": "off", "attack": 0, "decay": 0.2, "sustain": 0.5, "release": 0.15, "macroRate": 16, "macro": {"steps": [{"pitch": 0}, {"pitch": 4}, {"pitch": 7}, {"pitch": 12}, {"pitch": 16}, {"pitch": 19}, {"pitch": 24}, {"pitch": 19}], "loop": [6, 8]}}, "notes": [[0, 60, 0.6, 0.7]]},
|
|
281
|
+
"blip": {"patch": {"wave": "square", "filter": "off", "attack": 0, "decay": 0.06, "sustain": 0, "release": 0.02}, "notes": [[0, 81, 0.06, 0.7]]},
|
|
282
|
+
"click": {"patch": {"wave": "sine", "noise": 0.4, "filter": "off", "attack": 0, "decay": 0.015, "sustain": 0, "release": 0.01}, "notes": [[0, 96, 0.02, 0.8]]},
|
|
283
|
+
"zap": {"patch": {"wave": "saw", "cutoff": 5000, "vibrato": 7, "vibratoRate": 32, "attack": 0, "decay": 0.3, "sustain": 0, "release": 0.05}, "notes": [[0, 84, 0.3, 0.8, 55, 0.3]]},
|
|
284
|
+
"bounce": {"patch": {"wave": "sine", "filter": "off", "attack": 0, "decay": 0.14, "sustain": 0, "release": 0.04}, "notes": [[0, 55, 0.14, 0.9, 69, 0.07]]},
|
|
285
|
+
"pop": {"patch": {"wave": "sine", "filter": "off", "attack": 0, "decay": 0.07, "sustain": 0, "release": 0.02}, "notes": [[0, 70, 0.07, 0.9, 86, 0.04]]},
|
|
286
|
+
"win": {"patch": {"wave": "square", "wave2": "square", "level2": 0.5, "pitch2": 12, "filter": "off", "attack": 0, "decay": 0.3, "sustain": 0.4, "release": 0.2, "echo": 0.2}, "notes": [[0, 72, 0.1, 0.7], [0.11, 76, 0.1, 0.7], [0.22, 79, 0.1, 0.7], [0.33, 84, 0.45, 0.8]]},
|
|
287
|
+
"lose": {"patch": {"wave": "triangle", "wave2": "square", "level2": 0.3, "cutoff": 2500, "attack": 0, "decay": 0.3, "sustain": 0.5, "release": 0.2}, "notes": [[0, 67, 0.16, 0.8], [0.17, 63, 0.16, 0.8], [0.34, 60, 0.16, 0.8], [0.51, 55, 0.5, 0.8, 43, 0.5]]}
|
|
288
|
+
},
|
|
289
|
+
"chords": {"5": [0, 7], "6": [0, 4, 7, 9], "7": [0, 4, 7, 10], "9": [0, 4, 7, 10, 14], "major": [0, 4, 7], "maj": [0, 4, 7], "minor": [0, 3, 7], "min": [0, 3, 7], "m": [0, 3, 7], "dim": [0, 3, 6], "aug": [0, 4, 8], "sus2": [0, 2, 7], "sus4": [0, 5, 7], "sus": [0, 5, 7], "power": [0, 7], "octave": [0, 12], "m6": [0, 3, 7, 9], "dom7": [0, 4, 7, 10], "maj7": [0, 4, 7, 11], "m7": [0, 3, 7, 10], "min7": [0, 3, 7, 10], "dim7": [0, 3, 6, 9], "m7b5": [0, 3, 6, 10], "mmaj7": [0, 3, 7, 11], "add9": [0, 4, 7, 14], "maj9": [0, 4, 7, 11, 14], "m9": [0, 3, 7, 10, 14]},
|
|
290
|
+
"orders": {"up": 0, "down": 1, "updown": 2, "written": 3, "aswritten": 3, "given": 3, "random": 4, "updown2": 5, "together": 6, "stab": 6},
|
|
291
|
+
"macroWaves": {"sine": 0, "triangle": 1, "saw": 2, "pulse": 3, "square": 3, "noise": 4},
|
|
292
|
+
"lengths": {"whole": 4, "half": 2, "quarter": 1, "eighth": 0.5, "sixteenth": 0.25, "thirtysecond": 0.125},
|
|
293
|
+
"lengthLetters": {"w": 4, "h": 2, "q": 1, "e": 0.5, "s": 0.25, "t": 0.125},
|
|
294
|
+
"midi": {"types": ["noteOff", "noteOn", "notePressure", "control", "program", "pressure", "bend", "sysex", "timeCode", "songPosition", "songSelect", "tuneRequest", "clock", "start", "continue", "stop", "sensing", "reset", "other", "devices", "tempo", "learned", "set"], "does": ["devices", "listen", "input", "output", "send", "play", "through", "map", "learn", "unmap", "clock", "stop"], "functions": ["onMidi", "onMidiNote", "onMidiNoteOff", "onMidiControl", "onMidiDevices", "useMidiHooks", "sendMidi", "sendMidiNote", "sendMidiNoteOff", "sendMidiControl", "sendMidiBend", "sendMidiProgram", "sendMidiClock", "midiDevices", "useMidiInput", "useMidiOutput", "midiInput", "midiOutput", "receiveMidi", "stopMidi"], "synthFunctions": ["playFromMidi", "sendToMidi", "mapMidi", "learn", "unmapMidi", "midiInput", "midiOutput"], "choices": {"settings": ["device", "channel"], "all": "all", "none": "no devices", "unavailable": "no MIDI here"}},
|
|
295
|
+
"voices": {"robot": {"help": "A machine: the voice multiplied by a 50 Hz wave, ringing in a metal pipe.", "values": {"fx.ring": 1, "fx.ring_freq": 50, "fx.comb": 0.45, "fx.comb_freq": 110, "fx.comb_feedback": 0.8, "fx.bits": 10}}, "chipmunk": {"help": "Small and squeaky: everything seven semitones higher.", "values": {"fx.pitch": 7}}, "monster": {"help": "Huge and deep: seven semitones lower, with a growl and a little room.", "values": {"fx.pitch": -7, "fx.drive": 0.25, "send.reverb": 0.3, "reverb.size": 0.7, "reverb.damping": 0.6}}, "cave": {"help": "Far inside a cave: long echoes and an enormous, dark space.", "values": {"send.delay": 0.5, "send.reverb": 0.6, "delay.time": 0.38, "delay.feedback": 0.55, "delay.tone": 2500, "reverb.type": 3, "reverb.size": 0.4, "reverb.damping": 0.55}}, "telephone": {"help": "Down an old phone line: only the middle of the voice, coarse and a little crushed.", "values": {"filter.type": 3, "filter.slope": 1, "filter.cutoff": 1200, "filter.resonance": 0.2, "fx.drive": 0.2, "fx.bits": 8, "fx.downsample": 6}}, "alien": {"help": "Not from here: higher, multiplied by a 180 Hz wave, with a slow metallic sweep.", "values": {"fx.pitch": 4, "fx.ring": 0.8, "fx.ring_freq": 180, "fx.comb": 0.4, "fx.comb_freq": 600, "fx.comb_feedback": 0.75, "fx.comb_rate": 0.4, "fx.comb_depth": 0.5, "send.chorus": 0.4}}, "whisper": {"help": "Hoarse and breathy: the voice's pitch blurred into noise, with its low end taken off.", "values": {"filter.type": 2, "filter.cutoff": 500, "filter.resonance": 0, "fx.ring": 1, "fx.ring_wave": 1, "fx.ring_freq": 350}}},
|
|
296
|
+
"buffer": {"functions": ["setSoundBuffer", "soundBuffer"], "words": {"small": 1024, "medium": 2048, "large": 4096, "auto": "auto"}, "least": 256, "most": 16384, "event": "buffer", "reads": ["frames", "delay", "thread", "underruns", "setting"], "threads": ["audio", "page"]},
|
|
297
|
+
"zones": {"functions": ["loadSamples"], "synthFunctions": ["setSamples", "zones"], "words": ["file", "note", "from", "to", "loud", "tune", "volume", "pan", "release", "loop", "oneShot", "asRecorded", "start"], "options": ["kit", "crossfade", "loop", "oneShot"], "most": 512, "layers": 4},
|
|
298
|
+
"song": {"does": ["play", "resume", "pause", "stop", "seek", "volume", "repeat", "mute", "solo", "loopPattern", "replace", "mix", "loop"], "functions": ["stop", "pause", "resume", "seek", "mute", "unmute", "solo", "loopPattern", "replace", "loop", "channel"], "channel": {"reads": ["samples", "level", "notes"], "sets": {"volume": [0, 2], "pan": [-1, 1], "reverb": [0, 1], "echo": [0, 1]}}, "keys": ["mixer", "effects", "loop"]},
|
|
299
|
+
"microphone": {"names": {"pitch": "pitchShift", "ring": "ringMod", "gain": "micGain", "gate": "micGate", "gaterelease": "micGateRelease"}, "gate": -50, "states": ["asking", "on", "refused", "none", "busy", "unavailable", "off"], "functions": ["useMicrophone", "stopMicrophone", "recordSound", "startRecording", "stopRecording", "recordOutput", "saveSound", "receiveSoundEvents", "voiceNames"], "micFunctions": ["listen", "mute", "record", "whenReady", "close"], "takeFunctions": ["onDone", "whenDone", "stopRecording"], "events": ["microphone", "recorded", "saved"], "longestRecording": 120, "usualRecording": 30}
|
|
300
|
+
}
|
|
301
|
+
''')
|
|
302
|
+
# NAMES:END
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
# ---------------------------------------------------------------- names, notes and mistakes
|
|
306
|
+
|
|
307
|
+
def _key(name):
|
|
308
|
+
"""A name without regard to capitals, `_`, `.`, `-` or spaces: vibrato_rate, vibratoRate and "vibrato.rate" are one."""
|
|
309
|
+
return re.sub(r"[_.\s-]", "", str(name).lower())
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def _snake(name):
|
|
313
|
+
"""A name in Python's spelling: vibratoRate and "filter.cutoff" are vibrato_rate and filter_cutoff."""
|
|
314
|
+
return re.sub(r"[.\s-]+", "_", re.sub(r"(?<=[a-z0-9])([A-Z])", r"_\1", name)).lower()
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def _shown(v):
|
|
318
|
+
if isinstance(v, str):
|
|
319
|
+
return json.dumps(v, ensure_ascii=False)
|
|
320
|
+
if isinstance(v, (list, tuple)):
|
|
321
|
+
return "[" + ", ".join(_shown(x) for x in v) + "]"
|
|
322
|
+
return str(v)
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def _is_number(v):
|
|
326
|
+
return isinstance(v, (int, float)) and not isinstance(v, bool) and math.isfinite(v)
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def _clamp(v, low, high):
|
|
330
|
+
return min(high, max(low, v))
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
_SETTINGS = {} # every way a setting can be written, as _key(name) -> its entry
|
|
334
|
+
_SHOWN = [] # the names to show a person: the friendly ones, then the engine's own
|
|
335
|
+
_FRIENDLY = [] # the friendly ones alone
|
|
336
|
+
_PRESETS = {} # _key(name) -> the name of a ready-made synth
|
|
337
|
+
_SOUNDS = {} # _key(name) -> the name of a ready-made sound
|
|
338
|
+
_DEFAULTS = {} # engine id -> the engine's own value for a setting
|
|
339
|
+
_BY_ID = {} # engine id -> the engine's name for a setting
|
|
340
|
+
_VOICES = {"rows": {}, "all": []} # the voice changers: name -> {engine id: value}, and every id any of them sets
|
|
341
|
+
_MIC_NAMES = {} # a setting as the microphone says it (_key) -> the name every synth knows it by
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def _read_names():
|
|
345
|
+
by_name = {}
|
|
346
|
+
for s in _NAMES.get("settings", ()):
|
|
347
|
+
entry = dict(s, scale=1)
|
|
348
|
+
by_name[s["name"]] = entry
|
|
349
|
+
_SETTINGS[_key(s["name"])] = entry
|
|
350
|
+
friendly = []
|
|
351
|
+
for alias, to in _NAMES.get("names", {}).items():
|
|
352
|
+
entry = by_name.get(to["setting"])
|
|
353
|
+
if entry is None:
|
|
354
|
+
continue
|
|
355
|
+
_SETTINGS[_key(alias)] = entry if to.get("scale", 1) == 1 else dict(entry, scale=to["scale"])
|
|
356
|
+
friendly.append(_snake(alias))
|
|
357
|
+
_FRIENDLY[:] = friendly
|
|
358
|
+
_SHOWN[:] = friendly + [n for n in (_snake(name) for name in by_name) if n not in friendly]
|
|
359
|
+
for name in _NAMES.get("synths", {}):
|
|
360
|
+
_PRESETS[_key(name)] = name
|
|
361
|
+
for name in _NAMES.get("sounds", {}):
|
|
362
|
+
_SOUNDS[_key(name)] = name
|
|
363
|
+
for s in _NAMES.get("settings", ()):
|
|
364
|
+
_DEFAULTS[s["id"]] = s["default"]
|
|
365
|
+
_BY_ID[s["id"]] = s["name"]
|
|
366
|
+
every = []
|
|
367
|
+
for name, voice in _NAMES.get("voices", {}).items():
|
|
368
|
+
row = _VOICES["rows"][name] = _patch_of(voice.get("values"), name, False)
|
|
369
|
+
every.extend(pid for pid in row if pid not in every)
|
|
370
|
+
_VOICES["all"] = every
|
|
371
|
+
_MIC_NAMES.update(_NAMES.get("microphone", {}).get("names", {}))
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def _setting(name):
|
|
375
|
+
return _SETTINGS.get(_key(name)) if isinstance(name, str) else None
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def _nearest(word, names):
|
|
379
|
+
"""The name in `names` nearest to `word`, when one is near enough to be what was meant."""
|
|
380
|
+
a = _key(word)
|
|
381
|
+
best = None
|
|
382
|
+
# A short word has to be very near: one slip in four letters, two in eight, three in anything longer.
|
|
383
|
+
best_cost = (1 if len(a) <= 4 else 2 if len(a) <= 8 else 3) + 1
|
|
384
|
+
for name in names:
|
|
385
|
+
b = _key(name)
|
|
386
|
+
if b.startswith(a) and len(a) >= 4:
|
|
387
|
+
return name
|
|
388
|
+
row = list(range(len(b) + 1))
|
|
389
|
+
for i in range(1, len(a) + 1):
|
|
390
|
+
last, row[0] = row[0], i
|
|
391
|
+
for j in range(1, len(b) + 1):
|
|
392
|
+
keep = row[j]
|
|
393
|
+
row[j] = min(row[j] + 1, row[j - 1] + 1, last + (0 if a[i - 1] == b[j - 1] else 1))
|
|
394
|
+
last = keep
|
|
395
|
+
if row[-1] < best_cost:
|
|
396
|
+
best_cost, best = row[-1], name
|
|
397
|
+
return best
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
def _meant(word, names):
|
|
401
|
+
near = _nearest(word, names) if isinstance(word, str) else None
|
|
402
|
+
return " Did you mean %s?" % _shown(near) if near else ""
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def _setting_value(entry, value, said):
|
|
406
|
+
"""A setting's value as the engine's number, from what a person wrote: a number, a choice by name ("saw"), True or False."""
|
|
407
|
+
kind, choices = entry["kind"], entry.get("choices") or []
|
|
408
|
+
if kind == "choice":
|
|
409
|
+
if isinstance(value, str):
|
|
410
|
+
word = value.lower().strip()
|
|
411
|
+
for w in (word, _NAMES.get("choiceWords", {}).get(word), word + "db"):
|
|
412
|
+
if w in choices:
|
|
413
|
+
return choices.index(w)
|
|
414
|
+
words = [w for w, to in _NAMES.get("choiceWords", {}).items() if to in choices]
|
|
415
|
+
raise ValueError("%s: %s is not one of its choices.%s They are: %s"
|
|
416
|
+
% (said, _shown(value), _meant(value, choices + words), ", ".join(choices)))
|
|
417
|
+
if _is_number(value) and ("%gdb" % value) in choices:
|
|
418
|
+
return choices.index("%gdb" % value)
|
|
419
|
+
if kind == "bool":
|
|
420
|
+
if value is True or value == "on":
|
|
421
|
+
return 1
|
|
422
|
+
if value is False or value == "off":
|
|
423
|
+
return 0
|
|
424
|
+
if not _is_number(value):
|
|
425
|
+
wants = ("one of " + ", ".join(choices) if kind == "choice" else "True or False" if kind == "bool"
|
|
426
|
+
else "a number from %g to %g" % (entry["min"] / entry["scale"], entry["max"] / entry["scale"]))
|
|
427
|
+
raise ValueError("%s needs %s, not %s" % (said, wants, _shown(value)))
|
|
428
|
+
return _clamp(value * entry["scale"], entry["min"], entry["max"])
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
def _patch_of(given, said, strict=True, into=None):
|
|
432
|
+
"""Settings as a person wrote them -> {engine id: number}, added to `into`. A mistake is said in plain words."""
|
|
433
|
+
into = {} if into is None else into
|
|
434
|
+
for name, value in (given or {}).items():
|
|
435
|
+
if name in ("note", "preset", "macro"):
|
|
436
|
+
continue
|
|
437
|
+
entry = _setting(name)
|
|
438
|
+
if entry is None:
|
|
439
|
+
if strict:
|
|
440
|
+
raise ValueError("%s: there is no setting called %s.%s" % (said, _shown(name), _meant(name, _SHOWN)))
|
|
441
|
+
continue
|
|
442
|
+
if entry["name"].endswith(".sample"):
|
|
443
|
+
into[entry["id"]] = 0
|
|
444
|
+
continue
|
|
445
|
+
try:
|
|
446
|
+
into[entry["id"]] = _setting_value(entry, value, "%s: %s" % (said, name))
|
|
447
|
+
# "square" is the pulse wave at an even width (the width is four ids after the wave).
|
|
448
|
+
if isinstance(value, str) and value.lower().strip() == "square" and "pulse" in (entry.get("choices") or ()):
|
|
449
|
+
into[entry["id"] + 4] = 0.5
|
|
450
|
+
except ValueError:
|
|
451
|
+
if strict:
|
|
452
|
+
raise
|
|
453
|
+
return into
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
_NO_VOICE = ("none", "plain", "off", "normal", "")
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
def _voice_name(name):
|
|
460
|
+
"""A voice's name as written -> its name here, "none" for the plain voice, or None for a name that is not a voice."""
|
|
461
|
+
if name is None or name is False:
|
|
462
|
+
return "none"
|
|
463
|
+
k = _key(name)
|
|
464
|
+
return "none" if k in _NO_VOICE else k if k in _VOICES["rows"] else None
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
def _wear_voice(worn, patch, name):
|
|
468
|
+
"""PUT A VOICE ON, as synths.js does (its wearVoice; the rule is in docs/commands.md under `set`). `worn` is
|
|
469
|
+
what is remembered between voices ({"voice", "under"}) and `patch` the settings by engine id. The first voice
|
|
470
|
+
keeps what the settings it may touch were; every voice after starts by putting those back, and "none" is how
|
|
471
|
+
things were before any. A setting of the whole engine (the echo's time, the reverb's size) is touched only if
|
|
472
|
+
this voice or the last names it. The player does the same from the one word sent, so only `patch` changes here."""
|
|
473
|
+
rows, every = _VOICES["rows"], _VOICES["all"]
|
|
474
|
+
row, last = rows.get(name, {}), rows.get(worn["voice"], {})
|
|
475
|
+
if worn["under"] is None:
|
|
476
|
+
worn["under"] = {pid: patch[pid] for pid in every if pid in patch}
|
|
477
|
+
for pid in every:
|
|
478
|
+
if pid >= 200 and pid not in row and pid not in last:
|
|
479
|
+
continue
|
|
480
|
+
patch[pid] = row[pid] if pid in row else worn["under"].get(pid, _DEFAULTS.get(pid))
|
|
481
|
+
worn["voice"] = name
|
|
482
|
+
if name == "none":
|
|
483
|
+
worn["under"] = None
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
def _sendable(given):
|
|
487
|
+
"""Settings as they are sent: as written, with a sample given as its id."""
|
|
488
|
+
return {name: (value._sample or value._id) if isinstance(value, (Synth, _Slot)) else value for name, value in given.items()}
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
class _Slot:
|
|
492
|
+
"""A sample by its id alone, among a synth's settings: one loaded from a file named there."""
|
|
493
|
+
|
|
494
|
+
def __init__(self, id):
|
|
495
|
+
self._sample = self._id = id
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
def _sample_file(s, path, note=None, loop=None, zone=False):
|
|
499
|
+
"""A sound file as a sample: the command that loads it is written, and the id it goes by given back. `zone`: it
|
|
500
|
+
is one recording of a sample map, so nothing is said of it alone if it cannot be loaded."""
|
|
501
|
+
sid = s.id("w")
|
|
502
|
+
c = {"op": "sample", "id": sid, "name": path}
|
|
503
|
+
if zone:
|
|
504
|
+
c["zone"] = True
|
|
505
|
+
if note is not None:
|
|
506
|
+
c["note"] = note_number(note)
|
|
507
|
+
if loop:
|
|
508
|
+
c["loop"] = list(loop) if isinstance(loop, (list, tuple)) else True
|
|
509
|
+
data = _file(path)
|
|
510
|
+
if data is not None:
|
|
511
|
+
c["bytes"] = data # Python has the file (it may be on another computer than the page)
|
|
512
|
+
else:
|
|
513
|
+
c["url"] = path # the page fetches it from beside the scene
|
|
514
|
+
s.command(c)
|
|
515
|
+
return sid
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
def _with_samples(s, given, patch, said, note=None, loop=None):
|
|
519
|
+
"""SAMPLES AMONG A SYNTH'S SETTINGS, as synths.js has them. `sample` is a sample from load_sample, a recording,
|
|
520
|
+
or the name of a sound file (which is loaded); giving one turns that oscillator's wave to "sample" unless a
|
|
521
|
+
wave is said too, so lead.sample = load_sample("piano.wav") is all it takes to put a recording under a synth's
|
|
522
|
+
filter and envelope. The wave "sample" with no sample to play is not set: it is said once, and the wave stays."""
|
|
523
|
+
out = dict(given)
|
|
524
|
+
|
|
525
|
+
def names(which):
|
|
526
|
+
return [n for n in out if (_setting(n) or {}).get("name") == which]
|
|
527
|
+
for name, value in given.items():
|
|
528
|
+
entry = _setting(name)
|
|
529
|
+
if entry is None or not entry["name"].endswith(".sample"):
|
|
530
|
+
continue
|
|
531
|
+
if isinstance(value, str) and value.strip():
|
|
532
|
+
out[name] = _Slot(_sample_file(s, value, note, loop))
|
|
533
|
+
elif not (isinstance(value, (Synth, _Slot)) and value._sample):
|
|
534
|
+
raise ValueError('%s: sample needs a sample from load_sample, a recording, or the name of a sound file, as in '
|
|
535
|
+
'lead.sample = load_sample("piano.wav"), not %s' % (said, _shown(value)))
|
|
536
|
+
wave = entry["name"].replace("sample", "wave")
|
|
537
|
+
if not names(wave):
|
|
538
|
+
out[wave] = "sample"
|
|
539
|
+
for which in ("osc1", "osc2"):
|
|
540
|
+
for name in names(which + ".wave"):
|
|
541
|
+
value = out[name]
|
|
542
|
+
if not (value == 4 and not isinstance(value, bool) or (isinstance(value, str) and value.lower().strip() == "sample")):
|
|
543
|
+
continue
|
|
544
|
+
if _setting(which + ".sample")["id"] in patch or names(which + ".sample"):
|
|
545
|
+
continue
|
|
546
|
+
wave = _setting(which + ".wave")
|
|
547
|
+
now = wave["choices"][int(patch.get(wave["id"], wave["default"]))]
|
|
548
|
+
_tell('%s = "sample" needs a sample to play, and this synth has none, so its wave stays %s. Give it one first: '
|
|
549
|
+
'lead.sample = load_sample("piano.wav"), or make_synth(sample="piano.wav").' % (name, _shown(now)))
|
|
550
|
+
del out[name]
|
|
551
|
+
return out
|
|
552
|
+
|
|
553
|
+
|
|
554
|
+
_SEMITONES = {"c": 0, "d": 2, "e": 4, "f": 5, "g": 7, "a": 9, "b": 11}
|
|
555
|
+
_NOTE = re.compile(r"^\s*([a-gA-G])([#b♯♭]*)(-?\d+)?\s*$")
|
|
556
|
+
|
|
557
|
+
|
|
558
|
+
def note_number(note):
|
|
559
|
+
"""A note as a number: "C4" is 60, "A4" is 69, "F#3" and "Bb5" work, and a number is itself."""
|
|
560
|
+
if _is_number(note):
|
|
561
|
+
return note
|
|
562
|
+
if isinstance(note, str):
|
|
563
|
+
m = _NOTE.match(note)
|
|
564
|
+
if m:
|
|
565
|
+
n = _SEMITONES[m.group(1).lower()]
|
|
566
|
+
for c in m.group(2):
|
|
567
|
+
n += 1 if c in "#♯" else -1
|
|
568
|
+
return 12 * ((4 if m.group(3) is None else int(m.group(3))) + 1) + n
|
|
569
|
+
try:
|
|
570
|
+
n = float(note)
|
|
571
|
+
if math.isfinite(n):
|
|
572
|
+
return int(n) if n == int(n) else n
|
|
573
|
+
except ValueError:
|
|
574
|
+
pass
|
|
575
|
+
raise ValueError('%s is not a note. Write a note as a name like "C4", "F#3" or "Bb5", or as a number, where 60 is middle C'
|
|
576
|
+
% _shown(note))
|
|
577
|
+
|
|
578
|
+
|
|
579
|
+
def _note_numbers(note):
|
|
580
|
+
return [note_number(n) for n in note] if isinstance(note, (list, tuple)) else [note_number(note)]
|
|
581
|
+
|
|
582
|
+
|
|
583
|
+
_KINDS_SHOWN = ["major", "minor", "dim", "aug", "sus2", "sus4", "5", "6", "m6", "7", "maj7", "m7", "dim7", "m7b5",
|
|
584
|
+
"mmaj7", "add9", "9", "maj9", "m9", "octave"]
|
|
585
|
+
_ROOT = re.compile(r"^\s*([a-gA-G][#b\u266f\u266d]*(?:-?\d+)?)\s*(.*?)\s*$")
|
|
586
|
+
|
|
587
|
+
|
|
588
|
+
def _chord_kind(kind):
|
|
589
|
+
"""The kind of chord a person named: "m7", "Maj7", "M7" (a capital M is major), "minor"."""
|
|
590
|
+
k = str(kind).strip()
|
|
591
|
+
if re.match(r"^M(?!aj|in|AJ|IN)", k):
|
|
592
|
+
k = "maj" + k[1:]
|
|
593
|
+
k = re.sub(r"[\s_-]", "", k.lower())
|
|
594
|
+
if k in ("maj", ""):
|
|
595
|
+
k = "major"
|
|
596
|
+
return _NAMES.get("chords", {}).get(k)
|
|
597
|
+
|
|
598
|
+
|
|
599
|
+
def chord_notes(name, kind=None):
|
|
600
|
+
"""The notes of a chord, as numbers: chord_notes("Cm7") is [60, 63, 67, 70], chord_notes("C3", "minor") is [48, 51, 55].
|
|
601
|
+
A letter followed by a number is always a note, so "C7" is the note C in octave 7: a seventh chord is "Cdom7",
|
|
602
|
+
or ("C", "7"). With no kind at all, it is a major chord."""
|
|
603
|
+
if isinstance(name, (list, tuple)):
|
|
604
|
+
return [note_number(n) for n in name]
|
|
605
|
+
root = name
|
|
606
|
+
if kind is None and isinstance(name, str):
|
|
607
|
+
m = _ROOT.match(name)
|
|
608
|
+
if m:
|
|
609
|
+
root, kind = m.group(1), m.group(2)
|
|
610
|
+
base = note_number(root)
|
|
611
|
+
steps = _chord_kind("" if kind is None else kind)
|
|
612
|
+
if steps is None:
|
|
613
|
+
raise ValueError("%s is not a kind of chord.%s The kinds are: %s"
|
|
614
|
+
% (_shown(kind), _meant(str(kind), list(_NAMES.get("chords", {}))), ", ".join(_KINDS_SHOWN)))
|
|
615
|
+
return [base + s for s in steps]
|
|
616
|
+
|
|
617
|
+
|
|
618
|
+
def _notes_of(what):
|
|
619
|
+
"""What play() is given -> a list of note numbers: a note, a list of notes, or a chord by name ("Cm7")."""
|
|
620
|
+
if isinstance(what, (list, tuple)):
|
|
621
|
+
return [note_number(n) for n in what]
|
|
622
|
+
if isinstance(what, str) and not _NOTE.match(what) and re.match(r"^\s*[a-gA-G]", what):
|
|
623
|
+
m = _ROOT.match(what)
|
|
624
|
+
if m and _chord_kind(m.group(2)) is not None:
|
|
625
|
+
return chord_notes(what)
|
|
626
|
+
if m:
|
|
627
|
+
near = _nearest(m.group(2), list(_NAMES.get("chords", {})))
|
|
628
|
+
raise ValueError('%s is not a note or a chord.%s A note is written "C4" or "F#3", and a chord "Cm", "Cmaj7" or "Csus4"'
|
|
629
|
+
% (_shown(what), " Did you mean %s?" % _shown(m.group(1) + near) if near else ""))
|
|
630
|
+
return [note_number(what)]
|
|
631
|
+
|
|
632
|
+
|
|
633
|
+
def _order_of(order, said):
|
|
634
|
+
"""The order of an arpeggio as the engine's number, from "up", "down", "up-down", "up-down2", "as written", "random" or "together"."""
|
|
635
|
+
if order is None:
|
|
636
|
+
return 0
|
|
637
|
+
if _is_number(order) and 0 <= order <= 6:
|
|
638
|
+
return int(order)
|
|
639
|
+
found = _NAMES.get("orders", {}).get(_key(order))
|
|
640
|
+
if found is None:
|
|
641
|
+
orders = ["up", "down", "up-down", "up-down2", "as written", "random", "together"]
|
|
642
|
+
raise ValueError("%s: %s is not an order an arpeggio can go in.%s The orders are: up, down, up-down, up-down2 (the top "
|
|
643
|
+
"and bottom notes twice), as written, random, together" % (said, _shown(order), _meant(str(order), orders)))
|
|
644
|
+
return found
|
|
645
|
+
|
|
646
|
+
|
|
647
|
+
def _chord_of(chord, kind, said):
|
|
648
|
+
"""The chord an arpeggio is given -> its notes as numbers: a list of notes, a chord by name, or a root and a kind."""
|
|
649
|
+
if chord is None:
|
|
650
|
+
raise ValueError('%s needs a chord, as in %s("Cm7"), %s("C4", "minor") or %s(["C4", "E4", "G4"])' % (said, said, said, said))
|
|
651
|
+
notes = chord_notes(chord, kind)
|
|
652
|
+
if len(notes) > 8:
|
|
653
|
+
raise ValueError("%s: an arpeggio has room for 8 notes, and this chord has %d" % (said, len(notes)))
|
|
654
|
+
return notes
|
|
655
|
+
|
|
656
|
+
|
|
657
|
+
# ---------------------------------------------------------------- patterns, and notes whose pitch moves
|
|
658
|
+
|
|
659
|
+
_NO_PATTERN = ("", "off", "none")
|
|
660
|
+
_PATTERN_WORDS = {"arppattern": "arpPattern", "trancegate": "tranceGate"}
|
|
661
|
+
|
|
662
|
+
|
|
663
|
+
def _pattern_word(name):
|
|
664
|
+
"""Is this the name of a pattern setting (arp_pattern, arpPattern, trance_gate, tranceGate)? The name it is sent by."""
|
|
665
|
+
return _PATTERN_WORDS.get(_key(name)) if isinstance(name, str) else None
|
|
666
|
+
|
|
667
|
+
|
|
668
|
+
def _arp_pattern_text(given, said="arp_pattern"):
|
|
669
|
+
"""A PATTERN AS TEXT, which is how it is kept, sent and read back, from what was given: the text itself, the name
|
|
670
|
+
of a built-in one, or a list of steps. None (and "off", "none", "", False) is no pattern. An arpeggio's steps
|
|
671
|
+
have spaces between them ("1 - 3 2 _ 1 3 5^"); in a list a step is that text or a number, and 0 or None a rest."""
|
|
672
|
+
if given is None or given is False:
|
|
673
|
+
return None
|
|
674
|
+
if isinstance(given, (list, tuple)):
|
|
675
|
+
given = " ".join("-" if step is None or (_is_number(step) and step == 0) else
|
|
676
|
+
str(int(step) if _is_number(step) and step == int(step) else step).strip() for step in given)
|
|
677
|
+
text = " ".join(given.split()) if isinstance(given, str) else None
|
|
678
|
+
names = _NAMES["arp"]["patternSteps"]
|
|
679
|
+
if text is not None and text.lower() in _NO_PATTERN:
|
|
680
|
+
return None
|
|
681
|
+
if text is not None and text.lower() in names:
|
|
682
|
+
return names[text.lower()]
|
|
683
|
+
steps = [] if text is None else text.split(" ")
|
|
684
|
+
if not steps or len(steps) > 32 or not all(_ARP_STEP.match(t) for t in steps):
|
|
685
|
+
raise ValueError("%s: a pattern is up to 32 steps with spaces between them, as in \"1 - 3 2 _ 1 3 5^\" (a number is that "
|
|
686
|
+
"note of the chord, - a rest, _ holds the last note, ' an octave up, ^ an accent), a list of them, or "
|
|
687
|
+
"one by name: %s; not %s" % (said, ", ".join(names), _shown(given)))
|
|
688
|
+
return text
|
|
689
|
+
|
|
690
|
+
|
|
691
|
+
def _gate_pattern_text(given, said="trance_gate"):
|
|
692
|
+
"""The trance gate's pattern as text: one character a step ("x.x.xx.x": x open, . closed, 0 to 9 a level between),
|
|
693
|
+
from that text, a list of levels from 0 to 1, or a name. None (and "off", "none", "", False) is no pattern."""
|
|
694
|
+
if given is None or given is False:
|
|
695
|
+
return None
|
|
696
|
+
if isinstance(given, (list, tuple)) and all(_is_number(v) and 0 <= v <= 1 for v in given):
|
|
697
|
+
given = "".join("x" if v >= 1 else "." if v <= 0 else str(int(math.floor(v * 9 + 0.5))) for v in given)
|
|
698
|
+
text = "".join(given.split()) if isinstance(given, str) else None
|
|
699
|
+
names = _NAMES["tranceGate"]["patterns"]
|
|
700
|
+
if text is not None and text.lower() in _NO_PATTERN:
|
|
701
|
+
return None
|
|
702
|
+
if text is not None and text.lower() in names:
|
|
703
|
+
return names[text.lower()]
|
|
704
|
+
if text is None or len(text) > 32 or not re.match(r"^[xX.0-9]+$", text):
|
|
705
|
+
raise ValueError("%s: a trance gate's pattern is up to 32 steps, one character each, as in \"x.x.xx.x\" (x is open, . is "
|
|
706
|
+
"closed, 0 to 9 a level between), a list of levels from 0 to 1, or one by name: %s; not %s"
|
|
707
|
+
% (said, ", ".join(names), _shown(given)))
|
|
708
|
+
return text.lower()
|
|
709
|
+
|
|
710
|
+
|
|
711
|
+
def _describe_pattern(which, text):
|
|
712
|
+
"""What a control is told a pattern is: its text now, the built-in ones by name, and the name of this one if it is one."""
|
|
713
|
+
table = _NAMES["arp"]["patternSteps"] if which == "arpPattern" else _NAMES["tranceGate"]["patterns"]
|
|
714
|
+
d = {"name": _snake(which), "kind": "pattern", "default": "", "value": text or "", "steps": 32, "patterns": dict(table)}
|
|
715
|
+
named = [name for name, steps in table.items() if text and steps == text]
|
|
716
|
+
if named:
|
|
717
|
+
d["named"] = named[0]
|
|
718
|
+
return d
|
|
719
|
+
|
|
720
|
+
|
|
721
|
+
def _moves_of(s, o, said, note=None):
|
|
722
|
+
"""HOW A NOTE'S PITCH MOVES, as a note command carries it (synths.js: movesOf), from what play() and start() were told:
|
|
723
|
+
glide the note slides to its pitch from the synth's last note: True (a sixteenth), seconds, or a note length
|
|
724
|
+
bend "scoop" (up into the note), "fall" (drooping away at its end), "dip" (down and back), or
|
|
725
|
+
{"to": semitones or a note, "over": seconds or a note length, "shape"}
|
|
726
|
+
slide a bend by so many semitones over a sixteenth: 12, or {"to", "over"}
|
|
727
|
+
vibrato True, or {"depth": semitones each way (0.3), "rate": times a second (6), "delay": before it begins ("eighth")}"""
|
|
728
|
+
out = {}
|
|
729
|
+
|
|
730
|
+
def time(value, what, usual):
|
|
731
|
+
if value is None or value is True:
|
|
732
|
+
return s.length(usual, what)
|
|
733
|
+
return s.length(value, what) if isinstance(value, str) else _amount(value, what, 0, 60)
|
|
734
|
+
glide, bend, slide, vibrato = o.get("glide"), o.get("bend"), o.get("slide"), o.get("vibrato")
|
|
735
|
+
if glide is not None and glide is not False:
|
|
736
|
+
out["glide"] = time(glide, "%s: glide" % said, "sixteenth")
|
|
737
|
+
if bend is not None and slide is not None:
|
|
738
|
+
raise ValueError("%s: a note has a bend or a slide, not both" % said)
|
|
739
|
+
given = bend if bend is not None else {"to": slide, "over": "sixteenth"} if _is_number(slide) else slide
|
|
740
|
+
if given is not None and given is not False:
|
|
741
|
+
b = {"shape": given} if isinstance(given, str) else given
|
|
742
|
+
shapes = _NAMES["moves"]["shapes"]
|
|
743
|
+
if not isinstance(b, dict) or not b or (b.get("shape") is not None and b["shape"] not in shapes) or (b.get("shape") is None and b.get("to") is None):
|
|
744
|
+
raise ValueError('%s: %s how far and how long, as in {"to": 2, "over": "eighth"}, not %s'
|
|
745
|
+
% (said, 'bend is "scoop", "fall", "dip", or' if bend is not None else "slide is a number of semitones, or", _shown(given)))
|
|
746
|
+
made = {}
|
|
747
|
+
if b.get("shape"):
|
|
748
|
+
made["shape"] = b["shape"]
|
|
749
|
+
if b.get("to") is not None:
|
|
750
|
+
if isinstance(b["to"], str): # a note by name is that far from the note being played
|
|
751
|
+
start = None if note is None else _notes_of(note)
|
|
752
|
+
if not start or len(start) != 1:
|
|
753
|
+
raise ValueError("%s: a bend to a note by name needs one note to bend from" % said)
|
|
754
|
+
made["to"] = note_number(b["to"]) - start[0]
|
|
755
|
+
else:
|
|
756
|
+
made["to"] = _amount(b["to"], "%s: bend to" % said, -96, 96)
|
|
757
|
+
made["over"] = time(b.get("over"), "%s: over" % said, "sixteenth" if b.get("shape") == "scoop" or bend is None else "eighth")
|
|
758
|
+
out["bend"] = made
|
|
759
|
+
if vibrato is not None and vibrato is not False:
|
|
760
|
+
v = {} if vibrato is True else vibrato
|
|
761
|
+
if not isinstance(v, dict):
|
|
762
|
+
raise ValueError('%s: vibrato is True, or its depth, rate and delay, as in {"depth": 0.5, "rate": 5}, not %s' % (said, _shown(vibrato)))
|
|
763
|
+
delay = v.get("delay")
|
|
764
|
+
out["vibrato"] = {"depth": 0.3 if v.get("depth") is None else _amount(v["depth"], "%s: vibrato depth" % said, 0, 12),
|
|
765
|
+
"rate": 6 if v.get("rate") is None else _amount(v["rate"], "%s: vibrato rate" % said, 0.1, 40),
|
|
766
|
+
"delay": s.length("eighth", said) if delay is None else s.length(delay, "%s: vibrato delay" % said)
|
|
767
|
+
if isinstance(delay, str) else _amount(delay, "%s: vibrato delay" % said, 0, 60)}
|
|
768
|
+
return out
|
|
769
|
+
|
|
770
|
+
|
|
771
|
+
# What an arpeggio may be told (synths.js: ARP_WORDS), kept between changes of its chord; and a step of a pattern written out.
|
|
772
|
+
_ARP_WORDS = ("speed", "step", "rate", "order", "style", "gate", "octaves", "swing", "accent", "accents", "pattern")
|
|
773
|
+
_ARP_STEP = re.compile(r"^(?:~?\d{1,2}[',]*\^?(?::\d)?|[-._])$")
|
|
774
|
+
|
|
775
|
+
|
|
776
|
+
def _arp_words(how):
|
|
777
|
+
"""The words of an arpeggio among what a call was given (taken out of it), without those left unsaid."""
|
|
778
|
+
return {w: v for w, v in ((w, how.pop(w, None)) for w in _ARP_WORDS) if v is not None}
|
|
779
|
+
|
|
780
|
+
|
|
781
|
+
def _arp_of(notes, root, o, said):
|
|
782
|
+
"""An arpeggio as a command carries it: semitones from the note the voice was started on, a rate and an order;
|
|
783
|
+
and, for one the engine's arpeggiator plays, its step in beats, style, gate, octaves, swing, accents or pattern."""
|
|
784
|
+
offsets = [n - root for n in notes]
|
|
785
|
+
if any(v != int(v) or not -128 <= v <= 127 for v in offsets):
|
|
786
|
+
raise ValueError("%s: the notes of an arpeggio have to be whole notes within ten octaves of the first one" % said)
|
|
787
|
+
speed = o.get("speed")
|
|
788
|
+
arp = {"offsets": [int(v) for v in offsets], "rate": 50 if speed is None else _amount(speed, "%s: speed" % said, 0.1, 1000),
|
|
789
|
+
"order": _order_of(o.get("order"), said)}
|
|
790
|
+
# Counted in beats: each step a note length ("sixteenth"), or so many beats, which follows the tempo.
|
|
791
|
+
step = o.get("step", o.get("rate"))
|
|
792
|
+
if step is not None:
|
|
793
|
+
if speed is not None:
|
|
794
|
+
raise ValueError('%s: say how fast one way: step (a note length, as in step="sixteenth") or speed (steps a second), '
|
|
795
|
+
"not both" % said)
|
|
796
|
+
arp["beats"] = _beats_long(step, "%s: step" % said) if isinstance(step, str) else _amount(step, "%s: step" % said, 1 / 64, 16)
|
|
797
|
+
del arp["rate"]
|
|
798
|
+
if o.get("style") is not None:
|
|
799
|
+
if o["style"] not in _NAMES["arp"]["styles"]:
|
|
800
|
+
raise ValueError('%s: style is "strike" (each step a new note) or "slide" (one voice whose pitch jumps), not %s'
|
|
801
|
+
% (said, _shown(o["style"])))
|
|
802
|
+
arp["style"] = o["style"]
|
|
803
|
+
if o.get("gate") is not None:
|
|
804
|
+
arp["gate"] = _amount(o["gate"], "%s: gate" % said, 0.05, 1)
|
|
805
|
+
if o.get("octaves") is not None:
|
|
806
|
+
arp["octaves"] = int(math.floor(_amount(o["octaves"], "%s: octaves" % said, 1, 4) + 0.5))
|
|
807
|
+
if o.get("swing") is not None:
|
|
808
|
+
arp["swing"] = _amount(o["swing"], "%s: swing" % said, 0, 0.75)
|
|
809
|
+
accent = o.get("accent", o.get("accents"))
|
|
810
|
+
if accent is not None:
|
|
811
|
+
if not isinstance(accent, (list, tuple)) or len(accent) > 8 or not all(_is_number(v) and 0 <= v <= 1 for v in accent):
|
|
812
|
+
raise ValueError("%s: accent is how hard each step is struck, up to eight numbers from 0 (a rest) to 1, as in "
|
|
813
|
+
"accent=[1, 0.5, 0.5, 0.5], not %s" % (said, _shown(accent)))
|
|
814
|
+
arp["accent"] = list(accent)
|
|
815
|
+
if o.get("pattern") is not None:
|
|
816
|
+
text = _arp_pattern_text(o["pattern"], "%s: pattern" % said)
|
|
817
|
+
arp["pattern"] = text or "" # "off" follows none, whatever the synth's own pattern is
|
|
818
|
+
return arp
|
|
819
|
+
|
|
820
|
+
|
|
821
|
+
def _check_macro(macro, said):
|
|
822
|
+
"""A step macro as a person wrote it: a list of steps, or {"steps", "loop", "slide", "release"}. A mistake is said here."""
|
|
823
|
+
if macro is None or macro is False:
|
|
824
|
+
return None
|
|
825
|
+
spec = {"steps": macro} if isinstance(macro, (list, tuple)) else macro
|
|
826
|
+
steps = spec.get("steps") if isinstance(spec, dict) else None
|
|
827
|
+
if not isinstance(steps, (list, tuple)) or not steps:
|
|
828
|
+
raise ValueError('%s: a macro needs a list of steps, as in [{"pitch": 0}, {"pitch": 4}, {"pitch": 7}], not %s' % (said, _shown(macro)))
|
|
829
|
+
if len(steps) > 64:
|
|
830
|
+
raise ValueError("%s: a macro has room for 64 steps, and this has %d" % (said, len(steps)))
|
|
831
|
+
waves = _NAMES.get("macroWaves", {})
|
|
832
|
+
for step in steps:
|
|
833
|
+
o = dict(zip(("wave", "pitch", "width", "cutoff", "volume"), step)) if isinstance(step, (list, tuple)) else step or {}
|
|
834
|
+
wave = o.get("wave")
|
|
835
|
+
if wave is not None and not _is_number(wave) and str(wave).lower() not in waves:
|
|
836
|
+
raise ValueError("%s: %s is not a wave a step can switch to.%s They are: %s"
|
|
837
|
+
% (said, _shown(wave), _meant(str(wave), list(waves)), ", ".join(waves)))
|
|
838
|
+
for name in ("pitch", "width", "cutoff", "volume"):
|
|
839
|
+
if o.get(name) is not None and not _is_number(o[name]):
|
|
840
|
+
raise ValueError("%s: a step's %s needs a number, not %s" % (said, name, _shown(o[name])))
|
|
841
|
+
return macro
|
|
842
|
+
|
|
843
|
+
|
|
844
|
+
def length_beats(name):
|
|
845
|
+
"""A note length by name as a number of beats: "quarter" is 1, "eighth." is 0.75, "1/16" is 0.25, "q3" is two
|
|
846
|
+
thirds. Also by letter (w h q e s t), "dotted quarter" and "eighth triplet". Not a length: None."""
|
|
847
|
+
if not isinstance(name, str):
|
|
848
|
+
return None
|
|
849
|
+
text = re.sub(r"[\s_-]", "", name.lower())
|
|
850
|
+
times = 1
|
|
851
|
+
if text.startswith("dotted"):
|
|
852
|
+
times, text = times * 1.5, text[6:]
|
|
853
|
+
if text.endswith("triplet"):
|
|
854
|
+
times, text = times * 2 / 3, text[:-7]
|
|
855
|
+
if text.endswith("."):
|
|
856
|
+
times, text = times * 1.5, text[:-1]
|
|
857
|
+
if text.endswith("note"):
|
|
858
|
+
text = text[:-4]
|
|
859
|
+
if len(text) > 1 and re.search(r"[a-z]3$", text):
|
|
860
|
+
times, text = times * 2 / 3, text[:-1]
|
|
861
|
+
fraction = re.match(r"^(\d+)/(\d+)$", text)
|
|
862
|
+
if fraction and int(fraction.group(2)) > 0:
|
|
863
|
+
return 4 * int(fraction.group(1)) / int(fraction.group(2)) * times
|
|
864
|
+
beats = _NAMES.get("lengths", {}).get(text, _NAMES.get("lengthLetters", {}).get(text))
|
|
865
|
+
return None if beats is None else beats * times
|
|
866
|
+
|
|
867
|
+
|
|
868
|
+
_NOT_A_LENGTH = ('a number, or a note length by name: "whole", "half", "quarter", "eighth", "sixteenth" '
|
|
869
|
+
'(also "1/4", "q", and "quarter." for a dotted one)')
|
|
870
|
+
|
|
871
|
+
|
|
872
|
+
def _named_beats(value, said):
|
|
873
|
+
beats = length_beats(value)
|
|
874
|
+
if beats is None:
|
|
875
|
+
raise ValueError("%s needs %s, not %s.%s" % (said, _NOT_A_LENGTH, _shown(value), _meant(value, list(_NAMES.get("lengths", {})))))
|
|
876
|
+
return beats
|
|
877
|
+
|
|
878
|
+
|
|
879
|
+
def _beats_long(value, said):
|
|
880
|
+
"""In a line of notes a length is beats as a number, or a note length by name. It gives beats."""
|
|
881
|
+
if value is None:
|
|
882
|
+
return 1
|
|
883
|
+
if isinstance(value, str):
|
|
884
|
+
return _named_beats(value, said)
|
|
885
|
+
if not _is_number(value) or not value > 0:
|
|
886
|
+
raise ValueError('%s needs a number of beats, more than 0, or a note length such as "quarter", not %s' % (said, _shown(value)))
|
|
887
|
+
return value
|
|
888
|
+
|
|
889
|
+
|
|
890
|
+
def _amount(value, said, least=0, most=1):
|
|
891
|
+
if not _is_number(value):
|
|
892
|
+
raise ValueError("%s needs a number from %g to %g, not %s" % (said, least, most, _shown(value)))
|
|
893
|
+
return _clamp(value, least, most)
|
|
894
|
+
|
|
895
|
+
|
|
896
|
+
def _seconds(value, said):
|
|
897
|
+
if not _is_number(value) or not value > 0:
|
|
898
|
+
raise ValueError("%s needs a number of seconds, more than 0, not %s" % (said, _shown(value)))
|
|
899
|
+
return value
|
|
900
|
+
|
|
901
|
+
|
|
902
|
+
_said = set()
|
|
903
|
+
|
|
904
|
+
|
|
905
|
+
def _tell(what):
|
|
906
|
+
"""Each thing said once, however often it happens. It does not stop the program."""
|
|
907
|
+
if what in _said:
|
|
908
|
+
return
|
|
909
|
+
_said.add(what)
|
|
910
|
+
print("synths: " + what, file=sys.stderr)
|
|
911
|
+
|
|
912
|
+
|
|
913
|
+
# ---------------------------------------------------------------- how the commands travel
|
|
914
|
+
|
|
915
|
+
class _Frames:
|
|
916
|
+
"""In a Pyctures scene: the commands of one frame are one batch, one command in the frame's own message."""
|
|
917
|
+
|
|
918
|
+
def __init__(self, pyctures):
|
|
919
|
+
self.p = pyctures
|
|
920
|
+
self.batch = None
|
|
921
|
+
self.into = None # the frame's list of commands that the batch was put in
|
|
922
|
+
ending = getattr(pyctures.S, "ending", None)
|
|
923
|
+
if isinstance(ending, list):
|
|
924
|
+
ending.append(self.stamp) # Pyctures calls it as the frame is sent
|
|
925
|
+
|
|
926
|
+
def add(self, c, clock):
|
|
927
|
+
cmds = self.p.S.cmds
|
|
928
|
+
if self.into is not cmds:
|
|
929
|
+
# The first sound of this frame. The batch is in the frame now, and is filled until the frame is sent.
|
|
930
|
+
self.batch = {"synths": COMMANDS, "now": clock(), "cmds": []}
|
|
931
|
+
self.into = cmds
|
|
932
|
+
line = getattr(self.p, "_user_line", lambda: 0)()
|
|
933
|
+
cmds.append(["call", line, 0, getattr(self.p, "H_SOUND", H_SOUND), "send", [self.batch]])
|
|
934
|
+
self.batch["cmds"].append(c)
|
|
935
|
+
# The clock when the batch leaves. A Pyctures that does not call stamp() has the time of the last command.
|
|
936
|
+
self.batch["now"] = clock()
|
|
937
|
+
|
|
938
|
+
def turn(self, clock):
|
|
939
|
+
"""Which turn of the program this is: a frame, or the call the page made (the top of the file, a key press)."""
|
|
940
|
+
return id(self.p.S.cmds), getattr(self.p.S, "frame_no", 0)
|
|
941
|
+
|
|
942
|
+
def clock(self):
|
|
943
|
+
"""The scene's clock. A scene that is being recorded as a video is given one frame at a time, each exactly
|
|
944
|
+
1/fps after the last however long it took, and Pyctures says what time that is (S.vt): the sound goes by
|
|
945
|
+
that clock too, so sound_time(), beat() and every time on a command are the video's own."""
|
|
946
|
+
stepped = getattr(self.p.S, "vt", None)
|
|
947
|
+
return time.perf_counter() if stepped is None else stepped
|
|
948
|
+
|
|
949
|
+
def stamp(self):
|
|
950
|
+
if self.batch is not None and self.into is self.p.S.cmds:
|
|
951
|
+
self.batch["now"] = self.clock()
|
|
952
|
+
|
|
953
|
+
def page(self, id):
|
|
954
|
+
"""The page's own object for a synth (or, with no id, for all the sound): what a dial or a keyboard in the page is tied to."""
|
|
955
|
+
p = self.p
|
|
956
|
+
sound = p.JsRef(getattr(p, "H_SOUND", H_SOUND))
|
|
957
|
+
return sound.output() if id is None else sound.synth(id)
|
|
958
|
+
|
|
959
|
+
def listen(self, heard):
|
|
960
|
+
"""Ask the page for what its MIDI desk says. It comes with the next frame, as a call from the page that
|
|
961
|
+
carries {"midi": the events as JSON text}, with each event's time already on Python's clock."""
|
|
962
|
+
def told(e):
|
|
963
|
+
text = e.get("midi") if hasattr(e, "get") else None
|
|
964
|
+
if text:
|
|
965
|
+
heard(json.loads(text))
|
|
966
|
+
p = self.p
|
|
967
|
+
p.JsRef(getattr(p, "H_SOUND", H_SOUND)).midi(told)
|
|
968
|
+
|
|
969
|
+
def hear(self, heard):
|
|
970
|
+
"""Ask the page for what its player says back (the microphone, a recording that is over, a saved file) and
|
|
971
|
+
for what was asked to be watched. It comes with the next frame, as {"sound": the events as JSON text}."""
|
|
972
|
+
def told(e):
|
|
973
|
+
text = e.get("sound") if hasattr(e, "get") else None
|
|
974
|
+
if text:
|
|
975
|
+
heard(json.loads(text))
|
|
976
|
+
p = self.p
|
|
977
|
+
p.JsRef(getattr(p, "H_SOUND", H_SOUND)).events(told)
|
|
978
|
+
|
|
979
|
+
def watch(self, id, what="reading"):
|
|
980
|
+
"""Ask the page to say, with every frame for a while, what a synth is doing (no id: all the sound), or
|
|
981
|
+
where the song is. Asked again twice a second while a program goes on reading."""
|
|
982
|
+
p = self.p
|
|
983
|
+
sound = p.JsRef(getattr(p, "H_SOUND", H_SOUND))
|
|
984
|
+
if what == "position":
|
|
985
|
+
sound.watchSong()
|
|
986
|
+
else:
|
|
987
|
+
sound.watch(id)
|
|
988
|
+
|
|
989
|
+
def keeps_files(self):
|
|
990
|
+
"""Whether Python has real files beside the scene (`pyctures run`); in a browser the page keeps them."""
|
|
991
|
+
return not getattr(self.p.S, "page_files", False)
|
|
992
|
+
|
|
993
|
+
|
|
994
|
+
class _Direct:
|
|
995
|
+
"""For a host that was connected: each command is handed over at once, as a batch of its own."""
|
|
996
|
+
|
|
997
|
+
def __init__(self, send):
|
|
998
|
+
self.send = send
|
|
999
|
+
|
|
1000
|
+
def add(self, c, clock):
|
|
1001
|
+
self.send({"synths": COMMANDS, "now": clock(), "cmds": [c]})
|
|
1002
|
+
|
|
1003
|
+
def turn(self, clock):
|
|
1004
|
+
return _turn_by_the_clock(self, clock)
|
|
1005
|
+
|
|
1006
|
+
def page(self, id):
|
|
1007
|
+
return None
|
|
1008
|
+
|
|
1009
|
+
def listen(self, heard):
|
|
1010
|
+
pass # a host hands over what its player's on_midi gave with receive_midi(events)
|
|
1011
|
+
|
|
1012
|
+
def hear(self, heard):
|
|
1013
|
+
pass # and what its player's on_sound_events gave with receive_sound_events(events)
|
|
1014
|
+
|
|
1015
|
+
def watch(self, id, what="reading"):
|
|
1016
|
+
pass
|
|
1017
|
+
|
|
1018
|
+
def keeps_files(self):
|
|
1019
|
+
return False
|
|
1020
|
+
|
|
1021
|
+
|
|
1022
|
+
class _Nowhere:
|
|
1023
|
+
"""No player: the program carries on, in silence."""
|
|
1024
|
+
|
|
1025
|
+
def add(self, c, clock):
|
|
1026
|
+
pass
|
|
1027
|
+
|
|
1028
|
+
def turn(self, clock):
|
|
1029
|
+
return _turn_by_the_clock(self, clock)
|
|
1030
|
+
|
|
1031
|
+
def page(self, id):
|
|
1032
|
+
return None
|
|
1033
|
+
|
|
1034
|
+
def listen(self, heard):
|
|
1035
|
+
pass
|
|
1036
|
+
|
|
1037
|
+
def hear(self, heard):
|
|
1038
|
+
pass
|
|
1039
|
+
|
|
1040
|
+
def watch(self, id, what="reading"):
|
|
1041
|
+
pass
|
|
1042
|
+
|
|
1043
|
+
def keeps_files(self):
|
|
1044
|
+
return False
|
|
1045
|
+
|
|
1046
|
+
|
|
1047
|
+
def _turn_by_the_clock(transport, clock):
|
|
1048
|
+
"""With no frames to count, what is written within a twentieth of a second of the last thing is the same turn."""
|
|
1049
|
+
now = clock()
|
|
1050
|
+
last = getattr(transport, "_last", None)
|
|
1051
|
+
if last is None or now - last[0] > 0.05:
|
|
1052
|
+
last = transport._last = [now, (getattr(transport, "_last", None) or [0, 0])[1] + 1]
|
|
1053
|
+
last[0] = now
|
|
1054
|
+
return last[1]
|
|
1055
|
+
|
|
1056
|
+
|
|
1057
|
+
_host = [None] # the sound of a host that called connect()
|
|
1058
|
+
_silent = [None] # the sound of a program with no player
|
|
1059
|
+
|
|
1060
|
+
|
|
1061
|
+
def connect(send, ids=""):
|
|
1062
|
+
"""For a host: take every command yourself, as `send(batch)` is called with {"synths": 1, "now", "cmds"}.
|
|
1063
|
+
`ids` begins every id made, for two programs that share one player. connect(None) undoes it."""
|
|
1064
|
+
_host[0] = _Sound(_Direct(send), ids) if send is not None else None
|
|
1065
|
+
|
|
1066
|
+
|
|
1067
|
+
def _sound():
|
|
1068
|
+
"""The sound of the program that is running: where its commands go, and what it has made."""
|
|
1069
|
+
if _host[0] is not None:
|
|
1070
|
+
return _host[0]
|
|
1071
|
+
# Looked up each time: in a browser every scene has its own copy of the module.
|
|
1072
|
+
p = sys.modules.get("pyctures")
|
|
1073
|
+
S = getattr(p, "S", None)
|
|
1074
|
+
if S is not None and getattr(S, "ns", None) is not None:
|
|
1075
|
+
mine = getattr(S, "_synths", None)
|
|
1076
|
+
if mine is None:
|
|
1077
|
+
mine = _Sound(_Frames(p))
|
|
1078
|
+
S._synths = mine # kept with the run, so a new run starts with nothing made
|
|
1079
|
+
return mine
|
|
1080
|
+
if _silent[0] is None:
|
|
1081
|
+
_silent[0] = _Sound(_Nowhere())
|
|
1082
|
+
if not (S is not None and getattr(S, "started", 0)): # a scene that has been stopped says nothing
|
|
1083
|
+
_no_player()
|
|
1084
|
+
return _silent[0]
|
|
1085
|
+
|
|
1086
|
+
|
|
1087
|
+
def _no_player():
|
|
1088
|
+
"""A program started with plain Python: open it with Pyctures if that can be done, or say how to."""
|
|
1089
|
+
main = sys.modules.get("__main__")
|
|
1090
|
+
file = getattr(main, "__file__", None)
|
|
1091
|
+
name = os.path.basename(file) if file else "your_program.py"
|
|
1092
|
+
try:
|
|
1093
|
+
import importlib.util
|
|
1094
|
+
has_pyctures = importlib.util.find_spec("pyctures") is not None
|
|
1095
|
+
except (ImportError, ValueError):
|
|
1096
|
+
has_pyctures = False
|
|
1097
|
+
started_plainly = bool(
|
|
1098
|
+
file and file.endswith(".py") and os.path.isfile(file) and sys.argv
|
|
1099
|
+
and os.path.abspath(sys.argv[0]) == os.path.abspath(file)
|
|
1100
|
+
and not sys.flags.interactive and sys.platform != "emscripten")
|
|
1101
|
+
if started_plainly and has_pyctures and not os.environ.get("SYNTHS_NO_BROWSER"):
|
|
1102
|
+
print("synths: the sound is made in a web browser, so %s is being opened in one.\n"
|
|
1103
|
+
" This is the same as: pyctures run %s\n"
|
|
1104
|
+
" Click the page to turn the sound on. Press Ctrl+C here to stop." % (name, name), file=sys.stderr)
|
|
1105
|
+
import subprocess
|
|
1106
|
+
try:
|
|
1107
|
+
code = subprocess.call([sys.executable, "-m", "pyctures", "run", os.path.abspath(file)])
|
|
1108
|
+
except KeyboardInterrupt:
|
|
1109
|
+
code = 0
|
|
1110
|
+
raise SystemExit(code)
|
|
1111
|
+
if has_pyctures:
|
|
1112
|
+
_tell("the sound is made in a web browser, so this program plays nothing when it is started with plain Python. "
|
|
1113
|
+
"Run it with: pyctures run %s" % name)
|
|
1114
|
+
else:
|
|
1115
|
+
_tell("the sound is made in a web browser, by Pyctures, so this program plays nothing when it is started with "
|
|
1116
|
+
"plain Python. Install Pyctures (pip install pyctures) and run it with: pyctures run %s" % name)
|
|
1117
|
+
|
|
1118
|
+
|
|
1119
|
+
# ---------------------------------------------------------------- one program's sound
|
|
1120
|
+
|
|
1121
|
+
class _Sound:
|
|
1122
|
+
def __init__(self, transport, prefix=""):
|
|
1123
|
+
self.transport = transport
|
|
1124
|
+
self.prefix = prefix
|
|
1125
|
+
self.clock = getattr(transport, "clock", None) or time.perf_counter
|
|
1126
|
+
self.ids = 0
|
|
1127
|
+
self.handles = 0
|
|
1128
|
+
self.tempo = 120
|
|
1129
|
+
self.beat_zero = self.clock()
|
|
1130
|
+
self.main = None
|
|
1131
|
+
self.chip = None
|
|
1132
|
+
self.output = None
|
|
1133
|
+
self.lines = {} # synth id -> the time its line of notes has reached
|
|
1134
|
+
self.turn = None # when the lines begun in this turn of the program begin
|
|
1135
|
+
self.turn_of = None # what told this turn from the last (see _Frames.turn)
|
|
1136
|
+
self.writing = None # inside loop_notes: synth id -> [its steps, the beat it has reached]
|
|
1137
|
+
self._midi = None # MIDI, made the first time it is used (see _Midi)
|
|
1138
|
+
self.mic = None # the microphone, once it has been asked for
|
|
1139
|
+
self.takes = {} # sample id -> a recording that is being made
|
|
1140
|
+
self.saves = {} # the number of a save_sound -> what it gave back
|
|
1141
|
+
self.asks = 0
|
|
1142
|
+
self.hearing = False # the page has been asked for what its player says back
|
|
1143
|
+
self.readings = {} # synth id ("" for all the sound) -> what it was doing when the page last said
|
|
1144
|
+
self.watched = {} # what the page was asked to watch -> when it was last asked
|
|
1145
|
+
self.position = dict(_NO_SONG)
|
|
1146
|
+
self.buffer_set = None # what set_sound_buffer asked for: "auto" or frames
|
|
1147
|
+
self.buffer_asked = False
|
|
1148
|
+
self.buffer_said = None # the buffer as the player last said it
|
|
1149
|
+
# THE SETTINGS EVERY SYNTH SHARES (the echo's time, the reverb's type): there is one echo and one reverb,
|
|
1150
|
+
# so one value of each, whoever set it. Kept here by engine id, and what every synth and sound_output() read.
|
|
1151
|
+
self.for_all = {}
|
|
1152
|
+
|
|
1153
|
+
@property
|
|
1154
|
+
def midi(self):
|
|
1155
|
+
if self._midi is None:
|
|
1156
|
+
self._midi = _Midi(self)
|
|
1157
|
+
return self._midi
|
|
1158
|
+
|
|
1159
|
+
def length(self, value, said):
|
|
1160
|
+
"""A LENGTH is seconds as a number, or a note length by name, which is so many beats at the tempo now."""
|
|
1161
|
+
if isinstance(value, str):
|
|
1162
|
+
return _named_beats(value, said) * 60 / self.tempo
|
|
1163
|
+
if not _is_number(value) or not value > 0:
|
|
1164
|
+
raise ValueError('%s needs a number of seconds, more than 0, or a note length such as "quarter", not %s' % (said, _shown(value)))
|
|
1165
|
+
return value
|
|
1166
|
+
|
|
1167
|
+
def line_at(self, key):
|
|
1168
|
+
"""Where a synth's line of notes has reached. Lines begun in the same turn of the program (the same frame
|
|
1169
|
+
of a scene) begin on the same instant, so a melody and a bass written one after the other play together."""
|
|
1170
|
+
now = self.transport.turn(self.clock)
|
|
1171
|
+
if now != self.turn_of or self.turn is None:
|
|
1172
|
+
self.turn_of, self.turn = now, self.clock()
|
|
1173
|
+
at = self.lines.get(key)
|
|
1174
|
+
return self.turn if at is None or at < self.turn else at
|
|
1175
|
+
|
|
1176
|
+
def write(self, synth, what, long, volume, pan, said):
|
|
1177
|
+
"""The next thing in a synth's line: notes (or None, a rest) that last so many beats."""
|
|
1178
|
+
beats = _beats_long(long, said)
|
|
1179
|
+
vel = None if volume is None else _amount(volume, "%s: volume" % said)
|
|
1180
|
+
if self.writing is not None:
|
|
1181
|
+
line = self.writing.setdefault(synth._id, [[], 0])
|
|
1182
|
+
if what:
|
|
1183
|
+
line[0].append([line[1], what, beats * _HELD, 0.8 if vel is None else vel])
|
|
1184
|
+
line[1] += beats
|
|
1185
|
+
return
|
|
1186
|
+
at = self.line_at(synth._id)
|
|
1187
|
+
if what:
|
|
1188
|
+
c = {"op": "note", "id": synth._id, "note": what, "len": beats * 60 / self.tempo * _HELD, "t": at}
|
|
1189
|
+
if vel is not None:
|
|
1190
|
+
c["vel"] = vel
|
|
1191
|
+
if pan is not None:
|
|
1192
|
+
c["pan"] = _amount(pan, "%s: pan" % said, -1, 1)
|
|
1193
|
+
self.command(c)
|
|
1194
|
+
self.lines[synth._id] = at + beats * 60 / self.tempo
|
|
1195
|
+
|
|
1196
|
+
def command(self, c):
|
|
1197
|
+
self.transport.add(c, self.clock)
|
|
1198
|
+
|
|
1199
|
+
def share(self, patch, names=None):
|
|
1200
|
+
"""What a patch says of the settings every synth shares is what they are, for everyone: of the settings
|
|
1201
|
+
named, or with no names of every one it has."""
|
|
1202
|
+
ids = patch if names is None else [(_setting(n) or {}).get("id", -1) for n in names]
|
|
1203
|
+
for pid in ids:
|
|
1204
|
+
if pid >= 200 and pid in patch:
|
|
1205
|
+
self.for_all[pid] = patch[pid]
|
|
1206
|
+
|
|
1207
|
+
def hear_back(self):
|
|
1208
|
+
"""Make sure this program is handed what the player says back. Asked once, the first time something needs it."""
|
|
1209
|
+
if not self.hearing:
|
|
1210
|
+
self.hearing = True
|
|
1211
|
+
self.transport.hear(self.receive)
|
|
1212
|
+
|
|
1213
|
+
def watch(self, key, id, what="reading"):
|
|
1214
|
+
now = self.clock()
|
|
1215
|
+
if now - self.watched.get(key, -10) > 0.5:
|
|
1216
|
+
self.watched[key] = now
|
|
1217
|
+
self.hear_back()
|
|
1218
|
+
self.transport.watch(id, what)
|
|
1219
|
+
|
|
1220
|
+
def reading(self, id):
|
|
1221
|
+
"""What a synth is doing, as the page last said: {"samples", "level", "notes"}. Nothing yet: empty."""
|
|
1222
|
+
key = id or ""
|
|
1223
|
+
self.watch(key, id)
|
|
1224
|
+
return self.readings.get(key, _NO_READING)
|
|
1225
|
+
|
|
1226
|
+
def receive(self, events):
|
|
1227
|
+
"""What the player says back, as a list of plain dicts (see receive_sound_events)."""
|
|
1228
|
+
if isinstance(events, dict):
|
|
1229
|
+
events = [events]
|
|
1230
|
+
for e in events or ():
|
|
1231
|
+
if not isinstance(e, dict):
|
|
1232
|
+
continue
|
|
1233
|
+
kind = e.get("type")
|
|
1234
|
+
if kind == "recorded":
|
|
1235
|
+
take = self.takes.pop(e.get("id"), None)
|
|
1236
|
+
if take is not None:
|
|
1237
|
+
take._finish(e)
|
|
1238
|
+
elif kind == "saved":
|
|
1239
|
+
saved = self.saves.pop(e.get("ask"), None)
|
|
1240
|
+
if saved is not None:
|
|
1241
|
+
saved._finish(e)
|
|
1242
|
+
elif kind == "microphone":
|
|
1243
|
+
if self.mic is not None:
|
|
1244
|
+
self.mic._said(e)
|
|
1245
|
+
elif kind == "reading":
|
|
1246
|
+
self.readings[e.get("id") or ""] = {"samples": list(e.get("samples") or ()), "level": list(e.get("level") or (0, 0)),
|
|
1247
|
+
"notes": list(e.get("notes") or ()),
|
|
1248
|
+
"arpStep": e.get("arpStep", -1), "tranceGateStep": e.get("tranceGateStep", -1)}
|
|
1249
|
+
elif kind == "position":
|
|
1250
|
+
self.position = {k: e.get(k, v) for k, v in _NO_SONG.items()}
|
|
1251
|
+
elif kind == _NAMES["buffer"]["event"]:
|
|
1252
|
+
self.buffer_said = {k: e.get(k) for k in _NAMES["buffer"]["reads"]}
|
|
1253
|
+
|
|
1254
|
+
def id(self, letter):
|
|
1255
|
+
self.ids += 1
|
|
1256
|
+
return "%s%s%d" % (self.prefix, letter, self.ids)
|
|
1257
|
+
|
|
1258
|
+
def when(self, said, after=None, at=None, beat=None):
|
|
1259
|
+
"""When a call should happen, on Python's clock."""
|
|
1260
|
+
if isinstance(after, str):
|
|
1261
|
+
after = self.length(after, "%s: after" % said) # after="half": a note length from now
|
|
1262
|
+
for name, value in (("at", at), ("beat", beat), ("after", after)):
|
|
1263
|
+
if value is not None and not _is_number(value):
|
|
1264
|
+
raise ValueError("%s: %s needs a number, not %s" % (said, name, _shown(value)))
|
|
1265
|
+
if at is not None:
|
|
1266
|
+
return at
|
|
1267
|
+
if beat is not None:
|
|
1268
|
+
return self.beat_zero + beat * 60 / self.tempo
|
|
1269
|
+
if after is not None:
|
|
1270
|
+
return self.clock() + after
|
|
1271
|
+
return self.clock()
|
|
1272
|
+
|
|
1273
|
+
def main_synth(self):
|
|
1274
|
+
"""The synth play_note and play_notes use."""
|
|
1275
|
+
if self.main is None:
|
|
1276
|
+
self.main = _make_synth(self, "keys", {})
|
|
1277
|
+
return self.main
|
|
1278
|
+
|
|
1279
|
+
|
|
1280
|
+
_HELD = 0.95 # how much of its length a note in a line sounds for
|
|
1281
|
+
# What is read back before the page has said anything: no wave, no level, no notes; and no song.
|
|
1282
|
+
_NO_READING = {"samples": [], "level": [0, 0], "notes": []}
|
|
1283
|
+
_NO_SONG = {"state": "none", "order": 0, "pattern": 0, "row": 0, "tick": 0, "speed": 0, "tempo": 0, "rounds": 0, "rows": 0, "orders": 0}
|
|
1284
|
+
|
|
1285
|
+
|
|
1286
|
+
def _steps_of(tune, step, beat, hold, volume, said):
|
|
1287
|
+
""" "C4 E4:q G4 - C5:2", or a list of notes, chords (lists) and rests (None) -> the steps of a tune, in the
|
|
1288
|
+
steps' own units. `step` is how long a step is in those units, and `beat` how many of the units a beat is."""
|
|
1289
|
+
items = [i for i in re.split(r"[\s,|]+", tune.strip()) if i] if isinstance(tune, str) else tune
|
|
1290
|
+
if not isinstance(items, (list, tuple)) or not items:
|
|
1291
|
+
raise ValueError('%s needs notes to play, as in play_notes("C4 E4 G4 - C5"), not %s' % (said, _shown(tune)))
|
|
1292
|
+
hold = 0.9 if hold is None else _amount(hold, "%s: hold" % said, 0.01, 4)
|
|
1293
|
+
vel = 0.8 if volume is None else _amount(volume, "%s: volume" % said)
|
|
1294
|
+
steps = []
|
|
1295
|
+
at = 0
|
|
1296
|
+
for item in items:
|
|
1297
|
+
what, long = item, step
|
|
1298
|
+
if isinstance(item, str):
|
|
1299
|
+
m = re.match(r"^(.*?)(?::([^:]+))?$", item)
|
|
1300
|
+
what = m.group(1)
|
|
1301
|
+
# After a colon: a number is so many steps, and a note length ("q", "eighth", "1/4") is itself.
|
|
1302
|
+
if m.group(2) and re.match(r"^\d*\.?\d+$", m.group(2)):
|
|
1303
|
+
long = float(m.group(2)) * step
|
|
1304
|
+
elif m.group(2):
|
|
1305
|
+
beats = length_beats(m.group(2))
|
|
1306
|
+
if beats is None:
|
|
1307
|
+
raise ValueError('%s: in %s, %s is not a length. After the colon write how many steps (":2") or a note '
|
|
1308
|
+
'length (":q", ":h", ":eighth", ":1/4")' % (said, _shown(item), _shown(m.group(2))))
|
|
1309
|
+
long = beats * beat
|
|
1310
|
+
if re.match(r"^[-._~]*$", what):
|
|
1311
|
+
what = None
|
|
1312
|
+
elif "+" in what:
|
|
1313
|
+
what = what.split("+")
|
|
1314
|
+
if what is not None:
|
|
1315
|
+
steps.append([_whole(at), _notes_of(what), _whole(long * hold), vel])
|
|
1316
|
+
at += long
|
|
1317
|
+
return steps, _whole(at)
|
|
1318
|
+
|
|
1319
|
+
|
|
1320
|
+
def _whole(n):
|
|
1321
|
+
"""A number without a needless fraction: 2.0 is 2."""
|
|
1322
|
+
return int(n) if isinstance(n, float) and n == int(n) else n
|
|
1323
|
+
|
|
1324
|
+
|
|
1325
|
+
class Note:
|
|
1326
|
+
"""A note that is sounding: what start() gives back."""
|
|
1327
|
+
|
|
1328
|
+
def __init__(self, sound, synth, h):
|
|
1329
|
+
object.__setattr__(self, "_sound", sound)
|
|
1330
|
+
object.__setattr__(self, "_synth", synth)
|
|
1331
|
+
object.__setattr__(self, "_h", h)
|
|
1332
|
+
|
|
1333
|
+
def _voice(self, **change):
|
|
1334
|
+
s = self._sound
|
|
1335
|
+
s.command(dict({"op": "voice", "id": self._synth, "h": self._h}, t=s.clock(), **change))
|
|
1336
|
+
return self
|
|
1337
|
+
|
|
1338
|
+
def stop(self, *, after=None, at=None, beat=None):
|
|
1339
|
+
"""Let it go: it dies away over the synth's release. held.stop(at=sound_time() + 1) lets go then."""
|
|
1340
|
+
s = self._sound
|
|
1341
|
+
s.command({"op": "off", "id": self._synth, "h": self._h, "t": s.when("stop", after, at, beat)})
|
|
1342
|
+
return self
|
|
1343
|
+
|
|
1344
|
+
def vibrato(self, how=True):
|
|
1345
|
+
"""Give the note a vibrato as it sounds: held.vibrato(), held.vibrato({"depth": 0.5, "rate": 5, "delay": 0}),
|
|
1346
|
+
and held.vibrato(False) stops it."""
|
|
1347
|
+
s = self._sound
|
|
1348
|
+
made = False if how is False else _moves_of(s, {"vibrato": {"delay": 0} if how is True else how}, "vibrato")["vibrato"]
|
|
1349
|
+
return self._voice(vibrato=made)
|
|
1350
|
+
|
|
1351
|
+
def cut(self):
|
|
1352
|
+
"""Cut it off at once."""
|
|
1353
|
+
return self._voice(kill=True)
|
|
1354
|
+
|
|
1355
|
+
def slide_to(self, note, time=0):
|
|
1356
|
+
"""Slide to another note over that many seconds."""
|
|
1357
|
+
return self._voice(pitch=note_number(note), glide=time)
|
|
1358
|
+
|
|
1359
|
+
def __setattr__(self, name, value):
|
|
1360
|
+
if name == "pitch":
|
|
1361
|
+
self.slide_to(value, 0)
|
|
1362
|
+
elif name == "frequency":
|
|
1363
|
+
self._voice(freq=_amount(value, "frequency", 1, 20000))
|
|
1364
|
+
elif name == "volume":
|
|
1365
|
+
self._voice(vol=_amount(value, "volume", 0, 4))
|
|
1366
|
+
elif name == "pan":
|
|
1367
|
+
self._voice(pan=_amount(value, "pan", -1, 1))
|
|
1368
|
+
else:
|
|
1369
|
+
object.__setattr__(self, name, value)
|
|
1370
|
+
|
|
1371
|
+
|
|
1372
|
+
class Arp(Note):
|
|
1373
|
+
"""An arpeggio that is sounding: what arp() and start_arp() give back. Its chord can be changed as it plays, and
|
|
1374
|
+
so can its speed, step, order, gate, octaves, swing, accent and pattern: arp.gate = 0.3."""
|
|
1375
|
+
|
|
1376
|
+
def __init__(self, sound, synth, h, root, how, now):
|
|
1377
|
+
Note.__init__(self, sound, synth, h)
|
|
1378
|
+
object.__setattr__(self, "_root", root)
|
|
1379
|
+
object.__setattr__(self, "_how", dict(how))
|
|
1380
|
+
object.__setattr__(self, "_now", now)
|
|
1381
|
+
|
|
1382
|
+
def change(self, chord, kind=None, *, after=None, at=None, beat=None, **how):
|
|
1383
|
+
"""Another chord, without starting the note again: arp.change("Fm"), arp.change("F4", "minor"), or a list of
|
|
1384
|
+
notes. What an arpeggio may be told can be said here too: arp.change("Fm", gate=0.3)."""
|
|
1385
|
+
told = _arp_words(how)
|
|
1386
|
+
if how:
|
|
1387
|
+
raise ValueError("change takes a chord, %s, after, at and beat, not %s" % (", ".join(_NAMES["arp"]["words"]), ", ".join(how)))
|
|
1388
|
+
kept = self._how
|
|
1389
|
+
# A new speed takes the place of a step, and a step of a speed.
|
|
1390
|
+
if "speed" in told:
|
|
1391
|
+
kept.pop("step", None)
|
|
1392
|
+
kept.pop("rate", None)
|
|
1393
|
+
elif "step" in told or "rate" in told:
|
|
1394
|
+
kept.pop("speed", None)
|
|
1395
|
+
kept.update(told)
|
|
1396
|
+
s = self._sound
|
|
1397
|
+
s.command({"op": "voice", "id": self._synth, "h": self._h,
|
|
1398
|
+
"arp": _arp_of(_chord_of(chord, kind, "change"), self._root, kept, "change"),
|
|
1399
|
+
"t": s.when("change", after, at, beat)})
|
|
1400
|
+
object.__setattr__(self, "_now", (chord, kind))
|
|
1401
|
+
return self
|
|
1402
|
+
|
|
1403
|
+
def __setattr__(self, name, value):
|
|
1404
|
+
if name == "chord":
|
|
1405
|
+
self.change(value)
|
|
1406
|
+
elif name in _NAMES["arp"]["words"]:
|
|
1407
|
+
self.change(*self._now, **{name: value})
|
|
1408
|
+
else:
|
|
1409
|
+
Note.__setattr__(self, name, value)
|
|
1410
|
+
|
|
1411
|
+
|
|
1412
|
+
class Tune:
|
|
1413
|
+
"""A tune that is playing: what play_notes() gives back."""
|
|
1414
|
+
|
|
1415
|
+
def __init__(self, sound, id):
|
|
1416
|
+
self._sound = sound
|
|
1417
|
+
self.id = id
|
|
1418
|
+
|
|
1419
|
+
def stop(self):
|
|
1420
|
+
"""Stop it. Notes it has started finish by themselves."""
|
|
1421
|
+
self._sound.command({"op": "stop", "id": self.id, "t": self._sound.clock()})
|
|
1422
|
+
return self
|
|
1423
|
+
|
|
1424
|
+
|
|
1425
|
+
class Song:
|
|
1426
|
+
"""A song that is playing: what play_song() gives back."""
|
|
1427
|
+
|
|
1428
|
+
def __init__(self, sound, id):
|
|
1429
|
+
object.__setattr__(self, "_sound", sound)
|
|
1430
|
+
object.__setattr__(self, "id", id)
|
|
1431
|
+
object.__setattr__(self, "_mixed", {})
|
|
1432
|
+
|
|
1433
|
+
def _do(self, what, **more):
|
|
1434
|
+
self._sound.command(dict({"op": "song", "id": self.id, "do": what}, t=self._sound.clock(), **more))
|
|
1435
|
+
return self
|
|
1436
|
+
|
|
1437
|
+
def stop(self):
|
|
1438
|
+
"""Stop it, and go back to its start."""
|
|
1439
|
+
return self._do("stop")
|
|
1440
|
+
|
|
1441
|
+
def pause(self):
|
|
1442
|
+
"""Hold it where it is, until resume()."""
|
|
1443
|
+
return self._do("pause")
|
|
1444
|
+
|
|
1445
|
+
def resume(self):
|
|
1446
|
+
return self._do("resume")
|
|
1447
|
+
|
|
1448
|
+
def seek(self, order, row=0):
|
|
1449
|
+
"""Go to a place in the song: a part of it (its order list, from 0), then a row of the pattern there."""
|
|
1450
|
+
return self._do("seek", order=int(_amount(order, "seek: the order", 0, 65535)), row=int(_amount(row, "seek: the row", 0, 255)))
|
|
1451
|
+
|
|
1452
|
+
def _channel(self, channel, said):
|
|
1453
|
+
if channel is None or channel == "all":
|
|
1454
|
+
return {}
|
|
1455
|
+
if not isinstance(channel, int) or isinstance(channel, bool) or not 0 <= channel <= 31:
|
|
1456
|
+
raise ValueError("%s needs the number of a channel, counting from 0, or nothing for every channel, not %s"
|
|
1457
|
+
% (said, _shown(channel)))
|
|
1458
|
+
return {"channel": channel}
|
|
1459
|
+
|
|
1460
|
+
def mute(self, channel=None, on=True):
|
|
1461
|
+
"""Silence one channel of the song (they count from 0), or with no channel all of them: song.mute(2),
|
|
1462
|
+
and song.mute(2, False) to hear it again."""
|
|
1463
|
+
return self._do("mute", on=bool(on), **self._channel(channel, "mute"))
|
|
1464
|
+
|
|
1465
|
+
def unmute(self, channel=None):
|
|
1466
|
+
return self._do("mute", on=False, **self._channel(channel, "unmute"))
|
|
1467
|
+
|
|
1468
|
+
def solo(self, channel=None, on=True):
|
|
1469
|
+
"""Hear only the channels that are soloed: song.solo(2), and song.solo(2, False) or song.solo("all", False)
|
|
1470
|
+
to hear the rest again."""
|
|
1471
|
+
return self._do("solo", on=bool(on), **self._channel(channel, "solo"))
|
|
1472
|
+
|
|
1473
|
+
def loop_pattern(self, on=True):
|
|
1474
|
+
"""Stay on the pattern that is playing, round and round, until loop_pattern(False)."""
|
|
1475
|
+
return self._do("loopPattern", on=bool(on))
|
|
1476
|
+
|
|
1477
|
+
def replace(self, song, *, files=None, patches=None):
|
|
1478
|
+
"""Put an edited song in the place of this one as it plays: its bytes, its JSON as text, or the song as a
|
|
1479
|
+
dictionary. When the two have the same shape nothing stops, and the change is heard from the next row."""
|
|
1480
|
+
if isinstance(song, (bytes, bytearray)):
|
|
1481
|
+
more = {"bytes": base64.b64encode(bytes(song)).decode("ascii")}
|
|
1482
|
+
elif isinstance(song, str) and song.strip():
|
|
1483
|
+
more = {"text": song}
|
|
1484
|
+
elif isinstance(song, dict):
|
|
1485
|
+
more = {"text": json.dumps(song)}
|
|
1486
|
+
else:
|
|
1487
|
+
raise ValueError("replace needs the new song: its bytes, its JSON as text, or the song itself as a "
|
|
1488
|
+
"dictionary, not %s" % _shown(song))
|
|
1489
|
+
if files:
|
|
1490
|
+
more["files"] = {name: base64.b64encode(bytes(data)).decode("ascii") if isinstance(data, (bytes, bytearray)) else data
|
|
1491
|
+
for name, data in files.items()}
|
|
1492
|
+
if patches:
|
|
1493
|
+
more["patches"] = _song_patches(patches)
|
|
1494
|
+
return self._do("replace", **more)
|
|
1495
|
+
|
|
1496
|
+
def channel(self, n):
|
|
1497
|
+
"""One channel of the song (the first is 0), for a meter or a scope of its own: song.channel(3).level(),
|
|
1498
|
+
.samples(), .notes(). A frame late in a Pyctures scene, as a synth's are."""
|
|
1499
|
+
if not isinstance(n, int) or isinstance(n, bool) or not 0 <= n <= 31:
|
|
1500
|
+
raise ValueError("channel needs the number of a channel, counting from 0, not %s" % _shown(n))
|
|
1501
|
+
if n not in self._mixed:
|
|
1502
|
+
self._mixed[n] = _Channel(self, n)
|
|
1503
|
+
return self._mixed[n]
|
|
1504
|
+
|
|
1505
|
+
def loop(self, start=None, end=None):
|
|
1506
|
+
"""A stretch of the song that goes round: song.loop([2, 0], [4, 0]) plays from the top of place 2 in the order
|
|
1507
|
+
up to the top of place 4 and back, each a place in the order and a row. song.loop(None) lets it run on."""
|
|
1508
|
+
def place(p, said):
|
|
1509
|
+
if p is None or p is False:
|
|
1510
|
+
return None
|
|
1511
|
+
where = [p, 0] if isinstance(p, int) and not isinstance(p, bool) else p
|
|
1512
|
+
whole = lambda v: isinstance(v, int) and not isinstance(v, bool) and v >= 0
|
|
1513
|
+
if not isinstance(where, (list, tuple)) or not 1 <= len(where) <= 2 or not all(whole(v) for v in where):
|
|
1514
|
+
raise ValueError("loop: %s is a place in the song's order and a row there, as in song.loop([2, 0], [4, 0]), not %s" % (said, _shown(p)))
|
|
1515
|
+
return [where[0], where[1] if len(where) > 1 else 0]
|
|
1516
|
+
return self._do("loop", start=place(start, "the start"), end=place(end, "the end"))
|
|
1517
|
+
|
|
1518
|
+
@property
|
|
1519
|
+
def position(self):
|
|
1520
|
+
"""Where the song is, for drawing it: {"state", "order", "pattern", "row", "tick", "speed", "tempo", "rounds",
|
|
1521
|
+
"rows", "orders"}, where state is "playing", "paused", "stopped" or "none". In a Pyctures scene it is what
|
|
1522
|
+
the page said with this frame, so a frame late; the first time it is read it is not known yet ("none")."""
|
|
1523
|
+
s = self._sound
|
|
1524
|
+
s.watch("song:position", None, "position")
|
|
1525
|
+
return dict(s.position)
|
|
1526
|
+
|
|
1527
|
+
def __setattr__(self, name, value):
|
|
1528
|
+
if name == "volume":
|
|
1529
|
+
self._do("volume", vol=_amount(value, "volume", 0, 2))
|
|
1530
|
+
elif name == "repeat":
|
|
1531
|
+
self._do("repeat", repeat=bool(value))
|
|
1532
|
+
else:
|
|
1533
|
+
object.__setattr__(self, name, value)
|
|
1534
|
+
|
|
1535
|
+
|
|
1536
|
+
class _Channel:
|
|
1537
|
+
"""One channel of a song: what it is doing, to read (samples(), level(), notes()), and its place in the song's
|
|
1538
|
+
mix, to move while the song plays: channel.volume = 0.6 (0 to 2), .pan (-1 to 1), .reverb and .echo (0 to 1).
|
|
1539
|
+
Each reads back what was last set here, and None until then: what the song itself says is the song's to know."""
|
|
1540
|
+
|
|
1541
|
+
def __init__(self, song, n):
|
|
1542
|
+
object.__setattr__(self, "_song", song)
|
|
1543
|
+
object.__setattr__(self, "_n", n)
|
|
1544
|
+
object.__setattr__(self, "_now", {})
|
|
1545
|
+
object.__setattr__(self, "_read", _Reading(song._sound, "song:channel:%d" % n))
|
|
1546
|
+
|
|
1547
|
+
def samples(self):
|
|
1548
|
+
return self._read.samples()
|
|
1549
|
+
|
|
1550
|
+
def level(self):
|
|
1551
|
+
return self._read.level()
|
|
1552
|
+
|
|
1553
|
+
def notes(self):
|
|
1554
|
+
return self._read.notes()
|
|
1555
|
+
|
|
1556
|
+
def __getattr__(self, name):
|
|
1557
|
+
if name in _NAMES["song"]["channel"]["sets"]:
|
|
1558
|
+
return self._now.get(name)
|
|
1559
|
+
raise AttributeError(name)
|
|
1560
|
+
|
|
1561
|
+
def __setattr__(self, name, value):
|
|
1562
|
+
sets = _NAMES["song"]["channel"]["sets"]
|
|
1563
|
+
if name not in sets:
|
|
1564
|
+
raise AttributeError("A song's channel has volume, pan, reverb and echo to set, not %s" % _shown(name))
|
|
1565
|
+
self._now[name] = _amount(value, "channel(%d).%s" % (self._n, name), sets[name][0], sets[name][1])
|
|
1566
|
+
self._song._do("mix", channel=self._n, **{name: self._now[name]})
|
|
1567
|
+
|
|
1568
|
+
|
|
1569
|
+
class _Reading:
|
|
1570
|
+
"""What one source of sound is doing, read back: samples(), level() and notes(), as a synth has them."""
|
|
1571
|
+
|
|
1572
|
+
def __init__(self, sound, id):
|
|
1573
|
+
self._sound, self._id = sound, id
|
|
1574
|
+
|
|
1575
|
+
def samples(self):
|
|
1576
|
+
return self._sound.reading(self._id)["samples"]
|
|
1577
|
+
|
|
1578
|
+
def level(self):
|
|
1579
|
+
return self._sound.reading(self._id)["level"]
|
|
1580
|
+
|
|
1581
|
+
def notes(self):
|
|
1582
|
+
return self._sound.reading(self._id)["notes"]
|
|
1583
|
+
|
|
1584
|
+
|
|
1585
|
+
# What a setting is measured in, as a dial shows it (a dial in Hz turns evenly by pitch).
|
|
1586
|
+
_UNITS = {"hz": "Hz", "seconds": "s", "semitones": "st", "cents": "ct", "octaves": "oct"}
|
|
1587
|
+
|
|
1588
|
+
|
|
1589
|
+
class Synth:
|
|
1590
|
+
"""A synth. Its settings are plain attributes: lead.cutoff = 800, print(lead.cutoff)."""
|
|
1591
|
+
|
|
1592
|
+
def __init__(self, sound, id, patch, note, length):
|
|
1593
|
+
put = object.__setattr__
|
|
1594
|
+
put(self, "_sound", sound)
|
|
1595
|
+
put(self, "_id", id)
|
|
1596
|
+
put(self, "_patch", patch)
|
|
1597
|
+
put(self, "_note", note)
|
|
1598
|
+
put(self, "_length", length)
|
|
1599
|
+
put(self, "_sample", None)
|
|
1600
|
+
put(self, "_page", None)
|
|
1601
|
+
put(self, "_macro", None)
|
|
1602
|
+
put(self, "_worn", None) # a voice changer, once one has been put on: {"voice", "under"}
|
|
1603
|
+
put(self, "_plain", None) # what the voice settings were before sound_like() first changed them
|
|
1604
|
+
put(self, "_loop", False)
|
|
1605
|
+
put(self, "_zones", None) # the `zones` command of its sample map, if it has one
|
|
1606
|
+
put(self, "_patterns", {}) # arpPattern and tranceGate, each as its text
|
|
1607
|
+
|
|
1608
|
+
# What is this object's own and not a setting of the engine.
|
|
1609
|
+
_OWN = ("voice", "loop")
|
|
1610
|
+
|
|
1611
|
+
def _name(self, name):
|
|
1612
|
+
"""A setting's name as this synth says it -> the name it has everywhere. (The microphone has words of its own.)"""
|
|
1613
|
+
return name
|
|
1614
|
+
|
|
1615
|
+
# --- settings as attributes
|
|
1616
|
+
def __getattr__(self, name):
|
|
1617
|
+
if name.startswith("_"):
|
|
1618
|
+
raise AttributeError(name)
|
|
1619
|
+
if name == "macro":
|
|
1620
|
+
return self._macro
|
|
1621
|
+
if _pattern_word(name):
|
|
1622
|
+
return self._patterns.get(_pattern_word(name), "")
|
|
1623
|
+
entry = _setting(self._name(name))
|
|
1624
|
+
if entry is None:
|
|
1625
|
+
raise AttributeError("A synth has no setting called %s.%s" % (_shown(name), _meant(name, _SHOWN)))
|
|
1626
|
+
shared = entry["id"] >= 200 and self._sound is not None
|
|
1627
|
+
value = (self._sound.for_all if shared else self._patch).get(entry["id"], entry["default"])
|
|
1628
|
+
if entry["kind"] == "choice":
|
|
1629
|
+
return entry["choices"][int(value)]
|
|
1630
|
+
if entry["kind"] == "bool":
|
|
1631
|
+
return value == 1
|
|
1632
|
+
if entry["scale"] != 1:
|
|
1633
|
+
value = value / entry["scale"]
|
|
1634
|
+
return int(value) if entry["kind"] == "int" and value == int(value) else value
|
|
1635
|
+
|
|
1636
|
+
def __setattr__(self, name, value):
|
|
1637
|
+
if name.startswith("_") or name in self._OWN:
|
|
1638
|
+
object.__setattr__(self, name, value)
|
|
1639
|
+
elif name == "macro":
|
|
1640
|
+
self.set_macro(value)
|
|
1641
|
+
elif _pattern_word(name):
|
|
1642
|
+
self._pattern(_pattern_word(name), value, self._sound.clock())
|
|
1643
|
+
elif name == "samples":
|
|
1644
|
+
self.set_samples(value) # lead.samples = {"C3": "a.wav"}; lead.samples() is still the wave for a scope
|
|
1645
|
+
elif _setting(self._name(name)) is not None:
|
|
1646
|
+
self.set({name: value})
|
|
1647
|
+
else:
|
|
1648
|
+
# A name that is nearly a setting is a slip, and says so; any other name is the program's own to use.
|
|
1649
|
+
near = _nearest(name, _SHOWN)
|
|
1650
|
+
if near:
|
|
1651
|
+
raise AttributeError("A synth has no setting called %s. Did you mean %s?" % (_shown(name), _shown(near)))
|
|
1652
|
+
object.__setattr__(self, name, value)
|
|
1653
|
+
|
|
1654
|
+
def __repr__(self):
|
|
1655
|
+
return "<synth %s>" % self._id
|
|
1656
|
+
|
|
1657
|
+
# --- notes
|
|
1658
|
+
def play(self, note=None, length=None, volume=None, *, pan=None, glide=None, bend=None, slide=None, vibrato=None,
|
|
1659
|
+
after=None, at=None, beat=None):
|
|
1660
|
+
"""A note for a while: lead.play("E4", 0.5). A list of notes is a chord. No note: the synth's own (a drum's).
|
|
1661
|
+
Its pitch can move: glide=True slides to it from the synth's last note, bend="scoop" (or "fall", "dip",
|
|
1662
|
+
{"to": 2, "over": "eighth"}), slide=12, vibrato=True."""
|
|
1663
|
+
s = self._sound
|
|
1664
|
+
moves = _moves_of(s, {"glide": glide, "bend": bend, "slide": slide, "vibrato": vibrato}, "play", note)
|
|
1665
|
+
c = {"op": "note", "id": self._id, "note": None if note is None else _notes_of(note),
|
|
1666
|
+
"len": self._length if length is None else s.length(length, "play: the length"),
|
|
1667
|
+
"t": s.when("play", after, at, beat)}
|
|
1668
|
+
if volume is not None:
|
|
1669
|
+
c["vel"] = _amount(volume, "play: volume")
|
|
1670
|
+
if pan is not None:
|
|
1671
|
+
c["pan"] = _amount(pan, "play: pan", -1, 1)
|
|
1672
|
+
c.update(moves)
|
|
1673
|
+
s.command(c)
|
|
1674
|
+
return self
|
|
1675
|
+
|
|
1676
|
+
def start(self, note=None, volume=None, *, pan=None, glide=None, bend=None, slide=None, vibrato=None,
|
|
1677
|
+
after=None, at=None, beat=None):
|
|
1678
|
+
"""Start a note that stays until it is stopped: held = lead.start("C4"), then held.stop(). Its pitch can move
|
|
1679
|
+
as play()'s can (glide, bend, slide, vibrato), and afterwards: held.slide_to("E4", 0.2), held.vibrato()."""
|
|
1680
|
+
s = self._sound
|
|
1681
|
+
moves = _moves_of(s, {"glide": glide, "bend": bend, "slide": slide, "vibrato": vibrato}, "start", note)
|
|
1682
|
+
s.handles = 1 if s.handles >= 0x7FFFFFF0 else s.handles + 1
|
|
1683
|
+
c = {"op": "note", "id": self._id, "note": None if note is None else _notes_of(note), "len": None,
|
|
1684
|
+
"h": s.handles, "t": s.when("start", after, at, beat)}
|
|
1685
|
+
if volume is not None:
|
|
1686
|
+
c["vel"] = _amount(volume, "start: volume")
|
|
1687
|
+
if pan is not None:
|
|
1688
|
+
c["pan"] = _amount(pan, "start: pan", -1, 1)
|
|
1689
|
+
c.update(moves)
|
|
1690
|
+
s.command(c)
|
|
1691
|
+
return Note(s, self._id, s.handles)
|
|
1692
|
+
|
|
1693
|
+
def arp(self, chord=None, *more, **how):
|
|
1694
|
+
"""A chord from one voice, as a fast arpeggio: the sound of a C64 tune. lead.arp("Cm7"), lead.arp("C4", "minor"),
|
|
1695
|
+
lead.arp(["C4", "E4", "G4"]), then a length in seconds (half a second unless you say), and speed= (steps a
|
|
1696
|
+
second, 50), order= ("up", "down", "up-down", "as written", "random"), volume=, pan=, after=."""
|
|
1697
|
+
return self._arp(chord, more, how, "arp", 0.5)
|
|
1698
|
+
|
|
1699
|
+
def start_arp(self, chord=None, *more, **how):
|
|
1700
|
+
"""The same, staying until it is stopped: arp = lead.start_arp("Am"), arp.change("F"), arp.stop()."""
|
|
1701
|
+
return self._arp(chord, more, how, "start_arp", None)
|
|
1702
|
+
|
|
1703
|
+
def _arp(self, chord, more, how, said, usual):
|
|
1704
|
+
kind, length = how.pop("kind", None), how.pop("length", None)
|
|
1705
|
+
for arg in more: # a kind of chord (text) and a length (a number, or a note length by name), in either order
|
|
1706
|
+
if isinstance(arg, str) and length_beats(arg) is None:
|
|
1707
|
+
kind = arg
|
|
1708
|
+
elif isinstance(arg, str) or _is_number(arg):
|
|
1709
|
+
length = arg
|
|
1710
|
+
told = _arp_words(how)
|
|
1711
|
+
volume, pan = how.pop("volume", None), how.pop("pan", None)
|
|
1712
|
+
when = {k: how.pop(k, None) for k in ("after", "at", "beat")}
|
|
1713
|
+
if how:
|
|
1714
|
+
raise ValueError("%s takes length, kind, %s, volume, pan, after, at and beat, not %s"
|
|
1715
|
+
% (said, ", ".join(_NAMES["arp"]["words"]), ", ".join(how)))
|
|
1716
|
+
notes = _chord_of(chord, kind, said)
|
|
1717
|
+
s = self._sound
|
|
1718
|
+
s.handles = 1 if s.handles >= 0x7FFFFFF0 else s.handles + 1
|
|
1719
|
+
c = {"op": "note", "id": self._id, "note": [notes[0]],
|
|
1720
|
+
"len": usual if length is None else self._sound.length(length, "%s: the length" % said), "h": s.handles,
|
|
1721
|
+
"arp": _arp_of(notes, notes[0], told, said), "t": s.when(said, **when)}
|
|
1722
|
+
if volume is not None:
|
|
1723
|
+
c["vel"] = _amount(volume, "%s: volume" % said)
|
|
1724
|
+
if pan is not None:
|
|
1725
|
+
c["pan"] = _amount(pan, "%s: pan" % said, -1, 1)
|
|
1726
|
+
s.command(c)
|
|
1727
|
+
return Arp(s, self._id, s.handles, notes[0], told, (chord, kind))
|
|
1728
|
+
|
|
1729
|
+
def set_macro(self, macro):
|
|
1730
|
+
"""A step macro: a short list of steps that every note of this synth plays through, fifty a second (the setting
|
|
1731
|
+
macro_rate), each changing the wave, pitch, pulse width, cutoff or volume. It is what makes chip drums.
|
|
1732
|
+
lead.set_macro([{"wave": "noise", "pitch": 24}, {"wave": "square"}, {"pitch": -5, "volume": 0.8}]); None takes it away.
|
|
1733
|
+
{"steps": [...], "loop": True, "slide": True, "release": a step} says more."""
|
|
1734
|
+
_check_macro(macro, "set_macro")
|
|
1735
|
+
object.__setattr__(self, "_macro", macro or None)
|
|
1736
|
+
self._sound.command({"op": "macro", "id": self._id, "macro": macro or None, "t": self._sound.clock()})
|
|
1737
|
+
return self
|
|
1738
|
+
|
|
1739
|
+
def stop(self, note=None):
|
|
1740
|
+
"""Let go of every note of this synth, or of one note: lead.stop("C4")."""
|
|
1741
|
+
c = {"op": "off", "id": self._id, "t": self._sound.clock()}
|
|
1742
|
+
if note is not None:
|
|
1743
|
+
c["note"] = _note_numbers(note)
|
|
1744
|
+
self._sound.command(c)
|
|
1745
|
+
return self
|
|
1746
|
+
|
|
1747
|
+
def note_on(self, note, velocity=0.8, *, at=None):
|
|
1748
|
+
"""A key going down: the note stays until note_off(note). What a keyboard calls. at= is a time of
|
|
1749
|
+
sound_time(): the key goes down on that exact sample, for a sequencer of your own that looks ahead."""
|
|
1750
|
+
s = self._sound
|
|
1751
|
+
s.command({"op": "note", "id": self._id, "note": _note_numbers(note), "len": None,
|
|
1752
|
+
"vel": _amount(velocity, "note_on: velocity"), "t": s.when("note_on", None, at, None)})
|
|
1753
|
+
|
|
1754
|
+
def note_off(self, note, *, at=None):
|
|
1755
|
+
"""A key coming up; at= a time of sound_time()."""
|
|
1756
|
+
s = self._sound
|
|
1757
|
+
s.command({"op": "off", "id": self._id, "note": _note_numbers(note), "t": s.when("note_off", None, at, None)})
|
|
1758
|
+
|
|
1759
|
+
def play_notes(self, notes, step=None, *, repeat=None, volume=None, hold=None, after=None, at=None, beat=None):
|
|
1760
|
+
"""A tune on this synth: lead.play_notes("C4 E4 G4 - C5", 0.25, repeat=True)."""
|
|
1761
|
+
s = self._sound
|
|
1762
|
+
said = "play_notes"
|
|
1763
|
+
# A step given in seconds makes a tune in seconds. A step by name ("eighth"), or none (an eighth), makes
|
|
1764
|
+
# a tune in beats, which keeps to the tempo and changes speed when set_tempo is called as it plays.
|
|
1765
|
+
in_beats = not _is_number(step)
|
|
1766
|
+
if step is None:
|
|
1767
|
+
step = 0.5
|
|
1768
|
+
elif in_beats:
|
|
1769
|
+
if length_beats(step) is None:
|
|
1770
|
+
raise ValueError("%s: the time of a step needs %s, not %s" % (said, _NOT_A_LENGTH, _shown(step)))
|
|
1771
|
+
step = length_beats(step)
|
|
1772
|
+
else:
|
|
1773
|
+
step = _seconds(step, "%s: the time of a step" % said)
|
|
1774
|
+
steps, length = _steps_of(notes, step, 1 if in_beats else 60 / s.tempo, hold, volume, said)
|
|
1775
|
+
id = s.id("t")
|
|
1776
|
+
times = True if repeat is True else 1 if repeat in (None, False) else _amount(repeat, "%s: repeat" % said, 1, 1e9)
|
|
1777
|
+
c = {"op": "tune", "id": id, "synth": self._id, "steps": steps, "length": length, "repeat": times,
|
|
1778
|
+
"t": s.when(said, after, at, beat)}
|
|
1779
|
+
if in_beats:
|
|
1780
|
+
c["beats"] = True
|
|
1781
|
+
s.command(c)
|
|
1782
|
+
return Tune(s, id)
|
|
1783
|
+
|
|
1784
|
+
# --- a tune a line at a time
|
|
1785
|
+
def note(self, what=None, length=None, *, volume=None, pan=None):
|
|
1786
|
+
"""The next note of this synth's line: it begins where the last one ended. lead.note("C4", "quarter"), then
|
|
1787
|
+
lead.note("E4", "eighth"). The length is a note length by name or a number of beats (a quarter with nothing
|
|
1788
|
+
said). A chord's name or a list of notes plays them together."""
|
|
1789
|
+
if what is None:
|
|
1790
|
+
raise ValueError('note needs a note, as in note("C4", "quarter"). For a silence, use rest("quarter")')
|
|
1791
|
+
self._sound.write(self, _notes_of(what), length, volume, pan, "note")
|
|
1792
|
+
return self
|
|
1793
|
+
|
|
1794
|
+
def chord(self, name=None, kind=None, length=None, *, volume=None, pan=None):
|
|
1795
|
+
"""The next thing in the line is a chord: lead.chord("Cm", "whole"), lead.chord("C4", "minor", "half"),
|
|
1796
|
+
lead.chord(["C4", "E4", "G4"])."""
|
|
1797
|
+
if kind is not None and (not isinstance(kind, str) or length_beats(kind) is not None):
|
|
1798
|
+
kind, length = None, kind # chord("Cm", "whole"): the second thing is the length
|
|
1799
|
+
if name is None:
|
|
1800
|
+
raise ValueError('chord needs a chord, as in chord("Cm", "whole")')
|
|
1801
|
+
self._sound.write(self, chord_notes(name, kind), length, volume, pan, "chord")
|
|
1802
|
+
return self
|
|
1803
|
+
|
|
1804
|
+
def rest(self, length=None):
|
|
1805
|
+
"""A silence in the line: lead.rest("quarter")."""
|
|
1806
|
+
self._sound.write(self, None, length, None, None, "rest")
|
|
1807
|
+
return self
|
|
1808
|
+
|
|
1809
|
+
def start_line(self, *, after=None, at=None, beat=None):
|
|
1810
|
+
"""Begin this synth's line again from now (or after=, at=, beat=): what is written next starts there."""
|
|
1811
|
+
s = self._sound
|
|
1812
|
+
s.line_at(self._id)
|
|
1813
|
+
s.lines[self._id] = max(s.when("start_line", after, at, beat), s.turn)
|
|
1814
|
+
return self
|
|
1815
|
+
|
|
1816
|
+
# --- settings
|
|
1817
|
+
def set(self, settings=None, value=None, *, glide=None, slide=None, after=None, at=None, beat=None, **more):
|
|
1818
|
+
"""Change several settings at once: lead.set(cutoff=800, resonance=0.4). glide=2 slides there over two seconds
|
|
1819
|
+
(slide=2 is the same, an older word for it). lead.set("cutoff", 800) and lead.set({"cutoff": 800}) are the
|
|
1820
|
+
same thing written other ways."""
|
|
1821
|
+
if isinstance(settings, str):
|
|
1822
|
+
given = {settings: value}
|
|
1823
|
+
elif isinstance(settings, dict):
|
|
1824
|
+
given = dict(settings)
|
|
1825
|
+
elif settings is None:
|
|
1826
|
+
given = {}
|
|
1827
|
+
else:
|
|
1828
|
+
raise ValueError("set needs settings, as in set(cutoff=800), not %s" % _shown(settings))
|
|
1829
|
+
given.update(more)
|
|
1830
|
+
if not given:
|
|
1831
|
+
raise ValueError("set needs settings, as in set(cutoff=800)")
|
|
1832
|
+
s = self._sound
|
|
1833
|
+
t = s.when("set", after, at, beat)
|
|
1834
|
+
given = self._given(given, t)
|
|
1835
|
+
if not given:
|
|
1836
|
+
return self
|
|
1837
|
+
_patch_of(given, "set", True, self._patch)
|
|
1838
|
+
s.share(self._patch, given)
|
|
1839
|
+
c = {"op": "set", "id": self._id, "set": _sendable(given), "t": t}
|
|
1840
|
+
if glide is None:
|
|
1841
|
+
glide = slide
|
|
1842
|
+
if glide is not None:
|
|
1843
|
+
c["glide"] = _amount(glide, "set: glide", 0, 60)
|
|
1844
|
+
s.command(c)
|
|
1845
|
+
return self
|
|
1846
|
+
|
|
1847
|
+
def _given(self, given, at):
|
|
1848
|
+
"""What set() was given, ready to send: a voice is put on, and the rest go by the names they have everywhere."""
|
|
1849
|
+
out = {}
|
|
1850
|
+
for name, value in given.items():
|
|
1851
|
+
if _key(name) == "voice":
|
|
1852
|
+
self._wear(value, at)
|
|
1853
|
+
elif _pattern_word(name):
|
|
1854
|
+
self._pattern(_pattern_word(name), value, at)
|
|
1855
|
+
else:
|
|
1856
|
+
out[self._name(name)] = value
|
|
1857
|
+
return _with_samples(self._sound, out, self._patch, "set")
|
|
1858
|
+
|
|
1859
|
+
# --- patterns
|
|
1860
|
+
def _pattern(self, which, pattern, at):
|
|
1861
|
+
"""PATTERNS. lead.arp_pattern = "1 - 3 2 _ 1 3 5^" (or "trance", or a list of steps) is the rhythm every
|
|
1862
|
+
arpeggio of this synth follows unless it says another, the one that is sounding from its next step.
|
|
1863
|
+
pad.trance_gate = "x.x.xx.x" is the trance gate: the synth's sound turned up and down in that pattern of steps,
|
|
1864
|
+
in time with the tempo, so a held chord becomes a rhythm. Each reads back as its text, "" for none, and None
|
|
1865
|
+
or "off" takes it away."""
|
|
1866
|
+
text = (_arp_pattern_text if which == "arpPattern" else _gate_pattern_text)(pattern, _snake(which))
|
|
1867
|
+
self._patterns[which] = text or ""
|
|
1868
|
+
# The gate opens and closes all the way unless its depth says otherwise, as the player will make it.
|
|
1869
|
+
if which == "tranceGate":
|
|
1870
|
+
depth = _setting("gate.depth")
|
|
1871
|
+
if depth and text and not self._patch.get(depth["id"]):
|
|
1872
|
+
self._patch[depth["id"]] = 1
|
|
1873
|
+
elif depth and not text:
|
|
1874
|
+
self._patch[depth["id"]] = 0
|
|
1875
|
+
self._sound.command({"op": "set", "id": self._id, which: text or "", "t": at})
|
|
1876
|
+
|
|
1877
|
+
def set_trance_gate(self, pattern="eighths", **how):
|
|
1878
|
+
"""The trance gate and its settings in one line: pad.set_trance_gate("x.x.xx.x", rate="eighth", depth=0.8,
|
|
1879
|
+
smooth=0.02). A rate may be a note length. The steps begin at set_tempo."""
|
|
1880
|
+
words = _NAMES["tranceGate"]["words"]
|
|
1881
|
+
given = {}
|
|
1882
|
+
for word, value in how.items():
|
|
1883
|
+
if word not in words:
|
|
1884
|
+
raise ValueError("set_trance_gate: there is no %s to say here.%s It may be: %s" % (_shown(word), _meant(word, words), ", ".join(words)))
|
|
1885
|
+
given["gate." + word] = 1 / _beats_long(value, "set_trance_gate: rate") if word == "rate" and isinstance(value, str) else value
|
|
1886
|
+
self._pattern("tranceGate", pattern, self._sound.clock())
|
|
1887
|
+
if given:
|
|
1888
|
+
self.set(given)
|
|
1889
|
+
return self
|
|
1890
|
+
|
|
1891
|
+
def arp_step(self):
|
|
1892
|
+
"""Which step of its arpeggio this synth is on now, counted from 0 (within the pattern, if it has one), or -1
|
|
1893
|
+
with none sounding: for drawing it. A frame late in a Pyctures scene, as samples() is."""
|
|
1894
|
+
return self._sound.reading(self._id).get("arpStep", -1)
|
|
1895
|
+
|
|
1896
|
+
def trance_gate_step(self):
|
|
1897
|
+
"""Which step of its trance gate this synth is on now, counted from 0, or -1 with no gate. A frame late."""
|
|
1898
|
+
return self._sound.reading(self._id).get("tranceGateStep", -1)
|
|
1899
|
+
|
|
1900
|
+
def get(self, name):
|
|
1901
|
+
"""A setting by its name: lead.get("cutoff") is lead.cutoff."""
|
|
1902
|
+
if _key(name) == "arpstep":
|
|
1903
|
+
return self.arp_step()
|
|
1904
|
+
if _key(name) == "trancegatestep":
|
|
1905
|
+
return self.trance_gate_step()
|
|
1906
|
+
if _pattern_word(name):
|
|
1907
|
+
return self._patterns.get(_pattern_word(name), "")
|
|
1908
|
+
return getattr(self, _snake(name) if _key(name) in ("voice", "loop", "listening") else name)
|
|
1909
|
+
|
|
1910
|
+
# --- a voice changer
|
|
1911
|
+
@property
|
|
1912
|
+
def voice(self):
|
|
1913
|
+
"""A voice changer on everything this synth plays: lead.voice = "robot". The voices are robot, chipmunk,
|
|
1914
|
+
monster, cave, telephone, alien and whisper (voice_names()), and "none" takes it off. Each is a handful of
|
|
1915
|
+
settings (the pitch shifter, the ring modulator, the comb, the filter, the echo), which can be turned
|
|
1916
|
+
further once the voice is on. Choosing another voice first puts back what the last one changed."""
|
|
1917
|
+
return self._worn["voice"] if self._worn else "none"
|
|
1918
|
+
|
|
1919
|
+
@voice.setter
|
|
1920
|
+
def voice(self, name):
|
|
1921
|
+
self._wear(name, self._sound.clock())
|
|
1922
|
+
|
|
1923
|
+
def _wear(self, name, at=None, send=True):
|
|
1924
|
+
# A control that keeps a choice as its number in the list gives the number.
|
|
1925
|
+
choices = ["none"] + list(_VOICES["rows"])
|
|
1926
|
+
if _is_number(name) and 0 <= name < len(choices) and name == int(name):
|
|
1927
|
+
name = choices[int(name)]
|
|
1928
|
+
voice = _voice_name(name) if isinstance(name, str) or name is None or name is False else None
|
|
1929
|
+
if voice is None:
|
|
1930
|
+
names = list(_VOICES["rows"])
|
|
1931
|
+
raise ValueError('%s is not a voice.%s The voices are: %s, and "none"' % (_shown(name), _meant(name, names), ", ".join(names)))
|
|
1932
|
+
if self._worn is None:
|
|
1933
|
+
object.__setattr__(self, "_worn", {"voice": "none", "under": None})
|
|
1934
|
+
# A voice may change what every synth shares (the cave's reverb), and takes it back when it is taken off.
|
|
1935
|
+
self._patch.update(self._sound.for_all)
|
|
1936
|
+
_wear_voice(self._worn, self._patch, voice)
|
|
1937
|
+
self._sound.share(self._patch)
|
|
1938
|
+
if send:
|
|
1939
|
+
self._sound.command({"op": "set", "id": self._id, "voice": voice, "t": self._sound.clock() if at is None else at})
|
|
1940
|
+
|
|
1941
|
+
def set_samples(self, files, **o):
|
|
1942
|
+
"""Several recordings for this synth to play in the place of its wave: lead.samples = {"C2": "piano-C2.wav",
|
|
1943
|
+
"C3": "piano-C3.wav"}, or lead.set_samples(files, crossfade=3). What load_samples takes; None takes them away."""
|
|
1944
|
+
s = self._sound
|
|
1945
|
+
if files is None or files is False:
|
|
1946
|
+
if self._zones:
|
|
1947
|
+
s.command({"op": "zones", "id": self._id, "zones": None, "t": s.clock()})
|
|
1948
|
+
object.__setattr__(self, "_zones", None)
|
|
1949
|
+
return self
|
|
1950
|
+
zones = _zones_command(s, self._id, files, _map_words(o, "set_samples", ""), "set_samples", False)
|
|
1951
|
+
self._patch[_setting("sample")["id"]] = 0
|
|
1952
|
+
s.command(zones)
|
|
1953
|
+
object.__setattr__(self, "_zones", zones)
|
|
1954
|
+
if self.wave != "sample":
|
|
1955
|
+
self.set({"wave": "sample"})
|
|
1956
|
+
return self
|
|
1957
|
+
|
|
1958
|
+
def zones(self):
|
|
1959
|
+
"""This synth's recordings as it was given them: [{"file", "note", ...}], or None with none. (The keys each
|
|
1960
|
+
was worked out to play are the page's to say: a control tied to the synth reads them there.)"""
|
|
1961
|
+
if not self._zones:
|
|
1962
|
+
return None
|
|
1963
|
+
return [{k: (list(v) if isinstance(v, list) else v) for k, v in zone.items() if k != "sample"} for zone in self._zones["zones"]]
|
|
1964
|
+
|
|
1965
|
+
def sound_like(self, other):
|
|
1966
|
+
"""Give this the voice and the voice effects of another: take.sound_like(mic) makes a recording sound as the
|
|
1967
|
+
microphone does now, whatever voice has been chosen and however its dials have been turned since."""
|
|
1968
|
+
if not isinstance(other, Synth):
|
|
1969
|
+
raise ValueError("sound_like needs the microphone or a synth to sound like, as in take.sound_like(mic), not %s" % _shown(other))
|
|
1970
|
+
_dress_like(self, other)
|
|
1971
|
+
return self
|
|
1972
|
+
|
|
1973
|
+
def record(self, length=None, **how):
|
|
1974
|
+
"""Record what this synth plays into a sample: take = lead.record(4). On the microphone, it records the
|
|
1975
|
+
microphone. See record_sound."""
|
|
1976
|
+
return _record_from(self._sound, dict(how, length=length, source=self), "record")
|
|
1977
|
+
|
|
1978
|
+
@property
|
|
1979
|
+
def loop(self):
|
|
1980
|
+
"""A sample or a recording that goes round and round while its note is held: drum.loop = True, or
|
|
1981
|
+
[first frame, the frame after the last]."""
|
|
1982
|
+
return self._loop
|
|
1983
|
+
|
|
1984
|
+
@loop.setter
|
|
1985
|
+
def loop(self, on):
|
|
1986
|
+
object.__setattr__(self, "_loop", list(on) if isinstance(on, (list, tuple)) else bool(on))
|
|
1987
|
+
if self._sample:
|
|
1988
|
+
self._sound.command({"op": "sample", "id": self._sample, "loop": self._loop, "t": self._sound.clock()})
|
|
1989
|
+
|
|
1990
|
+
# --- what it is doing now, for a program that draws it
|
|
1991
|
+
def samples(self):
|
|
1992
|
+
"""The wave this synth is making, a list of numbers from -1 to 1: draw it for a scope. In a Pyctures scene
|
|
1993
|
+
it is what the page said with this frame, so a frame late, and empty until the page has first said."""
|
|
1994
|
+
return self._sound.reading(self._id)["samples"]
|
|
1995
|
+
|
|
1996
|
+
def level(self):
|
|
1997
|
+
"""How loud this synth is, [left, right], each from 0 to 1: a meter shows it. A frame late, as samples() is."""
|
|
1998
|
+
return self._sound.reading(self._id)["level"]
|
|
1999
|
+
|
|
2000
|
+
def notes(self):
|
|
2001
|
+
"""The notes this synth is sounding, as numbers: a keyboard lights them. A frame late, as samples() is."""
|
|
2002
|
+
return self._sound.reading(self._id)["notes"]
|
|
2003
|
+
|
|
2004
|
+
def describe(self, name):
|
|
2005
|
+
"""What a setting can be: {"min", "max", "default", "kind", "choices", "unit"}. A dial asks this."""
|
|
2006
|
+
if _key(name) == "voice":
|
|
2007
|
+
return {"name": "voice", "kind": "choice", "min": 0, "max": len(_VOICES["rows"]), "default": "none",
|
|
2008
|
+
"choices": ["none"] + list(_VOICES["rows"])}
|
|
2009
|
+
if _pattern_word(name):
|
|
2010
|
+
return _describe_pattern(_pattern_word(name), self._patterns.get(_pattern_word(name)))
|
|
2011
|
+
entry = _setting(self._name(name))
|
|
2012
|
+
if entry is None:
|
|
2013
|
+
return None
|
|
2014
|
+
d = {"name": _snake(name), "kind": entry["kind"], "min": entry["min"] / entry["scale"],
|
|
2015
|
+
"max": entry["max"] / entry["scale"], "default": entry["default"] / entry["scale"]}
|
|
2016
|
+
if entry.get("choices"):
|
|
2017
|
+
d["choices"] = list(entry["choices"])
|
|
2018
|
+
# The other words a choice may be set by: {"square": "pulse"}.
|
|
2019
|
+
aliases = {word: to for word, to in _NAMES.get("choiceWords", {}).items() if to in d["choices"] and word not in d["choices"]}
|
|
2020
|
+
if aliases:
|
|
2021
|
+
d["aliases"] = aliases
|
|
2022
|
+
# "sample" is a wave only for a synth that has been given a sample to play.
|
|
2023
|
+
holds = _setting(entry["name"].replace("wave", "sample")) if entry["name"] in ("osc1.wave", "osc2.wave") else None
|
|
2024
|
+
if holds is not None and holds["id"] not in getattr(self, "_patch", {}):
|
|
2025
|
+
d["choices"] = [c for c in d["choices"] if c != "sample"]
|
|
2026
|
+
d["max"] = len(d["choices"]) - 1
|
|
2027
|
+
unit = "octaves" if entry["scale"] == 12 else entry.get("unit")
|
|
2028
|
+
if _UNITS.get(unit):
|
|
2029
|
+
d["unit"] = _UNITS[unit]
|
|
2030
|
+
# A time from nothing to twenty seconds is turned evenly by how much longer it gets, from a thousandth of a second.
|
|
2031
|
+
if unit == "seconds" and entry["min"] == 0 and entry["max"] >= 1:
|
|
2032
|
+
d["min"], d["log"] = 0.001, True
|
|
2033
|
+
# Values that mean something else (a reverb_decay of 0 is "auto"), as synths.js describes them. Where nothing
|
|
2034
|
+
# is one of them, the dial still starts from nothing: it is turned by ratio from a thousandth just above it.
|
|
2035
|
+
if entry.get("labels"):
|
|
2036
|
+
d["labels"] = {}
|
|
2037
|
+
for at, word in entry["labels"].items():
|
|
2038
|
+
at = float(at) / entry["scale"]
|
|
2039
|
+
d["labels"][int(at) if at == int(at) else at] = word
|
|
2040
|
+
if d.get("log") and 0 in d["labels"]:
|
|
2041
|
+
d["min"], d["log_from"] = 0, 0.001
|
|
2042
|
+
return d
|
|
2043
|
+
|
|
2044
|
+
def _told(self, name, value):
|
|
2045
|
+
"""A control in the page changed a setting by hand: remember it, so lead.cutoff reads what the dial says."""
|
|
2046
|
+
try:
|
|
2047
|
+
if _key(name) == "voice":
|
|
2048
|
+
self._wear(value, send=False)
|
|
2049
|
+
else:
|
|
2050
|
+
_patch_of({self._name(name): value}, "set", False, self._patch)
|
|
2051
|
+
self._sound.share(self._patch, [self._name(name)])
|
|
2052
|
+
except (ValueError, TypeError):
|
|
2053
|
+
pass
|
|
2054
|
+
|
|
2055
|
+
# --- MIDI: a keyboard plays this synth, its notes go out as MIDI, and a knob turns a setting
|
|
2056
|
+
def play_from_midi(self, on=True, *, channel=None, vibrato=None):
|
|
2057
|
+
"""A MIDI keyboard plays this synth: lead.play_from_midi(). It follows the notes, how hard each key is hit,
|
|
2058
|
+
the pitch wheel, the modulation wheel and the sustain pedal, where the devices are, so it is as quick with
|
|
2059
|
+
Python busy or on another computer. channel=2 listens to one channel alone; lead.play_from_midi(False) stops."""
|
|
2060
|
+
self._sound.midi.play(self, on, channel, vibrato)
|
|
2061
|
+
return self
|
|
2062
|
+
|
|
2063
|
+
def send_to_midi(self, on=True, *, channel=None, output=None):
|
|
2064
|
+
"""What this synth plays, tunes and all, also goes out as MIDI: bass.send_to_midi(channel=2).
|
|
2065
|
+
bass.send_to_midi(False) stops."""
|
|
2066
|
+
self._sound.midi.through(self, on, channel, output)
|
|
2067
|
+
return self
|
|
2068
|
+
|
|
2069
|
+
def map_midi(self, control=None, setting=None, on_change=None, *, min=None, max=None, channel=None):
|
|
2070
|
+
"""Knob or slider number `control` turns a setting, through the setting's whole range: lead.map_midi(74, "cutoff").
|
|
2071
|
+
The page changes the synth itself; lead.cutoff reads what the knob set, a frame later. A function given
|
|
2072
|
+
third is called with each new value; min= and max= narrow the range."""
|
|
2073
|
+
if not isinstance(control, int) or isinstance(control, bool) or not 0 <= control <= 119:
|
|
2074
|
+
raise ValueError('map_midi needs the number of a knob or slider, from 0 to 119, and then a setting, as in '
|
|
2075
|
+
'lead.map_midi(74, "cutoff"), not %s' % _shown(control))
|
|
2076
|
+
self._sound.midi.turn(self, "map", setting, on_change, min, max, channel, control)
|
|
2077
|
+
return self
|
|
2078
|
+
|
|
2079
|
+
def learn(self, setting=None, on_change=None, *, min=None, max=None, channel=None):
|
|
2080
|
+
"""The next knob or slider that is moved turns this setting from then on: lead.learn("cutoff")."""
|
|
2081
|
+
self._sound.midi.turn(self, "learn", setting, on_change, min, max, channel)
|
|
2082
|
+
return self
|
|
2083
|
+
|
|
2084
|
+
def midi_input(self):
|
|
2085
|
+
"""The device and the channel this synth plays from, for a select on the screen: see midi_input()."""
|
|
2086
|
+
return _midi_port("input", self)
|
|
2087
|
+
|
|
2088
|
+
def midi_output(self):
|
|
2089
|
+
"""Where this synth's notes go when it sends them on, for a select on the screen: see midi_output()."""
|
|
2090
|
+
return _midi_port("output", self)
|
|
2091
|
+
|
|
2092
|
+
def unmap_midi(self, setting=None):
|
|
2093
|
+
"""No knob turns this setting any more: lead.unmap_midi("cutoff"). With no name: none of this synth's."""
|
|
2094
|
+
self._sound.midi.unmap(self, setting)
|
|
2095
|
+
return self
|
|
2096
|
+
|
|
2097
|
+
@property
|
|
2098
|
+
def page(self):
|
|
2099
|
+
"""The page's own object for this synth, in a Pyctures scene: its dials and keys act on it there, at once."""
|
|
2100
|
+
if self._page is None:
|
|
2101
|
+
object.__setattr__(self, "_page", self._sound.transport.page(self._id))
|
|
2102
|
+
return self._page
|
|
2103
|
+
|
|
2104
|
+
def remove(self):
|
|
2105
|
+
"""Finished with it. It can still be played afterwards: it takes a place again."""
|
|
2106
|
+
self._sound.command({"op": "free", "id": self._id, "t": self._sound.clock()})
|
|
2107
|
+
return self
|
|
2108
|
+
|
|
2109
|
+
|
|
2110
|
+
_nothing = object()
|
|
2111
|
+
|
|
2112
|
+
|
|
2113
|
+
# ---------------------------------------------------------------- sample maps: several recordings as one instrument
|
|
2114
|
+
|
|
2115
|
+
_NAMED_NOTE = re.compile(r"(?<![A-Za-z0-9#])([A-Ga-g])(#|s|b)?(-?\d)(?![A-Za-z0-9#])")
|
|
2116
|
+
_NAMED_KEY = re.compile(r"(?<![A-Za-z0-9.])(\d{1,3})(?![A-Za-z0-9])")
|
|
2117
|
+
|
|
2118
|
+
|
|
2119
|
+
def note_in_name(file):
|
|
2120
|
+
"""The note in a sound file's name: "piano-C2.wav" is 36, and so are "Fs2", "F#2" and "Gb2" for their note, and
|
|
2121
|
+
a number alone ("60.wav", "kick_36.wav") is that key. It is the last such word in the name; None if there is none."""
|
|
2122
|
+
name = re.sub(r"\.[A-Za-z0-9]+$", "", re.sub(r"^.*[\\/]", "", str(file)))
|
|
2123
|
+
words = _NAMED_NOTE.findall(name)
|
|
2124
|
+
if words:
|
|
2125
|
+
letter, mark, octave = words[-1]
|
|
2126
|
+
try:
|
|
2127
|
+
return note_number(letter.upper() + ("#" if mark == "s" else mark) + octave)
|
|
2128
|
+
except ValueError:
|
|
2129
|
+
pass
|
|
2130
|
+
numbers = [int(n) for n in _NAMED_KEY.findall(name) if int(n) <= 127]
|
|
2131
|
+
return numbers[-1] if numbers else None
|
|
2132
|
+
|
|
2133
|
+
|
|
2134
|
+
# What may be said of one recording, and of all of them, in Python's spelling (synths.js: ZONE_WORDS and MAP_WORDS).
|
|
2135
|
+
_ZONE_WORDS = [_snake(w) for w in _NAMES["zones"]["words"]]
|
|
2136
|
+
_MAP_WORDS = [_snake(w) for w in _NAMES["zones"]["options"]]
|
|
2137
|
+
_ZONE_EXAMPLE = 'as in load_samples({"C2": "piano-C2.wav", "C3": "piano-C3.wav"}) or load_samples(["piano-C2.wav", "piano-C3.wav"])'
|
|
2138
|
+
|
|
2139
|
+
|
|
2140
|
+
def _zones_given(s, files, o, said):
|
|
2141
|
+
"""WHAT load_samples WAS GIVEN, as the zones of a `zones` command, with each file's `sample` command written on
|
|
2142
|
+
the way. The three ways of saying it: {"C2": "a.wav", "F#2": ["b1.wav", "b2.wav"]} (the note each was played at,
|
|
2143
|
+
and takes that go in turn), ["piano-C2.wav", "piano-Fs2.wav"] (the note is in the name), and a list of
|
|
2144
|
+
{"file", "note", "from", "to", "loud", "tune", "volume", "pan", "release", "loop", "one_shot", "start"}."""
|
|
2145
|
+
if not isinstance(files, (dict, list, tuple)):
|
|
2146
|
+
raise ValueError("%s needs the sound files of an instrument and the note each was played at, %s, not %s"
|
|
2147
|
+
% (said, _ZONE_EXAMPLE, _shown(files)))
|
|
2148
|
+
given = []
|
|
2149
|
+
|
|
2150
|
+
def one(what, note=None):
|
|
2151
|
+
if isinstance(what, str) and what.strip():
|
|
2152
|
+
given.append(dict({"file": what}, **({} if note is None else {"note": note})))
|
|
2153
|
+
elif isinstance(what, (list, tuple)) and note is not None:
|
|
2154
|
+
for take in what:
|
|
2155
|
+
one(take, note)
|
|
2156
|
+
elif not isinstance(what, dict) or not isinstance(what.get("file"), str) or not what["file"].strip():
|
|
2157
|
+
raise ValueError('%s: each recording is the name of a sound file, or {"file": "soft-C4.wav", "note": "C4", '
|
|
2158
|
+
'"loud": [0, 0.5]}, not %s' % (said, _shown(what)))
|
|
2159
|
+
else:
|
|
2160
|
+
zone = {_snake(word): value for word, value in what.items()}
|
|
2161
|
+
for word in zone:
|
|
2162
|
+
if word not in _ZONE_WORDS:
|
|
2163
|
+
raise ValueError("%s: a recording has no %s.%s It may have: %s"
|
|
2164
|
+
% (said, _shown(word), _meant(word, _ZONE_WORDS), ", ".join(_ZONE_WORDS)))
|
|
2165
|
+
if zone.get("note") is None and note is not None:
|
|
2166
|
+
zone["note"] = note
|
|
2167
|
+
given.append(zone)
|
|
2168
|
+
if isinstance(files, dict):
|
|
2169
|
+
for note, what in files.items():
|
|
2170
|
+
one(what, note)
|
|
2171
|
+
else:
|
|
2172
|
+
for what in files:
|
|
2173
|
+
one(what)
|
|
2174
|
+
if not given:
|
|
2175
|
+
raise ValueError("%s was given no sound files. It needs at least one, %s" % (said, _ZONE_EXAMPLE))
|
|
2176
|
+
loaded, zones = {}, []
|
|
2177
|
+
for g in given:
|
|
2178
|
+
note = note_in_name(g["file"]) if g.get("note") is None else note_number(g["note"])
|
|
2179
|
+
if note is None:
|
|
2180
|
+
raise ValueError("%s: the note of %s cannot be read from its name. Put the note in the name (piano-C4.wav, "
|
|
2181
|
+
'piano-Fs2.wav, 60.wav), or say it: load_samples({"C4": %s})' % (said, _shown(g["file"]), _shown(g["file"])))
|
|
2182
|
+
zone = {"file": g["file"], "note": note}
|
|
2183
|
+
for edge in ("from", "to"):
|
|
2184
|
+
if g.get(edge) is not None:
|
|
2185
|
+
zone[edge] = note_number(g[edge])
|
|
2186
|
+
if g.get("loud") is not None:
|
|
2187
|
+
loud = g["loud"]
|
|
2188
|
+
if (not isinstance(loud, (list, tuple)) or len(loud) != 2 or not all(_is_number(v) and 0 <= v <= 1 for v in loud)
|
|
2189
|
+
or loud[0] > loud[1]):
|
|
2190
|
+
raise ValueError("%s: loud is how hard a note is played to get this recording, from and to, as in "
|
|
2191
|
+
'"loud": [0, 0.5], not %s' % (said, _shown(loud)))
|
|
2192
|
+
zone["loud"] = list(loud)
|
|
2193
|
+
for word, least, most in (("tune", -1200, 1200), ("volume", 0, 4), ("pan", -1, 1), ("release", 0, 20)):
|
|
2194
|
+
if g.get(word) is not None:
|
|
2195
|
+
zone[word] = _amount(g[word], "%s: %s" % (said, word), least, most)
|
|
2196
|
+
if g.get("start") is not None:
|
|
2197
|
+
zone["start"] = _amount(g["start"], "%s: start" % said, 0, 1e9)
|
|
2198
|
+
if g.get("one_shot") is not None:
|
|
2199
|
+
zone["oneShot"] = bool(g["one_shot"])
|
|
2200
|
+
if g.get("as_recorded") is not None:
|
|
2201
|
+
zone["asRecorded"] = bool(g["as_recorded"])
|
|
2202
|
+
# One file is loaded once, however many zones play it.
|
|
2203
|
+
loop = g.get("loop", o.get("loop"))
|
|
2204
|
+
k = json.dumps([g["file"], note, loop or 0])
|
|
2205
|
+
if k not in loaded:
|
|
2206
|
+
loaded[k] = _sample_file(s, g["file"], note, loop, zone=True)
|
|
2207
|
+
zone["sample"] = loaded[k]
|
|
2208
|
+
zones.append(zone)
|
|
2209
|
+
return zones
|
|
2210
|
+
|
|
2211
|
+
|
|
2212
|
+
def _zones_command(s, id, files, o, said, own_release):
|
|
2213
|
+
"""The `zones` command for a synth, which the synth keeps to read its zones back."""
|
|
2214
|
+
c = {"op": "zones", "id": id, "zones": _zones_given(s, files, o, said), "t": s.clock()}
|
|
2215
|
+
if o.get("kit"):
|
|
2216
|
+
c["kit"] = True
|
|
2217
|
+
f = o.get("crossfade")
|
|
2218
|
+
if f is not None and f is not False:
|
|
2219
|
+
number = _is_number(f) and f >= 0
|
|
2220
|
+
both = (isinstance(f, dict) and set(f) <= {"keys", "loud"} and (f.get("keys") is None or (_is_number(f["keys"]) and f["keys"] >= 0))
|
|
2221
|
+
and (f.get("loud") is None or (_is_number(f["loud"]) and 0 <= f["loud"] <= 1)))
|
|
2222
|
+
if not number and not both:
|
|
2223
|
+
raise ValueError('%s: crossfade is how many keys neighbouring recordings fade into each other over, as in '
|
|
2224
|
+
'crossfade=3, or {"keys": 3, "loud": 0.1} for the loudness layers too, not %s' % (said, _shown(f)))
|
|
2225
|
+
c["crossfade"] = f
|
|
2226
|
+
if o.get("one_shot") is not None:
|
|
2227
|
+
c["oneShot"] = bool(o["one_shot"])
|
|
2228
|
+
# A release said for the whole synth is every zone's; with none said, each zone has one that suits its note.
|
|
2229
|
+
if own_release:
|
|
2230
|
+
c["release"] = False
|
|
2231
|
+
return c
|
|
2232
|
+
|
|
2233
|
+
|
|
2234
|
+
def _map_words(o, said, example):
|
|
2235
|
+
"""What was said of all the recordings, in one spelling; anything else is refused."""
|
|
2236
|
+
o = {_snake(word): value for word, value in (o or {}).items()}
|
|
2237
|
+
for word in o:
|
|
2238
|
+
if word not in _MAP_WORDS:
|
|
2239
|
+
raise ValueError("%s: there is no %s to say here.%s It may be: %s. A setting of the synth is set on the synth, "
|
|
2240
|
+
"as in lead.release = 0.5" % (said, _shown(word), _meant(word, _MAP_WORDS), ", ".join(_MAP_WORDS)))
|
|
2241
|
+
return o
|
|
2242
|
+
|
|
2243
|
+
|
|
2244
|
+
def _make_synth(s, preset, settings):
|
|
2245
|
+
names = [_snake(n) for n in _NAMES["synths"] if n != "sample"]
|
|
2246
|
+
if preset is not None:
|
|
2247
|
+
found = _PRESETS.get(_key(preset)) if isinstance(preset, str) else None
|
|
2248
|
+
if found is None or found == "sample":
|
|
2249
|
+
raise ValueError("make_synth(%s): there is no synth called %s.%s The synths are: %s"
|
|
2250
|
+
% (_shown(preset), _shown(preset), _meant(preset, names), ", ".join(names)))
|
|
2251
|
+
preset = found
|
|
2252
|
+
said = "make_synth(%s)" % (_shown(_snake(preset)) if preset else "")
|
|
2253
|
+
macro = settings.pop("macro", _nothing)
|
|
2254
|
+
if macro is not _nothing:
|
|
2255
|
+
_check_macro(macro, said)
|
|
2256
|
+
# Several recordings for its wave: what load_samples takes.
|
|
2257
|
+
recordings = settings.pop("samples", None)
|
|
2258
|
+
patch = _patch_of(_NAMES["plain"], said, False)
|
|
2259
|
+
made = _NAMES["synths"][preset] if preset else {}
|
|
2260
|
+
_patch_of(made, said, False, patch)
|
|
2261
|
+
# With a sound file for its sample, `note` and `loop` are the recording's: the note it is at, and whether it goes round.
|
|
2262
|
+
file = isinstance(settings.get("sample"), str)
|
|
2263
|
+
note, loop = (settings.pop("note", None), settings.pop("loop", None)) if file else (None, None)
|
|
2264
|
+
settings = _with_samples(s, settings, patch, said, note, loop)
|
|
2265
|
+
_patch_of(settings, said, True, patch)
|
|
2266
|
+
s.share(patch, settings)
|
|
2267
|
+
id = s.id("s")
|
|
2268
|
+
c = {"op": "synth", "id": id, "t": s.clock()}
|
|
2269
|
+
if preset:
|
|
2270
|
+
c["preset"] = preset
|
|
2271
|
+
if settings:
|
|
2272
|
+
c["set"] = _sendable(settings)
|
|
2273
|
+
if macro is not _nothing:
|
|
2274
|
+
c["macro"] = macro or None
|
|
2275
|
+
s.command(c)
|
|
2276
|
+
synth = Synth(s, id, patch, made.get("note", 60 if note is None else note_number(note)), 0.3)
|
|
2277
|
+
object.__setattr__(synth, "_macro", made.get("macro") if macro is _nothing else macro or None)
|
|
2278
|
+
if recordings is not None:
|
|
2279
|
+
synth.set_samples(recordings)
|
|
2280
|
+
return synth
|
|
2281
|
+
|
|
2282
|
+
|
|
2283
|
+
# ---------------------------------------------------------------- what a program calls
|
|
2284
|
+
|
|
2285
|
+
def make_synth(preset=None, settings=None, **more):
|
|
2286
|
+
"""A synth of your own: make_synth("bass"), make_synth("lead", cutoff=900), make_synth(wave="saw", attack=0.2).
|
|
2287
|
+
The ready-made ones: synth_names(). The settings: setting_names()."""
|
|
2288
|
+
if isinstance(preset, dict):
|
|
2289
|
+
preset, settings = None, preset
|
|
2290
|
+
given = dict(settings or {})
|
|
2291
|
+
given.update(more)
|
|
2292
|
+
if isinstance(given.get("preset"), str):
|
|
2293
|
+
preset = given["preset"]
|
|
2294
|
+
given.pop("preset", None)
|
|
2295
|
+
if preset is not None and not isinstance(preset, str):
|
|
2296
|
+
raise ValueError('make_synth needs the name of a synth or its settings, as in make_synth("bass") or '
|
|
2297
|
+
'make_synth(wave="saw"), not %s' % _shown(preset))
|
|
2298
|
+
return _make_synth(_sound(), preset, given)
|
|
2299
|
+
|
|
2300
|
+
|
|
2301
|
+
def _file(path):
|
|
2302
|
+
"""A file's bytes as base64, when Python can read it; else None, and the page is asked to fetch it."""
|
|
2303
|
+
try:
|
|
2304
|
+
with open(path, "rb") as f:
|
|
2305
|
+
return base64.b64encode(f.read()).decode("ascii")
|
|
2306
|
+
except OSError:
|
|
2307
|
+
return None
|
|
2308
|
+
|
|
2309
|
+
|
|
2310
|
+
def load_sample(path=None, note=None, loop=None, **settings):
|
|
2311
|
+
"""A recorded sound from a file beside your program: drum = load_sample("kick.wav"), then drum.play().
|
|
2312
|
+
It is a synth: drum.play("E4") plays it higher, drum.reverb = 0.3. loop=True goes round while a note lasts,
|
|
2313
|
+
and note="A3" says what note the recording is at (C4 unless you say)."""
|
|
2314
|
+
if not isinstance(path, str) or not path.strip():
|
|
2315
|
+
raise ValueError('load_sample(%s) needs the name of a sound file, as in load_sample("kick.wav")'
|
|
2316
|
+
% ("" if path is None else _shown(path)))
|
|
2317
|
+
s = _sound()
|
|
2318
|
+
said = "load_sample(%s)" % _shown(path)
|
|
2319
|
+
patch = _patch_of(_NAMES["plain"], said, False)
|
|
2320
|
+
_patch_of(_NAMES["synths"].get("sample"), said, False, patch)
|
|
2321
|
+
_patch_of(settings, said, True, patch)
|
|
2322
|
+
s.share(patch, settings)
|
|
2323
|
+
patch[_setting("sample")["id"]] = 0
|
|
2324
|
+
sid = _sample_file(s, path, note, loop)
|
|
2325
|
+
id = s.id("s")
|
|
2326
|
+
s.command({"op": "synth", "id": id, "preset": "sample", "set": dict(_sendable(settings), sample=sid), "t": s.clock()})
|
|
2327
|
+
synth = Synth(s, id, patch, 60 if note is None else note_number(note), None)
|
|
2328
|
+
object.__setattr__(synth, "_sample", sid)
|
|
2329
|
+
return synth
|
|
2330
|
+
|
|
2331
|
+
|
|
2332
|
+
def load_samples(files=None, **settings):
|
|
2333
|
+
"""Several recordings as one instrument: piano = load_samples({"C2": "piano-C2.wav", "C3": "piano-C3.wav"}).
|
|
2334
|
+
Each name is the note that file was played at, and every key plays the nearest recording, moved to its pitch;
|
|
2335
|
+
load_samples(["piano-C2.wav", "piano-Fs2.wav"]) reads the notes from the files' names. It gives a synth, as
|
|
2336
|
+
load_sample does: piano.play("E4"), piano.reverb = 0.3, keyboard(piano).
|
|
2337
|
+
kit=True is a drum kit: each key is its own sound, as recorded, and the keys between are silent.
|
|
2338
|
+
crossfade=3 fades neighbours into each other over three keys; loop=True has each go round while held.
|
|
2339
|
+
A list of {"file", "note", "from", "to", "loud": [0, 0.5], "tune", "volume", "release"} says everything: layers
|
|
2340
|
+
by how hard a note is played, the keys by hand, and several files for one note are takes that go in turn."""
|
|
2341
|
+
said = "load_samples"
|
|
2342
|
+
o = {_snake(k): settings.pop(k) for k in list(settings) if _snake(k) in _MAP_WORDS}
|
|
2343
|
+
s = _sound()
|
|
2344
|
+
patch = _patch_of(_NAMES["plain"], said, False)
|
|
2345
|
+
_patch_of(_NAMES["synths"].get("sample"), said, False, patch)
|
|
2346
|
+
_patch_of(settings, said, True, patch)
|
|
2347
|
+
patch[_setting("sample")["id"]] = 0
|
|
2348
|
+
s.share(patch, settings)
|
|
2349
|
+
id = s.id("s")
|
|
2350
|
+
own_release = any((_setting(k) or {}).get("name") == "amp.release" for k in settings)
|
|
2351
|
+
zones = _zones_command(s, id, files, o, said, own_release)
|
|
2352
|
+
c = {"op": "synth", "id": id, "preset": "sample", "t": s.clock()}
|
|
2353
|
+
if settings:
|
|
2354
|
+
c["set"] = _sendable(settings)
|
|
2355
|
+
s.command(c)
|
|
2356
|
+
s.command(zones)
|
|
2357
|
+
synth = Synth(s, id, patch, min(z["note"] for z in zones["zones"]) if zones.get("kit") else 60, None)
|
|
2358
|
+
object.__setattr__(synth, "_zones", zones)
|
|
2359
|
+
return synth
|
|
2360
|
+
|
|
2361
|
+
|
|
2362
|
+
def play_note(note=None, length=0.5, volume=None, *, pan=None, glide=None, bend=None, slide=None, vibrato=None,
|
|
2363
|
+
after=None, at=None, beat=None):
|
|
2364
|
+
"""A note on a pleasant synth: play_note("C4"), play_note("C4", 2, 0.3). A list of notes is a chord. Its pitch
|
|
2365
|
+
can move, as a synth's play() says: play_note("A4", 1, bend="scoop"), glide=True, slide=12, vibrato=True."""
|
|
2366
|
+
if note is None:
|
|
2367
|
+
raise ValueError('play_note needs a note, as in play_note("C4")')
|
|
2368
|
+
_sound().main_synth().play(note, length, volume, pan=pan, glide=glide, bend=bend, slide=slide, vibrato=vibrato,
|
|
2369
|
+
after=after, at=at, beat=beat)
|
|
2370
|
+
|
|
2371
|
+
|
|
2372
|
+
def play_notes(notes=None, step=None, *, repeat=None, volume=None, hold=None, after=None, at=None, beat=None):
|
|
2373
|
+
"""A tune: play_notes("C4 E4 G4 - C5:2", 0.25). "-" is a rest, ":2" lasts two steps, "C4+E4" is a chord.
|
|
2374
|
+
The second thing is the seconds in a step. repeat=True plays it round and round; .stop() ends it."""
|
|
2375
|
+
return _sound().main_synth().play_notes(notes, step, repeat=repeat, volume=volume, hold=hold, after=after, at=at, beat=beat)
|
|
2376
|
+
|
|
2377
|
+
|
|
2378
|
+
def note(what=None, length=None, *, volume=None, pan=None):
|
|
2379
|
+
"""A tune as ordinary lines, each note beginning where the last ended (on the synth play_note uses):
|
|
2380
|
+
note("C4", "quarter"), note("E4", "eighth"). A length is a note length by name, or a number of beats."""
|
|
2381
|
+
_sound().main_synth().note(what, length, volume=volume, pan=pan)
|
|
2382
|
+
|
|
2383
|
+
|
|
2384
|
+
def chord(name=None, kind=None, length=None, *, volume=None, pan=None):
|
|
2385
|
+
"""The next thing in the tune is a chord: chord("Cm", "half")."""
|
|
2386
|
+
_sound().main_synth().chord(name, kind, length, volume=volume, pan=pan)
|
|
2387
|
+
|
|
2388
|
+
|
|
2389
|
+
def rest(length=None):
|
|
2390
|
+
"""A silence in the tune: rest("eighth")."""
|
|
2391
|
+
_sound().main_synth().rest(length)
|
|
2392
|
+
|
|
2393
|
+
|
|
2394
|
+
def start_lines(*, after=None, at=None, beat=None):
|
|
2395
|
+
"""Begin every line of notes again together, from now (or after=, at=, beat=)."""
|
|
2396
|
+
s = _sound()
|
|
2397
|
+
s.lines.clear()
|
|
2398
|
+
s.turn_of, s.turn = s.transport.turn(s.clock), s.when("start_lines", after, at, beat)
|
|
2399
|
+
|
|
2400
|
+
|
|
2401
|
+
class Loop:
|
|
2402
|
+
"""A section of notes that goes round: what loop_notes() gives back. stop() ends it."""
|
|
2403
|
+
|
|
2404
|
+
def __init__(self, sound, times):
|
|
2405
|
+
self._sound = sound
|
|
2406
|
+
self._times = times
|
|
2407
|
+
self._made = []
|
|
2408
|
+
|
|
2409
|
+
def __enter__(self):
|
|
2410
|
+
s = self._sound
|
|
2411
|
+
if s.writing is not None:
|
|
2412
|
+
raise ValueError("loop_notes cannot be used inside another loop_notes")
|
|
2413
|
+
s.writing = {}
|
|
2414
|
+
return self
|
|
2415
|
+
|
|
2416
|
+
def __exit__(self, kind, error, trace):
|
|
2417
|
+
s = self._sound
|
|
2418
|
+
written, s.writing = s.writing, None
|
|
2419
|
+
if kind is not None:
|
|
2420
|
+
return False
|
|
2421
|
+
whole = max([line[1] for line in written.values()] or [0])
|
|
2422
|
+
if not whole > 0:
|
|
2423
|
+
raise ValueError("loop_notes: nothing was written to go round. Use note(), chord() and rest() inside it")
|
|
2424
|
+
# Every line of the section starts together, where the latest of those lines has reached.
|
|
2425
|
+
start = max(s.line_at(synth) for synth in written)
|
|
2426
|
+
for synth, (steps, _) in written.items():
|
|
2427
|
+
id = s.id("t")
|
|
2428
|
+
self._made.append(id)
|
|
2429
|
+
if steps:
|
|
2430
|
+
s.command({"op": "tune", "id": id, "synth": synth, "steps": [[_whole(a), n, _whole(l), v] for a, n, l, v in steps],
|
|
2431
|
+
"length": _whole(whole), "repeat": self._times, "beats": True, "t": start})
|
|
2432
|
+
s.lines[synth] = start if self._times is True else start + self._times * whole * 60 / s.tempo
|
|
2433
|
+
return False
|
|
2434
|
+
|
|
2435
|
+
def stop(self):
|
|
2436
|
+
s = self._sound
|
|
2437
|
+
for id in self._made:
|
|
2438
|
+
s.command({"op": "stop", "id": id, "t": s.clock()})
|
|
2439
|
+
return self
|
|
2440
|
+
|
|
2441
|
+
|
|
2442
|
+
def loop_notes(a=None, b=None, *, times=None):
|
|
2443
|
+
"""Write a section once and have it go round, on any synths, kept in time by the sound engine and following the tempo:
|
|
2444
|
+
|
|
2445
|
+
with loop_notes():
|
|
2446
|
+
kick.note("C3", "quarter")
|
|
2447
|
+
snare.note("A3", "quarter")
|
|
2448
|
+
|
|
2449
|
+
loop_notes(4) goes round four times, and the lines carry on after it. A function works too: loop_notes(section),
|
|
2450
|
+
loop_notes(4, section). What it gives back has stop()."""
|
|
2451
|
+
section = a if callable(a) else b if callable(b) else None
|
|
2452
|
+
count = times if times is not None else b if callable(a) else a
|
|
2453
|
+
if count is not None and count is not True and not callable(count):
|
|
2454
|
+
count = _amount(count, "loop_notes: how many times", 1, 1e9)
|
|
2455
|
+
loop = Loop(_sound(), True if count is None or count is True or callable(count) else count)
|
|
2456
|
+
if section is not None:
|
|
2457
|
+
with loop:
|
|
2458
|
+
section()
|
|
2459
|
+
return loop
|
|
2460
|
+
|
|
2461
|
+
|
|
2462
|
+
def main_synth():
|
|
2463
|
+
"""The synth that play_note and play_notes use: change it (main_synth().cutoff = 900), or tie a keyboard to it."""
|
|
2464
|
+
return _sound().main_synth()
|
|
2465
|
+
|
|
2466
|
+
|
|
2467
|
+
def _chip():
|
|
2468
|
+
# Arpeggios with no synth named are played on a plain square wave, as a sound chip would.
|
|
2469
|
+
s = _sound()
|
|
2470
|
+
if s.chip is None:
|
|
2471
|
+
s.chip = _make_synth(s, "chip", {})
|
|
2472
|
+
return s.chip
|
|
2473
|
+
|
|
2474
|
+
|
|
2475
|
+
def play_arp(chord=None, *more, **how):
|
|
2476
|
+
"""A chord as a fast arpeggio on a chip sound: play_arp("Cm7"), play_arp("C4", "minor", 1, speed=30, order="down")."""
|
|
2477
|
+
return _chip()._arp(chord, more, how, "play_arp", 0.5)
|
|
2478
|
+
|
|
2479
|
+
|
|
2480
|
+
def start_arp(chord=None, *more, **how):
|
|
2481
|
+
"""The same, staying until it is stopped, and able to change chord: arp = start_arp("Am"), arp.change("F")."""
|
|
2482
|
+
return _chip()._arp(chord, more, how, "start_arp", None)
|
|
2483
|
+
|
|
2484
|
+
|
|
2485
|
+
class _Output:
|
|
2486
|
+
"""Everything that is sounding, as a thing a scope, a meter and dials can be tied to: what sound_output() gives."""
|
|
2487
|
+
|
|
2488
|
+
def __init__(self, sound):
|
|
2489
|
+
self._sound = sound
|
|
2490
|
+
self._values = {"volume": 1}
|
|
2491
|
+
self._page = None
|
|
2492
|
+
|
|
2493
|
+
def describe(self, name):
|
|
2494
|
+
if _key(name) == "volume":
|
|
2495
|
+
return {"name": "volume", "kind": "float", "min": 0, "max": 2, "default": 1}
|
|
2496
|
+
entry = _setting(name)
|
|
2497
|
+
return Synth.describe(self, name) if entry is not None and entry["scope"] == "all" else None
|
|
2498
|
+
|
|
2499
|
+
def get(self, name):
|
|
2500
|
+
if _key(name) == "volume":
|
|
2501
|
+
return self._values["volume"]
|
|
2502
|
+
entry = _setting(name)
|
|
2503
|
+
if entry is None or entry["scope"] != "all":
|
|
2504
|
+
return None
|
|
2505
|
+
value = self._sound.for_all.get(entry["id"], entry["default"])
|
|
2506
|
+
if entry["kind"] == "choice":
|
|
2507
|
+
return entry["choices"][int(value)]
|
|
2508
|
+
if entry["kind"] == "bool":
|
|
2509
|
+
return value == 1
|
|
2510
|
+
return value / entry["scale"]
|
|
2511
|
+
|
|
2512
|
+
def set(self, name, value):
|
|
2513
|
+
if _key(name) == "volume":
|
|
2514
|
+
set_volume(value)
|
|
2515
|
+
self._values["volume"] = value
|
|
2516
|
+
else:
|
|
2517
|
+
set_effects({name: value})
|
|
2518
|
+
|
|
2519
|
+
def _told(self, name, value):
|
|
2520
|
+
"""A control in the page changed it by hand: remember it."""
|
|
2521
|
+
if _key(name) == "volume":
|
|
2522
|
+
self._values["volume"] = value
|
|
2523
|
+
else:
|
|
2524
|
+
try:
|
|
2525
|
+
self._sound.share(_patch_of({name: value}, "set", False))
|
|
2526
|
+
except (ValueError, TypeError):
|
|
2527
|
+
pass
|
|
2528
|
+
|
|
2529
|
+
def samples(self):
|
|
2530
|
+
"""The wave of everything that is sounding, numbers from -1 to 1. A frame late in a Pyctures scene."""
|
|
2531
|
+
return self._sound.reading(None)["samples"]
|
|
2532
|
+
|
|
2533
|
+
def level(self):
|
|
2534
|
+
"""How loud everything is, [left, right], each from 0 to 1."""
|
|
2535
|
+
return self._sound.reading(None)["level"]
|
|
2536
|
+
|
|
2537
|
+
def notes(self):
|
|
2538
|
+
"""The notes that are sounding, as numbers."""
|
|
2539
|
+
return self._sound.reading(None)["notes"]
|
|
2540
|
+
|
|
2541
|
+
@property
|
|
2542
|
+
def page(self):
|
|
2543
|
+
"""The page's own object for all the sound, in a Pyctures scene."""
|
|
2544
|
+
if self._page is None:
|
|
2545
|
+
self._page = self._sound.transport.page(None)
|
|
2546
|
+
return self._page
|
|
2547
|
+
|
|
2548
|
+
|
|
2549
|
+
def sound_output():
|
|
2550
|
+
"""All the sound together: scope(sound_output()) draws it, meter(sound_output()) shows how loud it is, and
|
|
2551
|
+
dial("volume", sound_output(), "volume") turns it up and down."""
|
|
2552
|
+
s = _sound()
|
|
2553
|
+
if s.output is None:
|
|
2554
|
+
s.output = _Output(s)
|
|
2555
|
+
return s.output
|
|
2556
|
+
|
|
2557
|
+
|
|
2558
|
+
def play_sound(name=None, volume=None, *, pitch=None, pan=None, length=None, after=None, at=None, beat=None):
|
|
2559
|
+
"""A ready-made game sound: play_sound("coin"). The sounds: sound_names(). length= cuts it short there, or holds
|
|
2560
|
+
its last note until then."""
|
|
2561
|
+
found = _SOUNDS.get(_key(name)) if isinstance(name, str) else None
|
|
2562
|
+
if found is None:
|
|
2563
|
+
raise ValueError("play_sound(%s): there is no sound called %s.%s The sounds are: %s"
|
|
2564
|
+
% ("" if name is None else _shown(name), _shown(name), _meant(name, list(_NAMES["sounds"])),
|
|
2565
|
+
", ".join(_NAMES["sounds"])))
|
|
2566
|
+
s = _sound()
|
|
2567
|
+
c = {"op": "sound", "name": found, "t": s.when("play_sound", after, at, beat)}
|
|
2568
|
+
if volume is not None:
|
|
2569
|
+
c["vol"] = _amount(volume, "play_sound: volume", 0, 4)
|
|
2570
|
+
if pitch is not None:
|
|
2571
|
+
c["pitch"] = _amount(pitch, "play_sound: pitch", -48, 48)
|
|
2572
|
+
if pan is not None:
|
|
2573
|
+
c["pan"] = _amount(pan, "play_sound: pan", -1, 1)
|
|
2574
|
+
if length is not None:
|
|
2575
|
+
c["len"] = s.length(length, "play_sound: length")
|
|
2576
|
+
s.command(c)
|
|
2577
|
+
|
|
2578
|
+
|
|
2579
|
+
def play_song(file=None, *, repeat=True, loop=None, volume=None, raw=None, patches=None, after=None, at=None, beat=None):
|
|
2580
|
+
"""Play a song: a tracker's .mod or .xm file, or a song in synths' own format (.json). song = play_song("tune.mod").
|
|
2581
|
+
It goes round until it is stopped: song.pause(), song.resume(), song.volume = 0.5, song.seek(2), song.stop().
|
|
2582
|
+
repeat=False plays it once. One song plays at a time, and your own notes and game sounds play over it.
|
|
2583
|
+
A song whose instruments are synths by name is told what they are with patches={"wobble": "bass",
|
|
2584
|
+
"pling": {"preset": "pluck", "echo": 0.4}}; a name that is a ready-made synth's needs no telling."""
|
|
2585
|
+
given = isinstance(file, (bytes, bytearray))
|
|
2586
|
+
if not given and (not isinstance(file, str) or not file.strip()):
|
|
2587
|
+
raise ValueError('play_song(%s) needs the name of a song file, as in play_song("tune.mod")'
|
|
2588
|
+
% ("" if file is None else _shown(file)))
|
|
2589
|
+
s = _sound()
|
|
2590
|
+
id = s.id("g")
|
|
2591
|
+
c = {"op": "song", "id": id, "do": "play", "repeat": (repeat if loop is None else loop) is not False,
|
|
2592
|
+
"t": s.when("play_song", after, at, beat)}
|
|
2593
|
+
if volume is not None:
|
|
2594
|
+
c["vol"] = _amount(volume, "play_song: volume", 0, 2)
|
|
2595
|
+
if raw is not None:
|
|
2596
|
+
c["raw"] = bool(raw)
|
|
2597
|
+
if patches is not None:
|
|
2598
|
+
c["patches"] = _song_patches(patches)
|
|
2599
|
+
if given:
|
|
2600
|
+
c["bytes"] = base64.b64encode(bytes(file)).decode("ascii")
|
|
2601
|
+
else:
|
|
2602
|
+
c["name"] = file
|
|
2603
|
+
data = _file(file)
|
|
2604
|
+
if data is None:
|
|
2605
|
+
c["url"] = file # the page fetches it from beside the scene, and the files it names
|
|
2606
|
+
else:
|
|
2607
|
+
c["bytes"] = data # Python has the file, and the WAV files one of synths' own songs names
|
|
2608
|
+
files = {}
|
|
2609
|
+
for name in _song_files(data):
|
|
2610
|
+
more = _file(os.path.join(os.path.dirname(file), name))
|
|
2611
|
+
if more is not None:
|
|
2612
|
+
files[name] = more
|
|
2613
|
+
if files:
|
|
2614
|
+
c["files"] = files
|
|
2615
|
+
s.command(c)
|
|
2616
|
+
return Song(s, id)
|
|
2617
|
+
|
|
2618
|
+
|
|
2619
|
+
def _song_files(data):
|
|
2620
|
+
"""The WAV files a song in synths' own format names for its samples (it is JSON; a .mod or .xm names none)."""
|
|
2621
|
+
try:
|
|
2622
|
+
song = json.loads(base64.b64decode(data).decode("utf-8"))
|
|
2623
|
+
return sorted({sample["file"] for sample in song.get("samples") or [] if isinstance(sample, dict) and isinstance(sample.get("file"), str)})
|
|
2624
|
+
except (ValueError, AttributeError, TypeError):
|
|
2625
|
+
return []
|
|
2626
|
+
|
|
2627
|
+
|
|
2628
|
+
def _song_patches(patches):
|
|
2629
|
+
"""What a song's instruments that are synths by name are, checked: {"wobble": "bass", "pling": {"preset": "pluck", "echo": 0.4}}."""
|
|
2630
|
+
if not isinstance(patches, dict):
|
|
2631
|
+
raise ValueError('play_song: patches needs names and what each is, as in patches={"wobble": "bass"}, not %s' % _shown(patches))
|
|
2632
|
+
names = [_snake(n) for n in _NAMES["synths"] if n != "sample"]
|
|
2633
|
+
out = {}
|
|
2634
|
+
for name, what in patches.items():
|
|
2635
|
+
said = "play_song: the patch %s" % _shown(name)
|
|
2636
|
+
if not isinstance(what, (str, dict)):
|
|
2637
|
+
raise ValueError("%s needs the name of a synth or settings, not %s" % (said, _shown(what)))
|
|
2638
|
+
preset = what if isinstance(what, str) else what.get("preset")
|
|
2639
|
+
found = None
|
|
2640
|
+
if preset is not None:
|
|
2641
|
+
found = _PRESETS.get(_key(preset)) if isinstance(preset, str) else None
|
|
2642
|
+
if found is None or found == "sample":
|
|
2643
|
+
raise ValueError("%s names a synth called %s, and there is none.%s The synths are: %s"
|
|
2644
|
+
% (said, _shown(preset), _meant(preset, names), ", ".join(names)))
|
|
2645
|
+
if isinstance(what, str):
|
|
2646
|
+
out[name] = found
|
|
2647
|
+
else:
|
|
2648
|
+
_patch_of(what, said, True)
|
|
2649
|
+
_check_macro(what.get("macro"), said)
|
|
2650
|
+
out[name] = dict(_sendable({k: v for k, v in what.items() if k != "preset"}), **({"preset": found} if found else {}))
|
|
2651
|
+
return out
|
|
2652
|
+
|
|
2653
|
+
|
|
2654
|
+
def stop_song():
|
|
2655
|
+
"""Stop every song."""
|
|
2656
|
+
s = _sound()
|
|
2657
|
+
s.command({"op": "song", "do": "stop", "t": s.clock()})
|
|
2658
|
+
|
|
2659
|
+
|
|
2660
|
+
def stop_sound():
|
|
2661
|
+
"""Stop everything that is sounding or waiting to sound. Synths and their settings stay."""
|
|
2662
|
+
s = _sound()
|
|
2663
|
+
s.command({"op": "stop", "t": s.clock()})
|
|
2664
|
+
|
|
2665
|
+
|
|
2666
|
+
def set_volume(volume):
|
|
2667
|
+
"""How loud everything is: 1 is normal, 0 is silent, 2 is twice as loud."""
|
|
2668
|
+
s = _sound()
|
|
2669
|
+
s.command({"op": "master", "volume": _amount(volume, "set_volume", 0, 4), "t": s.clock()})
|
|
2670
|
+
|
|
2671
|
+
|
|
2672
|
+
def mute_sound(on=True):
|
|
2673
|
+
"""Silence, until unmute_sound()."""
|
|
2674
|
+
s = _sound()
|
|
2675
|
+
s.command({"op": "master", "mute": bool(on), "t": s.clock()})
|
|
2676
|
+
|
|
2677
|
+
|
|
2678
|
+
def unmute_sound():
|
|
2679
|
+
mute_sound(False)
|
|
2680
|
+
|
|
2681
|
+
|
|
2682
|
+
def set_effects(settings=None, **more):
|
|
2683
|
+
"""The effects every synth shares: set_effects(echo_time=0.3, echo_feedback=0.5, reverb_size=0.8)."""
|
|
2684
|
+
if settings is not None and not isinstance(settings, dict):
|
|
2685
|
+
raise ValueError("set_effects needs settings, as in set_effects(echo_time=0.3), not %s" % _shown(settings))
|
|
2686
|
+
given = dict(settings or {})
|
|
2687
|
+
given.update(more)
|
|
2688
|
+
shared = [n for n in _FRIENDLY if _setting(n)["scope"] == "all"]
|
|
2689
|
+
if not given:
|
|
2690
|
+
raise ValueError("set_effects needs settings, as in set_effects(echo_time=0.3). They are: %s" % ", ".join(shared))
|
|
2691
|
+
patch = _patch_of(given, "set_effects", True)
|
|
2692
|
+
scope = {e["id"]: e["scope"] for e in _SETTINGS.values()}
|
|
2693
|
+
if any(scope[i] != "all" for i in patch):
|
|
2694
|
+
raise ValueError("set_effects is for the effects every synth shares: %s. A synth's own settings are set on "
|
|
2695
|
+
"the synth, as in lead.echo = 0.4" % ", ".join(shared))
|
|
2696
|
+
s = _sound()
|
|
2697
|
+
s.share(patch)
|
|
2698
|
+
s.command({"op": "master", "set": given, "t": s.clock()})
|
|
2699
|
+
|
|
2700
|
+
|
|
2701
|
+
def set_tempo(bpm):
|
|
2702
|
+
"""How fast the beat is, in beats a minute. Beat 0 is now."""
|
|
2703
|
+
s = _sound()
|
|
2704
|
+
s.tempo = _amount(bpm, "set_tempo", 20, 400)
|
|
2705
|
+
s.beat_zero = s.clock()
|
|
2706
|
+
s.command({"op": "tempo", "bpm": s.tempo, "t": s.beat_zero}) # tunes written in beats change speed from here on
|
|
2707
|
+
if s._midi is not None and s._midi.clock_on:
|
|
2708
|
+
s._midi.command({"do": "clock", "bpm": s.tempo, "t": s.clock()}) # a MIDI clock being sent keeps up
|
|
2709
|
+
|
|
2710
|
+
|
|
2711
|
+
def beats(n=1):
|
|
2712
|
+
"""How many seconds that many beats last: beats(2), or a note length by name, beats("half")."""
|
|
2713
|
+
return (_beats_long(n, "beats") if isinstance(n, str) else n) * 60 / _sound().tempo
|
|
2714
|
+
|
|
2715
|
+
|
|
2716
|
+
def beat():
|
|
2717
|
+
"""Which beat it is now (a fraction between beats)."""
|
|
2718
|
+
s = _sound()
|
|
2719
|
+
return (s.clock() - s.beat_zero) * s.tempo / 60
|
|
2720
|
+
|
|
2721
|
+
|
|
2722
|
+
def _buffer_size(asked):
|
|
2723
|
+
"""A sound buffer as it may be asked for, as synths.js reads it: "auto", a word (small, medium, large) or a
|
|
2724
|
+
number of frames, brought to the nearest power of two from 256 to 16384. (size, exact), or None."""
|
|
2725
|
+
b = _NAMES["buffer"]
|
|
2726
|
+
if asked is None:
|
|
2727
|
+
return "auto", True
|
|
2728
|
+
if isinstance(asked, str):
|
|
2729
|
+
word = asked.lower().strip()
|
|
2730
|
+
if word in b["words"]:
|
|
2731
|
+
return b["words"][word], True
|
|
2732
|
+
try:
|
|
2733
|
+
asked = float(word)
|
|
2734
|
+
except ValueError:
|
|
2735
|
+
return None
|
|
2736
|
+
if not _is_number(asked) or not asked > 0 or asked == float("inf"):
|
|
2737
|
+
return None
|
|
2738
|
+
low, high = int(math.log2(b["least"])), int(math.log2(b["most"]))
|
|
2739
|
+
size = 2 ** min(high, max(low, int(math.floor(math.log2(asked) + 0.5))))
|
|
2740
|
+
return size, size == asked
|
|
2741
|
+
|
|
2742
|
+
|
|
2743
|
+
def set_sound_buffer(size="auto"):
|
|
2744
|
+
"""How much sound is made at a time: set_sound_buffer("large") or set_sound_buffer(4096) for fewer clicks on a
|
|
2745
|
+
slow device, "small" (1024) for a keyboard that feels late, "medium" (2048), and "auto" to let the page choose
|
|
2746
|
+
and change it again. A number is frames: a power of two from 256 to 16384. It matters on a page with no https
|
|
2747
|
+
address, where sound is made on the page's thread; on the audio thread it sets how far ahead everything is
|
|
2748
|
+
scheduled instead. It gives what sound_buffer() does."""
|
|
2749
|
+
got = _buffer_size(size)
|
|
2750
|
+
if got is None:
|
|
2751
|
+
raise ValueError('set_sound_buffer needs "small", "medium", "large", "auto" or a number of frames from 256 to '
|
|
2752
|
+
'16384, as in set_sound_buffer("large"), not %s' % _shown(size))
|
|
2753
|
+
if not got[1]:
|
|
2754
|
+
_tell("a sound buffer is a power of two from 256 to 16384, so %s is taken as %d." % (_shown(size), got[0]))
|
|
2755
|
+
s = _sound()
|
|
2756
|
+
s.buffer_set = got[0]
|
|
2757
|
+
s.hear_back()
|
|
2758
|
+
s.buffer_asked = True
|
|
2759
|
+
s.command({"op": "buffer", "size": got[0], "t": s.clock()})
|
|
2760
|
+
return sound_buffer()
|
|
2761
|
+
|
|
2762
|
+
|
|
2763
|
+
def sound_buffer():
|
|
2764
|
+
"""The buffer as the page last said it: {"frames", "delay", "thread", "underruns", "setting"}. `delay` is the
|
|
2765
|
+
milliseconds from a key to its note, `thread` is "audio" (the audio thread: nothing to worry about), "page" (the
|
|
2766
|
+
page's thread, which a busy page makes click) or None before the page has said, `underruns` how many times the
|
|
2767
|
+
sound came too late, and `setting` what was asked for ("auto" or frames). It is a frame late."""
|
|
2768
|
+
s = _sound()
|
|
2769
|
+
if not s.buffer_asked:
|
|
2770
|
+
s.hear_back()
|
|
2771
|
+
s.buffer_asked = True
|
|
2772
|
+
s.command({"op": "buffer", "t": s.clock()})
|
|
2773
|
+
if s.buffer_said:
|
|
2774
|
+
return dict(s.buffer_said)
|
|
2775
|
+
return {"frames": 0, "delay": 0, "thread": None, "underruns": 0, "setting": "auto" if s.buffer_set is None else s.buffer_set}
|
|
2776
|
+
|
|
2777
|
+
|
|
2778
|
+
def sound_time():
|
|
2779
|
+
"""The sound clock now, in seconds: the time that at= is read against."""
|
|
2780
|
+
return _sound().clock()
|
|
2781
|
+
|
|
2782
|
+
|
|
2783
|
+
def sound_names():
|
|
2784
|
+
"""The names of the ready-made game sounds."""
|
|
2785
|
+
return list(_NAMES["sounds"])
|
|
2786
|
+
|
|
2787
|
+
|
|
2788
|
+
def synth_names():
|
|
2789
|
+
"""The names of the ready-made synths."""
|
|
2790
|
+
return [_snake(n) for n in _NAMES["synths"] if n != "sample"]
|
|
2791
|
+
|
|
2792
|
+
|
|
2793
|
+
def setting_names():
|
|
2794
|
+
"""The name of every setting a synth has."""
|
|
2795
|
+
return list(_SHOWN)
|
|
2796
|
+
|
|
2797
|
+
|
|
2798
|
+
# ---------------------------------------------------------------- MIDI
|
|
2799
|
+
#
|
|
2800
|
+
# MIDI is how music keyboards, drum pads, boxes of knobs and synths talk to each other and to a computer.
|
|
2801
|
+
# The devices are where the page is, so that is where MIDI is done (synths.js's docs/midi.md is the contract):
|
|
2802
|
+
#
|
|
2803
|
+
# - Every function here writes a command, {"op": "midi", "do": "send", "bytes": [144, 60, 102], "t": 12.25},
|
|
2804
|
+
# into the same batches as the sound's.
|
|
2805
|
+
# - A keyboard plays a synth ON THE PAGE (lead.play_from_midi()), and a knob turns a setting there
|
|
2806
|
+
# (lead.learn("cutoff")), without Python in between: so it feels immediate however slow or far away Python is.
|
|
2807
|
+
# - What was received is handed to Python as plain data with the next frame, each message with the time it
|
|
2808
|
+
# happened (on the clock that sound_time() reads), and the program's functions are called then.
|
|
2809
|
+
|
|
2810
|
+
# ---------------------------------------------------------------- the microphone, recording and saving
|
|
2811
|
+
#
|
|
2812
|
+
# The microphone is where the player is, so the page opens it, when the first `mic` command arrives and not
|
|
2813
|
+
# before; that is when the browser asks the person. From then on it is a synth by its id whose sound comes in
|
|
2814
|
+
# instead of being made, so `set` and a voice work on it. It is all commands (mic, record, save: synths.js's
|
|
2815
|
+
# docs/commands.md), and what the player says back (the microphone is on or is not, a recording is over, a file
|
|
2816
|
+
# is saved) arrives as plain data: with the next frame in a Pyctures scene, or from a host through
|
|
2817
|
+
# receive_sound_events(events). So `mic.state`, `take.done` and `take.seconds` are A FRAME LATE at the soonest,
|
|
2818
|
+
# and nothing here ever waits for them.
|
|
2819
|
+
|
|
2820
|
+
class _Until:
|
|
2821
|
+
"""Something to `await` in a scene that can wait: it looks again every frame, and never holds the program."""
|
|
2822
|
+
|
|
2823
|
+
def __init__(self, over, value):
|
|
2824
|
+
self.over, self.value = over, value
|
|
2825
|
+
|
|
2826
|
+
def __await__(self):
|
|
2827
|
+
while not self.over():
|
|
2828
|
+
p = sys.modules.get("pyctures")
|
|
2829
|
+
frame = getattr(p, "frame", None) if getattr(getattr(p, "S", None), "ns", None) is not None else None
|
|
2830
|
+
if frame is not None:
|
|
2831
|
+
yield from frame().__await__()
|
|
2832
|
+
else:
|
|
2833
|
+
yield
|
|
2834
|
+
return self.value()
|
|
2835
|
+
|
|
2836
|
+
|
|
2837
|
+
def _dress_like(synth, other):
|
|
2838
|
+
"""One synth is given the voice and the voice effects of another, as ordinary commands: what the other had
|
|
2839
|
+
before any voice, its voice, and what was turned after that (synths.js's dressLike). So a recording of the
|
|
2840
|
+
plain microphone plays back as the microphone sounds, and "none" still puts back the plain voice."""
|
|
2841
|
+
part = [pid for pid in _VOICES["all"] if pid < 200]
|
|
2842
|
+
now = lambda pid: synth._patch.get(pid, _DEFAULTS.get(pid))
|
|
2843
|
+
if synth.voice != "none":
|
|
2844
|
+
synth.voice = "none"
|
|
2845
|
+
if synth._plain is None:
|
|
2846
|
+
object.__setattr__(synth, "_plain", {pid: now(pid) for pid in part})
|
|
2847
|
+
|
|
2848
|
+
def send(pairs):
|
|
2849
|
+
changed = {_BY_ID[pid]: value for pid, value in pairs if value != now(pid)}
|
|
2850
|
+
if changed:
|
|
2851
|
+
Synth.set(synth, changed)
|
|
2852
|
+
under = other._worn["under"] if other._worn else None
|
|
2853
|
+
if under is not None:
|
|
2854
|
+
send([(pid, under[pid] if pid in under else synth._plain[pid]) for pid in part])
|
|
2855
|
+
synth.voice = other._worn["voice"]
|
|
2856
|
+
send([(pid, other._patch[pid]) for pid in part if pid in other._patch])
|
|
2857
|
+
else:
|
|
2858
|
+
send([(pid, other._patch[pid] if pid in other._patch else synth._plain[pid]) for pid in part])
|
|
2859
|
+
|
|
2860
|
+
|
|
2861
|
+
class Mic(Synth):
|
|
2862
|
+
"""The microphone: what use_microphone() gives. It is a synth whose sound comes in instead of being made, so it
|
|
2863
|
+
has a synth's settings (mic.echo = 0.4, mic.cutoff = 900), the voice changers (mic.voice = "robot") and what a
|
|
2864
|
+
dial, a scope and a meter look for. Its own words: `pitch` is the pitch shifter (-12 to 12 semitones), `ring`
|
|
2865
|
+
the ring modulator, `gain` how much the microphone is turned up, and `gate` the level in dB below which it is
|
|
2866
|
+
taken as silence ("off" for none). Nothing is heard until listen().
|
|
2867
|
+
|
|
2868
|
+
mic.state is "asking" until the page has said, then "on", or why not: "refused", "none", "busy" or
|
|
2869
|
+
"unavailable"; "off" once closed. It is told with a frame, so it is a frame late at the soonest."""
|
|
2870
|
+
|
|
2871
|
+
_OWN = Synth._OWN + ("listening", "state", "message")
|
|
2872
|
+
|
|
2873
|
+
def __init__(self, sound, id):
|
|
2874
|
+
Synth.__init__(self, sound, id, {}, 60, 0.3)
|
|
2875
|
+
put = object.__setattr__
|
|
2876
|
+
put(self, "_listening", False)
|
|
2877
|
+
put(self, "_waiting", [])
|
|
2878
|
+
put(self, "state", "asking")
|
|
2879
|
+
put(self, "message", "")
|
|
2880
|
+
|
|
2881
|
+
def __repr__(self):
|
|
2882
|
+
return "<the microphone: %s>" % self.state
|
|
2883
|
+
|
|
2884
|
+
def _name(self, name):
|
|
2885
|
+
return _MIC_NAMES.get(_key(name), name) if isinstance(name, str) else name
|
|
2886
|
+
|
|
2887
|
+
def _given(self, given, at):
|
|
2888
|
+
out = {}
|
|
2889
|
+
for name, value in given.items():
|
|
2890
|
+
k = _key(name)
|
|
2891
|
+
if k == "listening":
|
|
2892
|
+
self.listening = value
|
|
2893
|
+
elif k == "gate" and (value is False or value is None or value == "off"):
|
|
2894
|
+
out["micGate"] = -100
|
|
2895
|
+
else:
|
|
2896
|
+
out[name] = value
|
|
2897
|
+
# The microphone starts with no filter, so that it comes through untouched. Turning the cutoff asks for one.
|
|
2898
|
+
names = [(_setting(self._name(n)) or {}).get("name") for n in out]
|
|
2899
|
+
if ("filter.cutoff" in names or "filter.resonance" in names) and "filter.type" not in names and self.filter == "off":
|
|
2900
|
+
out["filter"] = "lowpass"
|
|
2901
|
+
return Synth._given(self, out, at)
|
|
2902
|
+
|
|
2903
|
+
def describe(self, name):
|
|
2904
|
+
if _key(name) == "listening":
|
|
2905
|
+
return {"name": "listening", "kind": "bool", "min": 0, "max": 1, "default": False}
|
|
2906
|
+
return Synth.describe(self, name)
|
|
2907
|
+
|
|
2908
|
+
def _told(self, name, value):
|
|
2909
|
+
if _key(name) == "listening":
|
|
2910
|
+
object.__setattr__(self, "_listening", bool(value))
|
|
2911
|
+
else:
|
|
2912
|
+
Synth._told(self, name, value)
|
|
2913
|
+
|
|
2914
|
+
def listen(self, on=True):
|
|
2915
|
+
"""Hear the microphone, through its voice and effects. With loudspeakers this can howl: headphones are best."""
|
|
2916
|
+
object.__setattr__(self, "_listening", bool(on))
|
|
2917
|
+
self._sound.command({"op": "mic", "id": self._id, "listen": self._listening, "t": self._sound.clock()})
|
|
2918
|
+
return self
|
|
2919
|
+
|
|
2920
|
+
def mute(self):
|
|
2921
|
+
"""Stop hearing it. It can still be recorded, and a scope and a meter still show it."""
|
|
2922
|
+
return self.listen(False)
|
|
2923
|
+
|
|
2924
|
+
@property
|
|
2925
|
+
def listening(self):
|
|
2926
|
+
return self._listening
|
|
2927
|
+
|
|
2928
|
+
@listening.setter
|
|
2929
|
+
def listening(self, on):
|
|
2930
|
+
self.listen(on is True or on == 1 or on == "on")
|
|
2931
|
+
|
|
2932
|
+
@property
|
|
2933
|
+
def ready(self):
|
|
2934
|
+
"""Whether there is a microphone: None while the person is still being asked, then True or False."""
|
|
2935
|
+
return None if self.state == "asking" else self.state == "on"
|
|
2936
|
+
|
|
2937
|
+
def on_ready(self, fn):
|
|
2938
|
+
"""Call a function when it is known whether there is a microphone: mic.on_ready(lambda there: ...)."""
|
|
2939
|
+
if not callable(fn):
|
|
2940
|
+
raise ValueError("on_ready needs a function to call, as in mic.on_ready(started), not %s" % _shown(fn))
|
|
2941
|
+
if self.state == "asking":
|
|
2942
|
+
self._waiting.append(fn)
|
|
2943
|
+
else:
|
|
2944
|
+
_call(fn, self.state == "on")
|
|
2945
|
+
return self
|
|
2946
|
+
|
|
2947
|
+
def when_ready(self):
|
|
2948
|
+
"""For a scene that can wait: `there = await mic.when_ready()` is True once the microphone is on, False
|
|
2949
|
+
if there is none. It looks again each frame and holds nothing up."""
|
|
2950
|
+
return _Until(lambda: self.state != "asking", lambda: self.state == "on")
|
|
2951
|
+
|
|
2952
|
+
def _said(self, e):
|
|
2953
|
+
if self.state == "off":
|
|
2954
|
+
return
|
|
2955
|
+
put = object.__setattr__
|
|
2956
|
+
put(self, "state", e.get("state") or "unavailable")
|
|
2957
|
+
put(self, "message", e.get("message") or "")
|
|
2958
|
+
if self.state != "asking":
|
|
2959
|
+
waiting, self._waiting[:] = list(self._waiting), []
|
|
2960
|
+
for fn in waiting:
|
|
2961
|
+
_call(fn, self.state == "on")
|
|
2962
|
+
|
|
2963
|
+
def close(self):
|
|
2964
|
+
"""Let the microphone go: the browser's recording light goes out. use_microphone() opens it again."""
|
|
2965
|
+
if self.state == "off":
|
|
2966
|
+
return self
|
|
2967
|
+
put = object.__setattr__
|
|
2968
|
+
put(self, "state", "off")
|
|
2969
|
+
put(self, "_listening", False)
|
|
2970
|
+
waiting, self._waiting[:] = list(self._waiting), []
|
|
2971
|
+
for fn in waiting:
|
|
2972
|
+
_call(fn, False)
|
|
2973
|
+
s = self._sound
|
|
2974
|
+
s.command({"op": "mic", "id": self._id, "do": "close", "t": s.clock()})
|
|
2975
|
+
if s.mic is self:
|
|
2976
|
+
s.mic = None
|
|
2977
|
+
return self
|
|
2978
|
+
|
|
2979
|
+
def remove(self):
|
|
2980
|
+
return self.close()
|
|
2981
|
+
|
|
2982
|
+
|
|
2983
|
+
def use_microphone(voice=None, *, listen=None, raw=None, **settings):
|
|
2984
|
+
"""The microphone: mic = use_microphone(). The first call has the page ask for it, and the browser asks the
|
|
2985
|
+
person; until then, and if the answer is no, everything still works and there is simply no sound from it.
|
|
2986
|
+
use_microphone("robot", listen=True) sets it up in one line; raw=False asks for the browser's own tidying
|
|
2987
|
+
(echo cancelling, noise removal, automatic level), which suits loudspeakers and spoils a voice changer.
|
|
2988
|
+
|
|
2989
|
+
It is a synth: mic.voice = "robot", mic.pitch = 7, mic.echo = 0.4, mic.gain = 2, mic.gate = -40. Nothing is
|
|
2990
|
+
heard until mic.listen(); wear headphones when you do. Recording does not need it."""
|
|
2991
|
+
if isinstance(voice, dict):
|
|
2992
|
+
settings = dict(voice, **settings)
|
|
2993
|
+
voice = settings.pop("voice", None)
|
|
2994
|
+
listen = settings.pop("listen", listen)
|
|
2995
|
+
raw = settings.pop("raw", raw)
|
|
2996
|
+
elif voice is not None and not isinstance(voice, str):
|
|
2997
|
+
raise ValueError('use_microphone takes a voice and settings, as in use_microphone("robot", echo=0.4), not %s' % _shown(voice))
|
|
2998
|
+
s = _sound()
|
|
2999
|
+
if s.mic is None:
|
|
3000
|
+
mic = Mic(s, s.id("m"))
|
|
3001
|
+
gate = _NAMES.get("microphone", {}).get("gate", -50)
|
|
3002
|
+
_patch_of({"micGate": gate}, "use_microphone", False, mic._patch)
|
|
3003
|
+
s.share(mic._patch, ["micGate"])
|
|
3004
|
+
c = {"op": "mic", "id": mic._id, "set": {"micGate": gate}, "t": s.clock()}
|
|
3005
|
+
if raw is False:
|
|
3006
|
+
c["raw"] = False
|
|
3007
|
+
s.hear_back()
|
|
3008
|
+
s.command(c)
|
|
3009
|
+
s.mic = mic
|
|
3010
|
+
if isinstance(s.transport, _Nowhere):
|
|
3011
|
+
mic._said({"state": "unavailable"})
|
|
3012
|
+
mic = s.mic
|
|
3013
|
+
if voice is not None:
|
|
3014
|
+
settings["voice"] = voice
|
|
3015
|
+
if settings:
|
|
3016
|
+
mic.set(settings)
|
|
3017
|
+
if listen is not None:
|
|
3018
|
+
mic.listen(bool(listen))
|
|
3019
|
+
return mic
|
|
3020
|
+
|
|
3021
|
+
|
|
3022
|
+
def stop_microphone():
|
|
3023
|
+
"""Let the microphone go, if it was asked for."""
|
|
3024
|
+
s = _sound()
|
|
3025
|
+
if s.mic is not None:
|
|
3026
|
+
s.mic.close()
|
|
3027
|
+
|
|
3028
|
+
|
|
3029
|
+
class Take(Synth):
|
|
3030
|
+
"""A recording, while it is being made and afterwards: what record_sound() gives back at once. It is a sample
|
|
3031
|
+
like one from load_sample, so take.play(), take.play("E4"), take.reverse = True and take.loop = True all work,
|
|
3032
|
+
and they work while it is still recording (it plays what there is so far).
|
|
3033
|
+
|
|
3034
|
+
take.done turns True and take.seconds says how long it is when it is over. In a Pyctures scene the page says
|
|
3035
|
+
so with a frame, so both change the frame AFTER the recording ended. take.on_done(fn) calls a function then,
|
|
3036
|
+
and `await take.when_done()` waits in a scene that can wait."""
|
|
3037
|
+
|
|
3038
|
+
_OWN = Synth._OWN + ("done", "recording", "seconds", "frames")
|
|
3039
|
+
|
|
3040
|
+
def __init__(self, sound, id, patch, sample):
|
|
3041
|
+
Synth.__init__(self, sound, id, patch, 60, None)
|
|
3042
|
+
put = object.__setattr__
|
|
3043
|
+
put(self, "_sample", sample)
|
|
3044
|
+
put(self, "_after", [])
|
|
3045
|
+
put(self, "done", False)
|
|
3046
|
+
put(self, "recording", True)
|
|
3047
|
+
put(self, "seconds", 0)
|
|
3048
|
+
put(self, "frames", 0)
|
|
3049
|
+
|
|
3050
|
+
def __repr__(self):
|
|
3051
|
+
return "<a recording, %s>" % ("%.2f seconds" % self.seconds if self.done else "still being made")
|
|
3052
|
+
|
|
3053
|
+
def on_done(self, fn):
|
|
3054
|
+
"""Call a function when the recording is over: take.on_done(lambda: take.play()). It is given the take, if
|
|
3055
|
+
it takes anything. One that is already over is called at once."""
|
|
3056
|
+
if not callable(fn):
|
|
3057
|
+
raise ValueError("on_done needs a function to call when the recording is over, as in "
|
|
3058
|
+
"take.on_done(lambda: take.play()), not %s" % _shown(fn))
|
|
3059
|
+
if self.done:
|
|
3060
|
+
_call(fn, self)
|
|
3061
|
+
else:
|
|
3062
|
+
self._after.append(fn)
|
|
3063
|
+
return self
|
|
3064
|
+
|
|
3065
|
+
def when_done(self):
|
|
3066
|
+
"""For a scene that can wait: `await take.when_done()` carries on when the recording is over. It looks
|
|
3067
|
+
again each frame and holds nothing up."""
|
|
3068
|
+
return _Until(lambda: self.done, lambda: self)
|
|
3069
|
+
|
|
3070
|
+
def stop_recording(self):
|
|
3071
|
+
"""End this recording now."""
|
|
3072
|
+
if not self.done:
|
|
3073
|
+
self._sound.command({"op": "record", "do": "stop", "id": self._sample, "t": self._sound.clock()})
|
|
3074
|
+
return self
|
|
3075
|
+
|
|
3076
|
+
def _finish(self, e):
|
|
3077
|
+
if self.done:
|
|
3078
|
+
return
|
|
3079
|
+
put = object.__setattr__
|
|
3080
|
+
put(self, "done", True)
|
|
3081
|
+
put(self, "recording", False)
|
|
3082
|
+
put(self, "seconds", e.get("seconds") or 0)
|
|
3083
|
+
put(self, "frames", e.get("frames") or 0)
|
|
3084
|
+
after, self._after[:] = list(self._after), []
|
|
3085
|
+
for fn in after:
|
|
3086
|
+
_call(fn, self)
|
|
3087
|
+
|
|
3088
|
+
|
|
3089
|
+
def _record_from(s, o, said):
|
|
3090
|
+
"""What record_sound, record_output, start_recording and a synth's record() all do."""
|
|
3091
|
+
names = _NAMES.get("microphone", {})
|
|
3092
|
+
longest = names.get("longestRecording", 120)
|
|
3093
|
+
source = o.pop("from", None) if o.get("source") is None else o.get("source")
|
|
3094
|
+
wid = s.id("w")
|
|
3095
|
+
c = {"op": "record", "id": wid}
|
|
3096
|
+
mic = None
|
|
3097
|
+
if source is None or source in ("mic", "microphone") or isinstance(source, Mic):
|
|
3098
|
+
c["from"] = "mic"
|
|
3099
|
+
mic = use_microphone()
|
|
3100
|
+
elif source == "output" or isinstance(source, _Output):
|
|
3101
|
+
c["from"] = "output"
|
|
3102
|
+
elif isinstance(source, Synth):
|
|
3103
|
+
c["from"], c["synth"] = "synth", source._id
|
|
3104
|
+
else:
|
|
3105
|
+
raise ValueError('%s: source needs the microphone, a synth or "output", not %s' % (said, _shown(source)))
|
|
3106
|
+
if o.get("length") is not None:
|
|
3107
|
+
c["len"] = s.length(o["length"], "%s: the length" % said)
|
|
3108
|
+
if c["len"] > longest:
|
|
3109
|
+
raise ValueError("%s: a recording can be up to %d seconds long, not %d" % (said, longest, round(c["len"])))
|
|
3110
|
+
elif o.get("max") is not None:
|
|
3111
|
+
c["max"] = _amount(o["max"], "%s: max" % said, 0.1, longest)
|
|
3112
|
+
if o.get("effects"):
|
|
3113
|
+
c["effects"] = True
|
|
3114
|
+
loop = o.get("loop")
|
|
3115
|
+
if loop:
|
|
3116
|
+
c["loop"] = list(loop) if isinstance(loop, (list, tuple)) else True
|
|
3117
|
+
c["t"] = s.when(said, o.get("after"), o.get("at"), o.get("beat"))
|
|
3118
|
+
sid = s.id("s")
|
|
3119
|
+
s.hear_back()
|
|
3120
|
+
s.command(c)
|
|
3121
|
+
s.command({"op": "synth", "id": sid, "preset": "sample", "set": {"sample": wid}, "t": s.clock()})
|
|
3122
|
+
patch = _patch_of(_NAMES["plain"], said, False)
|
|
3123
|
+
_patch_of(_NAMES["synths"].get("sample"), said, False, patch)
|
|
3124
|
+
patch[_setting("sample")["id"]] = 0
|
|
3125
|
+
take = Take(s, sid, patch, wid)
|
|
3126
|
+
if loop:
|
|
3127
|
+
object.__setattr__(take, "_loop", c["loop"])
|
|
3128
|
+
s.takes[wid] = take
|
|
3129
|
+
if mic is not None and not c.get("effects"):
|
|
3130
|
+
_dress_like(take, mic)
|
|
3131
|
+
if isinstance(s.transport, _Nowhere):
|
|
3132
|
+
s.takes.pop(wid, None)
|
|
3133
|
+
take._finish({}) # no player: it is over at once, and empty
|
|
3134
|
+
return take
|
|
3135
|
+
|
|
3136
|
+
|
|
3137
|
+
def record_sound(length=None, *, effects=False, source=None, max=None, loop=None, after=None, at=None, beat=None, **more):
|
|
3138
|
+
"""Record the microphone into a sample: take = record_sound(3) is three seconds (a note length by name works
|
|
3139
|
+
too). It gives the sample back at once and does not wait: your program carries on and the take fills up behind
|
|
3140
|
+
it (see Take: take.done, take.seconds, take.on_done(fn), take.play("E4")).
|
|
3141
|
+
|
|
3142
|
+
What is recorded is the plain voice, whether or not the microphone is being listened to, and the take is
|
|
3143
|
+
given the microphone's voice and effects, so it plays back as the microphone sounds and can be changed
|
|
3144
|
+
afterwards (take.voice = "monster"). effects=True records the voice after its effects instead, which needs
|
|
3145
|
+
listen(). source=lead records a synth, source="output" everything. With no microphone the take is empty,
|
|
3146
|
+
and done as soon as the page says so."""
|
|
3147
|
+
return _record_from(_sound(), dict(more, length=length, effects=effects, source=source, max=max, loop=loop,
|
|
3148
|
+
after=after, at=at, beat=beat), "record_sound")
|
|
3149
|
+
|
|
3150
|
+
|
|
3151
|
+
def start_recording(*, max=None, effects=False, source=None, loop=None, after=None, at=None, beat=None, **more):
|
|
3152
|
+
"""A recording with no set length: it runs until stop_recording(), or for 30 seconds, or max=seconds up to 120."""
|
|
3153
|
+
return _record_from(_sound(), dict(more, length=None, effects=effects, source=source, max=max, loop=loop,
|
|
3154
|
+
after=after, at=at, beat=beat), "start_recording")
|
|
3155
|
+
|
|
3156
|
+
|
|
3157
|
+
def stop_recording():
|
|
3158
|
+
"""End the recording that is being made."""
|
|
3159
|
+
s = _sound()
|
|
3160
|
+
s.command({"op": "record", "do": "stop", "t": s.clock()})
|
|
3161
|
+
|
|
3162
|
+
|
|
3163
|
+
def record_output(length=None, *, max=None, loop=None, after=None, at=None, beat=None):
|
|
3164
|
+
"""Record everything that is played, in stereo, for "save my tune": tune = record_output(10), then
|
|
3165
|
+
save_sound(tune, "tune.wav")."""
|
|
3166
|
+
return _record_from(_sound(), dict(length=length, source="output", max=max, loop=loop, after=after, at=at, beat=beat),
|
|
3167
|
+
"record_output")
|
|
3168
|
+
|
|
3169
|
+
|
|
3170
|
+
class Saved:
|
|
3171
|
+
"""What save_sound() gives back: saved.done turns True when the file has been made (a frame late in a Pyctures
|
|
3172
|
+
scene), saved.name is the file's name, saved.path where Python wrote it (None when the page kept or
|
|
3173
|
+
downloaded it), saved.bytes the WAV file itself when Python was given it, and saved.seconds how long it is.
|
|
3174
|
+
saved.on_done(fn) calls a function then, and `await saved.when_done()` waits in a scene that can wait."""
|
|
3175
|
+
|
|
3176
|
+
def __init__(self, name, write):
|
|
3177
|
+
self.name, self.path, self.bytes, self.seconds, self.done = name, None, None, 0, False
|
|
3178
|
+
self._write, self._after = write, []
|
|
3179
|
+
|
|
3180
|
+
def __repr__(self):
|
|
3181
|
+
return "<%s, %s>" % (self.name, "saved" if self.done else "being saved")
|
|
3182
|
+
|
|
3183
|
+
def on_done(self, fn):
|
|
3184
|
+
if not callable(fn):
|
|
3185
|
+
raise ValueError("on_done needs a function to call when the file is saved, not %s" % _shown(fn))
|
|
3186
|
+
if self.done:
|
|
3187
|
+
_call(fn, self)
|
|
3188
|
+
else:
|
|
3189
|
+
self._after.append(fn)
|
|
3190
|
+
return self
|
|
3191
|
+
|
|
3192
|
+
def when_done(self):
|
|
3193
|
+
return _Until(lambda: self.done, lambda: self)
|
|
3194
|
+
|
|
3195
|
+
def _finish(self, e):
|
|
3196
|
+
if self.done:
|
|
3197
|
+
return
|
|
3198
|
+
data = e.get("bytes")
|
|
3199
|
+
if isinstance(data, str):
|
|
3200
|
+
try:
|
|
3201
|
+
data = base64.b64decode(data)
|
|
3202
|
+
except ValueError:
|
|
3203
|
+
data = None
|
|
3204
|
+
elif isinstance(data, (list, tuple)):
|
|
3205
|
+
data = bytes(bytearray(data))
|
|
3206
|
+
self.bytes = bytes(data) if isinstance(data, (bytes, bytearray)) else None
|
|
3207
|
+
self.seconds = e.get("seconds") or 0
|
|
3208
|
+
if self._write and self.bytes is not None:
|
|
3209
|
+
try:
|
|
3210
|
+
folder = os.path.dirname(self.name)
|
|
3211
|
+
if folder:
|
|
3212
|
+
os.makedirs(folder, exist_ok=True)
|
|
3213
|
+
with open(self.name, "wb") as f:
|
|
3214
|
+
f.write(self.bytes)
|
|
3215
|
+
self.path = os.path.abspath(self.name)
|
|
3216
|
+
except OSError as err:
|
|
3217
|
+
_tell("save_sound(%s): the file could not be written (%s)." % (_shown(self.name), getattr(err, "strerror", None) or err))
|
|
3218
|
+
self.done = True
|
|
3219
|
+
after, self._after[:] = list(self._after), []
|
|
3220
|
+
for fn in after:
|
|
3221
|
+
_call(fn, self)
|
|
3222
|
+
|
|
3223
|
+
|
|
3224
|
+
def save_sound(what=None, name=None, *, download=None):
|
|
3225
|
+
"""A recording or a sample as a WAV file: save_sound(take, "hello.wav"). A recording that is still being made
|
|
3226
|
+
is saved when it is over.
|
|
3227
|
+
|
|
3228
|
+
WHERE IT GOES. Run by your own Python (`pyctures run`), the file is written beside your program, a frame or two
|
|
3229
|
+
after the recording is over (the page hands Python the file). In a web page it is downloaded, or kept by the
|
|
3230
|
+
app that shows the page. download=False saves nothing anywhere and hands Python the bytes: saved.bytes.
|
|
3231
|
+
It gives back a Saved at once (saved.done, saved.on_done(fn)) and never waits."""
|
|
3232
|
+
if not isinstance(what, Synth) or not what._sample:
|
|
3233
|
+
raise ValueError('save_sound needs a recording or a sample to save, as in save_sound(take, "hello.wav")')
|
|
3234
|
+
if name is not None and (not isinstance(name, str) or not name.strip()):
|
|
3235
|
+
raise ValueError('save_sound needs a name for the file, as in save_sound(take, "hello.wav"), not %s' % _shown(name))
|
|
3236
|
+
name = "sound.wav" if name is None else name.strip()
|
|
3237
|
+
if not name.lower().endswith(".wav"):
|
|
3238
|
+
name += ".wav"
|
|
3239
|
+
# A path from the program's folder, and never out of it.
|
|
3240
|
+
name = "/".join(p for p in name.replace("\\", "/").split("/") if p not in ("", ".", "..") and not p.endswith(":")) or "sound.wav"
|
|
3241
|
+
s = _sound()
|
|
3242
|
+
s.asks += 1
|
|
3243
|
+
c = {"op": "save", "id": what._sample, "name": name, "ask": s.asks, "t": s.clock()}
|
|
3244
|
+
write = download is not False and s.transport.keeps_files()
|
|
3245
|
+
if write or download is False:
|
|
3246
|
+
c["download"] = False # Python is given the bytes, and writes the file itself or keeps them
|
|
3247
|
+
saved = Saved(name, write)
|
|
3248
|
+
s.saves[s.asks] = saved
|
|
3249
|
+
s.hear_back()
|
|
3250
|
+
s.command(c)
|
|
3251
|
+
if isinstance(s.transport, _Nowhere):
|
|
3252
|
+
s.saves.pop(s.asks, None)
|
|
3253
|
+
saved._finish({})
|
|
3254
|
+
return saved
|
|
3255
|
+
|
|
3256
|
+
|
|
3257
|
+
def receive_sound_events(events):
|
|
3258
|
+
"""Hand the program what a player said back, just as the page would: a dict or a list of them, as synths.js's
|
|
3259
|
+
player.onSoundEvents gives them. {"type": "microphone", "state": "on"}, {"type": "recorded", "id", "frames",
|
|
3260
|
+
"seconds"}, {"type": "saved", "id", "ask", "name", "seconds", "bytes"} (bytes, or base64 text). Two more are
|
|
3261
|
+
understood, for a program that draws its sound: {"type": "reading", "id", "samples", "level", "notes"} (no
|
|
3262
|
+
id: all the sound) and {"type": "position", ...a song's place}."""
|
|
3263
|
+
_sound().receive(events)
|
|
3264
|
+
|
|
3265
|
+
|
|
3266
|
+
def voice_names():
|
|
3267
|
+
"""The voice changers a microphone, a recording or a synth can be given: mic.voice = "robot"."""
|
|
3268
|
+
return list(_VOICES["rows"])
|
|
3269
|
+
|
|
3270
|
+
|
|
3271
|
+
_MIDI_HOOKS = ("on_midi", "on_midi_note", "on_midi_note_off", "on_midi_control", "on_midi_devices")
|
|
3272
|
+
|
|
3273
|
+
|
|
3274
|
+
class MidiMessage(dict):
|
|
3275
|
+
"""What arrived, decoded: message.type ("noteOn", "noteOff", "control", "bend", "program", ...), message.note,
|
|
3276
|
+
message.velocity, message.control, message.value, message.channel, message.time, message.device and
|
|
3277
|
+
message.data (the bytes). Also message["note"]. What a kind of message does not have reads as None."""
|
|
3278
|
+
|
|
3279
|
+
def __getattr__(self, name):
|
|
3280
|
+
if name.startswith("__"):
|
|
3281
|
+
raise AttributeError(name)
|
|
3282
|
+
return self.get(name)
|
|
3283
|
+
|
|
3284
|
+
|
|
3285
|
+
def _call(fn, *args):
|
|
3286
|
+
"""Call a function of the program's with as many of `args` as it takes: on_midi_note(note, velocity) and
|
|
3287
|
+
on_midi_note(note, velocity, channel) are both right."""
|
|
3288
|
+
code = getattr(fn, "__code__", None)
|
|
3289
|
+
if code is not None and not code.co_flags & 0x04: # no *args of its own
|
|
3290
|
+
takes = code.co_argcount - (1 if getattr(fn, "__self__", None) is not None else 0)
|
|
3291
|
+
if takes < len(args):
|
|
3292
|
+
args = args[:takes]
|
|
3293
|
+
return fn(*args)
|
|
3294
|
+
|
|
3295
|
+
|
|
3296
|
+
class _Midi:
|
|
3297
|
+
"""One program's MIDI: the functions it gave, the devices it has been told of, and the settings tied to knobs."""
|
|
3298
|
+
|
|
3299
|
+
def __init__(self, sound):
|
|
3300
|
+
self.sound = sound
|
|
3301
|
+
self.on = {"all": [], "note": [], "note_off": [], "control": [], "devices": []}
|
|
3302
|
+
self.devices = {"inputs": [], "outputs": [], "available": None}
|
|
3303
|
+
self.listening = False
|
|
3304
|
+
self.joined = False
|
|
3305
|
+
self.asked = False
|
|
3306
|
+
self.clock_on = False
|
|
3307
|
+
self.bound = {} # synth id -> setting -> [the synth, a function to call with each new value]
|
|
3308
|
+
self.hooked = set() # the scene's own functions already taken by use_midi_hooks
|
|
3309
|
+
self.ports = {} # (which, a synth's id or None) -> what midi_input() and midi_output() gave
|
|
3310
|
+
self.held = {} # an input's notes held down now: note -> how hard
|
|
3311
|
+
self.last = [0, 0] # how hard the last note was hit, and when
|
|
3312
|
+
|
|
3313
|
+
# Every MIDI call sends a command (which is what has the page ask for permission) and makes sure this
|
|
3314
|
+
# program is handed what the page's desk says.
|
|
3315
|
+
def command(self, c):
|
|
3316
|
+
s = self.sound
|
|
3317
|
+
s.command(dict({"op": "midi"}, **c))
|
|
3318
|
+
if not self.joined:
|
|
3319
|
+
self.joined = True
|
|
3320
|
+
s.transport.listen(self.receive)
|
|
3321
|
+
|
|
3322
|
+
def ask(self):
|
|
3323
|
+
if not self.asked:
|
|
3324
|
+
self.asked = True
|
|
3325
|
+
self.command({"do": "devices"})
|
|
3326
|
+
|
|
3327
|
+
def listen(self):
|
|
3328
|
+
if not self.listening:
|
|
3329
|
+
self.listening = True
|
|
3330
|
+
self.command({"do": "listen"})
|
|
3331
|
+
|
|
3332
|
+
def add(self, which, fn, said, example):
|
|
3333
|
+
if not callable(fn):
|
|
3334
|
+
raise ValueError("%s needs a function to call, as in %s(%s), not %s" % (said, said, example, _shown(fn)))
|
|
3335
|
+
if fn not in self.on[which]:
|
|
3336
|
+
self.on[which].append(fn)
|
|
3337
|
+
return fn
|
|
3338
|
+
|
|
3339
|
+
def receive(self, events):
|
|
3340
|
+
"""What the page's desk says happened, as a list of plain dicts: the program's functions are called with it."""
|
|
3341
|
+
if isinstance(events, dict):
|
|
3342
|
+
events = [events]
|
|
3343
|
+
for m in events or ():
|
|
3344
|
+
if not isinstance(m, dict):
|
|
3345
|
+
continue
|
|
3346
|
+
m = MidiMessage(m)
|
|
3347
|
+
kind = m.get("type")
|
|
3348
|
+
if kind == "devices":
|
|
3349
|
+
self.devices = {"inputs": list(m.get("inputs") or ()), "outputs": list(m.get("outputs") or ()),
|
|
3350
|
+
"available": bool(m.get("available"))}
|
|
3351
|
+
# With no MIDI (`available` False) the page has said why, once, in its console; nothing more is said here.
|
|
3352
|
+
for fn in list(self.on["devices"]):
|
|
3353
|
+
_call(fn, self.devices_now())
|
|
3354
|
+
for port in list(self.ports.values()):
|
|
3355
|
+
port._devices_changed()
|
|
3356
|
+
elif kind == "set":
|
|
3357
|
+
# A knob turned a setting on the page: the synth here is told, so that reading it back gives what it is.
|
|
3358
|
+
b = self.bound.get(m.get("id"), {}).get(_key(m.get("setting")))
|
|
3359
|
+
if b is not None:
|
|
3360
|
+
b[0]._told(m.get("setting"), m.get("value"))
|
|
3361
|
+
if b[1] is not None:
|
|
3362
|
+
_call(b[1], b[0].get(m.get("setting")), m.get("setting"))
|
|
3363
|
+
elif kind == "noteOn":
|
|
3364
|
+
self.held[m.get("note")] = m.get("velocity") or 0
|
|
3365
|
+
self.last = [m.get("velocity") or 0, self.sound.clock()]
|
|
3366
|
+
for fn in list(self.on["note"]):
|
|
3367
|
+
_call(fn, m.get("note"), m.get("velocity"), m.get("channel"))
|
|
3368
|
+
elif kind == "noteOff":
|
|
3369
|
+
self.held.pop(m.get("note"), None)
|
|
3370
|
+
if self.on["note_off"]:
|
|
3371
|
+
for fn in list(self.on["note_off"]):
|
|
3372
|
+
_call(fn, m.get("note"), m.get("channel"))
|
|
3373
|
+
else:
|
|
3374
|
+
for fn in list(self.on["note"]):
|
|
3375
|
+
_call(fn, m.get("note"), 0, m.get("channel"))
|
|
3376
|
+
elif kind == "control":
|
|
3377
|
+
for fn in list(self.on["control"]):
|
|
3378
|
+
_call(fn, m.get("control"), m.get("value"), m.get("channel"))
|
|
3379
|
+
for fn in list(self.on["all"]):
|
|
3380
|
+
_call(fn, m)
|
|
3381
|
+
|
|
3382
|
+
def devices_now(self):
|
|
3383
|
+
d = self.devices
|
|
3384
|
+
return {"inputs": list(d["inputs"]), "outputs": list(d["outputs"]), "available": d["available"]}
|
|
3385
|
+
|
|
3386
|
+
# --- what is written
|
|
3387
|
+
def channel(self, channel, said):
|
|
3388
|
+
if channel is None:
|
|
3389
|
+
return 0
|
|
3390
|
+
if not isinstance(channel, int) or isinstance(channel, bool) or not 1 <= channel <= 16:
|
|
3391
|
+
raise ValueError("%s: channel needs a whole number from 1 to 16, not %s" % (said, _shown(channel)))
|
|
3392
|
+
return channel - 1
|
|
3393
|
+
|
|
3394
|
+
def whole(self, value, said, what, most=127):
|
|
3395
|
+
if not isinstance(value, int) or isinstance(value, bool) or not 0 <= value <= most:
|
|
3396
|
+
raise ValueError("%s needs %s, a whole number from 0 to %d, not %s" % (said, what, most, _shown(value)))
|
|
3397
|
+
return value
|
|
3398
|
+
|
|
3399
|
+
def notes(self, what, said):
|
|
3400
|
+
"""Notes as MIDI has them: whole numbers from 0 to 127."""
|
|
3401
|
+
out = []
|
|
3402
|
+
for n in _notes_of(what):
|
|
3403
|
+
n = int(math.floor(n + 0.5))
|
|
3404
|
+
if not 0 <= n <= 127:
|
|
3405
|
+
raise ValueError('%s: MIDI has the notes 0 to 127 ("C-1" to "G9"), and %s is outside them' % (said, _shown(what)))
|
|
3406
|
+
out.append(n)
|
|
3407
|
+
return out
|
|
3408
|
+
|
|
3409
|
+
def out(self, data, t, output):
|
|
3410
|
+
c = {"do": "send", "bytes": data, "t": t}
|
|
3411
|
+
if output is not None:
|
|
3412
|
+
c["output"] = str(output)
|
|
3413
|
+
self.command(c)
|
|
3414
|
+
|
|
3415
|
+
# --- a synth's
|
|
3416
|
+
def play(self, synth, on, channel, vibrato):
|
|
3417
|
+
if on is False:
|
|
3418
|
+
self.command({"do": "play", "id": synth._id, "on": False})
|
|
3419
|
+
return
|
|
3420
|
+
c = {"do": "play", "id": synth._id,
|
|
3421
|
+
"vibrato": [synth.get("vibrato") or 0, 0.5 if vibrato is None else _amount(vibrato, "play_from_midi: vibrato", 0, 12)]}
|
|
3422
|
+
if channel is not None:
|
|
3423
|
+
c["channel"] = self.channel(channel, "play_from_midi") + 1
|
|
3424
|
+
self.command(c)
|
|
3425
|
+
|
|
3426
|
+
def through(self, synth, on, channel, output):
|
|
3427
|
+
if on is False:
|
|
3428
|
+
self.command({"do": "through", "id": synth._id, "on": False})
|
|
3429
|
+
return
|
|
3430
|
+
c = {"do": "through", "id": synth._id, "channel": self.channel(channel, "send_to_midi") + 1, "note": synth._note}
|
|
3431
|
+
if output is not None:
|
|
3432
|
+
c["output"] = str(output)
|
|
3433
|
+
self.command(c)
|
|
3434
|
+
|
|
3435
|
+
def turn(self, synth, do, setting, on_change, low, high, channel, control=None):
|
|
3436
|
+
"""A setting of a synth that a knob turns: its range, and whether it moves by ratio (a pitch in Hz) or by amount."""
|
|
3437
|
+
said = "learn" if do == "learn" else "map_midi"
|
|
3438
|
+
if not isinstance(setting, str):
|
|
3439
|
+
raise ValueError("%s needs the name of a setting, as in %s, not %s"
|
|
3440
|
+
% (said, 'lead.learn("cutoff")' if do == "learn" else 'lead.map_midi(74, "cutoff")', _shown(setting)))
|
|
3441
|
+
entry = _setting(setting)
|
|
3442
|
+
if entry is None:
|
|
3443
|
+
raise ValueError("%s: there is no setting called %s.%s" % (said, _shown(setting), _meant(setting, _SHOWN)))
|
|
3444
|
+
if on_change is not None and not callable(on_change):
|
|
3445
|
+
raise ValueError("%s: what comes after the setting is a function to call with each new value, not %s" % (said, _shown(on_change)))
|
|
3446
|
+
low = entry["min"] / entry["scale"] if low is None else low
|
|
3447
|
+
high = entry["max"] / entry["scale"] if high is None else high
|
|
3448
|
+
if not _is_number(low) or not _is_number(high):
|
|
3449
|
+
raise ValueError("%s: min and max need numbers, not %s and %s" % (said, _shown(low), _shown(high)))
|
|
3450
|
+
self.bound.setdefault(synth._id, {})[_key(setting)] = [synth, on_change]
|
|
3451
|
+
ratio = entry.get("unit") == "hz" and entry["scale"] == 1 and low > 0 and high / low >= 100
|
|
3452
|
+
c = {"do": do, "id": synth._id, "setting": setting, "min": low, "max": high, "kind": entry["kind"],
|
|
3453
|
+
"curve": "exp" if ratio else "lin"}
|
|
3454
|
+
if control is not None:
|
|
3455
|
+
c["control"] = control
|
|
3456
|
+
if channel is not None:
|
|
3457
|
+
c["channel"] = self.channel(channel, said) + 1
|
|
3458
|
+
self.command(c)
|
|
3459
|
+
|
|
3460
|
+
def unmap(self, synth, setting):
|
|
3461
|
+
if setting is None:
|
|
3462
|
+
self.bound.pop(synth._id, None)
|
|
3463
|
+
self.command({"do": "unmap", "id": synth._id})
|
|
3464
|
+
else:
|
|
3465
|
+
self.bound.get(synth._id, {}).pop(_key(setting), None)
|
|
3466
|
+
self.command({"do": "unmap", "id": synth._id, "setting": setting})
|
|
3467
|
+
|
|
3468
|
+
def choose(self, which, name, said):
|
|
3469
|
+
many = which == "input" and isinstance(name, (list, tuple)) and all(isinstance(n, str) for n in name)
|
|
3470
|
+
if name is not None and not isinstance(name, str) and not many:
|
|
3471
|
+
raise ValueError('%s needs the name of a MIDI device, as in %s(midi_devices()["%ss"][0]), not %s'
|
|
3472
|
+
% (said, said, which, _shown(name)))
|
|
3473
|
+
known = self.devices[which + "s"]
|
|
3474
|
+
for n in ([] if name is None else [name] if isinstance(name, str) else name):
|
|
3475
|
+
if self.devices["available"] and n not in known:
|
|
3476
|
+
_tell("%s(%s): there is no MIDI %s called %s at the moment. %s. It will be used if it appears."
|
|
3477
|
+
% (said, _shown(n), which, _shown(n),
|
|
3478
|
+
"The %ss are: %s" % (which, ", ".join(_shown(k) for k in known)) if known else "No MIDI %s is plugged in" % which))
|
|
3479
|
+
self.command({"do": which, "name": list(name) if many else name})
|
|
3480
|
+
port = self.ports.get((which, None))
|
|
3481
|
+
if port is not None:
|
|
3482
|
+
port._chosen(name if isinstance(name, str) else None)
|
|
3483
|
+
|
|
3484
|
+
def stop(self):
|
|
3485
|
+
for fns in self.on.values():
|
|
3486
|
+
del fns[:]
|
|
3487
|
+
self.bound.clear()
|
|
3488
|
+
self.hooked.clear()
|
|
3489
|
+
self.listening = self.clock_on = False
|
|
3490
|
+
self.command({"do": "stop", "t": self.sound.clock()})
|
|
3491
|
+
|
|
3492
|
+
|
|
3493
|
+
def on_midi(fn=None):
|
|
3494
|
+
"""Everything that arrives, decoded: on_midi(heard), where heard(message) reads message.type, message.note,
|
|
3495
|
+
message.velocity, message.control, message.value, message.channel, message.time and message.device.
|
|
3496
|
+
In a Pyctures scene a function of your own called on_midi is called without this."""
|
|
3497
|
+
m = _sound().midi
|
|
3498
|
+
m.add("all", fn, "on_midi", "heard")
|
|
3499
|
+
m.listen()
|
|
3500
|
+
return fn
|
|
3501
|
+
|
|
3502
|
+
|
|
3503
|
+
def on_midi_note(fn=None):
|
|
3504
|
+
"""A key went down: on_midi_note(played), where played(note, velocity, channel) is given the note as a number
|
|
3505
|
+
(60 is middle C) and how hard it was hit, from 0 to 1. A key coming up calls it with velocity 0, unless
|
|
3506
|
+
on_midi_note_off is used. In a Pyctures scene a function of your own called on_midi_note is called without this."""
|
|
3507
|
+
m = _sound().midi
|
|
3508
|
+
m.add("note", fn, "on_midi_note", "played")
|
|
3509
|
+
m.listen()
|
|
3510
|
+
return fn
|
|
3511
|
+
|
|
3512
|
+
|
|
3513
|
+
def on_midi_note_off(fn=None):
|
|
3514
|
+
"""A key came up: on_midi_note_off(let_go), where let_go(note, channel)."""
|
|
3515
|
+
m = _sound().midi
|
|
3516
|
+
m.add("note_off", fn, "on_midi_note_off", "let_go")
|
|
3517
|
+
m.listen()
|
|
3518
|
+
return fn
|
|
3519
|
+
|
|
3520
|
+
|
|
3521
|
+
def on_midi_control(fn=None):
|
|
3522
|
+
"""A knob, slider or pedal moved: on_midi_control(turned), where turned(number, value, channel) is given which
|
|
3523
|
+
one (0 to 127; the sustain pedal is 64) and its value, from 0 to 1."""
|
|
3524
|
+
m = _sound().midi
|
|
3525
|
+
m.add("control", fn, "on_midi_control", "turned")
|
|
3526
|
+
m.listen()
|
|
3527
|
+
return fn
|
|
3528
|
+
|
|
3529
|
+
|
|
3530
|
+
def on_midi_devices(fn=None):
|
|
3531
|
+
"""The devices are known, or one was plugged in or pulled out: on_midi_devices(found), where found(devices)
|
|
3532
|
+
is given {"inputs": [names], "outputs": [names], "available": True or False}."""
|
|
3533
|
+
m = _sound().midi
|
|
3534
|
+
m.add("devices", fn, "on_midi_devices", "found")
|
|
3535
|
+
if m.devices["available"] is not None:
|
|
3536
|
+
_call(fn, m.devices_now())
|
|
3537
|
+
else:
|
|
3538
|
+
m.ask()
|
|
3539
|
+
return fn
|
|
3540
|
+
|
|
3541
|
+
|
|
3542
|
+
def use_midi_hooks(scene):
|
|
3543
|
+
"""For an app that runs scenes: the scene's own functions called on_midi_note, on_midi_note_off, on_midi_control,
|
|
3544
|
+
on_midi and on_midi_devices are called with what arrives. Give it the scene's names (a dict, a module or an
|
|
3545
|
+
object). Pyctures does this for every scene, so `def on_midi_note(note, velocity, channel):` is enough.
|
|
3546
|
+
Nothing is asked of the page unless the scene has at least one."""
|
|
3547
|
+
if scene is None or isinstance(scene, (str, int, float)):
|
|
3548
|
+
raise ValueError("use_midi_hooks needs the scene's names, with functions such as on_midi_note among them, not %s" % _shown(scene))
|
|
3549
|
+
get = scene.get if isinstance(scene, dict) else lambda name: getattr(scene, name, None)
|
|
3550
|
+
for name in _MIDI_HOOKS:
|
|
3551
|
+
fn = get(name)
|
|
3552
|
+
if not callable(fn) or fn is globals()[name]: # `from synths import *` gave the scene this module's own
|
|
3553
|
+
continue
|
|
3554
|
+
m = _sound().midi
|
|
3555
|
+
if (name, fn) not in m.hooked:
|
|
3556
|
+
m.hooked.add((name, fn))
|
|
3557
|
+
globals()[name](fn)
|
|
3558
|
+
|
|
3559
|
+
|
|
3560
|
+
def send_midi(data=None, *, output=None, after=None, at=None, beat=None):
|
|
3561
|
+
"""Any MIDI message at all, as its bytes: send_midi([0x90, 60, 100])."""
|
|
3562
|
+
try:
|
|
3563
|
+
listed = list(data) if isinstance(data, (list, tuple, bytes, bytearray)) else None
|
|
3564
|
+
except TypeError:
|
|
3565
|
+
listed = None
|
|
3566
|
+
if not listed or any(not isinstance(b, int) or isinstance(b, bool) or not 0 <= b <= 255 for b in listed) or listed[0] < 0x80:
|
|
3567
|
+
raise ValueError("send_midi needs the bytes of a MIDI message, beginning with its status byte, as in "
|
|
3568
|
+
"send_midi([0x90, 60, 100]), not %s" % _shown(data))
|
|
3569
|
+
s = _sound()
|
|
3570
|
+
s.midi.out(listed, s.when("send_midi", after, at, beat), output)
|
|
3571
|
+
|
|
3572
|
+
|
|
3573
|
+
def send_midi_note(note=None, velocity=0.8, length=0.5, *, channel=None, output=None, after=None, at=None, beat=None):
|
|
3574
|
+
"""A note out to another device: send_midi_note("C4", 0.8, length="quarter"). Velocity is how hard, from 0 to 1.
|
|
3575
|
+
It is let go after `length` (seconds, or a note length by name); length=None holds it until send_midi_note_off.
|
|
3576
|
+
A list of notes, or a chord by name, is a chord."""
|
|
3577
|
+
said = "send_midi_note"
|
|
3578
|
+
if note is None:
|
|
3579
|
+
raise ValueError('send_midi_note needs a note, as in send_midi_note("C4")')
|
|
3580
|
+
s = _sound()
|
|
3581
|
+
m = s.midi
|
|
3582
|
+
notes = m.notes(note, said)
|
|
3583
|
+
velocity = _amount(velocity, "%s: velocity" % said)
|
|
3584
|
+
long = None if length is None else s.length(length, "%s: the length" % said)
|
|
3585
|
+
ch = m.channel(channel, said)
|
|
3586
|
+
t = s.when(said, after, at, beat)
|
|
3587
|
+
for n in notes:
|
|
3588
|
+
if velocity > 0:
|
|
3589
|
+
m.out([0x90 | ch, n, max(1, _seven(velocity))], t, output)
|
|
3590
|
+
if long is not None or not velocity > 0:
|
|
3591
|
+
m.out([0x80 | ch, n, 0], t + (long if velocity > 0 else 0), output)
|
|
3592
|
+
|
|
3593
|
+
|
|
3594
|
+
def send_midi_note_off(note=None, *, channel=None, output=None, after=None, at=None, beat=None):
|
|
3595
|
+
"""Let go of a note that send_midi_note is holding: send_midi_note_off("C4")."""
|
|
3596
|
+
if note is None:
|
|
3597
|
+
raise ValueError('send_midi_note_off needs a note, as in send_midi_note_off("C4")')
|
|
3598
|
+
s = _sound()
|
|
3599
|
+
m = s.midi
|
|
3600
|
+
ch = m.channel(channel, "send_midi_note_off")
|
|
3601
|
+
t = s.when("send_midi_note_off", after, at, beat)
|
|
3602
|
+
for n in m.notes(note, "send_midi_note_off"):
|
|
3603
|
+
m.out([0x80 | ch, n, 0], t, output)
|
|
3604
|
+
|
|
3605
|
+
|
|
3606
|
+
def send_midi_control(number=None, value=None, *, channel=None, output=None, after=None, at=None, beat=None):
|
|
3607
|
+
"""Turn a knob on the other device: send_midi_control(74, 0.5). The number is the control's, 0 to 127; the
|
|
3608
|
+
value is from 0 to 1."""
|
|
3609
|
+
said = "send_midi_control"
|
|
3610
|
+
s = _sound()
|
|
3611
|
+
m = s.midi
|
|
3612
|
+
data = [0xb0 | m.channel(channel, said), m.whole(number, said, "the number of a control"), _seven(_amount(value, "%s: the value" % said))]
|
|
3613
|
+
m.out(data, s.when(said, after, at, beat), output)
|
|
3614
|
+
|
|
3615
|
+
|
|
3616
|
+
def send_midi_bend(bend=None, *, channel=None, output=None, after=None, at=None, beat=None):
|
|
3617
|
+
"""Bend the pitch on the other device: send_midi_bend(0.5), from -1 to 1, and 0 to let it go back."""
|
|
3618
|
+
s = _sound()
|
|
3619
|
+
m = s.midi
|
|
3620
|
+
b = _amount(bend, "send_midi_bend", -1, 1)
|
|
3621
|
+
v = int(_clamp(math.floor(8192 + b * (8192 if b < 0 else 8191) + 0.5), 0, 16383))
|
|
3622
|
+
m.out([0xe0 | m.channel(channel, "send_midi_bend"), v & 127, v >> 7], s.when("send_midi_bend", after, at, beat), output)
|
|
3623
|
+
|
|
3624
|
+
|
|
3625
|
+
def send_midi_program(number=None, *, channel=None, output=None, after=None, at=None, beat=None):
|
|
3626
|
+
"""Choose a sound on the other device by its number: send_midi_program(5)."""
|
|
3627
|
+
s = _sound()
|
|
3628
|
+
m = s.midi
|
|
3629
|
+
data = [0xc0 | m.channel(channel, "send_midi_program"), m.whole(number, "send_midi_program", "the number of a sound")]
|
|
3630
|
+
m.out(data, s.when("send_midi_program", after, at, beat), output)
|
|
3631
|
+
|
|
3632
|
+
|
|
3633
|
+
def send_midi_clock(on=True):
|
|
3634
|
+
"""Send the beat to other devices, so that their own sequencers keep time with set_tempo: send_midi_clock(),
|
|
3635
|
+
and send_midi_clock(False) to stop."""
|
|
3636
|
+
s = _sound()
|
|
3637
|
+
s.midi.clock_on = on is not False
|
|
3638
|
+
s.midi.command({"do": "clock", "on": s.midi.clock_on, "bpm": s.tempo, "t": s.clock()})
|
|
3639
|
+
|
|
3640
|
+
|
|
3641
|
+
def midi_devices():
|
|
3642
|
+
"""The devices there are: {"inputs": [names], "outputs": [names], "available": True, False or None}.
|
|
3643
|
+
The page is asked and answers with a later frame, so the first call gives empty lists and available None:
|
|
3644
|
+
use on_midi_devices(found) to be called when they are known, or ask again in update()."""
|
|
3645
|
+
m = _sound().midi
|
|
3646
|
+
m.ask()
|
|
3647
|
+
return m.devices_now()
|
|
3648
|
+
|
|
3649
|
+
|
|
3650
|
+
def use_midi_input(name=None):
|
|
3651
|
+
"""Listen to one input by its name (or a list of names), instead of all of them: use_midi_input("Keystation 49").
|
|
3652
|
+
No name: all again."""
|
|
3653
|
+
_sound().midi.choose("input", name, "use_midi_input")
|
|
3654
|
+
|
|
3655
|
+
|
|
3656
|
+
def use_midi_output(name=None):
|
|
3657
|
+
"""Send to one output by its name, instead of the first: use_midi_output("Minilogue"). No name: the first again."""
|
|
3658
|
+
_sound().midi.choose("output", name, "use_midi_output")
|
|
3659
|
+
|
|
3660
|
+
|
|
3661
|
+
class MidiPort:
|
|
3662
|
+
"""The MIDI device and the channel as a TARGET, which a select on the screen is tied to as a dial is tied to
|
|
3663
|
+
a synth: what midi_input(), lead.midi_input() and midi_output() give. It has two settings, "device" and
|
|
3664
|
+
"channel": port.get("device"), port.set("channel", 2), port.describe("device")["choices"], and port.device
|
|
3665
|
+
and port.channel as attributes. The choices follow what is plugged in, which the page says a frame late: until
|
|
3666
|
+
it has, a device reads "no devices". port.on_change(fn) calls fn(name) when a value or the choices change."""
|
|
3667
|
+
|
|
3668
|
+
def __init__(self, midi, which, synth):
|
|
3669
|
+
self._midi, self._which, self._synth = midi, which, synth
|
|
3670
|
+
self._device, self._channel, self._heard = None, None, []
|
|
3671
|
+
self._told = None # the name the page was last told, which is None while the chosen device is away
|
|
3672
|
+
self.names = list(_NAMES["midi"].get("choices", {}).get("settings", ("device", "channel")))
|
|
3673
|
+
|
|
3674
|
+
def _words(self):
|
|
3675
|
+
w = _NAMES["midi"].get("choices", {})
|
|
3676
|
+
return w.get("all", "all"), w.get("none", "no devices"), w.get("unavailable", "no MIDI here")
|
|
3677
|
+
|
|
3678
|
+
def _devices(self):
|
|
3679
|
+
d = self._midi.devices
|
|
3680
|
+
return list(d[self._which + "s"]), d["available"]
|
|
3681
|
+
|
|
3682
|
+
def get(self, setting):
|
|
3683
|
+
every, none, unavailable = self._words()
|
|
3684
|
+
if _key(setting) == "channel":
|
|
3685
|
+
return str(self._channel) if self._channel else every if self._which == "input" else "1"
|
|
3686
|
+
if _key(setting) != "device":
|
|
3687
|
+
return None
|
|
3688
|
+
known, available = self._devices()
|
|
3689
|
+
if available is False:
|
|
3690
|
+
return unavailable
|
|
3691
|
+
if not known:
|
|
3692
|
+
return none
|
|
3693
|
+
if self._device in known:
|
|
3694
|
+
return self._device
|
|
3695
|
+
return every if self._which == "input" else known[0] # (a chosen device that is away reads as what is used meanwhile)
|
|
3696
|
+
|
|
3697
|
+
def describe(self, setting):
|
|
3698
|
+
every, none, unavailable = self._words()
|
|
3699
|
+
numbers = [str(n) for n in range(1, 17)]
|
|
3700
|
+
if _key(setting) == "channel":
|
|
3701
|
+
return {"name": "channel", "kind": "text", "choices": [every] + numbers if self._which == "input" else numbers,
|
|
3702
|
+
"default": every if self._which == "input" else "1"}
|
|
3703
|
+
if _key(setting) != "device":
|
|
3704
|
+
return None
|
|
3705
|
+
known, available = self._devices()
|
|
3706
|
+
choices = [unavailable] if available is False else [none] if not known else ([every] + known if self._which == "input" else known)
|
|
3707
|
+
return {"name": "device", "kind": "text", "choices": choices, "default": choices[0]}
|
|
3708
|
+
|
|
3709
|
+
def set(self, setting, value):
|
|
3710
|
+
every, none, unavailable = self._words()
|
|
3711
|
+
c = {"do": self._which}
|
|
3712
|
+
if _key(setting) == "channel":
|
|
3713
|
+
if value is None or value == every:
|
|
3714
|
+
self._channel = None
|
|
3715
|
+
else:
|
|
3716
|
+
number = int(value) if isinstance(value, str) and value.strip().isdigit() else value
|
|
3717
|
+
self._midi.channel(number, "midi_%s: channel" % self._which) # 1 to 16, or it says so
|
|
3718
|
+
self._channel = number
|
|
3719
|
+
c["channel"] = self._channel
|
|
3720
|
+
elif _key(setting) == "device":
|
|
3721
|
+
if value is not None and not isinstance(value, str):
|
|
3722
|
+
raise ValueError("midi_%s: device needs the name of a MIDI device, not %s" % (self._which, _shown(value)))
|
|
3723
|
+
self._device = None if value in (None, every, none, unavailable) else value
|
|
3724
|
+
known, available = self._devices()
|
|
3725
|
+
# The page is told a name only while that device is there (or before it has said what is there).
|
|
3726
|
+
self._told = self._device if self._device in known or available is None else None
|
|
3727
|
+
c["name"] = self._told
|
|
3728
|
+
else:
|
|
3729
|
+
raise ValueError('midi_%s has "device" and "channel", and no %s' % (self._which, _shown(setting)))
|
|
3730
|
+
if self._synth is not None:
|
|
3731
|
+
c["id"] = self._synth._id
|
|
3732
|
+
self._midi.command(c)
|
|
3733
|
+
self._changed(_key(setting))
|
|
3734
|
+
return self
|
|
3735
|
+
|
|
3736
|
+
def on_change(self, fn):
|
|
3737
|
+
if not callable(fn):
|
|
3738
|
+
raise ValueError("on_change needs a function to call, as in on_change(lambda name: ...), not %s" % _shown(fn))
|
|
3739
|
+
self._heard.append(fn)
|
|
3740
|
+
return lambda: self._heard.remove(fn) if fn in self._heard else None
|
|
3741
|
+
|
|
3742
|
+
def _chosen(self, name):
|
|
3743
|
+
self._device = self._told = name
|
|
3744
|
+
self._changed("device")
|
|
3745
|
+
|
|
3746
|
+
def _changed(self, name):
|
|
3747
|
+
for fn in list(self._heard):
|
|
3748
|
+
_call(fn, name)
|
|
3749
|
+
|
|
3750
|
+
def _devices_changed(self):
|
|
3751
|
+
"""The devices came or went. A chosen device that is pulled out falls back, which is said once, and is
|
|
3752
|
+
chosen again when it returns: falling back is the sender's work (synths.js's docs/midi.md)."""
|
|
3753
|
+
known, available = self._devices()
|
|
3754
|
+
want = self._device if self._device in known else None
|
|
3755
|
+
if want != self._told:
|
|
3756
|
+
if want is None:
|
|
3757
|
+
_tell("the MIDI %s %s is not there any more, so %s is used. It will be chosen again if it comes back."
|
|
3758
|
+
% (self._which, _shown(self._device), "every input" if self._which == "input" else "the first output"))
|
|
3759
|
+
self._told = want
|
|
3760
|
+
c = {"do": self._which, "name": want}
|
|
3761
|
+
if self._synth is not None:
|
|
3762
|
+
c["id"] = self._synth._id
|
|
3763
|
+
self._midi.command(c)
|
|
3764
|
+
self._changed("device")
|
|
3765
|
+
|
|
3766
|
+
device = property(lambda self: self.get("device"), lambda self, value: self.set("device", value))
|
|
3767
|
+
channel = property(lambda self: self._channel or (self._words()[0] if self._which == "input" else 1),
|
|
3768
|
+
lambda self, value: self.set("channel", value))
|
|
3769
|
+
|
|
3770
|
+
def level(self):
|
|
3771
|
+
"""How hard the last note was hit, fading away: from 0 to 1, for a meter that is an activity light. It
|
|
3772
|
+
follows the notes Python has been handed, so only while the program listens (on_midi_note, on_midi)."""
|
|
3773
|
+
if self._which != "input":
|
|
3774
|
+
return 0
|
|
3775
|
+
self._midi.listen()
|
|
3776
|
+
hit, when = self._midi.last
|
|
3777
|
+
return hit * 0.5 ** ((self._midi.sound.clock() - when) / 0.09) # halving every 90 ms
|
|
3778
|
+
|
|
3779
|
+
def notes(self):
|
|
3780
|
+
"""The keys held down now, as numbers, from the notes Python has been handed."""
|
|
3781
|
+
if self._which != "input":
|
|
3782
|
+
return []
|
|
3783
|
+
self._midi.listen()
|
|
3784
|
+
return sorted(self._midi.held)
|
|
3785
|
+
|
|
3786
|
+
|
|
3787
|
+
def _midi_port(which, synth):
|
|
3788
|
+
m = _sound().midi if synth is None else synth._sound.midi
|
|
3789
|
+
key = (which, synth._id if synth is not None else None)
|
|
3790
|
+
if key not in m.ports:
|
|
3791
|
+
m.ports[key] = MidiPort(m, which, synth)
|
|
3792
|
+
m.ask()
|
|
3793
|
+
return m.ports[key]
|
|
3794
|
+
|
|
3795
|
+
|
|
3796
|
+
def midi_input():
|
|
3797
|
+
"""The MIDI device and channel listened to, as something a control is tied to: select("device", midi_input(),
|
|
3798
|
+
"device") and select("channel", midi_input(), "channel"). It is the choice use_midi_input makes in code.
|
|
3799
|
+
lead.midi_input() is the same for what plays one synth alone. See MidiPort."""
|
|
3800
|
+
return _midi_port("input", None)
|
|
3801
|
+
|
|
3802
|
+
|
|
3803
|
+
def midi_output():
|
|
3804
|
+
"""The MIDI device and channel sent to, as something a control is tied to: select("device", midi_output(), "device")."""
|
|
3805
|
+
return _midi_port("output", None)
|
|
3806
|
+
|
|
3807
|
+
|
|
3808
|
+
def receive_midi(events):
|
|
3809
|
+
"""Hand the program a message just as the page would: receive_midi({"type": "noteOn", "note": 60, "velocity": 0.8,
|
|
3810
|
+
"channel": 1}), or a list of them. A host whose player is elsewhere gives this what the player's on_midi gave;
|
|
3811
|
+
a scene uses it to pretend a keyboard is there."""
|
|
3812
|
+
_sound().midi.receive(events)
|
|
3813
|
+
|
|
3814
|
+
|
|
3815
|
+
def stop_midi():
|
|
3816
|
+
"""Finished with MIDI: every function forgotten, nothing played from a keyboard, sent on or tied to a knob,
|
|
3817
|
+
and the notes sent out let go."""
|
|
3818
|
+
s = _sound()
|
|
3819
|
+
if s._midi is not None:
|
|
3820
|
+
s._midi.stop()
|
|
3821
|
+
|
|
3822
|
+
|
|
3823
|
+
def _seven(v):
|
|
3824
|
+
return int(_clamp(math.floor(v * 127 + 0.5), 0, 127))
|
|
3825
|
+
|
|
3826
|
+
|
|
3827
|
+
_read_names()
|