KEMBAR78
ng-conf 2017: Angular Mischief Maker Slides | PDF
Mischief Maker
ENLIGHTEN
Observable
.fromPromise(navigator.requestMIDIAccess())
.map((midi: any) => Array.from(midi.inputs)) // convert from iterable
.map((devices: any) => devices.map(device => device[1])) // grab just the MIDIInput
.subscribe((devices: any) => this.devices = devices)
;
Connect to Devices
KNOW
private initMidiStream() {
const midiAccess$ = Observable.fromPromise(navigator.requestMIDIAccess());
const inputStream$ = midiAccess$.map((midi: any) => midi.inputs.values().next().value);
const messages$ = inputStream$
.filter(input => input !== undefined)
.flatMap(input => this.midiMessageAsObservable(input))
.map((message: any) => ({
status: message.data[0] & 0xf0,
data: [
message.data[1],
message.data[2],
],
}))
.subscribe(message => {
this.messages.unshift(message);
this.cd.detectChanges();
})
;
}
private midiMessageAsObservable(input) {
const source = new Subject();
input.onmidimessage = note => source.next(note);
return source.asObservable();
}
Capture Input
UNDERSTAND
const noteTransforms = {
48: 'C2', 49: 'C#2', 50: 'D2', 51: 'D#2', 52: 'E2', 53: 'F2',
54: 'F#2', 55: 'G2', 56: 'G#2', 57: 'A2', 58: 'A#2', 59: 'B2',
60: 'C3', 61: 'C#3', 62: 'D3', 63: 'D#3', 64: 'E3', 65: 'F3',
66: 'F#3', 67: 'G3', 68: 'G#3', 69: 'A3', 70: 'A#3', 71: 'B3'
};
const messages$ = inputStream$
.filter(input => input !== undefined)
.flatMap(input => this.midiMessageAsObservable(input))
.map((message: any) => {
const status = message.data[0] & 0xf0;
return {
status: status === 144 ? 'PRESSED' : 'RELEASED', // Good until its not ¯_(ツ)_/¯
name: noteTransforms[message.data[1]],
pressure: message.data[2]
}})
;
Convert Input
MANIFEST
private initSynth() {
this.fft = new Tone.Analyser('fft', 32);
this.waveform = new Tone.Analyser('waveform', 1024);
return new Tone.PolySynth(6, Tone.Synth, {
'oscillator': {
'type': 'fatsawtooth',
'count': 3,
'spread': 30
},
'envelope': {
'attack': 0.01,
'decay': 0.1,
'sustain': 0.5,
'release': 0.4,
'attackCurve': 'exponential'
},
})
.fan(this.fft, this.waveform)
.toMaster();
}
Initialize Tone.js Synth
private midiMessageReceived(message: any) {
let cmd = message.status >> 4;24 pt
let noteNumber = noteTransforms[message.data[0]];
let velocity = 0;
if (message.data.length > 1) {
velocity = message.data[1] / 120; // needs to be between 0 and 1 and sometimes it is over 100 ¯_(ツ)_/¯
}
// MIDI noteon with velocity=0 is the same as noteoff
if (cmd === 8 || ((cmd === 9) && (velocity === 0))) { // noteoff
this.noteOff(noteNumber);
} else if (cmd === 9) { // note on
this.noteOn(noteNumber, velocity);
}
}
noteOn(note, velocity) {
this.synth.triggerAttack(note, null, velocity);
}
noteOff(note) {
this.synth.triggerRelease(note);
}
Play Midi Note
PERCEIVE
constructor(
private cd: ChangeDetectorRef,
private sanitizer: DomSanitizer
) {
this.synth = this.initSynth();
this.audioRecorder = new Recorder(this.synth);
}
toggleRecording() {
if (this.isRecording) {
// stop recording
this.audioRecorder.stop();
this.audioRecorder.getBuffers(this.processBuffers.bind(this));
this.isRecording = false;
} else {
// start recording
if (!this.audioRecorder) {
return;
}
this.isRecording = true;
this.audioRecorder.clear();
this.audioRecorder.record();
}
}
New Recorder Instance
onBuffersProcessed(buffers) {
this.audioRecorder.exportWAV(this.onEncoded.bind(this));
}
onEncoded(blob) {
this.addBlob(blob);
this.setupDownload(blob, `myRecording${this.recIndex}.wav`);
this.recIndex++;
}
setupDownload(blob, filename) {
let url = (window.URL).createObjectURL(blob);
this.downloadLink = this.sanitizer.bypassSecurityTrustUrl(url);
this.downloadFile = filename || 'output.wav';
}
addBlob(blob) {
this.wavesurfer.loadBlob(blob);
}
Process Audio Buffers
REFLECT
export class TrackComponent implements OnInit, OnChanges {
@ViewChild('trackWave') trackWave;
@Input() blob;
@Input() playing;
waveSurfer = null;
ngOnInit() {
this.waveSurfer = WaveSurfer.create({
container: this.trackWave.nativeElement,
scrollParent: true,
waveColor: 'violet',
progressColor: 'purple'
});
// Loop
this.waveSurfer.on('finish', () => this.waveSurfer.playPause());
// Load
this.waveSurfer.loadBlob(this.blob);
}
ngOnChanges(changes: SimpleChanges) {
if (changes['playing']) {
if (!this.waveSurfer) { return; }
this.waveSurfer.playPause();
}
}
}
Encapsulate Track
EVOLVE
<!-- workspace.component.html -->
<app-track *ngFor="let blob of blobs" [blob]="blob" [playing]="isPlaying"></app-track>
// workspace.component.js
addBlob(blob) {
this.blobs = this.blobs.concat(blob);
}
togglePlaying() {
this.isPlaying = !this.isPlaying;
}
Multiple Tracks
REVIEW
First Verse
I'm so attracted to what I observe
Can't help reacting to the things I've heard
I never stress about the state of you
Because I'm first to know of anything you do
Chorus
It's all about, Angular
Baby you're ahead of the curve
You've got all the tools I prefer
I'mma let it out, ain't no doubt, all about that, Angular
Second Verse
I love the way you're way ahead of time
I say you're classy just to say you're fine
You're more than just a CLI to me
I think I might just love you universally
Chorus
It's all about, Angular
Baby you're ahead of the curve
You've got all the tools I prefer
I'mma let it out, ain't no doubt, all about that, Angular
Bridge
It's semantics but I know you're mine
Our romance is changing all the time
So progressive, I can catch you offline
Total package, you're one of a kind
SOLOS!
Chorus
It's all about, Angular
Baby you're ahead of the curve
You've got all the tools I prefer
I'mma let it out, ain't no doubt, all about that, Angular
ANGULAR!
https://github.com/onehungrymind/mischief-maker
WE❤YOU!
@simpulton
@rogertippingii
Thanks!

ng-conf 2017: Angular Mischief Maker Slides

  • 1.
  • 2.
  • 3.
    Observable .fromPromise(navigator.requestMIDIAccess()) .map((midi: any) =>Array.from(midi.inputs)) // convert from iterable .map((devices: any) => devices.map(device => device[1])) // grab just the MIDIInput .subscribe((devices: any) => this.devices = devices) ; Connect to Devices
  • 4.
  • 5.
    private initMidiStream() { constmidiAccess$ = Observable.fromPromise(navigator.requestMIDIAccess()); const inputStream$ = midiAccess$.map((midi: any) => midi.inputs.values().next().value); const messages$ = inputStream$ .filter(input => input !== undefined) .flatMap(input => this.midiMessageAsObservable(input)) .map((message: any) => ({ status: message.data[0] & 0xf0, data: [ message.data[1], message.data[2], ], })) .subscribe(message => { this.messages.unshift(message); this.cd.detectChanges(); }) ; } private midiMessageAsObservable(input) { const source = new Subject(); input.onmidimessage = note => source.next(note); return source.asObservable(); } Capture Input
  • 6.
  • 7.
    const noteTransforms ={ 48: 'C2', 49: 'C#2', 50: 'D2', 51: 'D#2', 52: 'E2', 53: 'F2', 54: 'F#2', 55: 'G2', 56: 'G#2', 57: 'A2', 58: 'A#2', 59: 'B2', 60: 'C3', 61: 'C#3', 62: 'D3', 63: 'D#3', 64: 'E3', 65: 'F3', 66: 'F#3', 67: 'G3', 68: 'G#3', 69: 'A3', 70: 'A#3', 71: 'B3' }; const messages$ = inputStream$ .filter(input => input !== undefined) .flatMap(input => this.midiMessageAsObservable(input)) .map((message: any) => { const status = message.data[0] & 0xf0; return { status: status === 144 ? 'PRESSED' : 'RELEASED', // Good until its not ¯_(ツ)_/¯ name: noteTransforms[message.data[1]], pressure: message.data[2] }}) ; Convert Input
  • 8.
  • 9.
    private initSynth() { this.fft= new Tone.Analyser('fft', 32); this.waveform = new Tone.Analyser('waveform', 1024); return new Tone.PolySynth(6, Tone.Synth, { 'oscillator': { 'type': 'fatsawtooth', 'count': 3, 'spread': 30 }, 'envelope': { 'attack': 0.01, 'decay': 0.1, 'sustain': 0.5, 'release': 0.4, 'attackCurve': 'exponential' }, }) .fan(this.fft, this.waveform) .toMaster(); } Initialize Tone.js Synth
  • 10.
    private midiMessageReceived(message: any){ let cmd = message.status >> 4;24 pt let noteNumber = noteTransforms[message.data[0]]; let velocity = 0; if (message.data.length > 1) { velocity = message.data[1] / 120; // needs to be between 0 and 1 and sometimes it is over 100 ¯_(ツ)_/¯ } // MIDI noteon with velocity=0 is the same as noteoff if (cmd === 8 || ((cmd === 9) && (velocity === 0))) { // noteoff this.noteOff(noteNumber); } else if (cmd === 9) { // note on this.noteOn(noteNumber, velocity); } } noteOn(note, velocity) { this.synth.triggerAttack(note, null, velocity); } noteOff(note) { this.synth.triggerRelease(note); } Play Midi Note
  • 11.
  • 12.
    constructor( private cd: ChangeDetectorRef, privatesanitizer: DomSanitizer ) { this.synth = this.initSynth(); this.audioRecorder = new Recorder(this.synth); } toggleRecording() { if (this.isRecording) { // stop recording this.audioRecorder.stop(); this.audioRecorder.getBuffers(this.processBuffers.bind(this)); this.isRecording = false; } else { // start recording if (!this.audioRecorder) { return; } this.isRecording = true; this.audioRecorder.clear(); this.audioRecorder.record(); } } New Recorder Instance
  • 13.
    onBuffersProcessed(buffers) { this.audioRecorder.exportWAV(this.onEncoded.bind(this)); } onEncoded(blob) { this.addBlob(blob); this.setupDownload(blob,`myRecording${this.recIndex}.wav`); this.recIndex++; } setupDownload(blob, filename) { let url = (window.URL).createObjectURL(blob); this.downloadLink = this.sanitizer.bypassSecurityTrustUrl(url); this.downloadFile = filename || 'output.wav'; } addBlob(blob) { this.wavesurfer.loadBlob(blob); } Process Audio Buffers
  • 14.
  • 15.
    export class TrackComponentimplements OnInit, OnChanges { @ViewChild('trackWave') trackWave; @Input() blob; @Input() playing; waveSurfer = null; ngOnInit() { this.waveSurfer = WaveSurfer.create({ container: this.trackWave.nativeElement, scrollParent: true, waveColor: 'violet', progressColor: 'purple' }); // Loop this.waveSurfer.on('finish', () => this.waveSurfer.playPause()); // Load this.waveSurfer.loadBlob(this.blob); } ngOnChanges(changes: SimpleChanges) { if (changes['playing']) { if (!this.waveSurfer) { return; } this.waveSurfer.playPause(); } } } Encapsulate Track
  • 16.
  • 17.
    <!-- workspace.component.html --> <app-track*ngFor="let blob of blobs" [blob]="blob" [playing]="isPlaying"></app-track> // workspace.component.js addBlob(blob) { this.blobs = this.blobs.concat(blob); } togglePlaying() { this.isPlaying = !this.isPlaying; } Multiple Tracks
  • 18.
  • 19.
    First Verse I'm soattracted to what I observe Can't help reacting to the things I've heard I never stress about the state of you Because I'm first to know of anything you do
  • 20.
    Chorus It's all about,Angular Baby you're ahead of the curve You've got all the tools I prefer I'mma let it out, ain't no doubt, all about that, Angular
  • 21.
    Second Verse I lovethe way you're way ahead of time I say you're classy just to say you're fine You're more than just a CLI to me I think I might just love you universally
  • 22.
    Chorus It's all about,Angular Baby you're ahead of the curve You've got all the tools I prefer I'mma let it out, ain't no doubt, all about that, Angular
  • 23.
    Bridge It's semantics butI know you're mine Our romance is changing all the time So progressive, I can catch you offline Total package, you're one of a kind
  • 24.
  • 25.
    Chorus It's all about,Angular Baby you're ahead of the curve You've got all the tools I prefer I'mma let it out, ain't no doubt, all about that, Angular
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.