添加一些关于midi播放的项目

This commit is contained in:
terryLP
2025-03-24 14:30:56 +08:00
parent e31eb22077
commit 498b4ef13b
699 changed files with 186162 additions and 1 deletions
@@ -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
+22
View File
@@ -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
+21
View File
@@ -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.
+37
View File
@@ -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.
File diff suppressed because it is too large Load Diff
+29
View File
@@ -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);
}
}
}