-
-
Notifications
You must be signed in to change notification settings - Fork 989
Expand file tree
/
Copy pathsound.rb
More file actions
4264 lines (3477 loc) · 186 KB
/
Copy pathsound.rb
File metadata and controls
4264 lines (3477 loc) · 186 KB
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
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#--
# This file is part of Sonic Pi: http://sonic-pi.net
# Full project source: https://github.com/samaaron/sonic-pi
# License: https://github.com/samaaron/sonic-pi/blob/main/LICENSE.md
#
# Copyright 2013, 2014, 2015, 2016 by Sam Aaron (http://sam.aaron.name).
# All rights reserved.
#
# Permission is granted for use, copying, modification, and
# distribution of modified versions of this work as long as this
# notice is included.
#++
require 'tmpdir'
require 'fileutils'
require 'thread'
require 'net/http'
require 'multi_json'
require 'uri'
require_relative "../blanknode"
require_relative "../chainnode"
require_relative "../fxnode"
require_relative "../fxreplacenode"
require_relative "../lazynode"
require_relative "../synthtracker"
require_relative "../version"
require_relative "../tuning"
require_relative "../sample_loader"
require_relative "support/docsystem"
module SonicPi
module Lang
module Sound
class BufferLookup
def initialize(blk)
@blk = blk
end
def [](*args)
@blk.call(*args)
end
end
include SonicPi::Util
include SonicPi::Lang::Support::DocSystem
DEFAULT_PLAY_OPTS = {
amp: "The amplitude of the note",
amp_slide: "The duration in beats for amplitude changes to take place",
pan: "The stereo position of the sound. -1 is left, 0 is in the middle and 1 is on the right. You may use a value in between -1 and 1 such as 0.25",
pan_slide: "The duration in beats for the pan value to change",
attack: "Amount of time (in beats) for sound to reach full amplitude (attack_level). A short attack (i.e. 0.01) makes the initial part of the sound very percussive like a sharp tap. A longer attack (i.e 1) fades the sound in gently.",
decay: "Amount of time (in beats) for the sound to move from full amplitude (attack_level) to the sustain amplitude (sustain_level).",
sustain: "Amount of time (in beats) for sound to remain at sustain level amplitude. Longer sustain values result in longer sounds. Full length of sound is attack + decay + sustain + release.",
release: "Amount of time (in beats) for sound to move from sustain level amplitude to silent. A short release (i.e. 0.01) makes the final part of the sound very percussive (potentially resulting in a click). A longer release (i.e 1) fades the sound out gently.",
attack_level: "Amplitude level reached after attack phase and immediately before decay phase",
decay_level: "Amplitude level reached after decay phase and immediately before sustain phase. Defaults to sustain_level unless explicitly set",
sustain_level: "Amplitude level reached after decay phase and immediately before release phase.",
env_curve: "Select the shape of the curve between levels in the envelope. 1=linear, 2=exponential, 3=sine, 4=welch, 6=squared, 7=cubed",
slide: "Default slide time in beats for all slide opts. Individually specified slide opts will override this value",
pitch: "Pitch adjustment in semitones. 1 is up a semitone, 12 is up an octave, -12 is down an octave etc. Decimal numbers can be used for fine tuning.",
on: "If specified and false/nil/0 will stop the synth from being played. Ensures all opts are evaluated."}
def self.included(base)
base.instance_exec {alias_method :sonic_pi_mods_sound_initialize_old, :initialize}
base.instance_exec do
define_method(:initialize) do |*splat, &block|
sonic_pi_mods_sound_initialize_old(*splat, &block)
ports, msg_queue = *splat
@server_init_args = splat.take(4)
@mod_sound_home_dir = Dir.home
@simple_sampler_args = [:amp, :amp_slide, :amp_slide_shape, :amp_slide_curve, :pan, :pan_slide, :pan_slide_shape, :pan_slide_curve, :cutoff, :cutoff_slide, :cutoff_slide_shape, :cutoff_slide_curve, :lpf, :lpf_slide, :lpf_slide_shape, :lpf_slide_curve, :hpf, :hpf_slide, :hpf_slide_shape, :hpf_slide_curve, :rate, :slide, :beat_stretch, :rpitch, :attack, :decay, :sustain, :release, :attack_level, :decay_level, :sustain_level, :env_curve]
init_tuning
@sample_paths_cache = {}
@sample_loader = SampleLoader.new("#{Paths.samples_path}/**")
@job_groups = {}
@job_group_mutex = Mutex.new
@job_mixers = {}
@job_mixers_mutex = Mutex.new
@job_busses = {}
@job_busses_mutex = Mutex.new
current_spider_time_lambda = lambda { __get_spider_time }
@mod_sound_studio = Studio.new(ports, msg_queue, @system_state, @register_cue_event_lambda, current_spider_time_lambda)
buf_lookup = lambda do |name, duration=nil|
# scale duration to the current BPM
duration ||= 8
raise ArgumentError, "Buffer duration should be a numerical value greater than zero. You used #{duration}" unless duration.is_a?(Numeric) && duration.positive?
duration = duration * (__get_spider_sleep_mul || 1)
name = name.to_sym
buf, cached = @mod_sound_studio.allocate_buffer(name, duration)
__info "Initialised buffer #{name.inspect}, #{duration}s" unless cached
buf
end
@buffer_lookup_w_hash_syntax = BufferLookup.new(buf_lookup)
@mod_sound_studio_checker = Thread.new do
# kill all jobs if an error occured in the studio
__system_thread_locals.set_local(:sonic_pi_local_thread_group, :studio_checker)
Thread.current.priority = 200
Kernel.loop do
Kernel.sleep 5
begin
error = @mod_sound_studio.error_occurred?
if error
__stop_jobs
end
rescue Exception => e
__info "exception: #{e.message}, #{e.backtrace}"
__stop_jobs
end
end
end
@life_hooks.on_init do |job_id, payload|
# Do nothing for now
@mod_sound_studio.start
end
# @life_hooks.on_killed do |job_id, payload|
# # Do nothing for now
# end
# @life_hooks.on_completed do |job_id, payload|
# # Do nothing for now
# end
@life_hooks.on_all_completed do |silent=false|
@mod_sound_studio.pause(silent)
end
@life_hooks.on_exit do |job_id, payload|
__system_thread_locals.set_local(:sonic_pi_local_thread_group, "job_remover-#{job_id}".freeze)
Thread.current.priority = -10
shutdown_job_mixer(job_id)
kill_job_group(job_id)
free_job_bus(job_id)
end
@cue_events.add_handler("/exit", @cue_events.gensym("/mods-sound-exit")) do |payload|
@mod_sound_studio.shutdown
nil
end
end
end
end
def live_audio(*params)
args, opts = split_params_and_merge_opts_array(params)
synth_name = if truthy?(opts[:stereo])
"sonic-pi-live_audio_stereo"
else
"sonic-pi-live_audio_mono"
end
opts.delete(:stereo)
raise "live_audio requires a name" if args.empty?
id = args[0]
sn_sym = synth_name.to_sym
info = Synths::SynthInfo.get_info(sn_sym)
synth_name = info ? info.scsynth_name : synth_name
if args.size > 1 && ((args[1] == nil) || (args[1] == :stop))
# kill synth
return @mod_sound_studio.kill_live_synth(id)
end
unless __thread_locals.get(:sonic_pi_mod_sound_synth_silent)
__delayed_message "live_audio #{id.inspect}, #{arg_h_pp(opts)}"
end
trigger_live_synth(synth_name, opts, current_group, info, false, current_out_bus, false, :tail, id)
end
doc name: :live_audio,
introduced: Version.new(3,0,0),
summary: "A named audio stream live from your soundcard",
args: [[:name, :symbol]],
returns: :SynthNode,
opts: {:input => "The audio card input to read audio from.",
:stereo => "If set to truthy value (true, 1) will read from two consecutive audio card inputs."},
# opts: {sound_in_stereo: {doc: "",
# default: true
# }},
accepts_block: false,
args_size: 0,
opts_keys: [:sound_in_stereo],
doc: "Create a named synthesiser which works similar to `play`, `sample` or `synth`. Rather than synthesising the sound mathematically or playing back recorded audio, it streams audio live from your sound card.
However, unlike `play`, `sample` and `synth`, which allow multiple similar synths to play at the same time (i.e. a chord) only one `live_audio` synth of a given name may exist in the system at any one time. This is similar to `live_loop` where only one live loop of each name may exist at any one time. See examples for further information.
An additional difference is that `live_audio` will create an infinitely long synth rather than be timed to an envelope like the standard `synth` and `sample` synths. This is particularly suitable for working with continuous incoming audio streams where the source of the audio is unknown (for example, it may be a guitar, an analog synth or an electronic violin). If the source is continuous, then it may not be suited to being stitched together by successive enveloped calls to something like: `synth :sound_in, attack: 0, sustain: 4, release: 0`. If we were to `live_loop` this with a `sleep 4` to match the sustain duration, we would get something that emulated a continuous stream, but for certain inputs you'll hear clicking at the seams between each successive call to `synth` where the final part of the audio signal from the previous synth doesn't precisely match up with the start of the signal in the next synth due to very minor timing differences.
Another important feature of `live_audio` is that it will automatically move an existing `live_audio` synth into the current FX context. This means you can live code the FX chain around the live stream and it will update automatically. See examples.
To stop a `live_audio` synth, use the `:stop` arg: `live_audio :foo, :stop`.
.
",
examples: ["
# Basic usage
live_audio :foo # Play whatever audio is coming into the sound card on input 1
",
"
# Specify an input
live_audio :foo, input: 3 # Play whatever audio is coming into the sound card on input 3
",
"
# Work with stereo input
live_audio :foo, input: 3, stereo: true # Play whatever audio is coming into the sound card on inputs 3 and 4
# as a stereo stream
",
"# Switching audio contexts (i.e. changing FX)
live_audio :guitar # Play whatever audio is coming into the sound card on input 1
sleep 2 # Wait for 2 seconds then...
with_fx :reverb do
live_audio :guitar # Add reverb to the audio from input 1
end
sleep 2 # Wait for another 2 seconds then...
live_audio :guitar # Remove the reverb from input 1
",
"
# Working with live_loops
live_loop :foo do
with_fx [:reverb, :distortion, :echo].choose do # chooses a new FX each time round the live loop
live_audio :voice # the audio stream from input 1 will be moved to the
end # new FX and the old FX will complete and finish as normal.
sleep 8
end",
"
# Stopping
live_audio :foo #=> start playing audio from input 1
live_audio :bar, input: 2 #=> start playing audio from input 2
sleep 3 #=> wait for 3s...
live_audio :foo, :stop #=> stop playing audio from input 1
#=> (live_audio :bar is still playing)
"
]
# def opts
# __thread_locals.get(:sonic_pi_mod_sound_defn_args_opts, [[], {}])[1]
# end
# def args
# __thread_locals.get(:sonic_pi_mod_sound_defn_args_opts, [[], {}])[0]
# end
# Deprecated fns
def current_sample_pack_aliases(*args)
raise "Sorry, current_sample_pack_aliases is no longer supported since v2.10. Please read Section 3.7 of the tutorial for a more powerful replacement."
end
def with_sample_pack_as(*args)
raise "Sorry, with_sample_pack_as is no longer supported since v2.10. Please read Section 3.7 of the tutorial for a more powerful replacement."
end
def use_sample_pack_as(*args)
raise "Sorry, use_sample_pack_as is no longer supported since v2.10. Please read Section 3.7 of the tutorial for a more powerful replacement."
end
def use_sample_pack(pack, &block)
raise "Sorry, use_sample_pack is no longer supported since v2.11. \n Please read Section 3.7 of the tutorial for a more powerful replacement."
end
def with_sample_pack(pack, &block)
raise "Sorry, with_sample_pack is no longer supported since v2.11. \n Please read Section 3.7 of the tutorial for a more powerful replacement."
end
# End deprecated methods
def reboot
if @mod_sound_studio.rebooting
__info "Already rebooting sound server"
return nil
end
@sample_loader.reset!
__no_kill_block do
__stop_other_jobs
res = @mod_sound_studio.reboot
if res
__info "Reboot successful - sound server ready."
else
__info "Reboot unsuccessful - reboot already in progress."
end
end
stop
end
def scsynth_info
@mod_sound_studio.scsynth_info
end
doc name: :scsynth_info,
introduced: Version.new(2,11,0),
summary: "Return information about the internal SuperCollider sound server",
args: [],
returns: :SPMap,
opts: nil,
accepts_block: false,
doc: "Create a map of information about the running audio synthesiser SuperCollider. ",
examples: [
"puts scsynth_info #=> (map sample_rate: 44100.0,
# sample_dur: 2.2675736545352265e-05,
# radians_per_sample: 0.00014247585204429924,
# control_rate: 689.0625,
# control_dur: 0.001451247138902545,
# subsample_offset: 0.0,
# num_output_busses: 16.0,
# num_input_busses: 16.0,
# num_audio_busses: 1024.0,
# num_control_busses: 4096.0,
# num_buffers: 4096.0)",
]
def sample_free(*paths)
paths.each do |p|
p = [p] unless is_list_like?(p)
filts_and_sources, _ = sample_split_filts_and_opts(p)
resolve_sample_paths(filts_and_sources).each do |path|
if sample_loaded?(path)
@mod_sound_studio.free_sample([path])
__info "Freed sample: #{unify_tilde_dir(path).inspect}"
end
end
end
end
doc name: :sample_free,
introduced: Version.new(2,9,0),
summary: "Free a sample on the synth server",
args: [[:path, :string]],
returns: nil,
opts: nil,
accepts_block: false,
doc: "Frees the memory and resources consumed by loading the sample on the server. Subsequent calls to `sample` and friends will re-load the sample on the server.
You may also specify the same set of source and filter pre-args available to `sample` itself. `sample_free` will then free all matching samples. See `sample`'s docs for more information.",
examples: ["
sample :loop_amen # The Amen break is now loaded into memory and played
sleep 2
sample :loop_amen # The Amen break is not loaded but played from memory
sleep 2
sample_free :loop_amen # The Amen break is freed from memory
sample :loop_amen # the Amen break is re-loaded and played",
"
puts sample_info(:loop_amen).to_i # This returns the buffer id of the sample i.e. 1
puts sample_info(:loop_amen).to_i # The buffer id remains constant whilst the sample
# is loaded in memory
sample_free :loop_amen
puts sample_info(:loop_amen).to_i # The Amen break is re-loaded and gets a *new* id.",
"
sample :loop_amen
sample :ambi_lunar_land
sleep 2
sample_free :loop_amen, :ambi_lunar_land
sample :loop_amen # re-loads and plays amen
sample :ambi_lunar_land # re-loads and plays lunar land",
"# Using source and filter pre-args
dir = \"/path/to/sample/dir\"
sample_free dir # frees any loaded samples in \"/path/to/sample/dir\"
sample_free dir, 1 # frees sample with index 1 in \"/path/to/sample/dir\"
sample_free dir, :foo # frees sample with name \"foo\" in \"/path/to/sample/dir\"
sample_free dir, /[Bb]ar/ # frees sample which matches regex /[Bb]ar/ in \"/path/to/sample/dir\"
",
]
def buffer(*args)
if args.empty?
@buffer_lookup_w_hash_syntax
else
@buffer_lookup_w_hash_syntax[*args]
end
end
doc name: :buffer,
introduced: Version.new(3,0,0),
summary: "Initialise or return named buffer",
args: [[:symbol, :name], [:number, :duration]],
alt_args: [[:symbol, :name]],
returns: :buffer,
opts: nil,
accepts_block: false,
doc: "Initialise or return a named buffer with a specific duration (defaults to 8 beats). Useful for working with the `:record` FX. If the buffer is requested with a different duration, then a new buffer will be initialised and the old one recycled.",
examples: ["
buffer(:foo) # load a 8s buffer and name it :foo
b = buffer(:foo) # return cached buffer and bind it to b
puts b.duration #=> 8.0",
"
buffer(:foo, 16) # load a 16s buffer and name it :foo
",
"
use_bpm 120
buffer(:foo, 16) # load a 8s buffer and name it :foo
# (this isn't 16s as the BPM has been
# doubled from the default of 60)
",
"
buffer(:foo) # init a 8s buffer and name it :foo
buffer(:foo, 8) # return cached 8s buffer (has the same duration)
buffer(:foo, 10) # init a new 10s buffer and name it :foo
buffer(:foo, 10) # return cached 10s buffer
buffer(:foo) # init a 8s buffer and name it :foo
buffer(:foo) # return cached 8s buffer (has the same duration)"]
def sample_free_all
@mod_sound_studio.free_all_samples
end
doc name: :sample_free_all,
introduced: Version.new(2,9,0),
summary: "Free all loaded samples on the synth server",
args: [],
returns: nil,
opts: nil,
accepts_block: false,
doc: "Unloads all samples therefore freeing the memory and resources consumed. Subsequent calls to `sample` and friends will re-load the sample on the server.",
examples: ["
sample :loop_amen # load and play :loop_amen
sample :ambi_lunar_land # load and play :ambi_lunar_land
sleep 2
sample_free_all
sample :loop_amen # re-loads and plays amen"]
def start_amp_monitor
@mod_sound_studio.start_amp_monitor
end
def current_amp
@mod_sound_studio.amp
end
def should_trigger?(args_h)
return true unless args_h.key?(:on)
on = args_h.delete(:on)
truthy?(on)
end
def use_timing_guarantees(v, &block)
raise "use_timing_guarantees does not work with a do/end block. Perhaps you meant with_timing_guarantees" if block
__thread_locals.set(:sonic_pi_mod_sound_timing_guarantees, v)
end
doc name: :use_timing_guarantees,
introduced: Version.new(2,10,0),
summary: "Inhibit synth triggers if too late",
doc: "If set to true, synths will not trigger if it is too late. If false, some synth triggers may be late.",
args: [[:bool, :true_or_false]],
opts: nil,
accepts_block: true,
examples: ["
use_timing_guarantees true
sample :loop_amen #=> if time is behind by any margin, this will not trigger",
"
use_timing_guarantees false
sample :loop_amen #=> unless time is too far behind, this will trigger even when late."]
def with_timing_guarantees(v, &block)
raise "with_timing_guarantees requires a do/end block. Perhaps you meant use_timing_guarantees" unless block
current = __thread_locals.get(:sonic_pi_mod_sound_timing_guarantees)
__thread_locals.set(:sonic_pi_mod_sound_timing_guarantees, v)
res = block.call
__thread_locals.set(:sonic_pi_mod_sound_timing_guarantees, current)
res
end
doc name: :with_timing_guarantees,
introduced: Version.new(2,10,0),
summary: "Block-scoped inhibition of synth triggers if too late",
doc: "For the given block, if set to true, synths will not trigger if it is too late. If false, some synth triggers may be late. After the block has completed, the previous value is restored. ",
args: [[:bool, :true_or_false]],
opts: nil,
accepts_block: true,
examples: ["
with_timing_guarantees true do
sample :loop_amen #=> if time is behind by any margin, this will not trigger
end",
"
with_timing_guarantees false do
sample :loop_amen #=> unless time is too far behind, this will trigger even when late.
end"]
def use_external_synths(v, &block)
raise "use_external_synths does not work with a do/end block. Perhaps you meant with_external_synths" if block
__thread_locals.set(:sonic_pi_mod_sound_use_external_synths, v)
end
def use_timing_warnings(v, &block)
raise "use_timing_warnings does not work with a do/end block. Perhaps you meant with_timing_warnings" if block
__thread_locals.set(:sonic_pi_mod_sound_disable_timing_warnings, !v)
end
def with_timing_warnings(v, &block)
raise "with_timing_warnings requires a do/end block. Perhaps you meant use_timing_warnings" unless block
current = __thread_locals.get(:sonic_pi_mod_sound_disable_timing_warnings)
__thread_locals.set(:sonic_pi_mod_sound_disable_timing_warnings, !v)
res = block.call
__thread_locals.set(:sonic_pi_mod_sound_disable_timing_warnings, current)
res
end
def use_sample_bpm(sample_name, *args)
args_h = resolve_synth_opts_hash_or_array(args)
num_beats = args_h[:num_beats] || 1
# Don't use sample_duration as that is stretched to the current
# bpm!
scaling = __thread_locals.get(:sonic_pi_spider_arg_bpm_scaling)
__thread_locals.set(:sonic_pi_spider_arg_bpm_scaling, false)
sd = sample_duration(sample_name, *args)
__thread_locals.set(:sonic_pi_spider_arg_bpm_scaling, scaling)
use_bpm(num_beats * (60.0 / sd))
end
doc name: :use_sample_bpm,
introduced: Version.new(2,1,0),
summary: "Sample-duration-based bpm modification",
doc: "Modify bpm so that sleeping for 1 will sleep for the duration of the sample.",
args: [[:string_or_number, :sample_name_or_duration]],
opts: {:num_beats => "The number of beats within the sample. By default this is 1."},
accepts_block: false,
examples: ["use_sample_bpm :loop_amen #Set bpm based on :loop_amen duration
live_loop :dnb do
sample :bass_dnb_f
sample :loop_amen
sleep 1 #`sleep`ing for 1 actually sleeps for duration of :loop_amen
end",
"
use_sample_bpm :loop_amen, num_beats: 4 # Set bpm based on :loop_amen duration
# but also specify that the sample duration
# is actually 4 beats long.
live_loop :dnb do
sample :bass_dnb_f
sample :loop_amen
sleep 4 #`sleep`ing for 4 actually sleeps for duration of :loop_amen
# as we specified that the sample consisted of
# 4 beats
end"]
def with_sample_bpm(sample_name, *args, &block)
raise "with_sample_bpm must be called with a do/end block" unless block
args_h = resolve_synth_opts_hash_or_array(args)
num_beats = args_h[:num_beats] || 1
# Don't use sample_duration as that is stretched to the current
# bpm!
sd = sample_buffer(sample_name).duration
with_bpm(num_beats * (60.0 / sd), &block)
end
doc name: :with_sample_bpm,
introduced: Version.new(2,1,0),
summary: "Block-scoped sample-duration-based bpm modification",
doc: "Block-scoped modification of bpm so that sleeping for 1 will sleep for the duration of the sample.",
args: [[:string_or_number, :sample_name_or_duration]],
opts: {:num_beats => "The number of beats within the sample. By default this is 1."},
accepts_block: true,
requires_block: true,
examples: ["
live_loop :dnb do
with_sample_bpm :loop_amen do #Set bpm based on :loop_amen duration
sample :bass_dnb_f
sample :loop_amen
sleep 1 #`sleep`ing for 1 sleeps for duration of :loop_amen
end
end",
"live_loop :dnb do
with_sample_bpm :loop_amen, num_beats: 4 do # Set bpm based on :loop_amen duration
# but also specify that the sample duration
# is actually 4 beats long.
sample :bass_dnb_f
sample :loop_amen
sleep 4 #`sleep`ing for 4 sleeps for duration of :loop_amen
# as we specified that the sample consisted of
# 4 beats
end
end"]
def use_arg_bpm_scaling(bool, &block)
raise "use_arg_bpm_scaling does not work with a block. Perhaps you meant with_arg_bpm_scaling" if block
__thread_locals.set(:sonic_pi_spider_arg_bpm_scaling, bool)
end
doc name: :use_arg_bpm_scaling,
introduced: Version.new(2,0,0),
summary: "Enable and disable BPM scaling",
doc: "Turn synth argument bpm scaling on or off for the current thread. This is on by default. Note, using `rt` for args will result in incorrect times when used after turning arg bpm scaling off.",
args: [[:bool, :boolean]],
opts: nil,
accepts_block: false,
examples: ["
use_bpm 120
play 50, release: 2 # release is actually 1 due to bpm scaling
sleep 2 # actually sleeps for 1 second
use_arg_bpm_scaling false
play 50, release: 2 # release is now 2
sleep 2 # still sleeps for 1 second",
" # Interaction with rt
use_bpm 120
play 50, release: rt(2) # release is 2 seconds
sleep rt(2) # sleeps for 2 seconds
use_arg_bpm_scaling false
play 50, release: rt(2) # ** Warning: release is NOT 2 seconds! **
sleep rt(2) # still sleeps for 2 seconds"]
def with_arg_bpm_scaling(bool, &block)
raise "with_arg_bpm_scaling must be called with a do/end block. Perhaps you meant use_arg_bpm_scaling" unless block
current_scaling = __thread_locals.get(:sonic_pi_spider_arg_bpm_scaling)
__thread_locals.set(:sonic_pi_spider_arg_bpm_scaling, bool)
res = block.call
__thread_locals.set(:sonic_pi_spider_arg_bpm_scaling, current_scaling)
res
end
doc name: :with_arg_bpm_scaling,
introduced: Version.new(2,0,0),
summary: "Block-level enable and disable BPM scaling",
doc: "Turn synth argument bpm scaling on or off for the supplied block. Note, using `rt` for args will result in incorrect times when used within this block.",
args: [],
opts: nil,
accepts_block: true,
requires_block: true,
examples: ["use_bpm 120
play 50, release: 2 # release is actually 1 due to bpm scaling
sleep 2 # actually sleeps for 1 second
with_arg_bpm_scaling false do
play 50, release: 2 # release is now 2
sleep 2 # still sleeps for 1 second
end",
" # Interaction with rt
use_bpm 120
play 50, release: rt(2) # release is 2 seconds
sleep rt(2) # sleeps for 2 seconds
with_arg_bpm_scaling false do
play 50, release: rt(2) # ** Warning: release is NOT 2 seconds! **
sleep rt(2) # still sleeps for 2 seconds
end"]
def set_audio_latency!(delta_ms)
@mod_sound_studio.set_audio_latency!(delta_ms.to_f)
end
doc name: :set_audio_latency!,
introduced: Version.new(3,1,0),
summary: "Globally modify audio latency",
doc: "On some systems with certain configurations (such as wireless speakers, and even a typical Windows environment with the default audio drivers) the audio latency can be large. If all the user is doing is generating audio via calls such as `play`, `synth` and `sample`, then this latency essentially adds to the schedule ahead time and for the most part can be ignored. However, if the user is combining audio with external MIDI/OSC triggered events, this latency can result in a noticeable offset. This function allows you to address this offset by moving the audio events forwards and backwards in time.
So, for example, if your audio system has an audio latency of 150ms, you can compensate for this by setting Sonic Pi's latency to be a negative value: `set_audio_latency! -150`.",
args: [[:milliseconds, :number]],
opts: nil,
modifies_env: true,
accepts_block: false,
examples: ["set_audio_latency! 100 # Audio events will now be scheduled 100ms
# after the schedule ahead time",
"set_audio_latency! -200 # Audio events will now be scheduled 200ms
# before the schedule ahead time"
]
def set_recording_bit_depth!(d)
@mod_sound_studio.bit_depth = d
__info "Recording bit depth set to #{d}"
end
doc name: :set_recording_bit_depth!,
introduced: Version.new(2,11,0),
summary: "Set the bit depth for recording wav files",
doc: "When you hit the record button, Sonic Pi saves all the audio you can hear into a wav file. By default, this file uses a resolution of 16 bits which is the same as CD audio and good enough for most use cases. However, when working with professional equipment, it is common to want to work with even higher quality files such as 24 bits and even 32 bits. This function allows you to switch the default from 16 to one of 8, 16, 24 or 32.",
args: [[:bit_depth, :number]],
opts: nil,
modifies_env: true,
accepts_block: false,
examples: [
"
set_recording_bit_depth! 24 # Set recording bit depth to 24"]
def set_control_delta!(t)
@mod_sound_studio.control_delta = t
__info "Control delta set to #{t}"
end
doc name: :set_control_delta!,
introduced: Version.new(2,1,0),
summary: "Set control delta globally",
doc: "Specify how many seconds between successive modifications (i.e. trigger then controls) of a specific node on a specific thread. Set larger if you are missing control messages sent extremely close together in time.",
args: [[:time, :number]],
opts: nil,
modifies_env: true,
accepts_block: false,
examples: [
"
set_control_delta! 0.1 # Set control delta to 0.1
s = play 70, release: 8, note_slide: 8 # Play a note and set the slide time
control s, note: 82 # immediately start sliding note.
# This control message might not be
# correctly handled as it is sent at the
# same virtual time as the trigger.
# If you don't hear a slide, try increasing the
# control delta until you do."]
def use_debug(v, &block)
raise "use_debug does not work with a do/end block. Perhaps you meant with_debug" if block
__thread_locals.set(:sonic_pi_mod_sound_synth_silent, !v)
end
doc name: :use_debug,
introduced: Version.new(2,0,0),
summary: "Enable and disable debug",
doc: "Enable or disable messages created on synth triggers. If this is set to false, the synths will be silent until debug is turned back on. Silencing debug messages can reduce output noise and also increase performance on slower platforms. See `with_debug` for setting the debug value only for a specific `do`/`end` block.",
args: [[:true_or_false, :boolean]],
opts: nil,
accepts_block: false,
examples: ["use_debug true # Turn on debug messages", "use_debug false # Disable debug messages"]
def with_debug(v, &block)
raise "with_debug requires a do/end block. Perhaps you meant use_debug" unless block
current = __thread_locals.get(:sonic_pi_mod_sound_synth_silent)
__thread_locals.set(:sonic_pi_mod_sound_synth_silent, !v)
res = block.call
__thread_locals.set(:sonic_pi_mod_sound_synth_silent, current)
res
end
doc name: :with_debug,
introduced: Version.new(2,0,0),
summary: "Block-level enable and disable debug",
doc: "Similar to use_debug except only applies to code within supplied `do`/`end` block. Previous debug value is restored after block.",
args: [[:true_or_false, :boolean]],
opts: nil,
accepts_block: true,
requires_block: true,
examples: ["
# Turn on debugging:
use_debug true
play 80 # Debug message is sent
with_debug false do
#Debug is now disabled
play 50 # Debug message is not sent
sleep 1
play 72 # Debug message is not sent
end
# Debug is re-enabled
play 90 # Debug message is sent
"]
def use_arg_checks(v, &block)
raise "use_arg_checks does not work with a do/end block. Perhaps you meant with_arg_checks" if block
__thread_locals.set(:sonic_pi_mod_sound_check_synth_args, !!v)
end
doc name: :use_arg_checks,
introduced: Version.new(2,0,0),
summary: "Enable and disable arg checks",
doc: "When triggering synths, each argument is checked to see if it is sensible. When argument checking is enabled and an argument isn't sensible, you'll see an error in the debug pane. This setting allows you to explicitly enable and disable the checking mechanism. See with_arg_checks for enabling/disabling argument checking only for a specific `do`/`end` block.",
args: [[:true_or_false, :boolean]],
opts: nil,
accepts_block: false,
examples: ["
play 50, release: 5 # Args are checked
use_arg_checks false
play 50, release: 5 # Args are not checked"]
def with_arg_checks(v, &block)
raise "with_arg_checks requires a do/end block. Perhaps you meant use_arg_checks" unless block
current = __thread_locals.get(:sonic_pi_mod_sound_check_synth_args)
__thread_locals.set(:sonic_pi_mod_sound_check_synth_args, v)
res = block.call
__thread_locals.set(:sonic_pi_mod_sound_check_synth_args, current)
res
end
doc name: :with_arg_checks,
introduced: Version.new(2,0,0),
summary: "Block-level enable and disable arg checks",
doc: "Similar to `use_arg_checks` except only applies to code within supplied `do`/`end` block. Previous arg check value is restored after block.",
args: [[:true_or_false, :boolean]],
opts: nil,
accepts_block: true,
requires_block: true,
examples: ["
# Turn on arg checking:
use_arg_checks true
play 80, cutoff: 100 # Args are checked
with_arg_checks false do
#Arg checking is now disabled
play 50, release: 3 # Args are not checked
sleep 1
play 72 # Arg is not checked
end
# Arg checking is re-enabled
play 90 # Args are checked
"]
def use_synth(synth_name, *args, &block)
raise "use_synth does not accept opts such as #{arg_h_pp(resolve_synth_opts_hash_or_array(args))}. \n Consider using use_synth_defaults." unless args.empty?
raise "use_synth does not work with a do/end block. Perhaps you meant with_synth" if block
set_current_synth synth_name
end
doc name: :use_synth,
introduced: Version.new(2,0,0),
summary: "Switch current synth",
doc: "Switch the current synth to `synth_name`. Affects all further calls to `play`. See `with_synth` for changing the current synth only for a specific `do`/`end` block.",
args: [[:synth_name, :symbol]],
opts: nil,
accepts_block: false,
intro_fn: true,
examples: ["
play 50 # Plays with default synth
use_synth :mod_sine
play 50 # Plays with mod_sine synth"]
def with_synth(synth_name, *args, &block)
raise "with_synth does not accept opts such as #{arg_h_pp(resolve_synth_opts_hash_or_array(args))}. \n Consider using with_synth_defaults." unless args.empty?
raise "with_synth must be called with a do/end block. Perhaps you meant use_synth" unless block
orig_synth = current_synth_name
set_current_synth synth_name
res = block.call
set_current_synth orig_synth
res
end
doc name: :with_synth,
introduced: Version.new(2,0,0),
summary: "Block-level synth switching",
doc: "Switch the current synth to `synth_name` but only for the duration of the `do`/`end` block. After the `do`/`end` block has completed, the previous synth is restored.",
args: [[:synth_name, :symbol]],
opts: nil,
accepts_block: true,
requires_block: true,
examples: ["
play 50 # Plays with default synth
sleep 2
use_synth :supersaw
play 50 # Plays with supersaw synth
sleep 2
with_synth :saw_beep do
play 50 # Plays with saw_beep synth
end
sleep 2
# Previous synth is restored
play 50 # Plays with supersaw synth
"]
def recording_start
if @mod_sound_studio.recording?
__info "Already recording..."
else
__info "Start recording"
tmp_dir = Dir.mktmpdir("sonic-pi")
@tmp_path = File.expand_path("#{tmp_dir}/#{Random.rand(100000000)}.wav")
@mod_sound_studio.recording_start @tmp_path
end
end
doc name: :recording_start,
introduced: Version.new(2,0,0),
summary: "Start recording",
doc: "Start recording all sound to a `.wav` file stored in a temporary directory.",
args: [],
opts: nil,
accepts_block: false,
examples: [],
hide: true
def recording_stop
if @mod_sound_studio.recording?
__info "Stop recording"
@mod_sound_studio.recording_stop
else
__info "Recording already stopped"
end
end
doc name: :recording_stop,
introduced: Version.new(2,0,0),
summary: "Stop recording",
doc: "Stop current recording.",
args: [],
opts: nil,
accepts_block: false,
examples: [],
hide: true
def recording_save(filename)
__info "Stop recording" if @mod_sound_studio.recording_stop
if @tmp_path && File.exist?(@tmp_path)
FileUtils.mv(@tmp_path, filename)
@tmp_path = nil
__info "Saving recording to #{filename}"
else
__info "No recording to save"
end
end
doc name: :recording_save,
introduced: Version.new(2,0,0),
summary: "Save recording",
doc: "Save previous recording to the specified location",
args: [[:path, :string]],
opts: nil,
accepts_block: false,
examples: [],
hide: true
def recording_delete
__info "Deleting recording..."
FileUtils.rm @tmp_path if @tmp_path
end
doc name: :recording_delete,
doc: "After using `recording_start` and `recording_stop`, a temporary file is created until you decide to use `recording_save`. If you've decided you don't want to save it you can use this method to delete the temporary file straight away, otherwise the operating system will take care of deleting it later.",
args: [],