Skip to content

Instantly share code, notes, and snippets.

@geniuszxy
Last active July 9, 2021 14:19
Show Gist options
  • Save geniuszxy/6ea77fdf00db2340aa642eda30b378f7 to your computer and use it in GitHub Desktop.
Save geniuszxy/6ea77fdf00db2340aa642eda30b378f7 to your computer and use it in GitHub Desktop.
An org player written in love
--[[
== Usage ==
1. Get the control channel
ch = love.thread.getChannel('orgplayer')
2. Create a thread and start it
th = love.thread.newThread('love_orgplayer_thread.lua')
th:start()
3. To play an org file
ch:push('play')
ch:push(file)
4. To pause
ch:push('pause')
5. To stop
ch:push('stop')
6. To resume
ch:push('play')
ch:push(0)
7. To quit
ch:push('quit')
]]
local c_org = {}
local c_track_melodic = {}
local c_track_drumkit = {}
local mt_org = { __index = c_org }
local mt_track_melodic = { __index = c_track_melodic }
local mt_track_drumkit = { __index = c_track_drumkit }
local playing = nil -- Playing org object
local ch = nil -- Channel
local msg = nil
local message_handler = {}
local SAMPLERATE = 48000
local DRUMSAMPLERATIO = 22050 / SAMPLERATE
local BUFFERSAMPLES = 4800
local PERIODSIZE = {1024,512,256,128,64,32,16,8}
local PITCHCLASS = {0,0,0,0,0,0,0,0,0,0,0,0}
local PIDURATION = {4,8,12,16,20,24,28,32}
local floor = math.floor
-- move the track to next note index
local function track_nextNote(track)
local noteIndex = track.noteIndex + 1
local note = track.notes[noteIndex]
if not note then
track.frequency = 0
return
end
track.noteIndex = noteIndex
if note.note < 255 then
track:setFrequency(note.note)
track.startPosition = note.position
track.length = note.length
end
if note.volume < 255 then
track.amplitude = 10 ^ (note.volume / 0xf8 - 1)
end
if note.panning < 255 then
local pan = note.panning / 0x0c
if pan < 0.5 then
track.amplitudeL = track.amplitude
track.amplitudeR = track.amplitude * 20 ^ (2 * pan - 1)
else
track.amplitudeL = track.amplitude * 20 ^ (1 - 2 * pan)
track.amplitudeR = track.amplitude
end
end
end
-- set the note index of the track
local function track_setNoteIndex(track, beatIndex)
local notes = track.notes
local noteIndex = 0
for i = 1, #notes do
if notes[i].position >= beatIndex then
noteIndex = i - 1
break
end
end
track.noteIndex = noteIndex
track.frequency = 0
track_nextNote(track)
end
local function track_init(track)
track.noteIndex = 0
track.frequency = 0
track.startPosition = 0
track.length = 0
track.amplitude = 0
track.amplitudeL = 0
track.amplitudeR = 0
track_nextNote(track)
end
function c_org:init()
--self.beatIndex = 0
self.sampleIndex = 0
self.tempo = self.tempo / 1000
self.beatSamples = SAMPLERATE * self.tempo
self.source = love.audio.newQueueableSource(SAMPLERATE, 16, 2)
self.buffer = love.sound.newSoundData(BUFFERSAMPLES, SAMPLERATE, 16, 2)
end
function c_org:update()
for i = 0, BUFFERSAMPLES - 1 do
local sampleIndex = self.sampleIndex + i
local beatIndex = floor(sampleIndex / self.beatSamples)
--loop end
if beatIndex == self.loopEnd then
beatIndex = self.loopBeginning
sampleIndex = floor(self.loopBeginning * self.beatSamples)
self.sampleIndex = sampleIndex - i
--find note index
for id, track in pairs(self.tracks) do
track_setNoteIndex(track, beatIndex)
end
end
--sample
local L, R = 0, 0
local sampleTime = sampleIndex / SAMPLERATE
for id, track in pairs(self.tracks) do
local l, r = track:sample(self, sampleTime, beatIndex)
L = L + l
R = R + r
end
self.buffer:setSample(i, 1, L / 2)
self.buffer:setSample(i, 2, R / 2)
end
self.source:queue(self.buffer)
self.sampleIndex = self.sampleIndex + BUFFERSAMPLES
end
function c_track_melodic:init()
self.samples = require('instruments.m'..self.instrument)
track_init(self)
end
function c_track_melodic:setFrequency(note)
local pitchClass = (note % 12) + 1
local octave = floor(note / 12) + 1
self.frequency = (PITCHCLASS[pitchClass] + (self.pitch - 1000)) / PERIODSIZE[octave]
if self.pi == 1 then
self.maxSample = PIDURATION[octave] * #self.samples
end
end
function c_track_melodic:sample(org, sampleTime, beatIndex)
if beatIndex < self.startPosition
or self.frequency == 0
then
return 0, 0
end
if beatIndex >= self.startPosition + self.length then
track_nextNote(self)
return self:sample(org, sampleTime, beatIndex)
end
local v = floor((sampleTime - self.startPosition * org.tempo)
* self.frequency * #self.samples + 0.5)
if self.pi == 1 and v >= self.maxSample then
return 0, 0
end
v = self.samples:byte(v % #self.samples + 1)
if v > 127 then
v = v - 256
end
v = v / 256
return v * self.amplitudeL, v * self.amplitudeR
end
function c_track_drumkit:init()
track_init(self)
self.samples = require('instruments.d'..self.instrument)
end
function c_track_drumkit:setFrequency(note)
self.frequency = (note * 800 + 100) * DRUMSAMPLERATIO
end
function c_track_drumkit:sample(org, sampleTime, beatIndex)
if beatIndex < self.startPosition
or self.frequency == 0
then
return 0, 0
end
if beatIndex >= self.startPosition + self.length then
track_nextNote(self)
return self:sample(org, sampleTime, beatIndex)
end
local v = floor((sampleTime - self.startPosition * org.tempo) * self.frequency + 0.5)
-- drum play only once
if v >= #self.samples then
return 0, 0
end
v = self.samples:byte(v + 1) / 128 - 1
return v * self.amplitudeL, v * self.amplitudeR
end
-- Message Handlers
function message_handler.play()
local df = ch:demand() --play file
local org
if df ~= 0 then
if not df:open('r') then
error("Can't open file")
end
local content = df:read()
df:close()
org = loadstring(content)()
end
if org then
if not getmetatable(org) then
setmetatable(org, mt_org)
org:init()
for id, track in pairs(org.tracks) do
local mt_track = id <= 8 and mt_track_melodic or mt_track_drumkit
setmetatable(track, mt_track)
track:init()
end
end
if playing then
playing.source:stop()
end
playing = org
elseif not playing then
return
end
if playing.source:getFreeBufferCount() > 0 then
playing:update()
end
playing.source:play()
end
function message_handler.pause()
if playing then
playing.source:pause()
end
end
function message_handler.stop()
if playing then
playing.source:stop()
playing.sampleIndex = 0
--find note index
for id, track in pairs(playing.tracks) do
track_setNoteIndex(track, 0)
end
end
end
function message_handler.quit()
message_handler.stop()
end
-- Init
do
-- pitch classes
local FREQTABLE = {
261.62556530060, 277.18263097687, 293.66476791741,
311.12698372208, 329.62755691287, 349.22823143300,
369.99442271163, 391.99543598175, 415.30469757995,
440.00000000000, 466.16376151809, 493.88330125612,
}
for i = 1, #FREQTABLE do
PITCHCLASS[i] = FREQTABLE[i] * 128
end
-- require love modules
love.audio = require('love.audio')
love.sound = require('love.sound')
end
-- Thread Program
ch = love.thread.getChannel('orgplayer')
repeat
if playing and playing.source:isPlaying() then
msg = ch:pop()
else
msg = ch:demand()
end
local handler = message_handler[msg]
if handler then
handler()
end
if playing
and playing.source:isPlaying()
and playing.source:getFreeBufferCount() > 0
then
playing:update()
end
until
msg == 'quit'
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment