Last active
July 9, 2021 14:19
-
-
Save geniuszxy/6ea77fdf00db2340aa642eda30b378f7 to your computer and use it in GitHub Desktop.
An org player written in love
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
--[[ | |
== 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