From b0d4c5f7fb979234271660d23ddb72d21217f41c Mon Sep 17 00:00:00 2001 From: Elliott Brooks Date: Sun, 7 Jun 2020 18:39:52 +0100 Subject: [PATCH 1/3] Add service to playback notes from the fretboard --- .../fretonator/fretonator.component.html | 1 + .../fretonator/fretonator.component.scss | 6 ++ .../common/fretonator/fretonator.component.ts | 3 + .../playback/note-playback.service.spec.ts | 16 ++++ .../common/playback/note-playback.service.ts | 80 +++++++++++++++++++ apps/fretonator-web/src/app/util/constants.ts | 9 +++ 6 files changed, 115 insertions(+) create mode 100644 apps/fretonator-web/src/app/common/playback/note-playback.service.spec.ts create mode 100644 apps/fretonator-web/src/app/common/playback/note-playback.service.ts diff --git a/apps/fretonator-web/src/app/common/fretonator/fretonator.component.html b/apps/fretonator-web/src/app/common/fretonator/fretonator.component.html index ee2d784..addc30f 100644 --- a/apps/fretonator-web/src/app/common/fretonator/fretonator.component.html +++ b/apps/fretonator-web/src/app/common/fretonator/fretonator.component.html @@ -10,6 +10,7 @@ [attr.data-degree]="(fretMap | getFretFromFretMap: string : fret)?.degree" [attr.data-display-note]="(fretMap | getFretFromFretMap: string : fret)?.displayName" [attr.data-mode]="mode" + (click)="playbackService.playNote(stringName, fret)" > diff --git a/apps/fretonator-web/src/app/common/fretonator/fretonator.component.scss b/apps/fretonator-web/src/app/common/fretonator/fretonator.component.scss index 79f5c1c..366ba87 100644 --- a/apps/fretonator-web/src/app/common/fretonator/fretonator.component.scss +++ b/apps/fretonator-web/src/app/common/fretonator/fretonator.component.scss @@ -40,6 +40,12 @@ left: 0; right: 0; transform: translatey(calc(50% - 1px)); + opacity: .9; + cursor: pointer; + } + + &:hover:after{ + opacity: 1; } &:nth-child(-n + 13) { diff --git a/apps/fretonator-web/src/app/common/fretonator/fretonator.component.ts b/apps/fretonator-web/src/app/common/fretonator/fretonator.component.ts index e3486d1..591043e 100644 --- a/apps/fretonator-web/src/app/common/fretonator/fretonator.component.ts +++ b/apps/fretonator-web/src/app/common/fretonator/fretonator.component.ts @@ -1,5 +1,6 @@ import { Component, Input } from '@angular/core'; import { ChordMap, FretMap, Mode, Scale } from '../../util/types'; +import { NotePlaybackService } from '../playback/note-playback.service'; @Component({ selector: 'app-fretonator', @@ -16,5 +17,7 @@ export class FretonatorComponent { @Input() note: string; @Input() noteExtenderString: string; + constructor(public playbackService: NotePlaybackService) { } + frets = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]; } diff --git a/apps/fretonator-web/src/app/common/playback/note-playback.service.spec.ts b/apps/fretonator-web/src/app/common/playback/note-playback.service.spec.ts new file mode 100644 index 0000000..41cf335 --- /dev/null +++ b/apps/fretonator-web/src/app/common/playback/note-playback.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { NotePlaybackService } from './note-playback.service'; + +describe('NotePlaybackService', () => { + let service: NotePlaybackService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(NotePlaybackService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/apps/fretonator-web/src/app/common/playback/note-playback.service.ts b/apps/fretonator-web/src/app/common/playback/note-playback.service.ts new file mode 100644 index 0000000..c73f7e0 --- /dev/null +++ b/apps/fretonator-web/src/app/common/playback/note-playback.service.ts @@ -0,0 +1,80 @@ +import { Injectable } from '@angular/core'; +import { StringFrequencies } from '../../util/constants'; + +const SYNTH_BUFFER_SIZE = 4096; +const SYNTH_PLAY_DURATION = 2000; + +@Injectable({ + providedIn: 'root' +}) +export class NotePlaybackService { + private context: AudioContext; + + constructor() { + try { + // Feature sniff for web audio API + this.context = new (window.AudioContext || window['webkitAudioContext']); + } catch (e) { + // No browser support :( + } + } + + playNote(stringName, fret) { + if (this.context) { + let noteFrequency = this.getFrequency(stringName, fret); + this.pluckString(noteFrequency); + } + } + + private getFrequency(stringName, fret) { + // We're using stringName here, the case sensitive alt to string, to differentiate E/e strings. + let stringFrequency = StringFrequencies[stringName]; + let fretCents = fret * 100; + return stringFrequency * Math.pow(2, (fretCents / 1200)); + } + + private pluckString(frequency: number) { + // Use Karplus-Strong algo to simply synth guitar-like sounds. + // https://ccrma.stanford.edu/~jos/pasp/Karplus_Strong_Algorithm.html + let processor = this.context.createScriptProcessor(SYNTH_BUFFER_SIZE, 0, 1); + let signalPeriod = Math.round(this.context.sampleRate / frequency); + let currentSample = new Float32Array(signalPeriod); + // Fill sample with random noise -1 through +1 + this.fillWithNoise(currentSample, signalPeriod); + let n = 0; + processor.addEventListener('audioprocess', (e) => { + // Populate output buffer with signal + let outputBuffer = e.outputBuffer.getChannelData(0); + for (let i = 0; i < outputBuffer.length; i++) { + // Lowpass the signal by averaging it with the next point + currentSample[n] = (currentSample[n] + currentSample[(n + 1) % signalPeriod]) / 2; + // Copy output to the buffer, repeat + outputBuffer[i] = currentSample[n]; + n = (n + 1) % signalPeriod; + } + }); + // Filter the output + let bandpass = this.createBandpassFilter(frequency); + processor.connect(bandpass); + // Kill the processor after 2 seconds + setTimeout(() => { + bandpass.disconnect(); + processor.disconnect(); + }, SYNTH_PLAY_DURATION); + } + + private fillWithNoise(sample, signalPeriod){ + for (let i = 0; i < signalPeriod; i++) { + sample[i] = (2 * Math.random()) - 1; + } + } + + private createBandpassFilter(frequency){ + let bandpass = this.context.createBiquadFilter(); + bandpass.type = "bandpass"; + bandpass.frequency.value = Math.round(frequency); + bandpass.Q.value = 1 / 6; + bandpass.connect(this.context.destination); + return bandpass; + } +} diff --git a/apps/fretonator-web/src/app/util/constants.ts b/apps/fretonator-web/src/app/util/constants.ts index f7f7eca..0578647 100644 --- a/apps/fretonator-web/src/app/util/constants.ts +++ b/apps/fretonator-web/src/app/util/constants.ts @@ -440,3 +440,12 @@ export const Enharmonics = [ ['G#', 'A♭'], ['A#', 'B♭'] ]; + +export const StringFrequencies = { + 'e': 329.63, + 'B': 246.94, + 'G': 196.00, + 'D': 146.83, + 'A': 110.00, + 'E': 82.41 +} From 95e445b48051629d3f89ab00b02a8ec1a1ed9797 Mon Sep 17 00:00:00 2001 From: Elliott Brooks Date: Sun, 7 Jun 2020 19:25:49 +0100 Subject: [PATCH 2/3] linting feedback --- .../common/playback/note-playback.service.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/apps/fretonator-web/src/app/common/playback/note-playback.service.ts b/apps/fretonator-web/src/app/common/playback/note-playback.service.ts index c73f7e0..9661fd4 100644 --- a/apps/fretonator-web/src/app/common/playback/note-playback.service.ts +++ b/apps/fretonator-web/src/app/common/playback/note-playback.service.ts @@ -21,30 +21,30 @@ export class NotePlaybackService { playNote(stringName, fret) { if (this.context) { - let noteFrequency = this.getFrequency(stringName, fret); + const noteFrequency = this.getFrequency(stringName, fret); this.pluckString(noteFrequency); } } private getFrequency(stringName, fret) { // We're using stringName here, the case sensitive alt to string, to differentiate E/e strings. - let stringFrequency = StringFrequencies[stringName]; - let fretCents = fret * 100; + const stringFrequency = StringFrequencies[stringName]; + const fretCents = fret * 100; return stringFrequency * Math.pow(2, (fretCents / 1200)); } private pluckString(frequency: number) { // Use Karplus-Strong algo to simply synth guitar-like sounds. // https://ccrma.stanford.edu/~jos/pasp/Karplus_Strong_Algorithm.html - let processor = this.context.createScriptProcessor(SYNTH_BUFFER_SIZE, 0, 1); - let signalPeriod = Math.round(this.context.sampleRate / frequency); - let currentSample = new Float32Array(signalPeriod); + const processor = this.context.createScriptProcessor(SYNTH_BUFFER_SIZE, 0, 1); + const signalPeriod = Math.round(this.context.sampleRate / frequency); + const currentSample = new Float32Array(signalPeriod); // Fill sample with random noise -1 through +1 this.fillWithNoise(currentSample, signalPeriod); let n = 0; processor.addEventListener('audioprocess', (e) => { // Populate output buffer with signal - let outputBuffer = e.outputBuffer.getChannelData(0); + const outputBuffer = e.outputBuffer.getChannelData(0); for (let i = 0; i < outputBuffer.length; i++) { // Lowpass the signal by averaging it with the next point currentSample[n] = (currentSample[n] + currentSample[(n + 1) % signalPeriod]) / 2; @@ -54,7 +54,7 @@ export class NotePlaybackService { } }); // Filter the output - let bandpass = this.createBandpassFilter(frequency); + const bandpass = this.createBandpassFilter(frequency); processor.connect(bandpass); // Kill the processor after 2 seconds setTimeout(() => { @@ -70,7 +70,7 @@ export class NotePlaybackService { } private createBandpassFilter(frequency){ - let bandpass = this.context.createBiquadFilter(); + const bandpass = this.context.createBiquadFilter(); bandpass.type = "bandpass"; bandpass.frequency.value = Math.round(frequency); bandpass.Q.value = 1 / 6; From 982c84cf89bf55cc881f0eca1320ba22df1439f1 Mon Sep 17 00:00:00 2001 From: Elliott Brooks Date: Sun, 7 Jun 2020 19:41:58 +0100 Subject: [PATCH 3/3] Defer loading of audioContext until user has interacted with page --- .../common/playback/note-playback.service.ts | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/apps/fretonator-web/src/app/common/playback/note-playback.service.ts b/apps/fretonator-web/src/app/common/playback/note-playback.service.ts index 9661fd4..75d535e 100644 --- a/apps/fretonator-web/src/app/common/playback/note-playback.service.ts +++ b/apps/fretonator-web/src/app/common/playback/note-playback.service.ts @@ -10,17 +10,18 @@ const SYNTH_PLAY_DURATION = 2000; export class NotePlaybackService { private context: AudioContext; - constructor() { - try { - // Feature sniff for web audio API - this.context = new (window.AudioContext || window['webkitAudioContext']); - } catch (e) { - // No browser support :( - } - } + constructor() {} playNote(stringName, fret) { - if (this.context) { + if (!this.context) { + try { + // Feature sniff for web audio API + this.context = new (window.AudioContext || window['webkitAudioContext']); + } catch (e) { + // No browser support :( + } + } + if(this.context){ const noteFrequency = this.getFrequency(stringName, fret); this.pluckString(noteFrequency); }