-
Notifications
You must be signed in to change notification settings - Fork 2
/
main.lua
648 lines (553 loc) · 22.2 KB
/
main.lua
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
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
-- MIT License
--
-- Copyright (c) 2018 nabakin
--
-- 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.
--[[ Initialize Variables ]]
main = {}
local TSerial = require 'TSerial'
local MONITOR_REFRESH_RATE = 60
local KEY_FUNCTIONS = {}
local appdata_path = love.filesystem.getAppdataDirectory()
local operating_system = love.system.getOS()
local waveform = {}
local sleep_counter = 0
local sleep_time = 0
local fps_cap = 0
local last_frame_time = 0
local button_pressed = nil
local appdata_music_success = true
local rd_option_pressed = false
local rd_list = nil
local rd_string = ""
local rd_option = 0
local dragndrop = false
--- Reloads variables that affect the menu.
-- Necessary for returning to the main menu.
function main.reload()
waveform = {}
sleep_counter = 0
button_pressed = nil
appdata_music_success = true
rd_option_pressed = false
rd_list = nil
rd_string = ""
dragndrop = false
rd_option = 0
end
--[[ Core Function Callbacks ]]
--- Initializes core elements of Drop.
-- Callback for main Love2D thread.
function love.load()
--[[ Configuration ]]
-- Config initialization
local CURRENT_VERSION = 2
local DEFAULT_CONFIG = {
version = CURRENT_VERSION, -- Every time config format changes, 1 is added.
visualization = 3, -- Visualization to show on start. (session persistent)
shuffle = false, -- Enable/disable shuffle on start. (session persistent)
loop = false, -- Enable/disable loop on start. (session persistent)
volume = 0.5, -- Volume on start. (session persistent)
mute = false, -- Enable/disable mute on start. (session persistent)
fullscreen = false, -- Enable/disable fullscreen on start. (session persistent)
fade = false, -- Enable/disable fade on start. (session persistent)
fade_intensity_multiplier = 30, -- Amount of fade.
session_persistence = false, -- Options restored from previous session.
color = {0, 1, 0}, -- Color of visualization/music controls. Format: {r, g, b} [0-1]
fps_cap = 0, -- Places cap on fps (looks worse, but less cpu intensive). 0 for vsync.
sleep_time = 7, -- Seconds until overlay is put to sleep.
visualization_update = true, -- Update visualization when dragging scrubhead (false=less cpu intensive).
sampling_size = 2048, -- Number of audio samples to generate waveform from (maintain a power of 2).
window_size_persistence = true, -- Window size restored from previous session.
window_size = {1280, 720}, -- Size of window on start. (window size persistent)
window_location_persistence = false, -- Window position restored from previous session.
window_location = {420, 340, 1}, -- Location of window when persistent. (window location persistent)
init_location = "menu", -- Where to go on start. Options: "menu", "dragndrop", "sysaudio", or "appdata".
init_sysaudio_option = 0, -- Which system audio input to automatically select. Options: 0=show options, 1-infinity=audio input.
rd_sample_rate = 44100, -- [ADVANCED] Audio input device's sample rate (in hz). Change only if having issues visualizing audio.
rd_bit_depth = 16, -- [ADVANCED] Audio input device's bit depth. Change only if having issues visualizing audio.
rd_channels = 1 -- [ADVANCED] Audio input device's number of channels. Change only if having issues visualizing audio.
}
local CHECK_VALUES = {
version = function (v)
return type(v) == "number" and v >= 0
end,
visualization = function (v)
return type(v) == "number" and v >= 1 and v <= 4 and v == math.floor(v)
end,
shuffle = function (v)
return type(v) == "boolean"
end,
loop = function (v)
return type(v) == "boolean"
end,
mute = function (v)
return type(v) == "boolean"
end,
fullscreen = function (v)
return type(v) == "boolean"
end,
fade = function (v)
return type(v) == "boolean"
end,
fade_intensity_multiplier = function (v)
return type(v) == "number" and v >= 0
end,
volume = function (v)
return type(v) == "number" and v >= 0 and v <= 1
end,
session_persistence = function (v)
return type(v) == "boolean"
end,
color = function (v)
return type(v) == "table" and #v == 3 and type(v[1]) == "number" and v[1] >=0 and v[1] <=1 and type(v[2]) == "number" and v[2] >=0 and v[2] <=1 and type(v[3]) == "number" and v[3] >=0 and v[3] <=1
end,
fps_cap = function (v)
return type(v) == "number" and v >= 0
end,
sleep_time = function (v)
return type(v) == "number" and v >= 0
end,
visualization_update = function (v)
return type(v) == "boolean"
end,
sampling_size = function (v)
return type(v) == "number" and v == math.floor(v) and v%2 == 0
end,
window_size_persistence = function (v)
return type(v) == "boolean"
end,
window_size = function (v)
return type(v) == "table" and #v == 2 and type(v[1]) == "number" and type(v[2]) == "number"
end,
window_location_persistence = function (v)
return type(v) == "boolean"
end,
window_location = function (v)
return type(v) == "table" and #v == 3 and type(v[1]) == "number" and type(v[2]) == "number" and type(v[3]) == "number"
end,
init_location = function (v)
return type(v) == "string" and (v == "menu" or v == "sysaudio" or v == "appdata" or v == "dragndrop")
end,
init_sysaudio_option = function (v)
return type(v) == "number" and v >= 0 and v == math.floor(v) and v <= #(love.audio.getRecordingDevices())
end,
rd_sample_rate = function (v)
return type(v) == "number" and v > 0 and v == math.floor(v)
end,
rd_bit_depth = function (v)
return type(v) == "number" and v > 0 and v == math.floor(v) and v%2 == 0
end,
rd_channels = function (v)
return type(v) == "number" and v > 0 and v <= 2 and v == math.floor(v)
end
}
-- config.lua -> config table
config = TSerial.unpack(love.filesystem.read("config.lua"), true)
-- Validating config table.
if not config or CURRENT_VERSION < config.version then
print(os.date('[%H:%M] ').."config.lua is either missing, corrupt, or from a newer version. Recreating file...")
-- Use default config.
config = DEFAULT_CONFIG
love.filesystem.write("config.lua", TSerial.pack(config, false, true))
print(os.date('[%H:%M] ').."Done.")
elseif CURRENT_VERSION > config.version then
print(os.date('[%H:%M] ').."Old config.lua found. Updating it to the latest version...")
-- Transfer compatible configurations from old config to new config.
local dconfig = DEFAULT_CONFIG
for key, value in pairs(config) do
if dconfig[key] and CHECK_VALUES[key](value) then
dconfig[key] = value
end
end
-- Finalize config update.
dconfig.version = CURRENT_VERSION
config = dconfig
love.filesystem.write("config.lua", TSerial.pack(config, false, true))
print(os.date('[%H:%M] ').."Done.")
else
local invalid = false
-- Validate configurations. Resets configuration to default if invalid.
for key, value in pairs(config) do
if CHECK_VALUES[key] and not CHECK_VALUES[key](value) then
print(os.date('[%H:%M] ').."Error: Invalid "..key.." value detected in config.lua. Resetting to default.")
config[key] = DEFAULT_CONFIG[key]
invalid = true
print(os.date('[%H:%M] ').."Done.")
end
end
-- Finalize config fix.
if invalid then
love.filesystem.write("config.lua", TSerial.pack(config, false, true))
end
end
--------------------------------- Keyboard Actions ---------------------------------
KEY_FUNCTIONS = {
["up"] = function ()
local new_volume_rounded = math.floor((love.audio.getVolume()+.1)*10+0.5)/10
gui.buttons.volume.activate(new_volume_rounded)
end,
["down"] = function ()
local new_volume_rounded = math.floor((love.audio.getVolume()-.1)*10+0.5)/10
gui.buttons.volume.activate(new_volume_rounded)
end,
["right"] = function ()
gui.buttons.right.activate()
end,
["left"] = function ()
gui.buttons.left.activate()
end,
["s"] = function ()
gui.buttons.shuffle.activate()
end,
["l"] = function ()
gui.buttons.loop.activate()
end,
["i"] = function ()
visualization.setFade(not visualization.isFading())
end,
["m"] = function ()
audio.toggleMute()
end,
["1"] = function ()
visualization.setType(1)
end,
["2"] = function ()
visualization.setType(2)
end,
["3"] = function ()
visualization.setType(3)
end,
["4"] = function ()
visualization.setType(4)
end,
["escape"] = function ()
if love.window.getFullscreen() then
gui.buttons.fullscreen.activate()
end
end,
["f"] = function ()
gui.buttons.fullscreen.activate()
end,
["space"] = function ()
gui.buttons.playback.activate()
end,
-- Moves slowly through the visualization by the length of a frame. Used to compare visualizations.
[","] = function ()
audio.music.seekSong(audio.music.tellSong()-visualization.getSamplingSize()/(audio.getSampleRate()*audio.getChannels()))
end,
["."] = function ()
audio.music.seekSong(audio.music.tellSong()+visualization.getSamplingSize()/(audio.getSampleRate()*audio.getChannels()))
end
}
------------------------------------------------------------------------------------
----------------------------------- Main -------------------------------------------
audio = require 'audio'
visualization = require 'visualization'
gui = require 'gui'
sleep_time = config.sleep_time
fps_cap = config.fps_cap
rd_option = config.init_sysaudio_option
love.keyboard.setKeyRepeat(true)
gui.load()
-- If refresh rate can be determined, use it.
local potential_refresh_rate = ({love.window.getMode()})[3].refreshrate
if potential_refresh_rate ~= 0 then
MONITOR_REFRESH_RATE = potential_refresh_rate
end
--[[ Init Location Jumping ]]
if config.init_location == "sysaudio" then
rd_option_pressed = true
rd_list = love.audio.getRecordingDevices()
-- Check for valid option.
if rd_option > 0 and rd_option <= #rd_list then
audio.recordingdevice.load(rd_list[rd_option])
rd_option_pressed = false
else
-- Prepare audio input list.
rd_string = "Choose audio input:\n"
for i,v in ipairs(rd_list) do
rd_string = rd_string..tostring(i)..") "..v:getName().."\n"
end
end
elseif config.init_location == "appdata" then
appdata_music_success = audio.music.load("music")
elseif config.init_location == "dragndrop" then
dragndrop = true
end
------------------------------------------------------------------------------------
end
--- Contains all time-oriented operations. Physics, audio playback, etc.
-- Callback for main Love2D thread.
-- @param dt number: Delta time between current call and last.
function love.update(dt)
if audio.music.exists() or audio.recordingdevice.isActive() then
-- Update queueable audio and sampling table.
if audio.recordingdevice.isActive() then
audio.recordingdevice.update()
else
audio.music.update()
end
if visualization.wouldChange() and not love.window.isMinimized() then
-- Performs FFT to generate waveform.
if audio.recordingdevice.isActive() then
if audio.recordingdevice.isReady() then
waveform = visualization.generateRecordingDeviceWaveform()
end
else
waveform = visualization.generateMusicWaveform()
end
end
-- Sleep timer for overlay.
if not gui.extra.sleep() then
sleep_counter = sleep_counter+dt
if sleep_counter > sleep_time then
gui.extra.sleep(true)
sleep_counter = 0
end
end
end
end
--- Contains all graphics drawing operations.
-- Callback for main Love2D thread.
function love.draw()
--[[ Menu/Visualization drawing ]]
if gui.buttons.menu.isActive() then
love.graphics.setColor(1, 1, 1)
love.graphics.setFont(gui.graphics.getBigFont())
local graphics_width = gui.graphics.getWidth()
local graphics_height = gui.graphics.getHeight()
if rd_option_pressed then
love.graphics.printf(rd_string, graphics_width/80, graphics_height/2-2.5*love.graphics.getFont():getHeight(), graphics_width, "left")
else
if dragndrop then
love.graphics.printf("Drag and drop music files/folders here", graphics_width/80, graphics_height/2-5*love.graphics.getFont():getHeight()/2, graphics_width, "center")
elseif appdata_music_success then
love.graphics.printf("Drop music files/folders here or press the corresponding number:\n1) Play system audio\n2) Play music in appdata", graphics_width/80, graphics_height/2-5*love.graphics.getFont():getHeight()/2, graphics_width, "left")
else
love.graphics.printf("Failed to play music from your appdata. Copy songs to \""..appdata_path.."/LOVE/Drop/music\" to make this work or just drag and drop music onto this window.", graphics_width/80, graphics_height/2-5*love.graphics.getFont():getHeight()/2, graphics_width, "left")
end
end
elseif not love.window.isMinimized() then
visualization.draw(waveform)
end
--[[ Overlay drawing ]]
if not gui.extra.sleep() then
gui.overlay()
end
-- FPS Limiter
if fps_cap > 0 then
local slack = 1/fps_cap - (love.timer.getTime()-last_frame_time)
if slack > 0 then love.timer.sleep(slack) end
last_frame_time = love.timer.getTime()
--[[ Manual detection for when behind windows or minimized. Only works on Mac.
Saves a lot of cpu. Likely error-prone because it's a bad implementation (no other way). ]]
elseif operating_system == "OS X" and love.timer.getFPS() > MONITOR_REFRESH_RATE+6 then
local slack = 1/(MONITOR_REFRESH_RATE+10) - (love.timer.getTime()-last_frame_time)
if slack > 0 then love.timer.sleep(slack) end
last_frame_time = love.timer.getTime()
end
end
--[[ Input Function Callbacks ]]
--- Handles all mouse button press input.
-- Callback for main Love2D thread.
-- @param x number: x coordinate of mouse.
-- @param y number: y coordinate of mouse.
-- @param key number: Mouse button pressed.
-- @param istouch boolean: True when touchscreen press. False otherwise.
-- @param presses number: Number of presses in a short time frame.
function love.mousepressed(x, y, key, istouch, presses)
-- Reset sleep counter.
gui.extra.sleep(false)
sleep_counter = 0
if key == 1 then
button_pressed = gui.buttons.getButton(x, y)
-- Detects if scrub bar clicked and moves to the corresponding point in time.
if audio.music.exists() and gui.buttons.scrubbar.inBoundsX(x) and gui.buttons.scrubbar.inBoundsY(y) then
gui.buttons.scrubbar.activate(x)
end
end
end
--- Handles all mouse button release input.
-- Callback for main Love2D thread.
-- @param x number: x coordinate of mouse.
-- @param y number: y coordinate of mouse.
-- @param key number: Mouse button released.
-- @param istouch boolean: True when touchscreen release. False otherwise.
-- @param presses number: Number of releases in a short time frame.
function love.mousereleased(x, y, key, istouch, presses)
-- Verifies releasing on the same button as pressed with some polymorphism.
if key == 1 and button_pressed and button_pressed.inBoundsX(x) and button_pressed.inBoundsY(y) then
button_pressed.activate()
end
button_pressed = nil
-- If scrubbar is being dragged, stop.
if gui.buttons.scrubbar.isActive() then
gui.buttons.scrubbar.deactivate(x)
end
end
--- Handles all mouse movement input.
-- Callback for main Love2D thread.
-- @param x number: x coordinate of mouse.
-- @param y number: y coordinate of mouse.
-- @param dx number: Distance along x since last call.
-- @param dy number: Distance along y since last call.
-- @param istouch boolean: True when touchscreen press. False otherwise.
function love.mousemoved(x, y, dx, dy, istouch)
-- Reset sleep counter.
gui.extra.sleep(false)
sleep_counter = 0
gui.buttons.setCursorIcon(x, y)
-- Update scrubhead/music position. Makes scrubhead draggable.
if gui.buttons.scrubbar.isActive() and gui.buttons.scrubbar.inBoundsX(x) then
gui.buttons.scrubbar.activate(x)
end
end
--- Called when mouse loses/gains window focus.
-- Callback for main Love2D thread.
-- @param focus boolean: True when gains. False otherwise.
function love.mousefocus(focus)
-- If scrubbar is being dragged, stop.
if gui.buttons.scrubbar.isActive() then
gui.buttons.scrubbar.deactivate(gui.buttons.scrubbar.getScrubheadPosition())
end
end
--- Handles all key press input.
-- Callback for main Love2D thread.
-- @param key string: Key pressed.
-- @param scancode number: Number representation of key.
-- @param isrepeat boolean: True if keypress event repeats. False otherwise.
function love.keypressed(key, scancode, isrepeat)
-- Reset sleep counter.
gui.extra.sleep(false)
sleep_counter = 0
-- Menu controls.
local key_int = tonumber(key)
if gui.buttons.menu.isActive() and key_int and not dragndrop then
-- Audio input options.
if rd_option_pressed then
if key_int > 0 and key_int <= #rd_list then
audio.recordingdevice.load(rd_list[key_int])
rd_option_pressed = false
end
-- Menu options.
else
-- Select system audio.
if key_int == 1 then
rd_list = love.audio.getRecordingDevices()
-- If init_sysaudio_option configured in config, use. Start RD instantly.
if rd_option > 0 and rd_option <= #rd_list then
audio.recordingdevice.load(rd_list[rd_option])
rd_option_pressed = false
-- Obtain user input. Have user select from audio input options.
else
rd_option_pressed = true
rd_string = "Choose audio input:\n"
for i,v in ipairs(rd_list) do
rd_string = rd_string..tostring(i)..") "..v:getName().."\n"
end
end
-- Select music from appdata.
elseif key_int == 2 then
appdata_music_success = audio.music.load("music")
end
end
-- Player controls.
else
local function catch_nil() end
(KEY_FUNCTIONS[key] or catch_nil)()
end
end
--- Called on window resize.
-- Callback for main Love2D thread.
-- @param w number: Width window resized to.
-- @param h number: Height window resized to.
function love.resize(w, h)
gui.scale()
end
--- Called when directory dropped onto window.
-- Callback for main Love2D thread.
-- @param path string: Path of directory dropped.
function love.directorydropped(path)
audio.music.load(path)
end
--- Called when file dropped onto window.
-- Callback for main Love2D thread.
-- @param file File: Love2D object representing dropped file.
function love.filedropped(file)
audio.music.addSong(file)
end
--- Called when exiting Drop.
-- Callback for main Love2D thread.
-- @return boolean: True to cancel and keep Drop alive. False to quit.
function love.quit()
--[[ Save config (for session persistence) ]]
local write_config = false
-- If need to update config values, set flag to true and save new values.
if config.window_size_persistence then
local new_window_size
if love.window.getFullscreen() then
new_window_size = {gui.graphics.getWindowedDimensions()}
else
new_window_size = {love.graphics.getDimensions()}
end
if config.window_size[1] ~= new_window_size[1] or config.window_size[2] ~= new_window_size[2] then
config.window_size = new_window_size
write_config = true
end
end
-- If need to update config values, set flag to true and save new values.
if config.window_location_persistence then
local new_window_location
if love.window.getFullscreen() then
new_window_location = {gui.graphics.getWindowedPosition()}
else
new_window_location = {love.window.getPosition()}
end
if config.window_location[1] ~= new_window_location[1] or config.window_location[2] ~= new_window_location[2] or config.window_location[3] ~= new_window_location[3] then
config.window_location = new_window_location
write_config = true
end
end
-- If need to update config values, set flag to true and save new values.
if config.session_persistence then
local visualization_type = visualization.getType()
local shuffle = audio.isShuffling()
local loop = audio.isLooping()
local mute = audio.isMuted()
local volume = math.floor((mute and audio.getUnmuteVolume() or not audio.music.exists() and audio.music.getVolume() or love.audio.getVolume())*10+0.5)/10
local fullscreen = love.window.getFullscreen()
local fade = visualization.isFading()
if config.visualization ~= visualization_type or config.shuffle ~= shuffle or config.loop ~= loop or config.volume ~= volume or config.mute ~= mute or config.fullscreen ~= fullscreen or config.fade ~= fade then
config.visualization = visualization_type
config.shuffle = shuffle
config.loop = loop
config.volume = volume
config.mute = mute
config.fullscreen = fullscreen
config.fade = fade
write_config = true
end
end
-- If config has been changed, update config file.
if write_config then
love.filesystem.write("config.lua", TSerial.pack(config, false, true))
end
return false
end