-
Notifications
You must be signed in to change notification settings - Fork 2
/
api_notes.txt
252 lines (177 loc) · 10 KB
/
api_notes.txt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
Architecture
------------
The front-end will look pretty much the same as normal Sonic-Pi: Play, Stop buttons and a big old code block to edit.
When you press play, a Web Worker is launched with the code from the UI and a unique ID number. The worker compiles the Ruby using Opal and then begins execution.
Each API command causes a message to be issued from the worker back to the main page. These messages are then decoded into calls to the Web Audio API.
The messages will look something like:
0 PLAY [synth] 50
0 CHORD [synth] 30 50 70
1 SET_FX [fx chain]
0 SKIP 100
The ID numbers mean that a seperate offset can be stored for each worker which is then increment on skips. Once stop is pressed all workers are terminated and the command buffer is flushed.
When a worker is created, compilation should take place and then the starting skip offset retreived from the AudioContext to keep the timing as tight as possible.
Once we have the compiled code, we don't need to recompile for workers created as a result of in_thread calls. Bonza!
The rest of this section details a few hacks that are required to implement some of the Sonic Pi API in a browser-based Javascript environment.
Hack 1) There's no sleep in Javascript
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
The solution to the missing sleep is to simply not sleep at all, rather issue a SKIP command. The only downside to this is that workers will produce commands as fast as the CPU can run the program, leading to high CPU usage and a rather hefty buffer of commands waiting to be actioned.
I've thought of and sketched out various solutions:
- Macro transforms on the code
- Impossible for the general case - cannot convert synchronous code to asynchronous code
- Custom Ruby interpreter in JS
- Possible by ripping apart Opal but kinda hard work when you think about the entire stdlib
The "solution" I've gone with was to abuse the synchronous XHR to make a request to a web server with the response then delayed by a given amount. http://httpbin.org/ already supports this but I may need to code my own if I end up being rate limited.
Since the network is inherently variable, it is extremely unlikely that the request will complete in exactly the time requested. As such, each worker will keep a tally of how many skips it has issued vs how long it has actually slept doing XHR requests. The user can then configure an optimum amount to be "ahead" of the processing. It will, of course, ship with a sensible default.
As a final catch all, the main thread will rate limit the number of commands coming from a single worker and terminate it if the buffer grows too large. This will most likely cause all sound to stop as it is genuinely an error state.
If I build my own special sleep server then the main page can feed back queue size information to allow the sleep to be customised.
Hack 2) in_thread is fork and there's no fork in Javascript
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
My implementation of in_thread is totally fudgy and approaching immoral but should just about do the job.
Each worker has a counter of the number of times in_thread has been called. This counter is essentially the thread_id. When it is called, the worker posts this back to the main page (e.g. IN_THREAD 2).
The main page then creates a new worker but initialises it with the number. The worker then executes the program, discarding any commands and not sleeping until it reaches the in_thread call with the correct index. It then resumes normal execution.
Hack 3) we can't tell workers about on_keypress because they are busy executing programs
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
I can't think of a solution right now. Maybe attempt to steal the block passed to the function and run it in a special worker?
Hack 4) cue and sync are conditional waits and there's none of that in Javascript
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
The solution here is to send cue and sync as commands to the main page and maintain seperate queues of commands from each worker. Commands are then submitted to the sound engine round-robin, with Sync and Cue becoming cross-queue barriers.
When a cue is processed maybe we can rewrite the Sync as a sleep of the appropriate amount (sleep until the current time of the other queue?)
Concepts
--------
Synths: instruments that can play notes (play, play_chord etc)
Args: TODO
Notes: notes played on instruments
:amp - the amplitude this note
:amp_slide - TODO: change from what to what? current to new? maybe useful with control call
:pan - ronseal
:pan_slide - TODO: change from what to what? current to new?
:attack - duration until peak (A in ADSR)
:sustain - duration note stays at full amplitude (S in ADSR)
:release - duration the note fades away (R in ADSR)
FX: guitar pedal style effects that can be applied to the output (with_fx)
Args: TODO
Sample packs: a folder full of audio clips which can then be played by name (sample :foo plays foo.wav etc)
We could in theory support both on server clip banks and local folders with the ability to upload and share?
:rate
Synthdefs: SuperCollider compatible synth defs. Supporting these may be difficult :)
The original code base has Note, Scale and Chord objects which I should consider porting wholesale as it will be a total pain otherwise.
Spider API
----------
All the functions from spiderapi.rb. These will mostly be implemented in pure ruby and then compiled using Opal.
Some may be moved out into native js calls if it happens to speed up compilation times (it probably won't).
defonce - pure ruby impl
define - pure ruby impl
on_keypress - TODO: maybe exec in custom worker?
comment - pure ruby impl
uncomment - pure ruby impl
print - console.log or better in the UI
puts - as above
dice - pure ruby impl (use rand() since nice Random class is Ruby 1.9 [sad soup] )
one_in
rrand
rrand_i
rand
rand_i
choose
use_random_seed - pure ruby impl (use srand() because Ruby 1.8 doesn't have nice Random class)
with_random_seed
rt - mildly crazy api for beats in the given number of seconds, pure ruby
sleep - see architecture bit
wait
cue - these are basically pthead conditions [lol]
sync - see architecture for how we hack this to work
in_thread - see architecture bit
Worker Local Settings
---------------------
Ronseal. Used to work out arguments to commands which are then sent to the sound engine.
The with versions need to be pure ruby implementations. Everything else depends on how much we move out for compile reasons.
use_bpm
with_bpm
current_bpm
use_arg_bpm_scaling
with_arg_bpm_scaling
set_sched_ahead_time! - NOT IMPLEMENTED (covered by worker architecture). Should just do nothing (maybe with a warning?)
current_sched_ahead_time
use_debug
with_debug
current_debug
use_arg_checks - TODO: need to establish exactly what these checks are
with_arg_checks
current_arg_checks
use_transpose
with_transpose
current_transpose
use_synth
with_synth
current_synth
use_merged_synth_defaults(synth_args) - MERGE
with_merged_synth_defaults(synth_args)
use_synth_defaults(synth_args) - DON'T MERGE, ASSIGN
with_synth_defaults(synth_args)
current_synth_defaults
load_synthdefs - only worth it if we eventually support SuperCollider synthdefs
use_fx(fx_args) - Not currently implemented in Sonic Pi, no reason why we couldn't
with_fx(fx_args) - Is implemented
use_sample_pack(pack)
use_sample_pack_as(pack, name)
with_sample_pack(pack)
with_sample_pack_as(pack, name)
current_sample_pack
current_sample_pack_aliases - TODO eh?
load_sample(path) - TODO: what format is the information returned, does this add it to the current pack?
load_samples(...)
sample_info(path) - returns the info you get back from load_sample
sample_buffer - load_sample again
sample_names - TODO: wtf do all these bad boys do?
all_sample_names
sample_groups
set_volume! - between 0 and 5
current_volume
status - hash of useful debug
Commands
--------
Note IDs: worker_id-note_id (incrementing number)
recording_start
{op: "recording_start"}
recording_stop
{op: "recording_stop"}
recording_save(filename)
{op: "recording_save", args: {
filename: "..."
}}
recording_delete
{op: "recording_delete"}
play(note, note_args) - delegates to play chord, single message type?
play_pattern(notes, note_args) - ends up calling play, sleep 1, play, sleep 1 etc
play_pattern_timed(notes, times, note_args) - calls play, sleep times[0], play, sleep times[1] etc
play_chord(notes, note_args)
{id: note_id, op: "play_chord", args: note_args}
synth(name, synth_args) - like play but for a named synth
{id: note_id, op: "play_chord", args: synth_args ++ {"synth": "..."}}
control(node_handle, synth_args) - pass some new args to a running node, sample or FX block
{id: note_id, op: "control", args: synth_args}
stop(node_handle) - stops a running sound
{id: note_id, op: "stop"}
sample_duration(name) - returns the length of the sample. accepts :rate parameter just like sample
- TODO: see if we can get away without decoding the sample again in the worker thread
- answer we probably can't :(
sample(name) - play the sample
sleep - see hackery section above
{op: "sleep", args: {"time", n}}
Utility
-------
midi_to_hz
hz_to_midi
scale(tonic, name, args) - return an array of note numbers for the given scale at the given tonic. Takes :num_octaves parameter.
chord(tonic, name) - like scale but for chords!
note(note, args) - convert from note name (C, D etc) into a number to use with play. Takes :octave parameter.
note_info(note, args) - return SonicPi::Note from note name. Takes :octave parameter.
Plan of attack
--------------
1) Stub out Ruby API
2) Build compiler and worker infrastructure, emitting events (args based on nice lovely ruby hashes)
- Normal stuff
- sleep support (lol)
- in_thread support (even bigger lol)
3) Hook up events to Web Audio API
- Profit!