Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- local args = {...}
- if #args > 1 then
- error("Usage: tracker [path]")
- return
- end
- local getFileExtension = function(path)
- local dot = path:find(".", 1, true)
- if dot then
- return path:sub(dot + 1)
- end
- return nil
- end
- if not fs.exists(args[1]) then
- error("File does not exist")
- return
- end
- if fs.isDir(args[1]) then
- error("Cannot track a directory")
- return
- end
- if not getFileExtension(args[1]) == "xm" then
- error("File is not an XM file")
- return
- end
- local file = fs.open(args[1], "rb")
- file.seek("set", 0)
- local function read_string(bytes)
- return file.read(bytes)
- end
- local function read_int(bytes)
- if bytes == 0 then return end
- if bytes == 1 then return string.byte(file.read(1)) end
- local sum = string.byte(file.read(1))
- for i = 2, bytes do
- sum = sum + string.byte(file.read(1)) * 256 ^ (i - 1)
- end
- return sum
- end
- local sbuf = {} --song buffer
- local pbuf = {} --pattern buffer
- if file then
- --#region songheader
- if not (read_string(17) == "Extended Module: ") then
- error("File is not an XM file")
- end
- sbuf.song_name = read_string(20):gsub("%z", "")
- if not (read_int(1) == 0x1A) then
- error("File is not an XM file")
- end
- sbuf.tracker_name = read_string(20):gsub("%z", "")
- local tmp = read_int(1)
- sbuf.version = read_int(1) .. "." .. tmp
- if sbuf.version ~= "1.4" then
- error("XM file is too old! (version " .. sbuf.version .. ")")
- end
- sbuf.header_size = read_int(4)
- sbuf.song_length = read_int(2)
- sbuf.restart_position = read_int(2)
- sbuf.number_of_channels = read_int(2)
- sbuf.number_of_patterns = read_int(2)
- --make sure this is <= 256
- if sbuf.number_of_patterns > 256 then
- error("XM file has too many patterns! (max 256, file may be corrupt)")
- end
- sbuf.number_of_instruments = read_int(2)
- --make sure this is <= 128
- if sbuf.number_of_instruments > 128 then
- error("XM file has too many instruments! (max 128, file may be corrupt)")
- end
- sbuf.flags = read_int(2)
- --we can't handle amiga, so if bit 0 is 0, error
- if bit.band(sbuf.flags, 1) == 0 then
- error("XM file is in Amiga frequency table, which is not supported")
- end
- sbuf.default_tempo = read_int(2)
- sbuf.default_bpm = read_int(2)
- --read for (number of patterns) bytes for the pattern order table
- sbuf.pattern_order_table = {}
- for _ = 1, sbuf.number_of_patterns do
- sbuf.pattern_order_table[#sbuf.pattern_order_table + 1] = read_int(1)
- end
- --#endregion songheader
- --#region patterns
- sbuf.patterns = {}
- for i = 1, sbuf.number_of_patterns do
- local pattern = {}
- local prepos = file.seek()
- pattern.header_size = read_int(4)
- pattern.packing_type = read_int(1)
- --this should be 0 per spec, so if it isn't, error
- if pattern.packing_type ~= 0 then
- error("Unsupported packing type: " .. pattern.packing_type)
- end
- pattern.number_of_rows = read_int(2)
- -- should be 1 <= number_of_rows <= 256
- if pattern.number_of_rows < 1 or pattern.number_of_rows > 256 then
- error("Invalid number of rows: " .. pattern.number_of_rows)
- end
- pattern.packed_data_size = read_int(2)
- --read for (packed_data_size) bytes for the packed pattern data
- file.seek("set", prepos + pattern.header_size)
- if pattern.packed_data_size > 0 then
- pattern.data = {}
- for y = 1, pattern.number_of_rows do
- pattern.data[y] = {}
- for x = 1, sbuf.number_of_channels do
- local dbyte = read_int(1)
- pattern.data[y][x] = {}
- if bit32.btest(dbyte, 0x80) then
- if dbyte ~= 0x80 then
- if bit32.btest(dbyte, 0x01) then
- pattern.data[y][x].note = read_int(1)
- end
- if bit32.btest(dbyte, 0x02) then
- pattern.data[y][x].instrument = read_int(1)
- end
- if bit32.btest(dbyte, 0x04) then
- pattern.data[y][x].volume = read_int(1) - 16
- end
- if bit32.btest(dbyte, 0x08) then
- pattern.data[y][x].effect = read_int(1)
- end
- if bit32.btest(dbyte, 0x10) then
- if not pattern.data[y][x].effect then pattern.data[y][x].effect = 0 end
- pattern.data[y][x].effect_param = read_int(1)
- end
- end
- else
- pattern.data[y][x].note = dbyte
- pattern.data[y][x].instrument = read_int(1)
- pattern.data[y][x].volume = read_int(1) - 16
- pattern.data[y][x].effect = read_int(1)
- pattern.data[y][x].effect_param = read_int(1)
- end
- end
- end
- end
- sbuf.patterns[i] = pattern
- end
- --#endregion patterns
- --#region instruments
- sbuf.instruments = {}
- for i = 1, sbuf.number_of_instruments do
- local inst = {}
- inst.header_size = read_int(4)
- inst.name = read_string(22):gsub("%z", "")
- inst.type = read_int(1)
- inst.number_of_samples = read_int(2)
- if inst.number_of_samples > 0 then
- inst.samples = {}
- inst.samplesByNumber = {}
- inst.volumeEnvelope = {points = {}}
- inst.panningEnvelope = {points = {}}
- inst.vibrato = {}
- for j = 1, inst.number_of_samples do inst.samplesByNumber[j] = {} end
- local samplesize = read_int(4)
- for j = 1, 96 do inst.samples[j] = inst.samplesByNumber[read_int(1)+1] end
- for j = 1, 12 do inst.volumeEnvelope.points[j] = {x = read_int(2), y = read_int(2)} end
- for j = 1, 12 do inst.panningEnvelope.points[j] = {x = read_int(2), y = read_int(2)} end
- for j = read_int(1) + 1, 12 do inst.volumeEnvelope.points[j] = nil end
- for j = read_int(1) + 1, 12 do inst.panningEnvelope.points[j] = nil end
- inst.volumeEnvelope.sustain = read_int(1) + 1
- inst.volumeEnvelope.loopStart = read_int(1) + 1
- inst.volumeEnvelope.loopEnd = read_int(1) + 1
- inst.panningEnvelope.sustain = read_int(1) + 1
- inst.panningEnvelope.loopStart = read_int(1) + 1
- inst.panningEnvelope.loopEnd = read_int(1) + 1
- inst.volumeEnvelope.loopType = read_int(1)
- inst.panningEnvelope.loopType = read_int(1)
- inst.vibrato.type = read_int(1)
- inst.vibrato.sweep = read_int(1)
- inst.vibrato.depth = read_int(1)
- inst.vibrato.rate = read_int(1)
- inst.fadeOut = read_int(2)
- file.seek("cur", inst.header_size - 241)
- for j = 1, inst.number_of_samples do
- local sample = inst.samplesByNumber[j]
- sample.sample_size = read_int(4)
- sample.loop_start = read_int(4)
- sample.loop_length = read_int(4)
- sample.volume = read_int(1)
- sample.finetune = read_int(1)
- if sample.finetune > 0x7F then sample.finetune = sample.finetune - 256 end
- sample.type = read_int(1)
- sample.pan = read_int(1) --math.max((file.read() - 128) / 127, -1)
- sample.note = read_int(1)
- if sample.note > 0x7F then sample.note = sample.note - 256 end
- sample.data_type = read_int(1) -- reserved, but used by some trackers to store the sample type (0 = PCM, 4 = ADPCM)
- sample.name = read_string(22):gsub("[ %z]+$", "")
- file.seek("cur", samplesize - 40)
- end
- --we've read past the headers, so now read the sample data
- for j = 1, inst.number_of_samples do
- local sample = inst.samplesByNumber[j]
- sample.wavetable = {}
- if bit32.btest(sample.type, 0x10) then
- local last = 0
- for i = 1, sample.sample_size / 2 do
- local d = read_int(2)
- if d > 0x7FFF then d = d - 65536 end
- d = d / (d > 0 and 65535 or 65536)
- sample.wavetable[i], last = last + d, d
- end
- else
- local last = 0
- for i = 1, sample.sample_size do
- local d = read_int(1)
- if d > 0x7F then d = d - 256 end
- sample.wavetable[i], last = math.max(math.min((last + d) / ((last + d) > 0 and 127 or 128), 1), -1), last + d
- while last > 127 do last = last - 256 end
- while last < -128 do last = last + 256 end
- end
- end
- end
- else
- --seek past the instrument data
- file.seek("cur", inst.header_size - 29)
- end
- sbuf.instruments[i] = inst
- end
- end
- file.close()
- print("Done!")
- --print this all to a debug file
- local debug_file = fs.open("debug.txt", "w")
- debug_file.write(" Song name: " .. sbuf.song_name .. "\n")
- debug_file.write("Created with: " .. sbuf.tracker_name .. "\n")
- debug_file.write(" Version: " .. sbuf.version .. "\n")
- debug_file.write(" Header size: " .. sbuf.header_size .. "\n")
- debug_file.write(" Song length: " .. sbuf.song_length .. "\n")
- debug_file.write("Restart pos.: " .. sbuf.restart_position .. "\n")
- debug_file.write(" # channels: " .. sbuf.number_of_channels .. "\n")
- debug_file.write(" # patterns: " .. sbuf.number_of_patterns .. "\n")
- debug_file.write(" # insts: " .. sbuf.number_of_instruments .. "\n")
- debug_file.write(" Flags: " .. sbuf.flags .. "\n")
- debug_file.write("Default temp: " .. sbuf.default_tempo .. "\n")
- debug_file.write(" Default BPM: " .. sbuf.default_bpm .. "\n")
- debug_file.write("\nPattern order: ")
- for i = 1, #sbuf.pattern_order_table do
- debug_file.write(sbuf.pattern_order_table[i] .. " ")
- end
- debug_file.write("\n\n")
- debug_file.write("Patterns:\n")
- --write out the patterns like you'd see them in a tracker: such as C-4 01 00 00 00 (and values should be padded to 2 digits, except for note), each channel separated by 10 spaces
- --any nonexistent notes should be written as "XXX", and any nonexistent other values should be written as "XX"
- --[[for i = 1, #sbuf.patterns do
- debug_file.write("Pattern " .. i .. ":\n")
- for y = 1, sbuf.patterns[i].number_of_rows do
- for x = 1, sbuf.number_of_channels do
- local note = sbuf.patterns[i].data[y][x].note
- local instrument = sbuf.patterns[i].data[y][x].instrument
- local volume = sbuf.patterns[i].data[y][x].volume
- local effect = sbuf.patterns[i].data[y][x].effect
- local effect_param = sbuf.patterns[i].data[y][x].effect_param
- if note then
- if note == 97 then
- note = "XXX"
- else
- note = string.format("%02X", note)
- end
- else
- note = "XX"
- end
- if instrument then
- instrument = string.format("%02X", instrument)
- else
- instrument = "XX"
- end
- if volume then
- volume = string.format("%02X", volume)
- else
- volume = "XX"
- end
- if effect then
- effect = string.format("%02X", effect)
- else
- effect = "XX"
- end
- if effect_param then
- effect_param = string.format("%02X", effect_param)
- else
- effect_param = "XX"
- end
- debug_file.write(note .. " " .. instrument .. " " .. volume .. " " .. effect .. " " .. effect_param .. " ")
- end
- debug_file.write("\n")
- end
- debug_file.write("\n")
- sleep(0) --prevent crashing from failing to yield
- end]]
- --write out the instruments (without samples)
- debug_file.write("Instruments:\n")
- for i = 1, #sbuf.instruments do
- debug_file.write("Instrument " .. i .. ":\n")
- debug_file.write(" Header size: " .. sbuf.instruments[i].header_size .. "\n")
- debug_file.write(" # samples: " .. sbuf.instruments[i].number_of_samples .. "\n")
- debug_file.write(" Name: " .. sbuf.instruments[i].name .. "\n")
- debug_file.write("\n")
- end
- debug_file.close()
- print("Testing samples")
- --play the first sample of the first instrument
- --[[
- Example usage of the speaker:
- local speaker = peripheral.find("speaker")
- local t, dt = 0, 2 * math.pi * 220 / 48000
- while true do
- local buffer = {}
- for i = 1, 16 * 1024 * 8 do
- buffer[i] = math.floor(math.sin(t) * 127)
- t = (t + dt) % (math.pi * 2)
- end
- while not speaker.playAudio(buffer) do
- os.pullEvent("speaker_audio_empty")
- end
- end
- ]]
- periphemu.create("left", "speaker")
- --returns an iterator that returns the next note in the sample at a given pitch change
- local function changePitch(sample, changeSemitones)
- changeSemitones = changeSemitones + 3 - (12 * 4) --the samples start at A3, so add 3 to start at C4, and subtract 12 * 4 to start at C0
- local t = 0
- local dt = 2 * math.pi * 440 * 2 ^ (changeSemitones / 12) / 48000
- return function()
- t = (t + dt) % (math.pi * 2)
- return sample.wavetable[math.floor(t * #sample.wavetable / (math.pi * 2)) + 1]
- end
- end
- local speaker = peripheral.find("speaker")
- local sample = sbuf.instruments[10].samplesByNumber[1]
- local note = 0
- --play a note, as defined by note (pass note to changePitch)
- local ev, ev1
- while true do
- local buffer = {}
- local pitch = changePitch(sample, note)
- local pitch2 = changePitch(sample, note + 4)
- local pitch3 = changePitch(sample, note + 7)
- for i = 1, 16 * 1024 * 8 do
- buffer[i] = math.floor(pitch() * 127)
- buffer[i] = buffer[i] + math.floor(pitch2() * 127)
- buffer[i] = buffer[i] + math.floor(pitch3() * 127)
- buffer[i] = math.floor(buffer[i] / 3)
- end
- ev, ev1 = nil, nil
- while not speaker.playAudio(buffer) do
- ev, ev1 = os.pullEvent()
- if ev == "speaker_audio_empty" then
- --do nothing
- elseif ev == "key" then
- term.clearLine(1)
- term.setCursorPos(1, 1)
- if ev1 == keys.up then
- note = note + 1
- speaker.stop()
- term.write("Note: " .. note)
- break
- elseif ev1 == keys.down then
- note = note - 1
- speaker.stop()
- term.write("Note: " .. note)
- break
- end
- end
- end
- end
Advertisement
Add Comment
Please, Sign In to add comment