· 5 years ago · Jan 01, 2021, 12:36 AM
1local args = {...};
2local menu = {};
3local box = nil;
4local song = nil;
5local action = 'mainList';
6local isPlaying = false;
7local stopFlag = false;
8local settings = { rptMode = "allRandom"; }; -- "repeat" is reserved in lua
9local maxtoshow = 15;
10
11if turtle ~= nil then
12 maxtoshow = 9;
13end
14
15function mountIronNoteblock()
16 for _,side in ipairs({"left"; "right"; "back"; "bottom"; "top"; "front"}) do
17 if peripheral.getType(side) == "iron_noteblock" then
18 box = peripheral.wrap(side);
19 return side;
20 end
21 end
22 return nil;
23end
24
25function newSong(x, selectedIndex)
26 return {
27 fh = x;
28 index = selectedIndex;
29 length = 0;
30 height = 0;
31 name = "";
32 author = "";
33 originalAuthor = "";
34 description = "";
35 tempo = 10.00; --tks per second
36 autoSaving = 0;
37 autoSaveDur = 0;
38 timeSig = 4;
39 minSpent = 0;
40 leftClicks = 0;
41 rightClicks = 0;
42 blocksAdded = 0;
43 blocksRemoved = 0;
44 midi = "";
45 music = { wait={}; inst={}; note={}; };
46 };
47end
48
49function loadMenu(fromDir)
50 if fs.isDir(fromDir) then
51 for _, file in ipairs(fs.list(fromDir)) do
52 if fs.isDir(file) == false and string.find(file, ".nbs", -4, true) ~= nil then -- if file and ends in ".nbs"
53 menu[#menu+1] = { d=fromDir; fn=file };
54 end
55 end
56 end
57 return #menu;
58end
59
60function readInt(fh) --little endian, fh is open in rb mode
61 local ret = 0;
62 local x = fh.read();
63 if x == nil then return nil; end
64 ret = x;
65 x = fh.read();
66 if x == nil then return nil; end
67 ret = (x * 0x100) + ret;
68 x = fh.read();
69 if x == nil then return nil; end
70 ret = (x * 0x10000) + ret;
71 x = fh.read();
72 if x == nil then return nil; end
73 ret = (x * 0x1000000) + ret;
74 return ret;
75end
76
77function readShort(fh) --little endian, fh is open in rb mode
78 local ret = 0;
79 local x = fh.read();
80 if x == nil then return nil; end
81 ret = x;
82 x = fh.read();
83 if x == nil then return nil; end
84 ret = (x * 0x100) + ret;
85 return ret;
86end
87
88function readString(fh, len) --fh is open in rb mode
89 local ret = "";
90 local x = 0;
91 for i = 1, len do
92 x = fh.read();
93 if x == nil then return nil; end
94 ret = ret .. string.char(x);
95 end
96 return ret;
97end
98
99function readHeader()
100 song.length = readShort(song.fh);
101 song.height = readShort(song.fh);
102 song.name = readString(song.fh, readInt(song.fh));
103 song.author = readString(song.fh, readInt(song.fh));
104 song.originalAuthor = readString(song.fh, readInt(song.fh));
105 song.description = readString(song.fh, readInt(song.fh));
106 song.tempo = 1.000 / ( readShort(song.fh) / 100.00 );
107 song.autoSaving = song.fh.read();
108 song.autoSaveDur = song.fh.read();
109 song.timeSig = song.fh.read();
110 song.minSpent = readInt(song.fh);
111 song.leftClicks = readInt(song.fh);
112 song.rightClicks = readInt(song.fh);
113 song.blocksAdded = readInt(song.fh);
114 song.blocksRemoved = readInt(song.fh);
115 song.midi = readString(song.fh, readInt(song.fh));
116end
117
118function readNotes()
119 local curtk = 1;
120 local tk = -1;
121 local layer = -1;
122 local inst = 0;
123 local note = 33; -- MC is 33 to 57
124
125 while true do
126 tk = readShort(song.fh);
127 if tk == nil then return false; end
128 if tk == 0 then break; end
129 while true do
130 song.music.wait[curtk] = (tk * song.tempo) * 0.965; -- * 0.965 to speed it up a bit because lua slow
131 layer = readShort(song.fh); --can't do anything with this info (yet?)
132 if layer == nil then return false; end
133 if layer == 0 then break; end
134 song.music.inst[curtk]=song.fh.read();
135 if song.music.inst[curtk] == 0 then
136 song.music.inst[curtk] = 0;
137 elseif song.music.inst[curtk] == 2 then
138 song.music.inst[curtk] = 1;
139 elseif song.music.inst[curtk] == 3 then
140 song.music.inst[curtk] = 2;
141 elseif song.music.inst[curtk] == 4 then
142 song.music.inst[curtk] = 3;
143 elseif song.music.inst[curtk] == 1 then
144 song.music.inst[curtk] = 4;
145 end
146 song.music.note[curtk]=song.fh.read()-33;
147 tk = 0;
148 curtk = curtk + 1;
149 end
150 end
151 return true;
152end
153
154function showInfo()
155 term.clear();
156 print("Now Playing: \n\n\n\n\n\n " .. song.name);
157 print("\n\n\n\n\n\nAuthor: " .. song.author);
158 print("Original Author: " .. song.originalAuthor);
159 print("Description: ");
160 print(song.description);
161 parallel.waitForAny(function()
162 _, key = os.pullEvent("key");
163 end, function()
164 while true do
165 if not isPlaying then break; end
166 os.sleep(0.125);
167 end
168 end);
169 if settings.rptMode == "none" or isPlaying then --song finished in single play mode or key pressed exit
170 action = 'mainList';
171 end
172end
173
174function getRepeateMode() -- returns the name of currently selected repeat mode
175 local rptText = "";
176
177 if settings.rptMode == "allRandom" then
178 rptText = "All (Random)";
179 elseif settings.rptMode == "allOrdered" then
180 rptText = "All (In Order)";
181 elseif settings.rptMode == "one" then
182 rptText = "One (Loop Song)";
183 elseif settings.rptMode == "none" then
184 rptText = "None";
185 end
186
187 return rptText;
188end
189
190function changeRepeatMode() -- cycles to next repeat mode and returns its name
191 if settings.rptMode == "allRandom" then
192 settings.rptMode = "allOrdered";
193 elseif settings.rptMode == "allOrdered" then
194 settings.rptMode = "one";
195 elseif settings.rptMode == "one" then
196 settings.rptMode = "none";
197 elseif settings.rptMode == "none" then
198 settings.rptMode = "allRandom";
199 end
200
201 return getRepeateMode();
202end
203
204function options()
205 local selectedIndex = 1;
206 while true do
207 term.clear();
208 local opts = {};
209 opts[1] = "Show Now Playing";
210 opts[2] = "Repeat: " .. getRepeateMode();
211 opts[3] = "Next Song";
212 opts[4] = "Stop";
213 opts[5] = "Back To Song List";
214 -- "Load Playlist" --> default song lists & any saved lists
215 -- "Add " .. selected main menu song .. " to a playlist"
216 -- "Queue " .. selected main menu song -- takes priority over repeat mode
217 -- "Load songs from ..." -- prompts for folder path, erases current queue
218
219 for i = 1, #opts do
220 if i == selectedIndex then
221 print("> " .. opts[i]);
222 else
223 print(" " .. opts[i]);
224 end
225 end
226 print("----------------------\nUse Arrow keys and Enter to navigate.\n");
227 _, key = os.pullEvent("key");
228
229 if key == 208 or key == 31 then -- down or s
230 selectedIndex = selectedIndex + 1;
231 if selectedIndex > #opts then selectedIndex = 1; end
232 elseif key == 200 or key == 17 then -- up or w
233 selectedIndex = selectedIndex-1;
234 if selectedIndex < 1 then selectedIndex = 1; end
235 elseif key == 28 or key == 57 then -- enter or space
236 if selectedIndex == 1 and isPlaying then
237 action = 'nowPlaying';
238 break;
239 elseif selectedIndex == 2 then
240 changeRepeatMode();
241 elseif selectedIndex == 3 then
242 skipSong();
243 break;
244 elseif selectedIndex == 4 then
245 stopSong();
246 action = 'mainList';
247 break;
248 else
249 action = 'mainList';
250 break;
251 end
252 end
253 end
254end
255
256function mainList(startat, selectedIndex)
257 term.clear();
258 for i = startat, #menu do
259 if startat + maxtoshow <= i then break end
260 if i == selectedIndex then
261 print("> " .. menu[i].fn);
262 else
263 print(" " .. menu[i].fn);
264 end
265 end
266 print("----------------------\nUse Arrow keys and Enter to navigate.");
267 print("Press m to access options.")
268 _, key = os.pullEvent("key");
269
270 if key == 208 or key == 31 then -- down or s
271 selectedIndex = selectedIndex + 1;
272 if selectedIndex >= startat + maxtoshow then startat = startat + maxtoshow; end
273 if selectedIndex > #menu then selectedIndex = 1; startat = 1; end
274 elseif key == 200 or key == 17 then -- up or w
275 selectedIndex = selectedIndex-1;
276 if selectedIndex < 1 then selectedIndex = 1; end
277 if selectedIndex < startat then startat = startat - maxtoshow; end
278 if startat < 1 then startat = 1; end
279 elseif key == 205 or key == 32 then -- right or d
280 selectedIndex = startat + maxtoshow;
281 if selectedIndex > #menu then selectedIndex = 1; end
282 startat = selectedIndex;
283 elseif key == 203 or key == 30 then -- left or a
284 selectedIndex = startat - maxtoshow;
285 startat = startat - maxtoshow;
286 if selectedIndex < 1 then selectedIndex = 1; end
287 if startat < 1 then startat = 1; end
288 elseif key == 28 or key == 57 then -- enter or space
289 action = 'playSong';
290 elseif key == 50 or key == 19 then -- m or r
291 action = 'options';
292 end
293
294 return startat, selectedIndex;
295end
296
297function continueWith() -- returns a menu index of the next song based on the selected repeat mode
298 local contWith = 1;
299 if #menu > 0 then
300 if settings.rptMode == "allRandom" then
301 contWith = math.random(#menu);
302 elseif settings.rptMode == "allOrdered" then
303 contWith = song.index + 1;
304 if contWith > #menu then
305 contWith = 1;
306 end
307 elseif settings.rptMode == "one" then
308 contWith = song.index;
309 end
310 end
311 return contWith;
312end
313
314function playNotes(doReturn)
315 while true do
316 if action == 'songReady' then
317 isPlaying = true;
318 action = 'nowPlaying';
319 os.queueEvent("playStarted");
320 for i = 1, #song.music.wait - 1 do
321 if song.music.wait[i] ~= 0 then
322 os.sleep(song.music.wait[i]);
323 if stopFlag then break; end
324 end
325 pcall(box.playNote, song.music.inst[i], song.music.note[i]);
326 end
327 isPlaying = false;
328 os.queueEvent("playEnded");
329 if not stopFlag then --song finished (instead of controller terminated)
330 if #menu > 0 and settings.rptMode ~= "none" then
331 menuAt(continueWith()); --continue playing songs based on current repeat mode
332 end
333 end
334 end
335 if doReturn ~= nil and doReturn == true then break; end
336 os.sleep(0.25);
337 end
338end
339
340function stopSong()
341 if isPlaying then
342 stopFlag = true;
343 parallel.waitForAny(function()
344 while true do
345 if not isPlaying then break; end
346 os.sleep(0.125);
347 end
348 end);
349 stopFlag = false;
350 end
351end
352
353function menuAt(x) -- plays the song on the menu at index x
354 stopSong();
355 song = newSong(fs.open(menu[x].d .. "/" .. menu[x].fn, "rb"), x);
356 readHeader();
357 readNotes();
358 song.fh.close();
359 action = 'songReady';
360end
361
362function skipSong()
363 if #menu > 0 and settings.rptMode ~= "none" then
364 menuAt(continueWith()); --continue playing songs based on current repeat mode
365 else
366 stopSong();
367 end
368end
369
370function controller() --handles actions
371 local startat = 1;
372 local selectedIndex = 1;
373 while true do
374 if action == 'mainList' then
375 startat, selectedIndex = mainList(startat, selectedIndex);
376 elseif action == 'playSong' then
377 menuAt(selectedIndex);
378 action = 'songReady';
379 elseif action == 'songReady' then
380 os.sleep(0.125);
381 elseif action == 'nowPlaying' then
382 showInfo();
383 elseif action == 'options' then
384 options();
385 end
386 end
387end
388
389function clearMenu()
390 menu = {};
391end
392
393function menuTable() -- returns all the loaded songs that would show up in the menu
394 return menu;
395end
396
397function launchUI()
398 if box == nil then
399 print("No Iron Noteblock Detected");
400 return;
401 end
402
403 if args[1] == nil or fs.isDir(args[1]) == false then
404 args[1] = "songs";
405 if fs.isDir("songs") == false then
406 fs.makeDir("songs");
407 end
408 elseif args[1] ~= nil and fs.isDir(args[1]) == true then
409 loadMenu(args[1]);
410 end
411
412 clearMenu();
413 loadMenu("songs");
414 loadMenu("rom/songs"); -- \Desktop\MindCrack\minecraft\mods\ComputerCraft\lua\rom\songs (ComputerCraft folder inside \mods\ must be created, it is not there by default)
415
416 parallel.waitForAll(playNotes, controller);
417end
418
419function current() -- returns convinient 'song' table with complete header info, notes, and etc, --OR-- nil if nothing is playing (useful)
420 if isPlaying == true then
421 return song; --full song table, has everything
422 else
423 return nil;
424 end
425end
426
427mountIronNoteblock();
428if shell ~= nil then --ran normally, not loaded as API
429 launchUI();
430else --else it was loaded as an api so don't do anything else automatically
431 settings.rptMode = "none";
432end