Skip to content

Commit b0d4c5f

Browse files
committed
Add service to playback notes from the fretboard
1 parent 5742a8e commit b0d4c5f

File tree

6 files changed

+115
-0
lines changed

6 files changed

+115
-0
lines changed

apps/fretonator-web/src/app/common/fretonator/fretonator.component.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
[attr.data-degree]="(fretMap | getFretFromFretMap: string : fret)?.degree"
1111
[attr.data-display-note]="(fretMap | getFretFromFretMap: string : fret)?.displayName"
1212
[attr.data-mode]="mode"
13+
(click)="playbackService.playNote(stringName, fret)"
1314
></div>
1415
</ng-container>
1516
</ng-template>

apps/fretonator-web/src/app/common/fretonator/fretonator.component.scss

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,12 @@
4040
left: 0;
4141
right: 0;
4242
transform: translatey(calc(50% - 1px));
43+
opacity: .9;
44+
cursor: pointer;
45+
}
46+
47+
&:hover:after{
48+
opacity: 1;
4349
}
4450

4551
&:nth-child(-n + 13) {

apps/fretonator-web/src/app/common/fretonator/fretonator.component.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Component, Input } from '@angular/core';
22
import { ChordMap, FretMap, Mode, Scale } from '../../util/types';
3+
import { NotePlaybackService } from '../playback/note-playback.service';
34

45
@Component({
56
selector: 'app-fretonator',
@@ -16,5 +17,7 @@ export class FretonatorComponent {
1617
@Input() note: string;
1718
@Input() noteExtenderString: string;
1819

20+
constructor(public playbackService: NotePlaybackService) { }
21+
1922
frets = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12];
2023
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { TestBed } from '@angular/core/testing';
2+
3+
import { NotePlaybackService } from './note-playback.service';
4+
5+
describe('NotePlaybackService', () => {
6+
let service: NotePlaybackService;
7+
8+
beforeEach(() => {
9+
TestBed.configureTestingModule({});
10+
service = TestBed.inject(NotePlaybackService);
11+
});
12+
13+
it('should be created', () => {
14+
expect(service).toBeTruthy();
15+
});
16+
});
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import { Injectable } from '@angular/core';
2+
import { StringFrequencies } from '../../util/constants';
3+
4+
const SYNTH_BUFFER_SIZE = 4096;
5+
const SYNTH_PLAY_DURATION = 2000;
6+
7+
@Injectable({
8+
providedIn: 'root'
9+
})
10+
export class NotePlaybackService {
11+
private context: AudioContext;
12+
13+
constructor() {
14+
try {
15+
// Feature sniff for web audio API
16+
this.context = new (window.AudioContext || window['webkitAudioContext']);
17+
} catch (e) {
18+
// No browser support :(
19+
}
20+
}
21+
22+
playNote(stringName, fret) {
23+
if (this.context) {
24+
let noteFrequency = this.getFrequency(stringName, fret);
25+
this.pluckString(noteFrequency);
26+
}
27+
}
28+
29+
private getFrequency(stringName, fret) {
30+
// We're using stringName here, the case sensitive alt to string, to differentiate E/e strings.
31+
let stringFrequency = StringFrequencies[stringName];
32+
let fretCents = fret * 100;
33+
return stringFrequency * Math.pow(2, (fretCents / 1200));
34+
}
35+
36+
private pluckString(frequency: number) {
37+
// Use Karplus-Strong algo to simply synth guitar-like sounds.
38+
// https://ccrma.stanford.edu/~jos/pasp/Karplus_Strong_Algorithm.html
39+
let processor = this.context.createScriptProcessor(SYNTH_BUFFER_SIZE, 0, 1);
40+
let signalPeriod = Math.round(this.context.sampleRate / frequency);
41+
let currentSample = new Float32Array(signalPeriod);
42+
// Fill sample with random noise -1 through +1
43+
this.fillWithNoise(currentSample, signalPeriod);
44+
let n = 0;
45+
processor.addEventListener('audioprocess', (e) => {
46+
// Populate output buffer with signal
47+
let outputBuffer = e.outputBuffer.getChannelData(0);
48+
for (let i = 0; i < outputBuffer.length; i++) {
49+
// Lowpass the signal by averaging it with the next point
50+
currentSample[n] = (currentSample[n] + currentSample[(n + 1) % signalPeriod]) / 2;
51+
// Copy output to the buffer, repeat
52+
outputBuffer[i] = currentSample[n];
53+
n = (n + 1) % signalPeriod;
54+
}
55+
});
56+
// Filter the output
57+
let bandpass = this.createBandpassFilter(frequency);
58+
processor.connect(bandpass);
59+
// Kill the processor after 2 seconds
60+
setTimeout(() => {
61+
bandpass.disconnect();
62+
processor.disconnect();
63+
}, SYNTH_PLAY_DURATION);
64+
}
65+
66+
private fillWithNoise(sample, signalPeriod){
67+
for (let i = 0; i < signalPeriod; i++) {
68+
sample[i] = (2 * Math.random()) - 1;
69+
}
70+
}
71+
72+
private createBandpassFilter(frequency){
73+
let bandpass = this.context.createBiquadFilter();
74+
bandpass.type = "bandpass";
75+
bandpass.frequency.value = Math.round(frequency);
76+
bandpass.Q.value = 1 / 6;
77+
bandpass.connect(this.context.destination);
78+
return bandpass;
79+
}
80+
}

apps/fretonator-web/src/app/util/constants.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -440,3 +440,12 @@ export const Enharmonics = [
440440
['G#', 'A♭'],
441441
['A#', 'B♭']
442442
];
443+
444+
export const StringFrequencies = {
445+
'e': 329.63,
446+
'B': 246.94,
447+
'G': 196.00,
448+
'D': 146.83,
449+
'A': 110.00,
450+
'E': 82.41
451+
}

0 commit comments

Comments
 (0)