Skip to content

Commit 8f9e312

Browse files
authored
Merge pull request #40 from ghth-admin/add-note-sounds
Audio playback notes from the fretboard
2 parents 5742a8e + 982c84c commit 8f9e312

File tree

6 files changed

+116
-0
lines changed

6 files changed

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

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)