Skip to content

Commit 5f32e65

Browse files
committed
[modules:piano] simple sound generator
- piano 3.5 CDEFGAB CDEFGAB CDEFGAB CDEFG - use waud.js wavheader and tonegenerator - use audio tag to play sound, modern browser support
1 parent 3a848c0 commit 5f32e65

File tree

5 files changed

+350
-0
lines changed

5 files changed

+350
-0
lines changed

modules/piano/config

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
name=Simple Piano
2+
port=9090

modules/piano/index.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
const path = require('path');
2+
const fs = require('fs');
3+
const express = require('express');
4+
const body_parser = require('body-parser');
5+
const app = express();
6+
7+
const static_dir = path.join(__dirname, 'static');
8+
9+
app.use('/', express.static(static_dir));
10+
11+
app.listen(9090, '0.0.0.0', () => {
12+
console.log(`Nodepad is listening at 0.0.0.0:9090`);
13+
});

modules/piano/readme

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Simple Piano
2+
running: 9090
3+
params: (no params)

modules/piano/static/index.html

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
<!doctype>
2+
<html>
3+
<head>
4+
<meta charset='utf-8'>
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
6+
<title>Piano</title>
7+
<style>
8+
.pianokey {
9+
width: 30px;
10+
height: 100px;
11+
border: 1px solid black;
12+
display: inline-block;
13+
}
14+
</style>
15+
</head>
16+
<body>
17+
<div>
18+
<div class="pianokey" code="a">C</div>
19+
<div class="pianokey" code="b">D</div>
20+
<div class="pianokey" code="c">E</div>
21+
<div class="pianokey" code="d">F</div>
22+
<div class="pianokey" code="e">G</div>
23+
<div class="pianokey" code="f">A</div>
24+
<div class="pianokey" code="g">B</div>
25+
</div>
26+
<div>
27+
<div class="pianokey" code="h">C</div>
28+
<div class="pianokey" code="i">D</div>
29+
<div class="pianokey" code="j">E</div>
30+
<div class="pianokey" code="k">F</div>
31+
<div class="pianokey" code="l">G</div>
32+
<div class="pianokey" code="m">A</div>
33+
<div class="pianokey" code="n">B</div>
34+
</div>
35+
<div>
36+
<div class="pianokey" code="o">C</div>
37+
<div class="pianokey" code="p">D</div>
38+
<div class="pianokey" code="q">E</div>
39+
<div class="pianokey" code="r">F</div>
40+
<div class="pianokey" code="s">G</div>
41+
<div class="pianokey" code="t">A</div>
42+
<div class="pianokey" code="u">B</div>
43+
</div>
44+
<div>
45+
<div class="pianokey" code="v">C</div>
46+
<div class="pianokey" code="w">D</div>
47+
<div class="pianokey" code="x">E</div>
48+
<div class="pianokey" code="y">F</div>
49+
<div class="pianokey" code="z">G</div>
50+
</div>
51+
<script type="text/javascript" src="soundutils.js"></script>
52+
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/waud.js/0.9.16/waud.min.js"></script>
53+
<script>
54+
function format(raw) {
55+
return raw.map(function(x) {
56+
if (x < 0) x += 128;
57+
return String.fromCharCode(x);
58+
}).join('');
59+
}
60+
61+
function play(hz, sec) {
62+
var tone = module.exports.tone({ freq: hz, lengthInSecs: sec, volume: module.exports.MAX_8 });
63+
var header = module.exports.wavheader(tone.length);
64+
data = 'data:audio/wav;base64,' + btoa(header + format(tone));
65+
var base64Snd = new WaudSound(data, { autoplay: false, loop: false, volume: 0.5 });
66+
base64Snd.onLoad(function (snd) {snd.play();});
67+
base64Snd.onEnd(function (snd) {snd.destroy();});
68+
}
69+
70+
const note_step = Math.pow(2, 1/12);
71+
var note_map = {
72+
'a': -21,
73+
'b': -19,
74+
'c': -17,
75+
'd': -16,
76+
'e': -14,
77+
'f': -12,
78+
'g': -10,
79+
'h': -9,
80+
'i': -7,
81+
'j': -5,
82+
'k': -4,
83+
'l': -2,
84+
'm': 0, // 1A
85+
'n': 2,
86+
'o': 3,
87+
'p': 5,
88+
'q': 7,
89+
'r': 8,
90+
's': 10,
91+
't': 12,
92+
'u': 14,
93+
'v': 15,
94+
'w': 17,
95+
'x': 19,
96+
'y': 20,
97+
'z': 22
98+
}
99+
Object.keys(note_map).forEach(function(x) {
100+
note_map[x] = 440*Math.pow(note_step, note_map[x]);
101+
});
102+
Waud.init();
103+
104+
var data = '';
105+
var audio = document.getElementById('play');
106+
document.body.addEventListener('keydown', function(evt) {
107+
if (evt.keyCode < 65 || evt.keyCode > 91) return;
108+
play(note_map[evt.key] || 0, 0.5);
109+
});
110+
111+
var keys = document.querySelectorAll('.pianokey');
112+
for(var i = 0, n = keys.length; i<n; i++) {
113+
keys[i].addEventListener('mousedown', function(evt) {
114+
evt.target.style.backgroundColor = 'green';
115+
var code = evt.target.getAttribute('code');
116+
play(note_map[code] || 0, 0.5);
117+
});
118+
keys[i].addEventListener('mouseup', function(evt) {
119+
evt.target.style.backgroundColor = null;
120+
});
121+
keys[i].addEventListener('touchstart', function(evt) {
122+
evt.target.style.backgroundColor = 'green';
123+
var code = evt.target.getAttribute('code');
124+
play(note_map[code] || 0, 0.5);
125+
});
126+
keys[i].addEventListener('touchend', function(evt) {
127+
evt.target.style.backgroundColor = null;
128+
});
129+
}
130+
</script>
131+
</body>
132+
</html>

modules/piano/static/soundutils.js

Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
var module = {
2+
exports: {}
3+
};
4+
5+
(function () {
6+
// https://github.com/karlwestin/node-tonegenerator
7+
/*
8+
* ToneGenerator for node.js
9+
* generates raw PCM data for a tone,
10+
* specify frequency, length, volume and sampling rate
11+
*/
12+
13+
var shapes = {
14+
sine: function (i, cycle, volume) {
15+
// i / cycle => value between 0 and 1
16+
// 0 = beginning of circly
17+
// 0.25 Math.sin = 1
18+
// 0.5 Math.sin = 0
19+
// 0.75 Math.sin = -1
20+
// 1 Math.sin = 1
21+
return Math.min(volume * Math.sin((i/cycle) * Math.PI * 2), volume - 1);
22+
},
23+
triangle: function (i, cycle, volume) {
24+
var halfCycle = cycle / 2
25+
var level
26+
27+
if (i < halfCycle) {
28+
level = (volume * 2) * (i / halfCycle) - volume;
29+
} else {
30+
i = i - halfCycle
31+
level = -(volume * 2) * (i / halfCycle) + volume;
32+
}
33+
34+
return Math.min(level, volume - 1);
35+
},
36+
saw: function (i, cycle, volume) {
37+
return Math.min((volume * 2) * (i / cycle) - volume, volume - 1);
38+
},
39+
square: function (i, cycle, volume) {
40+
if(i > cycle / 2) {
41+
return volume - 1;
42+
}
43+
44+
return -volume;
45+
}
46+
}
47+
48+
function generateCycle(cycle, volume, shape) {
49+
var data = [];
50+
var tmp;
51+
var generator = typeof shape == 'function' ? shape : shapes[shape];
52+
if (!generator) {
53+
throw new Error('Invalid wave form: "' + shape + '" choose between: ' + Object.keys(shapes));
54+
}
55+
56+
for(var i = 0; i < cycle; i++) {
57+
tmp = generator(i, cycle, volume);
58+
data[i] = Math.round(tmp);
59+
}
60+
return data;
61+
}
62+
63+
function generateWaveForm(opts) {
64+
opts = opts || {}
65+
var freq = opts.freq || 440;
66+
var rate = opts.rate || 22050
67+
var lengthInSecs = opts.lengthInSecs || 2.0;
68+
var volume = opts.volume || 30;
69+
var shape = opts.shape || 'sine';
70+
71+
var cycle = Math.floor(rate/freq);
72+
var samplesLeft = lengthInSecs * rate;
73+
var cycles = samplesLeft/cycle;
74+
var ret = [];
75+
76+
for(var i = 0; i < cycles; i++) {
77+
ret = ret.concat(generateCycle(cycle, volume, shape));
78+
}
79+
return ret;
80+
};
81+
82+
module.exports.tone = function() {
83+
// to support both old interface and the new one:
84+
var opts = arguments[0]
85+
if (arguments.length > 1 && typeof opts === "number") {
86+
opts = {}
87+
opts.freq = arguments[0]
88+
opts.lengthInSecs = arguments[1]
89+
opts.volume = arguments[2]
90+
opts.rate = arguments[3]
91+
}
92+
93+
return generateWaveForm(opts)
94+
}
95+
96+
module.exports.MAX_16 = 32768;
97+
module.exports.MAX_8 = 128;
98+
99+
})();
100+
101+
(function() {
102+
103+
function pack_32b_le(num) {
104+
var a = 0, b = 0, c = 0, d = 0;
105+
a = num % 256;
106+
num >>= 8;
107+
b = num % 256;
108+
num >>= 8;
109+
c = num % 256;
110+
num >>= 8;
111+
d = num % 256;
112+
return (
113+
String.fromCharCode(a) +
114+
String.fromCharCode(b) +
115+
String.fromCharCode(c) +
116+
String.fromCharCode(d)
117+
);
118+
}
119+
120+
function pack_16b_le(num) {
121+
var a = 0, b = 0;
122+
a = num % 256;
123+
num >>= 8;
124+
b = num % 256;
125+
return (
126+
String.fromCharCode(a) +
127+
String.fromCharCode(b)
128+
);
129+
}
130+
// https://github.com/karlwestin/node-waveheadera
131+
// modified to browserify :-- Seven Lju
132+
133+
/*
134+
* WaveHeader
135+
*
136+
* writes a pcm wave header to a buffer + returns it
137+
*
138+
* taken form
139+
* from github.com/tooTallNate/node-wav
140+
* lib/writer.js
141+
*
142+
* the only reason for this module to exist is that i couldn't
143+
* understand how to use the one above, so I made my own.
144+
* You propably wanna use that one
145+
*/
146+
module.exports.wavheader = function generateHeader(length, options) {
147+
options = options || {};
148+
var RIFF = 'RIFF';
149+
var WAVE = 'WAVE';
150+
var fmt = 'fmt ';
151+
var data = 'data';
152+
153+
var MAX_WAV = 4294967295 - 100;
154+
var format = 1; // raw PCM
155+
var channels = options.channels || 1;
156+
var sampleRate = options.sampleRate || 44100;
157+
var bitDepth = options.bitDepth || 8;
158+
159+
var headerLength = 44;
160+
var dataLength = length || MAX_WAV;
161+
var fileSize = dataLength + headerLength;
162+
var header = '';
163+
164+
// write the "RIFF" identifier
165+
header += RIFF;
166+
// write the file size minus the identifier and this 32-bit int
167+
header += pack_32b_le(fileSize - 8);
168+
// write the "WAVE" identifier
169+
header += WAVE;
170+
// write the "fmt " sub-chunk identifier
171+
header += fmt;
172+
173+
// write the size of the "fmt " chunk
174+
// XXX: value of 16 is hard-coded for raw PCM format. other formats have
175+
// different size.
176+
header += pack_32b_le(16);
177+
// write the audio format code
178+
header += pack_16b_le(format);
179+
// write the number of channels
180+
header += pack_16b_le(channels);
181+
// write the sample rate
182+
header += pack_32b_le(sampleRate);
183+
// write the byte rate
184+
var byteRate = sampleRate * channels * bitDepth / 8;
185+
header += pack_32b_le(byteRate);
186+
// write the block align
187+
var blockAlign = channels * bitDepth / 8;
188+
header += pack_16b_le(blockAlign);
189+
// write the bits per sample
190+
header += pack_16b_le(bitDepth);
191+
// write the "data" sub-chunk ID
192+
header += data;
193+
// write the remaining length of the rest of the data
194+
header += pack_32b_le(dataLength);
195+
196+
// flush the header and after that pass-through "dataLength" bytes
197+
return header;
198+
};
199+
200+
})();

0 commit comments

Comments
 (0)