添加一些关于midi播放的项目
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
# For most projects, this workflow file will not need changing; you simply need
|
||||
# to commit it to your repository.
|
||||
#
|
||||
# You may wish to alter this file to override the set of languages analyzed,
|
||||
# or to provide custom queries or build logic.
|
||||
name: "CodeQL"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
pull_request:
|
||||
# The branches below must be a subset of the branches above
|
||||
branches: [master]
|
||||
schedule:
|
||||
- cron: '0 6 * * 6'
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
# Override automatic language detection by changing the below list
|
||||
# Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python']
|
||||
language: ['javascript']
|
||||
# Learn more...
|
||||
# https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
# We must fetch at least the immediate parents so that if this is
|
||||
# a pull request then we can checkout the head.
|
||||
fetch-depth: 2
|
||||
|
||||
# If this run was triggered by a pull request event, then checkout
|
||||
# the head of the pull request instead of the merge commit.
|
||||
- run: git checkout HEAD^2
|
||||
if: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v1
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
# By default, queries listed here will override any specified in a config file.
|
||||
# Prefix the list here with "+" to use these queries and those in the config file.
|
||||
# queries: ./path/to/local/query, your-org/your-repo/queries@main
|
||||
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v1
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 https://git.io/JvXDl
|
||||
|
||||
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
|
||||
# and modify them (or add more) to build your code if your project
|
||||
# uses a compiled language
|
||||
|
||||
#- run: |
|
||||
# make bootstrap
|
||||
# make release
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v1
|
||||
@@ -0,0 +1,22 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# dotenv environment variables file
|
||||
.env
|
||||
.env.test
|
||||
|
||||
.vscode
|
||||
|
||||
# meta info for mid file
|
||||
midi-storage/*.json
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020 Simon Churyakov
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,37 @@
|
||||
# arduino-music-box
|
||||
|
||||
It's Node.js + Arduino project to play any midi file.
|
||||
>使用node.js把指定的midi文件转为频率和时间,然后串口发送给arduino播放。
|
||||
|
||||
## Prerequisites
|
||||
|
||||
1. You need to provide the following environment variables to make it works (the easiest way is to create `.env` file in the project root folder)
|
||||
|
||||
```
|
||||
SERIAL_PORT=<NAME_OF_SERIAL_PORT>
|
||||
BOUD_RATE=<BOUD_RATE>
|
||||
MIDI_STORAGE_FOLDER=<MIDI_STORAGE_FOLDER>
|
||||
```
|
||||
|
||||
2. Electronics:
|
||||
- Arduino Uno
|
||||
- Piezo buzzer
|
||||
- Wires (optional)
|
||||
3. Circuit scheme
|
||||
1. v1 - https://github.com/simonchuryakov/arduino-music-box/blob/master/docs/circuit_v1.jpg
|
||||
2. v2 - https://github.com/simonchuryakov/arduino-music-box/blob/master/docs/circuit_v2.jpg
|
||||
|
||||
## How to run
|
||||
|
||||
To run the project you have to:
|
||||
|
||||
1. Upload sketch file from `src/arduino/play.ino` to your Arduino Uno using Arduino IDE or whatever you use for arduino development
|
||||
2. Keep Arduino Uno connected to your computer
|
||||
3. Run in the terminal:
|
||||
|
||||
```
|
||||
npm install
|
||||
node src/node/index.js
|
||||
```
|
||||
|
||||
By default it will start playing files from `midi-storage` (you can change it in the `index.js`)
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 40 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 54 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+1392
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"name": "arduino-music-box",
|
||||
"version": "0.0.1",
|
||||
"type": "module",
|
||||
"description": "",
|
||||
"main": "src/node/index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/simonchuryakov/arduino-music-box.git"
|
||||
},
|
||||
"author": "Simon Churyakov",
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/simonchuryakov/arduino-music-box/issues"
|
||||
},
|
||||
"homepage": "https://github.com/simonchuryakov/arduino-music-box#readme",
|
||||
"dependencies": {
|
||||
"@tonejs/midi": "2.0.23",
|
||||
"lodash": "4.17.21",
|
||||
"moment": "2.27.0",
|
||||
"serialport": "9.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"dotenv": "8.2.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
#define BUZZER_PIN 3
|
||||
#define GROUND_PIN -1
|
||||
#define SEPARATOR ','
|
||||
#define END_OF_NOTE ';'
|
||||
|
||||
void setup()
|
||||
{
|
||||
Serial.begin(115200);
|
||||
pinMode(BUZZER_PIN, OUTPUT);
|
||||
// Required for circle_v2 only
|
||||
pinMode(GROUND_PIN, OUTPUT);
|
||||
}
|
||||
|
||||
void loop()
|
||||
{
|
||||
// Required for circle_v2 only
|
||||
digitalWrite(GROUND_PIN, LOW);
|
||||
|
||||
if (Serial.available() > 0)
|
||||
{
|
||||
playNote();
|
||||
}
|
||||
}
|
||||
|
||||
void playNote()
|
||||
{
|
||||
String message = Serial.readStringUntil(END_OF_NOTE);
|
||||
|
||||
int delimiterIdx = message.indexOf(SEPARATOR);
|
||||
String frequencyStr = message.substring(0, delimiterIdx);
|
||||
String durationStr = message.substring(delimiterIdx + 1, message.length());
|
||||
|
||||
long frequency = frequencyStr.toInt();
|
||||
float duration = durationStr.toFloat();
|
||||
|
||||
// Handle pause
|
||||
if (frequency == -1)
|
||||
{
|
||||
delay(duration);
|
||||
return;
|
||||
}
|
||||
|
||||
tone(BUZZER_PIN, frequency, duration);
|
||||
delay(duration);
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
#define BUZZER_PIN 3
|
||||
#define GROUND_PIN -1
|
||||
#define SEPARATOR ','
|
||||
#define END_OF_NOTE ';'
|
||||
const int nokia[2][15] = {
|
||||
{-1,659,587,370,415,554,494,294,330,494,440,277,330,440,-1},
|
||||
{768,192,192,384,384,192,192,384,384,192,192,384,384,384,768}
|
||||
};
|
||||
int index = 0;
|
||||
void setup()
|
||||
{
|
||||
Serial.begin(115200);
|
||||
pinMode(BUZZER_PIN, OUTPUT);
|
||||
// Required for circle_v2 only
|
||||
pinMode(GROUND_PIN, OUTPUT);
|
||||
|
||||
}
|
||||
|
||||
void loop()
|
||||
{
|
||||
// Required for circle_v2 only
|
||||
digitalWrite(GROUND_PIN, LOW);
|
||||
|
||||
if (index<15)
|
||||
{
|
||||
playNote(index);
|
||||
index++;
|
||||
} else {
|
||||
delay(3000);
|
||||
index=0;
|
||||
}
|
||||
}
|
||||
|
||||
void playNote(int index)
|
||||
{
|
||||
//String message = Serial.readStringUntil(END_OF_NOTE);
|
||||
|
||||
//int delimiterIdx = message.indexOf(SEPARATOR);
|
||||
//String frequencyStr = message.substring(0, delimiterIdx);
|
||||
//String durationStr = message.substring(delimiterIdx + 1, message.length());
|
||||
|
||||
long frequency = nokia[0][index];
|
||||
float duration = nokia[1][index];
|
||||
|
||||
// Handle pause
|
||||
if (frequency == -1)
|
||||
{
|
||||
delay(duration);
|
||||
return;
|
||||
}
|
||||
|
||||
tone(BUZZER_PIN, frequency, duration);
|
||||
delay(duration);
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import path from "path";
|
||||
import dotenv from "dotenv";
|
||||
|
||||
import { parse } from "./midi/parser.js";
|
||||
import { SerialProxy } from "./serial/proxy.js";
|
||||
|
||||
dotenv.config();
|
||||
console.log('SERIAL_PORT:', process.env.SERIAL_PORT);
|
||||
console.log('BOUD_RATE:', process.env.BOUD_RATE);
|
||||
console.log('MIDI_STORAGE_FOLDER:', process.env.MIDI_STORAGE_FOLDER);
|
||||
|
||||
const serialPort = process.env.SERIAL_PORT;
|
||||
const serialOptions = {
|
||||
baudRate: parseInt(process.env.BOUD_RATE, 10),
|
||||
};
|
||||
|
||||
const midiStoragePath = path.join(
|
||||
process.cwd(),
|
||||
process.env.MIDI_STORAGE_FOLDER
|
||||
);
|
||||
|
||||
const proxy = new SerialProxy();
|
||||
const playList = ["45188.mid"];
|
||||
|
||||
const playMelody = (melody) => {
|
||||
let shouldWait = 0;
|
||||
|
||||
melody.forEach((data) => {
|
||||
setTimeout(() => {
|
||||
proxy.write(data.data);
|
||||
}, shouldWait);
|
||||
shouldWait += data.wait;
|
||||
});
|
||||
};
|
||||
|
||||
const start = (tracks) => {
|
||||
let shouldWait = 0;
|
||||
|
||||
tracks.forEach((fileName) => {
|
||||
const track = parse(path.join(midiStoragePath, fileName));
|
||||
|
||||
setTimeout(() => playMelody(track.melody), shouldWait);
|
||||
shouldWait += track.duration;
|
||||
});
|
||||
};
|
||||
|
||||
proxy.init(serialPort, serialOptions, (error) => {
|
||||
if (error) {
|
||||
console.log("Error: ", error.message);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Need to wait a bit, otherwise a few first notes are gone
|
||||
setTimeout(() => start(playList), 100);
|
||||
});
|
||||
@@ -0,0 +1,53 @@
|
||||
import moment from "moment";
|
||||
|
||||
// lines - octave idx, columns - frequency for the particular note
|
||||
const OCTAVES = [
|
||||
[16, 17, 18, 19, 21, 22, 23, 25, 26, 28, 29, 31],
|
||||
[33, 35, 37, 39, 41, 44, 46, 49, 52, 55, 58, 62],
|
||||
[65, 69, 73, 78, 82, 87, 93, 98, 104, 110, 117, 124],
|
||||
[131, 139, 147, 156, 165, 175, 185, 196, 208, 220, 233, 247],
|
||||
[262, 277, 294, 311, 330, 349, 370, 392, 415, 440, 466, 494],
|
||||
[523, 554, 587, 622, 659, 699, 740, 784, 831, 880, 932, 988],
|
||||
[1047, 1109, 1175, 1245, 1319, 1397, 1480, 1568, 1661, 1760, 1865, 1976],
|
||||
[2093, 2217, 2349, 2489, 2637, 2794, 2960, 3136, 3322, 3520, 3729, 3951],
|
||||
[4186, 4435, 4699, 4978, 5274, 5588, 5920, 6272, 6645, 7040, 7459, 7902],
|
||||
];
|
||||
|
||||
const noteToIdxDictionary = new Map([
|
||||
["C", 0],
|
||||
["C#", 1],
|
||||
["D", 2],
|
||||
["D#", 3],
|
||||
["E", 4],
|
||||
["F", 5],
|
||||
["F#", 6],
|
||||
["G", 7],
|
||||
["G#", 8],
|
||||
["A", 9],
|
||||
["A#", 10],
|
||||
["B", 11],
|
||||
]);
|
||||
|
||||
const DEFAULT_BPM = 120; // beats per minute
|
||||
|
||||
// Note name format is C#4 or D3
|
||||
export const getFrequencyByNote = (noteName) => {
|
||||
const name = noteName.replace(/[0-9]/, "");
|
||||
let octaveIdx = parseInt(noteName.slice(-1), 10);
|
||||
const noteIdx = noteToIdxDictionary.get(name);
|
||||
|
||||
return OCTAVES[octaveIdx][noteIdx];
|
||||
};
|
||||
|
||||
export const getPPQ = (header) => header.ppq;
|
||||
|
||||
export const getBPM = (header) => {
|
||||
try {
|
||||
return header.tempos[0].bpm;
|
||||
} catch {
|
||||
return DEFAULT_BPM;
|
||||
}
|
||||
};
|
||||
|
||||
export const getTicksToMsFn = (bpm, ppq) => (ticks) =>
|
||||
ticks * Math.round(moment.duration(1, "minute") / (bpm * ppq));
|
||||
@@ -0,0 +1,97 @@
|
||||
import fs from "fs";
|
||||
import midi from "@tonejs/midi";
|
||||
import _groupBy from "lodash/groupBy.js";
|
||||
import _chunk from "lodash/chunk.js";
|
||||
|
||||
import {
|
||||
getFrequencyByNote,
|
||||
getTicksToMsFn,
|
||||
getPPQ,
|
||||
getBPM,
|
||||
} from "./helper.js";
|
||||
|
||||
const SEPARATOR = ",";
|
||||
const NOTE_SEPARATOR = ";";
|
||||
const MIN_NOTE_NAME_LENGTH = 2;
|
||||
const MIDI_META_FILE_EXTENSION = ".json";
|
||||
|
||||
const toNotes = (track, toMs) => {
|
||||
const mainThemeNotes = Object.values(_groupBy(track.notes, "ticks")).map(
|
||||
(notes) => notes[0]
|
||||
);
|
||||
|
||||
let previousTicks = 0;
|
||||
let previousDurationTicks = 0;
|
||||
|
||||
return mainThemeNotes.reduce(
|
||||
(melody, { pitch, octave, name, ticks, durationTicks }, idx) => {
|
||||
let compoundName = "";
|
||||
|
||||
if (pitch !== undefined && octave !== undefined) {
|
||||
compoundName = `${pitch}${octave}`;
|
||||
}
|
||||
|
||||
if (name !== undefined && name.length >= MIN_NOTE_NAME_LENGTH) {
|
||||
compoundName = name;
|
||||
}
|
||||
|
||||
if (compoundName.length === 0) {
|
||||
throw new Error(`Not enough info to play note ${idx}`);
|
||||
}
|
||||
|
||||
// Push the pause in the melody if needed
|
||||
if (previousTicks + previousDurationTicks < ticks) {
|
||||
melody.push([-1, toMs(ticks - previousTicks - previousDurationTicks)]);
|
||||
}
|
||||
|
||||
const frequency = getFrequencyByNote(compoundName);
|
||||
const durationMs = toMs(durationTicks);
|
||||
|
||||
melody.push([frequency, durationMs]);
|
||||
previousTicks = ticks;
|
||||
previousDurationTicks = durationTicks;
|
||||
|
||||
return melody;
|
||||
},
|
||||
[]
|
||||
);
|
||||
};
|
||||
|
||||
export const parse = (
|
||||
midiFilePath,
|
||||
mainTrackIdx = 0,
|
||||
shouldGenerateMetaJson = false
|
||||
) => {
|
||||
const binaryData = fs.readFileSync(midiFilePath);
|
||||
const data = new midi.Midi(binaryData);
|
||||
|
||||
const ppq = getPPQ(data.header);
|
||||
const bpm = getBPM(data.header);
|
||||
const toMs = getTicksToMsFn(ppq, bpm);
|
||||
|
||||
const notes = toNotes(data.tracks[mainTrackIdx], toMs);
|
||||
|
||||
if (shouldGenerateMetaJson) {
|
||||
fs.writeFileSync(
|
||||
`${midiFilePath}${MIDI_META_FILE_EXTENSION}`,
|
||||
JSON.stringify(data)
|
||||
);
|
||||
}
|
||||
|
||||
let melodyDuration = 0;
|
||||
const melody = notes.reduce((melody, [frequency, duration]) => {
|
||||
melody.push({
|
||||
data: `${frequency}${SEPARATOR}${duration}${NOTE_SEPARATOR}`,
|
||||
wait: duration,
|
||||
});
|
||||
|
||||
melodyDuration += duration;
|
||||
|
||||
return melody;
|
||||
}, []);
|
||||
|
||||
return {
|
||||
melody,
|
||||
duration: melodyDuration,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
import SerialPort from "serialport";
|
||||
import moment from "moment";
|
||||
|
||||
export class SerialProxy {
|
||||
init(port, options, openCallback) {
|
||||
this.port = new SerialPort(port, options, openCallback);
|
||||
}
|
||||
|
||||
isOpen() {
|
||||
if (!this.port) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.port.isOpen;
|
||||
}
|
||||
|
||||
write(data) {
|
||||
if (this.port) {
|
||||
console.log(`${moment().format("DD/MM/YY HH:mm:ss.ms")}: ${data}`);
|
||||
|
||||
this.port.write(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user