main.lua 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107
  1. -- a toy drum sequencer working in 2D desktop mode
  2. package.path = package.path .. ";../?.lua" -- needed for chui.lua to be located in parent directory
  3. local chui = require'chui'
  4. local step_count = 12
  5. local bar_length = 4 * 60 / 120
  6. local instruments = {
  7. { pitch = 1.0, volume = 1, name='snare', sample_path='dm-snare.ogg' },
  8. { pitch = 1.4, volume = 1, name='hh-open', sample_path='dm-hatopen.ogg' },
  9. { pitch = 1.2, volume = 1, name='hh-closed', sample_path='dm-hatclosed.ogg' },
  10. { pitch = 0.8, volume = 1, name='bass', sample_path='dm-bass.ogg' },
  11. -- add more as desired
  12. }
  13. -- load in the samples; we later clone them to be able to play multiple samples at once
  14. for _, instrument in ipairs(instruments) do
  15. instrument.sample = lovr.audio.newSource(instrument.sample_path, {pitchable=true, spatial=false})
  16. end
  17. local function defaultdict(default_value_factory)
  18. local t = {}
  19. local metatable = {}
  20. metatable.__index = function(tbl, key)
  21. if not rawget(tbl, key) then
  22. rawset(tbl, key, default_value_factory(key))
  23. end
  24. return rawget(tbl, key)
  25. end
  26. return setmetatable(t, metatable)
  27. end
  28. local seq_table = defaultdict(function() return {} end)
  29. -- start building the UI; first the instrument lanes and then general controls
  30. local sequencer_panel = chui.panel{ palette=chui.palettes[6] }
  31. local volume_sliders = {}
  32. local pitch_sliders = {}
  33. for r, instrument in ipairs(instruments) do
  34. -- a trigger pushbutton to play the sample
  35. sequencer_panel:button{ span=0.3, callback=function() instrument.sample:clone():play() end }
  36. -- sample name
  37. sequencer_panel:label{ text=instrument.name }
  38. -- volume and pitch parameters
  39. volume_sliders[r] = sequencer_panel:slider{ span=1.5, text='vol', min=0, max=1, value= instrument.volume }
  40. pitch_sliders[r] = sequencer_panel:slider{ span=2, text='pitch', min=0.25, max=4, value= instrument.pitch }
  41. -- a row of toggle buttons that activate the sequencer
  42. for c = 1, step_count do
  43. sequencer_panel:toggle{span=0.6, callback = function(_, state)
  44. seq_table[r][c] = state
  45. end}
  46. end
  47. -- finish the row after each instrument's widgets to prepare for next
  48. sequencer_panel:row()
  49. end
  50. -- tempo and bar progress spans are manually adjusted to be aligned with previous widgets
  51. local tempo_slider = sequencer_panel:slider{ span=5.5, text='tempo', min=64, max=216, value=116, step=0.5 }
  52. local progress_bar = sequencer_panel:progress{ span=9.5, text='bar' }
  53. -- after adding the widgets, layout them in horizontally centered rows
  54. sequencer_panel:layout()
  55. local last_step = 7
  56. local time = 0
  57. lovr.graphics.setBackgroundColor(1,1,1)
  58. function lovr.update(dt)
  59. sequencer_panel:update(dt) -- allow ui to process interactions
  60. bar_length = 4 * 60 / tempo_slider:get() -- use slide to scale the tempo_slider
  61. time = time + dt / bar_length * step_count
  62. local bar_time = time % step_count -- bar time, normalized to [0, step_count] range
  63. progress_bar:set(bar_time / step_count)
  64. -- play the step notes if the bar timer beyond the previous step
  65. if math.floor(bar_time) ~= last_step then
  66. -- play any active toggle bar on this step
  67. last_step = math.floor(bar_time)
  68. for row, instrument in ipairs(instruments) do
  69. instrument.volume = volume_sliders[row]:get()
  70. instrument.pitch = pitch_sliders[row]:get()
  71. local tgl = seq_table[row][last_step + 1]
  72. if tgl then
  73. local sample = instrument.sample:clone()
  74. sample:setVolume(instrument.volume)
  75. sample:setPitch(instrument.pitch)
  76. sample:play()
  77. end
  78. end
  79. end
  80. end
  81. function lovr.draw(pass)
  82. -- use orthographic projection; dynamically adapt the panel size/position to the window dimensions
  83. local screen_width, screen_height = pass:getDimensions()
  84. local scale = screen_width / sequencer_panel.span[1] * 0.95
  85. sequencer_panel.pose:set(screen_width / 2, screen_height / 2, 0):scale(scale)
  86. pass:setProjection(1, mat4():orthographic(
  87. 0, screen_width, screen_height, 0,
  88. scale, -scale))
  89. sequencer_panel:draw(pass, true) -- draw panel itself and the interacting pointer
  90. end