-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathtallinn-in-december.js
169 lines (143 loc) · 4.69 KB
/
tallinn-in-december.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
const easymidi = require("easymidi");
const axios = require("axios");
const { Note, Scale } = require("@tonaljs/tonal");
const OPENWEATHERMAP_API_KEY = process.env.API_KEY;
const WEATHER_URL = "https://api.openweathermap.org/data/2.5/onecall";
const LATITUDE = parseFloat(process.env.LATITUDE);
const LONGITUDE = parseFloat(process.env.LONGITUDE);
const MIDI_CLOCK_PER_QUARTER_NOTE = 24; // From MIDI specification:
const MASTER_TEMPO = 40; // BPM = number of quarter notes per minute
const virtualInput = new easymidi.Input("Node.js input", true);
const virtualOutput = new easymidi.Output("Node.js output", true);
/**
* Get the weather for the given location.
*
* @param {number} lat - The latitude of the location.
* @param {number} lon - The longitude of the location.
* @param {Object} config - The configuration for the request.
* @returns {Object} The weather data.
*/
async function getCurrentWeather(lat, lon, config = {}) {
const { apiKey, apiUrl } = config;
const reqUrl = `${apiUrl}?lat=${lat}&lon=${lon}&appid=${apiKey}&units=metric`;
try {
const response = await axios.get(reqUrl);
return response.data;
} catch (error) {
console.error(error);
}
}
/**
* Convert the temperature from weather data into musical notes.
*
* @param {Object} weather - The weather data.
* @param {Object} tone - The Tone.js object.
* @returns {Object} The musical notes.
*/
function mappingTemperatureDataIntoNotes(weather) {
const { current, hourly, daily } = weather;
const temperatureToNotesMultiplier = Math.floor(Math.random() * 99) + 1;
const musicalRange = Scale.rangeOf("C pentatonic");
const musicalScale = musicalRange("C3", "C6");
const frequencies = musicalScale.map((note) => {
return {
note,
frequency: Note.freq(note),
};
});
const findCloserFrequency = (givenValue, frequencyList) => {
const closestFrequency = frequencyList.reduce((prev, curr) => {
const prevDiff = Math.abs(prev.frequency - givenValue);
const currDiff = Math.abs(curr.frequency - givenValue);
return prevDiff < currDiff ? prev : curr;
});
return closestFrequency.note;
};
const currentTemperature =
Math.abs(current.temp) * temperatureToNotesMultiplier;
const hourlyTemperature = hourly.map((hour) => {
return Math.abs(hour.temp) * temperatureToNotesMultiplier;
});
const dailyTemperature = daily.map((day) => {
return Math.abs(day.temp.day) * temperatureToNotesMultiplier;
});
return {
lead: findCloserFrequency(currentTemperature, frequencies),
melody1: hourlyTemperature.map((temperature) => {
return findCloserFrequency(temperature, frequencies);
}),
melody2: dailyTemperature.map((temperature) => {
return findCloserFrequency(temperature, frequencies);
}),
};
}
/**
* Randomize an integer between min and max.
* @param {Int} min
* @param {Int} max
* @returns
*/
function getRandomInt(min, max) {
const minimum = Math.ceil(min);
const maximum = max = Math.floor(max);
return Math.floor(Math.random() * (maximum - minimum + 1)) + minimum;
}
async function main() {
let weather = await getCurrentWeather(LATITUDE, LONGITUDE, {
apiKey: OPENWEATHERMAP_API_KEY,
apiUrl: WEATHER_URL,
});
let notesData = mappingTemperatureDataIntoNotes(weather);
let melodyLength1 = notesData.melody1.length;
let index1 = 0;
const basePad = notesData.lead;
let totalClockPerMinute = MIDI_CLOCK_PER_QUARTER_NOTE * MASTER_TEMPO;
let clockCounting = 1;
virtualInput.on("start", () => {
// The pad sound as a base layer
virtualOutput.send("noteon", {
note: Note.midi(basePad),
velocity: 127,
channel: 2,
});
});
virtualInput.on("stop", () => {
// The pad sound as a base layer
virtualOutput.send("noteoff", {
note: Note.midi(basePad),
velocity: 0,
channel: 2,
});
});
virtualInput.on("clock", () => {
if (clockCounting === totalClockPerMinute) {
// Reset the counter
clockCounting = 1;
}
if (index1 === melodyLength1) {
// The melody is over, do something
notesData = mappingTemperatureDataIntoNotes(weather);
melodyLength1 = notesData.melody1.length;
index1 = 0;
}
if (clockCounting % (MIDI_CLOCK_PER_QUARTER_NOTE * 2) === 0) {
// Note is off
virtualOutput.send("noteoff", {
note: Note.midi(notesData.melody1[index1]),
velocity: 0,
channel: 1,
});
}
if (clockCounting % (MIDI_CLOCK_PER_QUARTER_NOTE * 2) === 1) {
// Note is on
virtualOutput.send("noteon", {
note: Note.midi(notesData.melody1[index1]),
velocity: getRandomInt(100, 127),
channel: 1,
});
index1++;
}
clockCounting++;
});
}
main();