-
Notifications
You must be signed in to change notification settings - Fork 3
/
Copy pathflexFX.ts
1189 lines (1061 loc) · 49.2 KB
/
flexFX.ts
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
/* FlexFX */
/**
* Tools for creating composite sound-effects of class FlexFX that can be performed
* (either directly or queued-up) as defined; or with scaled pitch, volume or duration.
*/
//% color=#7c68b4
//% icon="\uf0a1"
//% block="FlexFX"
//% groups="['Playing (micro:bit V2)', 'Play-list (micro:bit V2)', 'Creating (micro:bit V2)']"
namespace flexFX {
// Simplify the selection of wave-shape...
export enum Wave {
//%block="silence"
Silence = -1, // (special case for queueing silent gaps onto the Play-list)
//%block="pure"
Sine = WaveShape.Sine,
//%block="buzzy"
Square = WaveShape.Square,
//%block="bright"
Triangle = WaveShape.Triangle,
//%block="harsh"
Sawtooth = WaveShape.Sawtooth,
//%block="noisy"
Noise = WaveShape.Noise
}
// Simplify the selection of frequency interpolation trajectory...
export enum Attack {
//% block="fast"
Fast = InterpolationCurve.Logarithmic,
//% block="medium"
Medium = InterpolationCurve.Curve,
//% block="even"
Even = InterpolationCurve.Linear,
//% block="delayed" *** option temporarily removed...
// Delayed = 99 later, mapped to Sine or Cosine, depending on slope of profile
}
// Simplify (slightly) the selection of modulation-style...
export enum Effect {
//% block="none"
None = SoundExpressionEffect.None,
//% block="vibrato"
Vibrato = SoundExpressionEffect.Vibrato,
//% block="tremolo"
Tremolo = SoundExpressionEffect.Tremolo,
//% block="warble"
Warble = SoundExpressionEffect.Warble
}
// drop-down selection of built-in FlexFXs
export enum BuiltInFlexFX {
//% block="chime"
Chime,
//% block="cry"
Cry,
//% block="flute"
Flute,
//% block="horn"
Horn,
//% block="hum"
Hum,
//% block="laugh"
Laugh,
//% block="miaow"
Miaow,
//% block="moan"
Moan,
//% block="moo"
Moo,
//% block="motor"
Motor,
//% block="query"
Query,
//% block="shout"
Shout,
//% block="siren"
Siren,
//% block="snore"
Snore,
//% block="ting"
Ting,
//% block="tweet"
Tweet,
//% block="uh-oh"
Uhoh,
//% block="violin"
Violin,
//% block="whale"
Whale,
//% block="woof"
Woof
}
// drop-down selection of built-in Tunes
export enum BuiltInTune {
//% block="Happy Birthday To You"
Birthday,
//% block="Jingle Bells"
JingleBells,
//% block="I'm a Little Teapot"
TeaPot,
//% block="If You're Happy and You Know It "
IfYoureHappy,
//% block="London Bridge is Burning Down"
LondonBridge,
//% block="Old MacDonald Had a Farm"
OldMacdonald,
//% block="The Bear Went Over the Mountain"
BearMountain,
//% block="Pop Goes the Weasel"
PopWeasel,
//% block="This Old Man, He Played One"
ThisOldMan,
//% block="She'll be Coming Round the Mountain"
RoundMountain,
//% block="Edelweiss"
Edelweiss,
//% block="New World Symphony (Dvorak)"
NewWorld,
//% block="Ode to Joy (Beethoven)"
OdeToJoy,
//% block="Violin Concerto in A Minor (Bach)"
BachViolin
}
// range-clamper:
function clamp(bottom: number, input: number, top: number): number {
return (Math.max(bottom, Math.min(input, top)));
}
// constants used in conversions between frequency & MIDI note-number:
// a SEMITONE ratio = 12th root of 2 (as 12 semitones make an octave, which doubles the frequency)
const SEMILOG = 0.057762265047; // = Math.log(2) / 12;
const SEMITONE = 1.0594630943593; // = Math.exp(SEMILOG)
const DELTA = 36.3763165623; // = (Math.log(440) / SEMILOG) - 69;
// convert a frequency in Hz to its Midi note-number
// (retaining microtonal fractions)
function hertzToMidi(pitch: number): number {
return ((Math.log(pitch) / SEMILOG) - DELTA);
}
// convert a Midi note-number to nearest integer frequency in Hz
// (based on A4 = 440 Hz = MIDI 69)
function midiToHertz(midi: number): number {
return (Math.round(440 * (2 ** ((midi - 69) / 12))));
}
// note-lengths in ticks (quarter-beats)
const QUAVER_TICKS = 2;
const CROTCHET_TICKS = 4;
const MINIM_TICKS = 8;
const SEMIBREVE_TICKS = 16;
// for default Tempo of 120 BPM...
const DEFAULT_TICKMS = 125; // = (60*1000) / (4*120)
// (Basically, a TuneStep is a musical Note, but renamed to avoid confusion with the native "Note")
class TuneStep {
ticks: number = -1; // note-extent, measured in quarter-beat "ticks"
midi: number = -1; // standard MIDI note-number
pitch: number = 0; // frequency in Hz
volume: number = 0; // UI volume [0..255] (gets quadrupled internally)
//debug: string = ""; // saves the EKO source, (just for debug)
// create using a 3-part EKO-notation specifier: {extent}{key}{octave}
// (we need to be defensive about parsing malformed EKO strings!)
constructor(spec: string) {
//this.debug = spec; // (save our input string for debug purposes)
let chars = spec.toUpperCase();
let here = 0;
let nExtent = this.countDigits(chars, here);
if (nExtent > 0) {
this.ticks = Math.min(parseInt(chars.substr(here, nExtent)), 64); // max 16 beats!
// now parse the Key
here += nExtent;
let key = this.parseKey(chars.charCodeAt(here));
here++;
// for a silent musical rest: key = 12, and {Octave} is absent
if ((key > -1) && (key < 12)) { // good Key-letter; not a Rest
this.volume = 255; // (as yet, EKO offers no way of adding dynamics)
// adjust for accidentals [# or b] ?
let nOctave = this.countDigits(chars, here);
if (nOctave == 0) { // no Octave digits found yet
let asc = chars.charCodeAt(here); // =the character after Key=letter
switch (asc) {
case 35: key++; // "#"
break;
case 66: key--; // "B"
break;
default: key = -999; // midi will be end up negative!
}
here++;
// keep looking for Octave digits
nOctave = this.countDigits(chars, here);
}
let octave = -1;
if (nOctave > 0) {
octave = Math.min(parseInt(chars.substr(here, nOctave)), 10); // quite high enough!
// (B10 is 31.6kHz; even young kids' hearing range stops at about 20kHz)
// get MIDI from key & octave (careful: MIDI for C0 is 12)
this.midi = 12 * (octave + 1) + key;
here += nOctave;
} else {
}
} // else a bad Key-letter, or a Rest
if (key === 12) { // for a Rest...
this.midi = 0; // ...lack of octave is OK
}
} // else a missing Extent
// check for errors and substitute an alert
if ((this.ticks < 0) // bad Extent?
|| (this.midi < 0) // bad Key or Octave?
|| (here < chars.length)) { // spurious extra chars?
// insert a long high-pitched C8 error-tone
this.ticks = 16;
this.midi = 108;
this.volume = 255;
}
this.pitch = midiToHertz(this.midi);
}
// count consecutive digits in text from start onwards
countDigits(text: string, start: number): number {
let i = start;
while (i < text.length) {
let asc = text.charCodeAt(i);
if ((asc < 48) || (asc > 57)) break;
i++;
}
return (i - start);
}
// parse the key as semitone-in-octave [0 to 11] or 12 for a Rest
parseKey(asc: number): number {
let semi = -1;
if (asc == 82) { // an "R" means a Rest
semi = 12;
} else {
if ((asc > 64) && (asc < 72)) { // ["A" to "G"]
// parse Key-letter into semitone [0 to 11]
semi = 2 * ((asc - 60) % 7);
if (semi > 4) semi--;
} // else bad Key-letter
}
return (semi);
}
}
class Tune {
title: string; // unique identifier
nNotes: number; // number of notes (steps) in Tune
nTicks: number; // overall duration of Tune in ticks
notes: TuneStep[]; // array of notes
// deconstruct the source-string of EKO note-specifiers
constructor(title: string, source: string) {
this.title = title;
this.notes = [];
this.nTicks = 0;
let specs = source.trim().split(" ");
this.nNotes = specs.length;
for (let i = 0; i < this.nNotes; i++) {
let nextNote = new TuneStep(specs[i]);
this.nTicks += nextNote.ticks;
this.notes.push(nextNote);
}
}
// method to add some more notes...
extend(source: string) {
let specs = source.split(" ");
let count = specs.length;
for (let i = 0; i < count; i++) {
let nextNote = new TuneStep(specs[i]);
this.nNotes++;
this.nTicks += nextNote.ticks;
this.notes.push(nextNote);
}
}
}
// just a wrapper for the performance...
class Play {
parts: SoundExpression[]; // the sound-strings for each of its parts
constructor() {
this.parts = [];
}
}
// activity events (for other components to synchronise with)
const FLEXFX_ACTIVITY_ID = 9050 // TODO: Check (somehow?) that this is a permissable value!
enum PLAYER {
STARTING = 1,
FINISHED = 2,
ALLPLAYED = 3,
}
/*
A FlexFX is a potentially composite sound-effect.
It can specify several component soundExpressions called "parts" that get played consecutively.
Each part has a [frequency,volume] start-point and end-point.
Apart from the first part, the start-point gets inherited from the previous end-point,
so an n-part FlexFX moves through (n+1) [frequency,volume] points.
It is built, one part at a time, using defineFlexFX() followed by zero or more extendFlexFX() calls.
*/
class FlexFX {
// properties
id: string; // identifier
nParts: number;
prototype: Play; // contains the [nParts] SoundExpressions forming this flexFX
fullDuration: number; // overall cumulative duration of prototype
peakVolume: number; // remembers the highest volume [0-1020] in the prototype
pitchAverage: number; // approximate average pitch (in Hz)
pitchMidi: number; // midi note-number of average pitch (counted in semitones)
pitchProfile: number[]; // contains [nParts + 1] scalable frequencies
volumeProfile: number[]; // contains [nParts + 1] scalable volumes [0-1020]
durationProfile: number[]; // contains [nParts] scalable durations
constructor(id: string) {
this.id = id;
this.initialise();
}
initialise() {
this.nParts = 0;
this.prototype = new Play;
this.fullDuration = 0;
this.peakVolume = 0;
this.pitchAverage = 0;
this.pitchMidi = 0;
this.pitchProfile = [];
this.volumeProfile = [];
this.durationProfile = [];
}
// internal tools...
protected goodPitch(pitch: number): number {
return Math.min(Math.max(pitch, 1), 9999);
}
protected goodVolume(volume: number): number {
return Math.min(Math.max(volume, 0), 1023);
}
protected goodDuration(duration: number): number {
return Math.min(Math.max(duration, 10), 9999);
}
// methods...
// begin setting up the very first part of a new FlexFX
startWith(startPitch: number, startVolume: number) {
this.pitchProfile.push(this.goodPitch(startPitch)); // pitchProfile[0]
let v = this.goodVolume(startVolume * 4); // internally, volumes are [0-1020]
this.volumeProfile.push(v); // volumeProfile[0]
this.peakVolume = v; // ...until proven otherwise
this.pitchAverage = startPitch;
}
// add the details of the next part (ensuring all parameters are sensible)
addPart(wave: Wave, attack: Attack, effect: Effect, endPitch: number, endVolume: number, duration: number) {
this.pitchProfile.push(this.goodPitch(endPitch));
let bigEndVol = this.goodVolume(endVolume * 4);
this.volumeProfile.push(bigEndVol);
this.peakVolume = Math.max(this.peakVolume, bigEndVol);
let d = this.goodDuration(duration);
this.durationProfile.push(d);
// turn our enums into simple numbers
let waveNumber: number = wave;
let effectNumber: number = effect;
let attackNumber: number = attack;
// start where the [pitch,volume] last ended:
// (this.nParts hasn't yet been incremented, so indexes the previous part)
let startPitch = this.pitchProfile[this.nParts];
let startVolume = this.volumeProfile[this.nParts]
if (wave == Wave.Silence) {
// ensure this part plays silently, while preserving the end-point of the previous part
// and the start-point of any following part
startVolume = 0;
bigEndVol = 0;
waveNumber = WaveShape.Sine; // arbitrarily, as silent!
} else {
// compute average pitch of this part
let blend = 0;
switch (attack) {
case Attack.Fast: blend = 0.1; // nearly all End pitch
break;
case Attack.Medium: blend = 0.2; // mostly End pitch
break;
case Attack.Even: blend = 0.5; // fifty-fifty
break;
// case Attack.Delayed: blend = 0.8; // mostly Start pitch
// break; *** option temporarily removed...
}
let pitch = (blend * startPitch) + ((1 - blend) * endPitch);
// update overall average pitch, weighted by duration of each part
let kilocycles = (this.pitchAverage * this.fullDuration + pitch * d);
this.pitchAverage = kilocycles / (this.fullDuration + d);
// update its MIDI equivalent (including microtonal fractions)
this.pitchMidi = hertzToMidi(this.pitchAverage);
}
this.fullDuration += d; // always add duration, even if silent
// create the SoundExpression
let soundExpr = music.createSoundExpression(waveNumber, startPitch, endPitch,
startVolume, bigEndVol, duration, effectNumber, attackNumber);
/* FUTURE ENHANCEMENT
The underlying implementation in "codal-microbit-v2/source/SoundSynthesizerEffects.cpp"
of the functions:
SoundSynthesizerEffects::exponentialRisingInterpolation()
and SoundSynthesizerEffects::exponentialFallingInterpolation()
currently contain maths bugs, so Attack.Delayed is being temporarily removed...
// add-in appropriate "shape" & "steps" parameters for Delayed effects
if (attack == Attack.Delayed) {
let tempSound = new soundExpression.Sound;
tempSound.src = soundExpr.getNotes();
if (endPitch > startPitch) {
tempSound.shape = soundExpression.InterpolationEffect.ExponentialRising; // (faked with Sin)
tempSound.steps = 90; // 1-degree steps
} else {
tempSound.shape = soundExpression.InterpolationEffect.ExponentialFalling; // (faked with Cos)
tempSound.steps = 90; // 1-degree steps
}
soundExpr = new SoundExpression(tempSound.src);
}
*/
// add new sound into the prototype
this.prototype.parts.push(soundExpr);
this.nParts++;
}
// Create a specifically tuned performance of this FlexFX
makeTunedPlay(pitch: number, volumeLimit: number, newDuration: number): Play {
let scaledVolumeLimit = volumeLimit * 4;
let play = new Play;
let sound = new soundExpression.Sound;
let pitchRatio = 1.0;
let volumeRatio = 1.0;
let durationRatio = 1.0;
// code defensively!
if (pitch * this.pitchAverage != 0) pitchRatio = pitch / this.pitchAverage;
if (scaledVolumeLimit * this.peakVolume != 0) volumeRatio = scaledVolumeLimit / this.peakVolume;
if (newDuration * this.fullDuration != 0) durationRatio = newDuration / this.fullDuration;
// apply ratios (where changed from 1.0) to relevant fields of each part in turn
for (let i = 0; i < this.nParts; i++) {
sound.src = this.prototype.parts[i].getNotes(); // current string
sound.frequency = this.goodPitch(this.pitchProfile[i] * pitchRatio);
sound.endFrequency = this.goodPitch(this.pitchProfile[i + 1] * pitchRatio);
if (volumeRatio != 1.0) {
sound.volume = this.goodVolume(this.volumeProfile[i] * volumeRatio);
sound.endVolume = this.goodVolume(this.volumeProfile[i + 1] * volumeRatio);
}
if (durationRatio != 1.0) {
sound.duration = this.goodDuration(this.durationProfile[i] * durationRatio);
}
play.parts[i] = new SoundExpression(sound.src); // modified string
}
return (play);
}
}
// Store a flexFX (overwriting any previous instance)
function storeFlexFX(target: FlexFX) {
// first delete any existing definition having this id (works even when missing!)
flexFXList.splice(flexFXList.indexOf(flexFXList.find(i => i.id === target.id), 1), 1);
// add this new definition
flexFXList.push(target);
}
// kick off the background player (if not already running)
function activatePlayer() {
if (!(playerActive || playerStopped)) {
playerActive = true;
control.inBackground(() => player());
}
}
// in turn, play everything currently on the playList
function player() {
let play = new Play;
while ((playList.length > 0) && !playerStopped) {
let soundString = "";
play = playList.shift();
let sound = play.parts[0].getNotes();
// look out for "silences" that have just one sound-string of "snnn..."
if (sound.charAt(0) == "s") {
let time = parseInt("0" + sound.slice(1).trim());
basic.pause(time); // just wait around...
} else {
// flatten the parts[] of sound-strings into a single comma-separated string
while (play.parts.length > 0) {
soundString += play.parts.shift().getNotes();
if (play.parts.length > 0) {
soundString += ",";
}
}
// now play it synchronously (from the player fiber's perspective!)
if (soundString.length > 0) {
control.raiseEvent(FLEXFX_ACTIVITY_ID, PLAYER.STARTING);
playerPlaying = true;
music.playSoundEffect(soundString, SoundExpressionPlayMode.UntilDone);
control.raiseEvent(FLEXFX_ACTIVITY_ID, PLAYER.FINISHED);
playerPlaying = false;
}
}
basic.pause(10); // always cede control briefly to scheduler
}
if (playList.length == 0) {
control.raiseEvent(FLEXFX_ACTIVITY_ID, PLAYER.ALLPLAYED);
} // else we were prematurely stopped by the playerStopped global flag
playerActive = false;
}
// ---- UI BLOCKS: PLAYING ----
/**
* perform a FlexFX
* @param flexId the identifier of the FlexFX to be played
* @param wait if true, it is played straightaway, else in the background
* @param pitch different base-frequency to use (in Hz)
* @param volumeLimit peak volume, as a number in the range 0-255
* @param newDuration how long (ms) the overall performance will last
*/
//% block="play FlexFX $flexId waiting? $wait||at pitch $pitch|with maximum volume $volumeLimit| lasting (ms) $newDuration"
//% group="Playing (micro:bit V2)"
//% inlineInputMode=inline
//% expandableArgumentMode="enabled"
//% weight=990
//% flexId.defl="ting"
//% wait.defl=true
//% pitch.min=50 pitch.max=2000 pitch.defl=0
//% volumeLimit.min=0 volumeLimit.max=255 volumeLimit.defl=200
//% newDuration.min=0 newDuration.max=10000 newDuration.defl=800
export function playFlexFX(flexId: string, wait: boolean = true,
pitch: number = 0, volumeLimit: number = 0, newDuration: number = 0) {
pitch = clamp(0, pitch, 2000);
volumeLimit = clamp(0, volumeLimit, 255);
newDuration = clamp(0, newDuration, 10000);
let target: FlexFX = flexFXList.find(i => i.id === flexId);
if (target == null) {
target = flexFXList.find(i => i.id === "***"); // "alert" sound
}
if (target != null) {
// compile and add our Play onto the playList
playList.push(target.makeTunedPlay(pitch, volumeLimit, newDuration));
activatePlayer(); // make sure it gets played (unless Stopped)
if (wait) {
awaitAllFinished(); // make sure it has been played
}
}
}
/**
* selector block to choose a FlexFX
* (returns the name of a built-in FlexFX)
*/
//% blockId="builtin_name" block="$flexFX"
//% group="Playing (micro:bit V2)"
//% weight=980
export function builtInFlexFX(flexFX: BuiltInFlexFX): string {
switch (flexFX) {
case BuiltInFlexFX.Chime: return "chime";
case BuiltInFlexFX.Cry: return "cry";
case BuiltInFlexFX.Flute: return "flute";
case BuiltInFlexFX.Horn: return "horn";
case BuiltInFlexFX.Hum: return "hum";
case BuiltInFlexFX.Laugh: return "laugh";
case BuiltInFlexFX.Miaow: return "miaow";
case BuiltInFlexFX.Moan: return "moan";
case BuiltInFlexFX.Moo: return "moo";
case BuiltInFlexFX.Motor: return "motor";
case BuiltInFlexFX.Query: return "query";
case BuiltInFlexFX.Shout: return "shout";
case BuiltInFlexFX.Siren: return "siren";
case BuiltInFlexFX.Snore: return "snore";
case BuiltInFlexFX.Ting: return "ting";
case BuiltInFlexFX.Tweet: return "tweet";
case BuiltInFlexFX.Uhoh: return "uhoh";
case BuiltInFlexFX.Violin: return "violin";
case BuiltInFlexFX.Whale: return "whale";
case BuiltInFlexFX.Woof: return "woof";
}
return "***" // error beep
}
/**
* use a FlexFX to play a Tune
* @param title the title of the Tune to be played
* @param flexId the identifier of the FlexFX to be used to play it
* @param wait if true, it is played to completion; else in the background
* @param transpose semitone steps by which to raise or lower all notes
* @param volumeLimit peak volume for every note, in the range 0-255
* @param tuneDuration how long (in ms) the overall performance should last
*/
//% block="play tune $title using FlexFX $flexId waiting? $wait||transposed by (semitones) $transpose|with maximum volume $volumeLimit|performance lasting (ms) $tuneDuration"
//% group="Playing (micro:bit V2)"
//% weight=970
//% inlineInputMode=inline
//% expandableArgumentMode="enabled"
//% title.defl="birthday"
//% flexId.defl="ting"
//% wait.defl=true
//% transpose.min=-60 transpose.max=60 transpose.defl=0
//% volumeLimit.min=0 volumeLimit.max=255 volumeLimit.defl=200
//% tuneDuration.min=50 tuneDuration.max=300000 tuneDuration.defl=0
export function playTune(title: string, flexId: string, wait: boolean = true,
transpose: number = 0, volumeLimit: number = 0, tuneDuration: number = 0) {
transpose = clamp(-60, transpose, 60); // +/- 5 octaves
volumeLimit = clamp(0, volumeLimit, 255);
tuneDuration = clamp(0, tuneDuration, 300000); // max 5 mins!
let flex: FlexFX = flexFXList.find(i => i.id === flexId);
if (flex == null) {
flex = flexFXList.find(i => i.id === "***"); // error-sound
}
let tune: Tune = tuneList.find(i => i.title === title);
if (tune == null) {
flex = flexFXList.find(i => i.id === "***"); // error-sound
tune = tuneList.find(i => i.title === "***"); // triple error-sound "tune"
}
if ((flex != null) && (flex != null)) {
let myTick = tickMs; // adopt current default tempo
if (tuneDuration != 0) {
myTick = tuneDuration / tune.nTicks; // tick-rate needed to achieve tuneDuration
}
for (let i = 0; i < tune.notes.length; i++) {
let note = tune.notes[i];
let ms = note.ticks * myTick;
let pitch = note.pitch;
if (note.volume == 0) { // if this note is a Rest, play silence
playSilence(ms);
} else {
if (transpose != 0) {
// apply transpose to MIDI then convert back to Hz
pitch = midiToHertz(note.midi + transpose);
}
// compile and add our Play onto the playList
playList.push(flex.makeTunedPlay(pitch, volumeLimit, ms));
}
}
activatePlayer(); // make sure it gets played (unless Stopped)
if (wait) {
awaitAllFinished(); // make sure it has been played
}
}
}
/**
* selector block to choose a Tune
* (returns the title of a built-in Tune)
*/
//% blockId="builtin_tune" block="$tune"
//% group="Playing (micro:bit V2)"
//% weight=960
export function builtInTune(tune: BuiltInTune): string {
switch (tune) {
case BuiltInTune.Birthday: return "birthday";
case BuiltInTune.JingleBells: return "jingleBells";
case BuiltInTune.TeaPot: return "teaPot";
case BuiltInTune.IfYoureHappy: return "ifYoureHappy";
case BuiltInTune.LondonBridge: return "londonBridge";
case BuiltInTune.OldMacdonald: return "oldMacdonald";
case BuiltInTune.BearMountain: return "bearMountain";
case BuiltInTune.PopWeasel: return "popWeasel";
case BuiltInTune.ThisOldMan: return "thisOldMan";
case BuiltInTune.RoundMountain: return "roundMountain";
case BuiltInTune.Edelweiss: return "edelweiss";
case BuiltInTune.NewWorld: return "newWorld";
case BuiltInTune.OdeToJoy: return "odeToJoy";
case BuiltInTune.BachViolin: return "bachViolin";
default: return "***"; // triple-beep "tune"
}
}
/**
* set the speed for playing future Tunes
* @param bpm the beats-per-minute (BPM) for playTune() to use
*/
//% block="set tempo (beats/minute) %bpm"
//% group="Playing (micro:bit V2)"
//% weight=950
//% bpm.min=30 bpm.max=480 bpm.defl=120
export function setNextTempo(bpm: number) { // CHANGES GLOBAL SETTING
bpm = clamp(30, bpm, 480);
tickMs = 15000 / bpm; // = (60*1000) / (4*bpm)
}
/**
* compose a Tune using EKO-notation (Extent-Key-Octave).
* @param title the name of the Tune to be created or replaced
* @param score a text-string listing the notes in the Tune
*/
//% block="compose tune $title with notes $score"
//% group="Playing (micro:bit V2)"
//% weight=940
//% title.defl="beethoven5"
//% score.defl="2R 2G4 2G4 2G4 8Eb4"
export function composeTune(title: string, score: string) {
// first delete any existing definition having this title (works even when missing!)
tuneList.splice(tuneList.indexOf(tuneList.find(i => i.title === title), 1), 1);
// add this new definition
tuneList.push(new Tune(title, score));
}
/**
* add notes to a Tune using EKO-notation (Extent-Key-Octave).
* @param title the name of the Tune to be extended
* @param score a text-string listing the notes to be added
*/
//% block="extend tune $title with extra notes $score"
// % group="Playing (micro:bit V2)"
//(group is deliberately commented-out to force showing sub-heading)
//% weight=930
//% title.defl="beethoven5"
//% score.defl="2R 2F4 2F4 2F4 8D4"
export function extendTune(title: string, score: string) {
let target: Tune = tuneList.find(i => i.title === title);
if (target == null) {
// OOPS! trying to extend a non-existent Tune:
// rather than fail, just create a new one
tuneList.push(new Tune(title, score));
} else {
target.extend(score);
}
}
// ---- UI BLOCKS: PLAY-LIST ----
/**
* await start of next FlexFX on the play-list (unless none)
*/
//% block="wait until next FlexFX starts"
//% group="Play-list (micro:bit V2)"
//% weight=890
//% advanced=true
export function awaitPlayStart() {
if (playList.length > 0) {
playerStopped = false; // in case it was
activatePlayer(); // it case it wasn't
control.waitForEvent(FLEXFX_ACTIVITY_ID, PLAYER.STARTING);
} // else nothing to wait for
}
/**
* await completion of FlexFX currently playing
*/
//% block="wait until current FlexFX finishes (unless none)"
//% group="Play-list (micro:bit V2)"
//% weight=880
//% advanced=true
export function awaitPlayFinish() {
if (playerPlaying) {
control.waitForEvent(FLEXFX_ACTIVITY_ID, PLAYER.FINISHED);
} // else nothing to wait for
}
/**
* await completion of everything on the play-list
*/
//% block="wait until everything played"
//% group="Play-list (micro:bit V2)"
//% weight=870
//% advanced=true
export function awaitAllFinished() {
if (playList.length > 0) {
playerStopped = false; // in case it was
activatePlayer(); // in case it wasn't
control.waitForEvent(FLEXFX_ACTIVITY_ID, PLAYER.ALLPLAYED);
} // else nothing to wait for
}
/**
* add a silent pause to the play-list
* @param ms length of pause (in millisecs)
*/
//% block="add a pause of $ms ms next in the play-list"
//% group="Play-list (micro:bit V2)"
//% weight=860
//% advanced=true
//% ms.defl=500
export function playSilence(ms: number) {
// adds a special-case sound-string of format "snnn.."
// so "s2500" adds a silence of 2.5 sec
ms = clamp(0, ms, 60000);
let play = new Play;
play.parts.push(new SoundExpression("s" + convertToText(Math.floor(ms))));
playList.push(play);
activatePlayer(); // make sure it gets played (unless Stopped)
}
/**
* check how many Plays are waiting
* (returns length of the play-list)
*/
//% block="length of play-list"
//% group="Play-list (micro:bit V2)"
//% weight=850
//% advanced=true
export function waitingToPlay(): number {
return playList.length;
}
/**
* suspend background playing from the play-list
*/
//% block="pause play-list"
//% group="Play-list (micro:bit V2)"
//% weight=840
//% advanced=true
export function stopPlaying() {
playerStopped = true;
}
/**
* resume background playing from the play-list
*/
//% block="play play-list"
//% group="Play-list (micro:bit V2)"
//% weight=830
//% advanced=true
export function startPlaying() {
playerStopped = false;
activatePlayer();
}
/**
* delete from the play-list everything left unplayed
*/
//% block="forget play-list"
//% group="Play-list (micro:bit V2)"
//% weight=820
//% advanced=true
export function deletePlaylist() {
while (playList.length > 0) { playList.pop() }
}
// Accessors for internal flags...
/**
* return "true" if playing is currently inhibited
*/
//% block="is paused"
//% group="Play-list (micro:bit V2)"
//% weight=815
//% advanced=true
export function isStopped(): boolean {
return playerStopped;
}
/**
* return "true" if a FlexFX is currently being played
*/
//% block="is playing"
//% group="Play-list (micro:bit V2)"
//% weight=810
//% advanced=true
export function isPlaying(): boolean {
return playerPlaying;
}
/**
* return "true" if the background player is running
*/
//% block="is active"
//% group="Play-list (micro:bit V2)"
//% weight=805
//% advanced=true
export function isActive(): boolean {
return playerActive;
}
// ---- UI BLOCKS: CREATING --
/**
* specify the first (or only) part of a new FlexFX
* @param flexId the identifier of the flexFX to be created or changed
* @param startPitch the initial frequency of the sound (in Hz)
* @param startVolume the initial volume of the sound (0 to 255)
* @param wave chooses the wave-form that characterises this sound
* @param attack how fast the sound moves from its initial to final pitch
* @param effect a possible modification to the sound, such as vibrato
* @param endPitch the final frequency of the sound (in Hz)
* @param endVolume the final volume of the sound (0 to 255)
* @param duration the duration of the sound (in ms)
*/
//% block="define FlexFX $flexId| using wave-shape $wave| with attack $attack| and effect $effect| pitch goes from $startPitch| to $endPitch|volume goes from $startVolume| to $endVolume|default duration=$duration"
//% group="Creating (micro:bit V2)"
//% weight=790
//% advanced=true
//% inlineInputMode=external
//% flexId.defl="new"
//% startPitch.min=25 startPitch.max=10000 startPitch.defl=1000
//% startVolume.min=0 startVolume.max=255 startVolume.defl=200
//% endPitch.min=25 endPitch.max=10000 endPitch.defl=500
//% endVolume.min=0 endVolume.max=255 endVolume.defl=100
//% duration.min=0 duration.max=10000 duration.defl=800
export function defineFlexFX(flexId: string, startPitch: number, startVolume: number,
wave: Wave, attack: Attack, effect: Effect,
endPitch: number, endVolume: number, duration: number) {
startPitch = clamp(25, startPitch, 10000);
startVolume = clamp(0, startVolume, 255);
endPitch = clamp(25, endPitch, 10000);
endVolume = clamp(0, endVolume, 255);
duration = clamp(0, duration, 10000);
// are we re-defining an existing flexFX?
let target: FlexFX = flexFXList.find(i => i.id === flexId);
if (target != null) {
target.initialise(); // yes, so clear it down
} else {
target = new FlexFX(flexId); // no, so get a new one
}
target.startWith(startPitch, startVolume);
target.addPart(wave, attack, effect, endPitch, endVolume, duration);
storeFlexFX(target);
}
/**
* continue an existing FlexFX from its current final frequency and volume
* @param flexId the identifier of the flexFX to be extended
* @param wave chooses the wave-form that characterises this next part
* @param attack how fast this part moves from its initial to final pitch
* @param effect a possible modification to this part, such as vibrato
* @param endPitch the new final frequency of the FlexFX (in Hz)
* @param endVolume the new final volume of the FlexFX (0 to 255)
* @param duration the additional duration of this new part (in ms)
*/
//% block="continue FlexFX $flexId| using wave-shape $wave| with attack $attack| and effect $effect| pitch goes to $endPitch|volume goes to $endVolume| extended by (ms) $duration"
//% group="Creating (micro:bit V2)"
//% weight=780
//% advanced=true
//% inlineInputMode=external
//% flexId.defl="new"
//% endPitch.min=25 endPitch.max=4000 endPitch.defl=500
//% endVolume.min=0 endVolume.max=255 endVolume.defl=200
//% duration.min=0 duration.max=10000 duration.defl=500
export function extendFlexFX(flexId: string, wave: Wave, attack: Attack, effect: Effect,
endPitch: number, endVolume: number, duration: number) {
endPitch = clamp(25, endPitch, 10000);
endVolume = clamp(0, endVolume, 255);
duration = clamp(0, duration, 10000);
// force our enums into numbers
let waveNumber: number = wave;
let effectNumber: number = effect;
let attackNumber: number = attack;
let target: FlexFX = flexFXList.find(i => i.id === flexId);
if (target == null) {
// OOPS! trying to extend a non-existent flexFX:
// rather than fail, just create a new one, but with flat profiles
defineFlexFX(flexId, waveNumber, endPitch, endPitch, endVolume, endVolume, duration, effectNumber, attackNumber);
} else {
// TODO: do we need to use waveNumber etc. ? Don't think so!
target.addPart(wave, attack, effect, endPitch, endVolume, duration);
}
storeFlexFX(target);
}