Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- local FlMMLTimeBase = 384
- local VolMacroSymbols = "ABCDEFGHIJKLMNOPQRSTUVWabcdefghijklmnopqrstuvw"
- local LenMacroSymbols = "XYZxyz"
- local VolMacroStep = 1
- assert(VolMacroStep == 1 or VolMacroStep == 3)
- function round(d)
- return math.floor(d + 0.5)
- end
- function clip(minValue, d, maxValue)
- return math.max(minValue, math.min(d, maxValue))
- end
- function ReadWaveFile(path)
- local read1 = function(file)
- return string.byte(file:read(1))
- end
- local read2 = function(file)
- local l = read1(file)
- local h = read1(file)
- return h * 0x100 + l
- end
- local read2s = function(file)
- local value = read2(file)
- if value < 0x8000 then
- return value
- else
- return value - 0x10000
- end
- end
- local read4 = function(file)
- local l = read2(file)
- local h = read2(file)
- return h * 0x10000 + l
- end
- local wave = {}
- local info = {}
- local file = io.open(path, "rb")
- if file == nil then
- return wave, info
- end
- -- verify RIFF/WAVE header
- if file:read(4) ~= "RIFF" then
- return wave, info
- end
- file:seek("cur", 4)
- if file:read(8) ~= "WAVEfmt " then
- return wave, info
- end
- local fmtChunkSize = read4(file)
- -- parse format header
- info.format = read2(file)
- info.channels = read2(file)
- info.samplerate = read4(file)
- local bytesPerSeconds = read4(file)
- local bytesPerBlock = read2(file) -- bytes/sample * channels
- info.bitwidth = read2(file)
- file:seek("cur", fmtChunkSize - 0x10)
- -- check unsupported format
- if info.format ~= 1 or (info.bitwidth ~= 8 and info.bitwidth ~= 16) then
- return wave, info
- end
- -- data chunk
- if file:read(4) ~= "data" then
- return wave, info
- end
- local dataChunkSize = read4(file)
- assert(dataChunkSize % bytesPerBlock == 0,
- "Invalid data chunk size (" .. dataChunkSize .. "/" .. bytesPerBlock .. ")")
- -- read whole samples to array
- for sampleIndex = 1, dataChunkSize / bytesPerBlock do
- for channelIndex = 1, info.channels do
- local value
- if info.bitwidth == 8 then
- value = (read1(file) - 0x80) / 0x80
- elseif info.bitwidth == 16 then
- value = read2s(file) / 0x8000
- else
- error("Unsupported bitwidth " .. info.bitwidth)
- end
- if wave[channelIndex] == nil then
- wave[channelIndex] = {}
- end
- wave[channelIndex][sampleIndex] = value
- end
- end
- return wave, info
- end
- -- [ volume converter ] ---------------
- assert(#VolMacroSymbols % 2 == 0)
- local VolMacroPatternCount = #VolMacroSymbols / 2
- function MakeVolCommandAbs(targetVolume)
- return "@v" .. targetVolume
- end
- function MakeVolCommandRel(curVolume, targetVolume)
- local diff = targetVolume - curVolume
- local distance = math.abs(diff)
- local volDiffInc, volDiffDec = "(", ")"
- if diff < 0 then
- volDiffInc, volDiffDec = volDiffDec, volDiffInc
- end
- local symbolIndex
- if VolMacroStep == 3 then
- symbolIndex = math.floor((distance + 1) / 3)
- if distance > (3 * VolMacroPatternCount + 1) then
- symbolIndex = math.floor(symbolIndex / 2)
- end
- else
- symbolIndex = distance - 2
- if distance > (VolMacroPatternCount + 2) then
- symbolIndex = math.floor(distance / 2) - 2
- end
- end
- if diff < 0 then
- symbolIndex = symbolIndex + VolMacroPatternCount
- end
- local macroSymbol = ""
- if symbolIndex >= 1 and symbolIndex <= #VolMacroSymbols then
- macroSymbol = VolMacroSymbols:sub(symbolIndex, symbolIndex)
- end
- if distance == 0 then
- return ""
- elseif distance == 1 then
- return volDiffInc
- elseif distance == 2 then
- return volDiffInc .. volDiffInc
- else
- if VolMacroStep == 3 then
- if distance <= (3 * VolMacroPatternCount + 1) then
- if distance % 3 == 0 then
- assert(macroSymbol ~= "")
- return "$" .. macroSymbol
- elseif distance % 3 == 1 then
- assert(macroSymbol ~= "")
- return volDiffInc .. "$" .. macroSymbol
- else -- if distance % 3 == 2 then
- assert(macroSymbol ~= "")
- return volDiffDec .. "$" .. macroSymbol
- end
- elseif distance % 6 == 0 and distance <= 6 * VolMacroPatternCount then
- assert(macroSymbol ~= "")
- return "$" .. macroSymbol .. "$" .. macroSymbol
- else
- return MakeVolCommandAbs(targetVolume)
- end
- else
- if distance <= (VolMacroPatternCount + 2) then
- assert(macroSymbol ~= "")
- return "$" .. macroSymbol
- elseif distance % 2 == 0 and distance >= 6 and distance <= 2 * (VolMacroPatternCount + 2) then
- assert(macroSymbol ~= "")
- return "$" .. macroSymbol .. "$" .. macroSymbol
- else
- return MakeVolCommandAbs(targetVolume)
- end
- end
- end
- end
- function MakeVolCommand(curVolume, targetVolume)
- local volRel = MakeVolCommandRel(curVolume, targetVolume)
- local volAbs = MakeVolCommandAbs(targetVolume)
- if #volRel <= #volAbs then
- return volRel
- else
- return volAbs
- end
- end
- local VolMacroDefs = ""
- if VolMacroPatternCount > 0 then
- local volIncMacros, volDecMacros = "", ""
- for index = 1, VolMacroPatternCount do
- volIncMacros = volIncMacros .. "$" .. VolMacroSymbols:sub(index, index) .. "="
- volDecMacros = volDecMacros .. "$" .. VolMacroSymbols:sub(VolMacroPatternCount + index, VolMacroPatternCount + index) .. "="
- if VolMacroStep == 3 then
- for i = 1, index do
- volIncMacros = volIncMacros .. "((("
- volDecMacros = volDecMacros .. ")))"
- end
- else
- volIncMacros = volIncMacros .. "(("
- volDecMacros = volDecMacros .. "))"
- for i = 1, index do
- volIncMacros = volIncMacros .. "("
- volDecMacros = volDecMacros .. ")"
- end
- end
- volIncMacros = volIncMacros .. ";"
- volDecMacros = volDecMacros .. ";"
- end
- VolMacroDefs = volIncMacros .. volDecMacros
- end
- -- [ length converter ] ---------------
- local SamplesPerNote = 96
- assert(FlMMLTimeBase % SamplesPerNote == 0)
- local LengthToNoteTable = {}
- -- reset
- LengthToNoteTable[1] = "c"
- for sampLen = 2, SamplesPerNote * 2 do
- LengthToNoteTable[sampLen] = ""
- end
- -- single note + dots
- for sampLen = 2, SamplesPerNote do
- if SamplesPerNote % sampLen == 0 then
- local baseLen = SamplesPerNote / sampLen
- LengthToNoteTable[sampLen] = "c" .. baseLen
- local len = sampLen
- local dots = ""
- local dotLen = sampLen
- while dotLen % 2 == 0 do
- dotLen = dotLen / 2
- dots = dots .. "."
- len = len + dotLen
- if len <= #LengthToNoteTable then
- assert(LengthToNoteTable[len] == "")
- LengthToNoteTable[len] = "c" .. baseLen .. dots
- end
- end
- end
- end
- -- tick
- for sampLen = 2, #LengthToNoteTable do
- local tick = sampLen * (FlMMLTimeBase / SamplesPerNote)
- local tickstring = "c" .. "%" .. tostring(tick)
- if LengthToNoteTable[sampLen] == "" or #LengthToNoteTable[sampLen] > #tickstring then
- LengthToNoteTable[sampLen] = tickstring
- end
- end
- -- continuous notes
- for sampLen = 2, #LengthToNoteTable do
- if LengthToNoteTable[sampLen] == "" or #LengthToNoteTable[sampLen] > sampLen then
- local note = ""
- for index = 1, sampLen do
- note = note .. "c"
- end
- LengthToNoteTable[sampLen] = note
- end
- end
- -- long note
- for sampLen = SamplesPerNote + 1, SamplesPerNote * 2 do
- local note = "c" .. LengthToNoteTable[sampLen - SamplesPerNote]
- if #note <= #LengthToNoteTable[sampLen] then
- LengthToNoteTable[sampLen] = note
- end
- end
- -- combination
- for sampLen = 3, #LengthToNoteTable do
- if #LengthToNoteTable[sampLen] > 2 then
- for i = 1, math.floor(sampLen / 2) do
- j = sampLen - i
- local note = LengthToNoteTable[j] .. LengthToNoteTable[i]
- if #note < #LengthToNoteTable[sampLen] then
- LengthToNoteTable[sampLen] = note
- end
- end
- end
- end
- -- macros
- local LenMacroDefs = ""
- if #LenMacroSymbols > 0 then
- for index = 1, #LenMacroSymbols do
- local sampLen = index + 2
- local macroSymbol =LenMacroSymbols:sub(index, index)
- LenMacroDefs = LenMacroDefs .. "$" .. macroSymbol .. "=" .. LengthToNoteTable[sampLen] .. ";"
- LengthToNoteTable[sampLen] = "$" .. macroSymbol
- end
- end
- -- combination
- for sampLen = 3, #LengthToNoteTable do
- if #LengthToNoteTable[sampLen] > 2 then
- for i = 1, math.floor(sampLen / 2) do
- j = sampLen - i
- local note = LengthToNoteTable[j] .. LengthToNoteTable[i]
- if #note < #LengthToNoteTable[sampLen] then
- LengthToNoteTable[sampLen] = note
- end
- end
- end
- end
- function MakeLenCommand(sampLen)
- local tick = sampLen * (FlMMLTimeBase / SamplesPerNote)
- local noteAbs = "c" .. "%" .. tostring(tick)
- local len = sampLen
- local noteRel = ""
- while len > SamplesPerNote * 2 do
- len = len - (SamplesPerNote * 2)
- noteRel = noteRel .. "c1c1"
- end
- noteRel = noteRel .. LengthToNoteTable[len]
- if #noteRel <= #noteAbs then
- return noteRel
- else
- return noteAbs
- end
- end
- -- [ core subroutines ] ---------------
- local ShowHistogram = false
- function SevenBitiseReal(d)
- return clip(0, (d + 1.0) * 0x40, 0x80)
- end
- function SevenBitise(d)
- return clip(0, math.floor((d + 1.0) * 0x40), 0x7f)
- end
- function ShowUsage()
- local options = {
- { name = "-L, -R", description = "specify target channel (left/right)" }
- }
- print("FlPCM [options] filename.wav (filename.mml)")
- print("Options:")
- for index = 1, #options do
- print(string.format("%s\t%s", options[index].name, options[index].description))
- end
- return
- end
- function PreprocessPCM(PCM)
- local PCM7 = {}
- local curVolume = SevenBitiseReal(PCM[1])
- for index = 1, #PCM do
- local nextVolume = SevenBitiseReal(PCM[index])
- local diff = nextVolume - curVolume
- local distance = math.abs(diff)
- PCM7[index] = clip(0, round(curVolume + diff), 0x7f)
- curVolume = nextVolume
- end
- return PCM7
- end
- function MakeHistogram(PCM7)
- local VolHistogram = {}
- local LenHistogram = {}
- for index = 1, 127 do
- VolHistogram[index] = 0
- end
- for index = 1, SamplesPerNote * 2 + 1 do
- LenHistogram[index] = 0
- end
- if #PCM7 <= 1 then
- return VolHistogram, LenHistogram
- end
- local curVolume = PCM7[1]
- local sampLen = 0
- for index = 1, #PCM7 do
- local nextVolume = PCM7[index]
- local diff = nextVolume - curVolume
- local distance = math.abs(diff)
- if distance ~= 0 then
- if sampLen > SamplesPerNote then
- LenHistogram[SamplesPerNote * 2 + 1] = LenHistogram[SamplesPerNote * 2 + 1] + 1
- else
- LenHistogram[sampLen] = LenHistogram[sampLen] + 1
- end
- VolHistogram[distance] = VolHistogram[distance] + 1
- sampLen = 0
- end
- sampLen = sampLen + 1
- curVolume = nextVolume
- end
- if sampLen > SamplesPerNote then
- LenHistogram[SamplesPerNote * 2 + 1] = LenHistogram[SamplesPerNote * 2 + 1] + 1
- else
- LenHistogram[sampLen] = LenHistogram[sampLen] + 1
- end
- return VolHistogram, LenHistogram
- end
- function ToFlPCM(MMLFile, PCM, PCMInfo)
- local PCM7 = PreprocessPCM(PCM)
- MMLFile:write(VolMacroDefs)
- MMLFile:write(LenMacroDefs)
- local sampLen = 0
- local curVolume = PCM7[1]
- MMLFile:write("$wv=@e1,0,0,128,0@5@w8q16l96")
- MMLFile:write(MakeVolCommandAbs(curVolume))
- for sampleIndex = 1, #PCM7 do
- local nextVolume = PCM7[sampleIndex]
- if curVolume ~= nextVolume then
- MMLFile:write(MakeLenCommand(sampLen))
- sampLen = 0
- end
- sampLen = sampLen + 1
- MMLFile:write(MakeVolCommand(curVolume, nextVolume))
- curVolume = nextVolume
- end
- if sampLen > 0 then
- MMLFile:write(MakeLenCommand(sampLen))
- end
- MMLFile:write(";\n")
- MMLFile:write("t" .. round(PCMInfo.samplerate * 60 / (SamplesPerNote / 4)) .. "\n")
- MMLFile:write(" $wv;\n")
- MMLFile:write("r384$wv;\n")
- MMLFile:write("r192$wv;\n")
- MMLFile:write("r128$wv;\n")
- if ShowHistogram then
- local VolHistogram, LenHistogram = MakeHistogram(PCM7)
- print("#", "vol", "len")
- for index = 1, math.max(127, SamplesPerNote * 2 + 1) do
- print(index,
- ((index <= 127) and VolHistogram[index] or ""),
- ((index <= SamplesPerNote * 2 + 1) and LenHistogram[index] or ""),
- ((index <= SamplesPerNote * 2) and MakeLenCommand(index) or ""))
- end
- end
- end
- -- [ application main ] ---------------
- local WhichChannel = 1
- -- parse commandline parameters
- local argi = 1
- if #arg == 0 then
- ShowUsage()
- return
- end
- while argi <= #arg and arg[argi]:sub(1, 1) == "-" do
- if arg[argi]:sub(2, 2) == "-" then
- local thisArg = arg[argi]:sub(3)
- argi = argi + 1
- if thisArg == "histogram" then
- ShowHistogram = true
- else
- print("Error: unknown option [--" .. thisArg .. "]")
- return
- end
- else
- local argCharIndex = 2
- local thisArg = arg[argi]:sub(2)
- argi = argi + 1
- for argCharIndex = 1, #thisArg do
- local thisArgChar = thisArg:sub(argCharIndex, argCharIndex)
- if thisArgChar == "L" then
- WhichChannel = 1
- elseif thisArgChar == "R" then
- WhichChannel = 2
- else
- print("Error: unknown option [-" .. thisArgChar .. "]")
- return
- end
- end
- end
- end
- if argi > #arg then
- print("Error: no input files")
- return
- elseif argi + 1 < #arg then
- print("Error: too many parameters")
- return
- end
- local WavFilePath = arg[argi]
- local MMLFile = io.stdout
- if argi + 1 == #arg then
- MMLFile = io.open(arg[argi + 1], "w")
- end
- local PCM, PCMInfo = ReadWaveFile(WavFilePath)
- if WhichChannel < 1 or WhichChannel > #PCM then
- WhichChannel = 1
- end
- if #PCM == 0 or #PCM[WhichChannel] == 0 then
- print("Error: Unsupported format")
- return
- end
- io.stderr:write(string.format("FlPCM: %d Hz, %d bit, %d channels, %d samples\n",
- PCMInfo.samplerate, PCMInfo.bitwidth, PCMInfo.channels, #PCM[WhichChannel]))
- ToFlPCM(MMLFile, PCM[WhichChannel], PCMInfo)
- MMLFile:close()
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement