Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support Bluetooth LE MIDI #4

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 67 additions & 18 deletions globals.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,10 @@ WebMidi.enable(function (err) { //check if WebMidi.js is enabled
midiSelectSlider = select("#slider");
midiSelectSlider.attribute("max", WebMidi.inputs.length - 1);
midiSelectSlider.input(inputChanged);
midiIn = WebMidi.inputs[midiSelectSlider.value()]
inputChanged();
if (midiSelectSlider.value()) {
midiIn = WebMidi.inputs[midiSelectSlider.value()]
inputChanged();
}
});

function inputChanged() {
Expand All @@ -78,10 +80,10 @@ function inputChanged() {
console.log("Received 'noteoff' message (" + e.note.number + ", " + e.velocity + ").");
noteOff(e.note.number, e.velocity);
})
midiIn.addListener('controlchange', 'all', function(e) {
midiIn.addListener('controlchange', 'all', function (e) {
console.log("Received control change message:", e.controller.number, e.value);
controllerChange(e.controller.number, e.value)
});
});
console.log(midiIn.name);
select("#device").html(midiIn.name);
};
Expand All @@ -90,11 +92,11 @@ function noteOn(pitch, velocity) {
totalNotesPlayed++;
notesThisFrame++;
totalIntensityScore += velocity;

// piano visualizer
isKeyOn[pitch] = 1;
if (nowPedaling) {
isPedaled[pitch] = 1;
isPedaled[pitch] = 1;
}
}

Expand All @@ -105,20 +107,20 @@ function noteOff(pitch, velocity) {
function controllerChange(number, value) {
// Receive a controllerChange
if (number == 64) {
cc64now = value;
cc64now = value;

if (value >= 64) {
nowPedaling = true;
for (let i = 0; i < 128; i++) {
// copy key on to pedal
isPedaled[i] = isKeyOn[i];
}
nowPedaling = true;
for (let i = 0; i < 128; i++) {
// copy key on to pedal
isPedaled[i] = isKeyOn[i];
}
} else if (value < 64) {
nowPedaling = false;
for (let i = 0; i < 128; i++) {
// reset isPedaled
isPedaled[i] = 0;
}
nowPedaling = false;
for (let i = 0; i < 128; i++) {
// reset isPedaled
isPedaled[i] = 0;
}
}
}

Expand All @@ -141,4 +143,51 @@ function changeColor() {
darkenedColor = keyOnColor.levels.map(x => floor(x * .7));
pedaledColor = color(`rgb(${darkenedColor[0]}, ${darkenedColor[1]}, ${darkenedColor[2]})`)
console.log(pedaledColor.levels);
}
}

async function requestBleMidi() {
const MIDI_SERVICE = '03b80e5a-ede8-4b33-a751-6ce34ec4c700';
const MIDI_CHARACTERISTIC = '7772e5db-3868-4112-a1a9-f2669d106bf3'
const bluetoothDevice = await navigator.bluetooth.requestDevice({
filters: [{
services: [MIDI_SERVICE]
}]
});
console.log(`Name: ${bluetoothDevice.name}`);
document.querySelector('#device').textContent = bluetoothDevice.name;
bluetoothDevice.addEventListener('gattserverdisconnected', onDisconnected);
const server = await bluetoothDevice.gatt.connect();
const service = await server.getPrimaryService(MIDI_SERVICE);
const characteristic = await service.getCharacteristic(MIDI_CHARACTERISTIC);
await characteristic.startNotifications();
characteristic.addEventListener('characteristicvaluechanged', handleMidiMessageReceived);
}

function onDisconnected(event) {
const device = event.target;
console.log('Device ' + device.name + ' is disconnected.');
}

function handleMidiMessageReceived(event) {
const { buffer } = event.target.value;
const eventData = new Uint8Array(buffer);
for (let i = 0; i < (eventData.length - 1) / 4; i++) {
const index = 4 * i;
const status = eventData[index + 2];
const value1 = eventData[index + 3];
const value2 = eventData[index + 4];
switch (status) {
case 144:
noteOn(value1, value2);
break;
case 128:
noteOff(value1, value2);
break;
case 176:
controllerChange(value1, value2);
break;
default:
break;
}
}
}
32 changes: 17 additions & 15 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,15 @@

<body>
<div id="main">
<div id="controls" class="center">
<div id="controls" class="center">
<div>
<h3>鋼琴鍵盤顯示器 by NiceChord</h3>
<div style="display: flex; justify-content: space-around;">
<div>
<h5>選擇 MIDI 裝置</h5>
<i class="bluetooth-icon" onclick="requestBleMidi()"></i>
<input id="slider" type="range" min="0" max="0" value="0">
<div id="device">Select Input: </div>
<div id="device">Select Input:</div>
</div>
<div style="display: flex; flex-direction: column; justify-content: center; align-items: start;">
<div>
Expand All @@ -39,27 +40,28 @@ <h5>選擇 MIDI 裝置</h5>
</div>
</div>
<br />

</div>

</div>
</div>

<div class="center">
<div id="piano-visualizer">
<!-- Our sketch will go here! -->
</div>
<span style="font-size: 11px;">
<div class="center">

<div id="piano-visualizer">
<!-- Our sketch will go here! -->
</div>
<span style="font-size: 11px;">
TIME:使用時間 | NOTE COUNT:總彈奏音符數 | NPS:最近一秒鐘彈奏音符數(括號為歷史最大值) | LEGATO:圓滑指數(最近一秒鐘平均來說有幾個鍵被同時按住) <br />
CALORIES:消耗熱量(估計值,好玩就好)| PEDALS:左右踏板深度顯示 <br />
(密技:點鍵盤最左上角的角落,可以儲存截圖) <br /><br />
</span>


覺得好用嗎?到 <a href="https://nicechord.com">NiceChord.com</a> 逛逛支持我!原始碼在 <a href="https://github.com/wiwikuan/pianometer">GitHub</a>。
</div>


</div>
覺得好用嗎?到 <a href="https://nicechord.com">NiceChord.com</a> 逛逛支持我!原始碼在 <a
href="https://github.com/wiwikuan/pianometer">GitHub</a>。
</div>


</div>
</body>

</html>
7 changes: 7 additions & 0 deletions style.css
Original file line number Diff line number Diff line change
Expand Up @@ -586,4 +586,11 @@ label[for=rainbow-mode-checkbox]:active:after {
input:checked + label[for=rainbow-mode-checkbox]:after {
left: calc(100% - 1.25px);
transform: translateX(-100%);
}

.bluetooth-icon {
display: inline-block;
background-image: url("data:image/svg+xml,%3Csvg fill='%230082FC' version='1.1' id='Capa_1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' viewBox='0 0 217.499 217.499' xml:space='preserve'%3E%3Cg id='SVGRepo_bgCarrier' stroke-width='0'%3E%3C/g%3E%3Cg id='SVGRepo_tracerCarrier' stroke-linecap='round' stroke-linejoin='round'%3E%3C/g%3E%3Cg id='SVGRepo_iconCarrier'%3E%3Cg%3E%3Cpath d='M123.264 108.749l45.597-44.488c1.736-1.693 2.715-4.016 2.715-6.441s-0.979-4.748-2.715-6.441l-50.038-48.82 c-2.591-2.528-6.444-3.255-9.78-1.853c-3.336 1.406-5.505 4.674-5.505 8.294v80.504l-42.331-41.3 c-3.558-3.471-9.255-3.402-12.727 0.156c-3.471 3.558-3.401 9.256 0.157 12.727l48.851 47.663l-48.851 47.663 c-3.558 3.471-3.628 9.169-0.157 12.727s9.17 3.628 12.727 0.156l42.331-41.3v80.504c0 3.62 2.169 6.888 5.505 8.294 c1.128 0.476 2.315 0.706 3.493 0.706c2.305 0 4.572-0.886 6.287-2.559l50.038-48.82c1.736-1.693 2.715-4.016 2.715-6.441 s-0.979-4.748-2.715-6.441L123.264 108.749z M121.539 30.354l28.15 27.465l-28.15 27.465V30.354z M121.539 187.143v-54.93 l28.15 27.465L121.539 187.143z'%3E%3C/path%3E%3C/g%3E%3C/g%3E%3C/svg%3E");
width: 15px;
height: 15px;
}