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()