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..75d535e --- /dev/null +++ b/apps/fretonator-web/src/app/common/playback/note-playback.service.ts @@ -0,0 +1,81 @@ +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() {} + + playNote(stringName, fret) { + 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); + } + } + + private getFrequency(stringName, fret) { + // We're using stringName here, the case sensitive alt to string, to differentiate E/e strings. + 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 + 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 + 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; + // Copy output to the buffer, repeat + outputBuffer[i] = currentSample[n]; + n = (n + 1) % signalPeriod; + } + }); + // Filter the output + const 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){ + const 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 +}