Advertisement
Guest User

conv_sd.lua

a guest
Aug 23rd, 2018
262
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Lua 15.58 KB | None | 0 0
  1. -- conversion fichier video en fichier sd-drive
  2. --
  3. -- version alpha 0.01
  4. ---
  5. -- Samuel DEVULDER Aout 2018
  6.  
  7. -- code experimental. essaye de determiner
  8. -- les meilleurs parametres (fps, taille ecran
  9. -- pour respecter le fps ci-dessous.
  10.  
  11. -- gray peut etre true ou false suivant qu'on
  12. -- veut une sortie couleur ou pas. Le gris est
  13. -- generalement plus rapide et/ou avec un ecran
  14. -- plus large.
  15.  
  16. -- Work in progress!
  17. -- le code doit etre nettoye et rendu plus
  18. -- amical pour l'utilisateur
  19.  
  20. local tmp = 'tmp'
  21. local img_pattern =  tmp..'/img%05d.bmp'
  22. local cycles = 199 -- cycles par échantillons audio
  23. local hz = math.floor(8000000/cycles+.5)/8
  24. local fps = 10
  25. local gray = true
  26. local dither = 2
  27. local ffmpeg = 'ffmpeg'
  28.  
  29. local file = arg[1]:gsub('^/cygdrive/(%w)/','%1:/')
  30.  
  31. local function exists(file)
  32.    local ok, err, code = os.rename(file, file)
  33.    if not ok then
  34.       if code == 13 then
  35.          -- Permission denied, but it exists
  36.          return true
  37.       end
  38.    end
  39.    return ok, err
  40. end
  41.  
  42. local function isdir(file)
  43.     return exists(file..'/')
  44. end
  45.  
  46. if not exists(file) then os.exit(0) end
  47.  
  48. -- nom fichier
  49. io.stdout:write('\n'..file..'\n')
  50. io.stdout:flush()
  51.  
  52. -- recherche la bonne taille d'image
  53. local x,y = 80,45
  54. local IN,line = assert(io.popen(ffmpeg..' -i "'..file ..'" 2>&1', 'r'))
  55. for line in IN:lines() do
  56.     local h,m,s = line:match('Duration: (%d+):(%d+):(%d+%.%d+),')
  57.     if h and m and s then duration = h*3600 + m*60 +s end
  58.     local a,b = line:match(', (%d+)x(%d+)')
  59.     if a and b then x,y=a,b end
  60. end
  61. IN:close()
  62. if not duration then error("Can't get duration!") end
  63. local function gcd(a,b)
  64.     while a ~= 0 do
  65.         a, b = b % a, a;
  66.     end
  67.     return b
  68. end
  69. local aspect_ratio=gcd(math.floor(x/4),math.floor(y/4))
  70. local max_ar = 1e38
  71. for i=2,10 do
  72.     local t = x*i/y
  73.     t = math.abs(t-math.floor(t+.5))
  74.     if t<max_ar then
  75.         max_ar = t
  76.         aspect_ratio = math.floor(x*i/y+.5)..':'..i
  77.     end
  78. end
  79. local w = 80
  80. local h = math.floor(w*y/x+.5)
  81. if h>50 then
  82.    h = 50
  83.    w = math.floor(h*x/y+.5)
  84. end
  85.  
  86. -- flux audio
  87. local AUDIO = {}
  88. function AUDIO:new(file, hz)
  89.     local o = {
  90.         stream = assert(io.popen(ffmpeg..' -i "'..file ..'" -v 0 -f u8 -ac 1 -ar '..math.floor(.5+8*hz)..' -acodec pcm_u8 -', 'rb')),
  91.         cor = {8,255}, -- volume auto
  92.         buf = '', -- buffer
  93.         running = true
  94.     }
  95.     setmetatable(o, self)
  96.     self.__index = self
  97.     return o
  98. end
  99. function AUDIO:close()
  100.     self.stream:close()
  101. end
  102. function AUDIO:next_sample()
  103.     local buf = self.buf
  104.     if buf:len()<=8 then
  105.         local t = self.stream:read(8192)
  106.         if not t then
  107.             self.running = false
  108.             t = string.char(0,0,0,0,0,0,0,0)
  109.         end
  110.         buf = buf .. t
  111.     end
  112.     local v = (buf:byte(1) + buf:byte(2) + buf:byte(3) + buf:byte(4) +
  113.                 buf:byte(5) + buf:byte(6) + buf:byte(7) + buf:byte(8))*.125
  114.     self.buf = buf:sub(9)
  115.     -- auto volume
  116.     if v<self.cor[2]     then self.cor[2]=v end
  117.     v = v-self.cor[2]
  118.     if v*self.cor[1]>255 then self.cor[1]=255/v end
  119.     v = v*self.cor[1]
  120.     -- dither
  121.     v = math.min(v + math.random(0,3), 255)
  122.     return math.floor(v/4)
  123. end
  124.  
  125. -- flux video
  126. local VIDEO = {}
  127. function VIDEO:new(file, w, h, fps, gray)
  128.     if isdir(tmp) then
  129.         os.execute('del >nul /s /q '..tmp)
  130.     else
  131.         os.execute('md '..tmp)
  132.     end
  133.  
  134.     local o = {
  135.         cpt = 1, -- compteur image
  136.         width = w,
  137.         height = h,
  138.         fps = fps or 10,
  139.         gray = gray or false,
  140.         image = {},
  141.         dither = nil,
  142.         expected_size = 54 + h*(math.floor((w*3+3)/4)*4),
  143.         running=true,
  144.         streams = {
  145.             inp = assert(io.open(file, 'rb')),
  146.             out = assert(io.popen(ffmpeg..' -i - -v 0 -r '..fps..' -s '..w..'x'..h..' -an '..img_pattern, 'wb')),
  147.         }
  148.     }
  149.     setmetatable(o, self)
  150.     self.__index = self
  151.    
  152.     return o
  153. end
  154. function VIDEO:close()
  155.     if io.type(self.streams.inp)=='file' then self.streams.inp:close() end
  156.     if io.type(self.streams.out)=='file' then self.streams.out:close() end
  157. end
  158. function VIDEO:init_dither()
  159.     local function bayer(t)
  160.         local m=#t
  161.         local n=#t[1]
  162.         local d={}
  163.         for i=1,2*m do
  164.             d[i] = {}
  165.             for j=1,2*n do
  166.                 d[i][j] = 0
  167.             end
  168.         end
  169.         for i=1,m do
  170.             for j=1,n do
  171.                 local z = 4*t[i][j]
  172.                 d[m*0+i][n*0+j] = z-3
  173.                 d[m*1+i][n*1+j] = z-2
  174.                 d[m*1+i][n*0+j] = z-1
  175.                 d[m*0+i][n*1+j] = z-0
  176.             end
  177.         end
  178.         return d
  179.     end
  180.     local m = {{1}}
  181.     for i=1,dither do m = bayer(m) end
  182.     local x = 0
  183.     for i=1,#m do
  184.         for j=1,#m[1] do
  185.             x = math.max(x, m[i][j])
  186.         end
  187.     end
  188.     x = 1/(x + 1)
  189.     for i = 1,#m do
  190.         for j = 1,#m[1] do
  191.             m[i][j] = m[i][j]*x
  192.         end
  193.     end
  194.     m.w = #m
  195.     m.h = #m[1]
  196.     function m:get(i,j)
  197.         return self[1+(i % self.w)][1+(j % self.h)]
  198.     end
  199.     self.dither = m
  200. end
  201. function VIDEO:linear(u)
  202.     return u<0.04045 and u/12.92 or (((u+0.055)/1.055)^2.4)
  203. end
  204. function VIDEO:pset(x,y, r,g,b)
  205.     if not self._linear then
  206.         self._linear = {}
  207.         for i=0,255 do self._linear[i] = self:linear(i/255) end
  208.     end
  209.     r,g,b = self._linear[r],self._linear[g],self._linear[b]
  210.     if not self.dither then VIDEO:init_dither() end
  211.     local d = self.dither:get(x,y)
  212.    
  213.     local o,p = x%2,math.floor(x/2) + y*160
  214.     local function v(v)
  215.         assert(0<=v and v<=3, 'v=' .. v)
  216.         local t = self.image[p]
  217.         if o==0 then
  218.             t = (t%4) + 4*v
  219.         else
  220.             t = (t-(t%4)) + v
  221.         end
  222.         self.image[p] = t
  223.         p = p+40
  224.     end
  225.    
  226.     if self.gray then
  227.         r = (.2126*r + .7152*g + .0722*b)*9 + d
  228.         if     r>=4 then    v(3)
  229.         elseif r>=2 then    v(2)
  230.         elseif r>=1 then    v(1)
  231.         else                v(0)   
  232.         end
  233.         if     r>=7 then    v(3)
  234.         elseif r>=5 then    v(2)
  235.         elseif r>=3 then    v(1)
  236.         else                v(0)   
  237.         end
  238.         if     r>=9 then    v(3)
  239.         elseif r>=8 then    v(2)
  240.         elseif r>=6 then    v(1)
  241.         else                v(0)   
  242.         end
  243.     else
  244.         v(math.floor(r*3 + d))
  245.         v(math.floor(g*3 + d))
  246.         v(math.floor(b*3 + d))
  247.     end
  248. end
  249. function VIDEO:read_bmp(bytecode) -- (https://www.gamedev.net/forums/topic/572784-lua-read-bitmap/)
  250.     -- Helper function: Parse a 16-bit WORD from the binary string
  251.     local function ReadWORD(str, offset)
  252.         local loByte = str:byte(offset);
  253.         local hiByte = str:byte(offset+1);
  254.         return hiByte*256 + loByte;
  255.     end
  256.  
  257.     -- Helper function: Parse a 32-bit DWORD from the binary string
  258.     local function ReadDWORD(str, offset)
  259.         local loWord = ReadWORD(str, offset);
  260.         local hiWord = ReadWORD(str, offset+2);
  261.         return hiWord*65536 + loWord;
  262.     end
  263.    
  264.     -------------------------
  265.     -- Parse BITMAPFILEHEADER
  266.     -------------------------
  267.     local offset = 1;
  268.     local bfType = ReadWORD(bytecode, offset);
  269.     if(bfType ~= 0x4D42) then
  270.         error("Not a bitmap file (Invalid BMP magic value)");
  271.         return;
  272.     end
  273.     local bfOffBits = ReadWORD(bytecode, offset+10);
  274.  
  275.     -------------------------
  276.     -- Parse BITMAPINFOHEADER
  277.     -------------------------
  278.     offset = 15; -- BITMAPFILEHEADER is 14 bytes long
  279.     local biWidth = ReadDWORD(bytecode, offset+4);
  280.     local biHeight = ReadDWORD(bytecode, offset+8);
  281.     local biBitCount = ReadWORD(bytecode, offset+14);
  282.     local biCompression = ReadDWORD(bytecode, offset+16);
  283.     if(biBitCount ~= 24) then
  284.         error("Only 24-bit bitmaps supported (Is " .. biBitCount .. "bpp)");
  285.         return;
  286.     end
  287.     if(biCompression ~= 0) then
  288.         error("Only uncompressed bitmaps supported (Compression type is " .. biCompression .. ")");
  289.         return;
  290.     end
  291.  
  292.     ---------------------
  293.     -- Parse bitmap image
  294.     ---------------------
  295.     local ox = math.floor((80 - biWidth)/4)*2
  296.     local oy = math.floor((50 - biHeight)/2)
  297.     local oo = 4*math.floor((biWidth*biBitCount/8 + 3)/4)
  298.     for y = biHeight-1, 0, -1 do
  299.         offset = bfOffBits + oo*y + 1;
  300.         for x = 0, biWidth-1 do
  301.             local b = bytecode:byte(offset);
  302.             local g = bytecode:byte(offset+1);
  303.             local r = bytecode:byte(offset+2);
  304.             offset = offset + 3;
  305.  
  306.             self:pset(ox + x, oy + biHeight-y-1, r, g, b);
  307.         end
  308.     end
  309. end
  310. function VIDEO:next_image()
  311.     if not self.running then return end
  312.    
  313.     -- nom nouvelle image
  314.     local name = img_pattern:format(self.cpt); self.cpt = self.cpt + 1
  315.     local buf = ''
  316.     local f = io.open(name,'rb')
  317.     if f then
  318.         buf = f:read(self.expected_size) or buf
  319.         f:close()
  320.     end
  321.        
  322.     -- si pas la bonne taille, on nourrit ffmpeg
  323.     -- jusqu'a obtenir un fichier BMP complet
  324.     local timeout = 5
  325.     while buf:len() ~= self.expected_size and timeout>0 do
  326.         buf = self.streams.inp:read(8192)
  327.         if buf then
  328.             self.streams.out:write(buf)
  329.             self.streams.out:flush()
  330.         else
  331.             if io.type(self.streams.out)=='file' then
  332.                 self.streams.out:close()
  333.             end
  334.             -- io.stdout:write('wait ' .. name ..'\n')
  335.             -- io.stdout:flush()
  336.             local t=os.time()+1
  337.             repeat until os.time()>t
  338.             timeout = timeout - 1
  339.         end
  340.         f = io.open(name,'rb')
  341.         if f then
  342.             buf = f:read(self.expected_size) or ''
  343.             f:close()
  344.             timeout = 5
  345.         else
  346.             buf = ''
  347.         end
  348.     end
  349.    
  350.     -- effacement temporaire
  351.     os.remove(name)
  352.  
  353.     if buf and buf:len()>0 then
  354.         -- nettoyage de l'image précédente
  355.         for i=0,7999+3 do self.image[i]=0 end
  356.         -- lecture image
  357.         self:read_bmp(buf)
  358.     else
  359.         self.running = false
  360.     end
  361. end
  362.  
  363. -- auto determination des parametres
  364. local stat = VIDEO:new(file,w,h,3,gray)
  365. stat.super_pset = stat.pset
  366. stat.histo = {n=0}; for i=0,255 do stat.histo[i]=0 end
  367. function stat:pset(x,y, r,g,b)
  368.     stat.histo[r] = stat.histo[r]+1
  369.     stat.histo[g] = stat.histo[g]+1
  370.     stat.histo[b] = stat.histo[b]+1
  371.     self:super_pset(x,y,r,g,b)
  372. end
  373. stat.super_next_image = stat.next_image
  374. stat.mill = {'|', '/', '-', '\\'}
  375. stat.mill[0] = stat.mill[4]
  376. function stat:next_image()
  377.     self:super_next_image()
  378.     io.stderr:write(string.format('> analyzing...%s %d%%\r', self.mill[self.cpt % 4], math.floor(100*(self.cpt/self.fps)/duration)))
  379.     io.stderr:flush()
  380. end
  381. stat.trames = 0
  382. stat.prev_img = {}
  383. for i=0,7999 do stat.prev_img[i]=-1 end
  384. function stat:count_trames()
  385.     local pos,prev,curr = 0,stat.prev_img,stat.image
  386.     for i=0,7999 do
  387.         if prev[i] ~= curr[i] then
  388.             stat.trames = stat.trames + 1
  389.             local k = i - pos
  390.             if k<=2 then
  391.                 prev[pos] = curr[pos]; pos = pos+1
  392.                 prev[pos] = curr[pos]; pos = pos+1
  393.                 prev[pos] = curr[pos]; pos = pos+1
  394.                 prev[pos] = curr[pos]; pos = pos+1
  395.             elseif k<=256 then
  396.                 pos = i
  397.                 prev[pos] = curr[pos]; pos = pos+1
  398.                 prev[pos] = curr[pos]; pos = pos+1
  399.             else
  400.                 pos = i
  401.                 prev[pos] = curr[pos]; pos = pos+1
  402.             end
  403.         end
  404.     end
  405. end
  406. while stat.running do
  407.     stat:next_image()
  408.     stat:count_trames()
  409. end
  410. io.stderr:write(string.rep(' ',79)..'\r')
  411. io.stderr:flush()
  412. local max_trames = 1000000/fps/cycles
  413. local avg_trames = (stat.trames/stat.cpt) * 1.11 -- 11% safety margin
  414. local ratio = max_trames / avg_trames
  415. if ratio>1 then
  416.     fps = math.min(math.floor(fps*ratio),30)
  417. elseif ratio<1 then
  418.     local zoom = ratio^.5
  419.     w=math.floor(w*zoom)
  420.     h=math.floor(h*zoom)
  421. end
  422. stat.total = 0
  423. for i=1,255 do
  424.     stat.total = stat.total + stat.histo[i]
  425. end
  426. stat.threshold_min = (gray and .03 or .03)*stat.total
  427. stat.min = 0
  428. for i=1,255 do
  429.     stat.min = stat.min + stat.histo[i]
  430.     if stat.min>stat.threshold_min then
  431.         stat.min = i-1
  432.         break
  433.     end
  434. end
  435. stat.max = 0
  436. stat.threshold_max = (gray and .03 or .05)*stat.total
  437. for i=254,1,-1 do
  438.     stat.max = stat.max + stat.histo[i]
  439.     if stat.max>stat.threshold_max then
  440.         stat.max = i+1
  441.         break
  442.     end
  443. end
  444. -- print(stat.min, stat.max)
  445. -- io.stdout:flush()
  446. local video_cor = {stat.min, 255/(stat.max - stat.min)}
  447.  
  448. -- fichier de sortie
  449. local OUT = assert(io.open(file:gsub('.*[/\\]',''):gsub('%.[%a%d]+','')..'.sd', 'wb'))
  450.  
  451. function file_content(file, size)
  452.     local INP = assert(io.open(file, 'rb'))
  453.     local buf = ''
  454.     while true do
  455.         local t = INP:read(256)
  456.         if not t then break end
  457.         buf = buf .. t .. string.rep(string.char(0),512-t:len())
  458.     end
  459.     size = size - buf:len()
  460.     if size<0 then
  461.         print('size',size)
  462.         error('File ' .. file .. ' is too big')
  463.     end
  464.     return buf .. string.rep(string.char(0),size)
  465. end
  466. OUT:write(file_content('bin/bootblk.raw', 512))
  467. OUT:write(file_content(gray and 'bin/player1.raw' or 'bin/player0.raw', 7*512))
  468.  
  469. -- conversion
  470.  
  471. local audio = AUDIO:new(file, hz)
  472. local video = VIDEO:new(file,w,h,fps,gray)
  473. local tstamp = 0
  474. local start = os.time()
  475. local cycles_per_img = 1000000 / fps
  476. local current_cycle  = cycles_per_img
  477. local completed, completed_imgs = 0,0
  478. local pos = 0
  479. local prev_img = {}
  480. for i=0,7999+3 do prev_img[i] = -1 end
  481. local blk = ''
  482.  
  483. video.super_pset = video.pset
  484. function video:pset(x,y, r,g,b)
  485.     local function f(x)
  486.         x = math.floor(.5 + (x-video_cor[1])*video_cor[2]);
  487.         return x<0 and 0 or x>255 and 255 or x
  488.     end
  489.     self:super_pset(x,y, f(r),f(g),f(b))
  490. end
  491.  
  492. function trame_video()
  493.     if current_cycle >= cycles_per_img then
  494.         current_cycle = current_cycle - cycles_per_img
  495.         -- image complete ?
  496.         if completed==0 then
  497.             for i=0,7999 do if prev_img[i]==video.image[i] then completed = completed + 1 end end
  498.             completed = completed / 8000
  499.         end
  500.         completed_imgs = completed_imgs + completed
  501.         completed = 0
  502.         -- infos
  503.         if video.cpt % video.fps == 0 then
  504.             tstamp = tstamp + 1
  505.             local d = os.time() - start; if d==0 then d=1000000000 end
  506.             local t = "> %d%% %d:%02d:%02d (%3.1fx) e=%5.3f a=(x%+d)*%.1g         \r"
  507.             t = t:format(
  508.                 math.floor(100*tstamp/duration+.5),
  509.                 math.floor(tstamp/3600), math.floor(tstamp/60)%60, tstamp%60,
  510.                 math.floor(100*tstamp/d)/100, completed_imgs/video.cpt,
  511.                 -audio.cor[2], audio.cor[1])
  512.             io.stdout:write(t)
  513.             io.stdout:flush()
  514.         end
  515.         -- image suivante
  516.         video:next_image()
  517.         if false then
  518.             -- force retour au début
  519.             local buf = {audio:next_sample()*4,0,0}
  520.             pos = 0
  521.             buf[1] = buf[1] + 2 + math.floor(pos/4096)
  522.             buf[2] = math.floor(pos/16) % 256
  523.             buf[3] = (pos%16)*16 + video.image[pos]
  524.             prev_img[pos] = video.image[pos]; pos = pos + 1
  525.             return string.char(buf[1], buf[2], buf[3])
  526.         end
  527.     end
  528.    
  529.    
  530.     local buf = {audio:next_sample()*4,0,0}
  531.     local k
  532.     if completed==0 then
  533.         for i=pos,7999 do
  534.             if prev_img[i] ~= video.image[i] then
  535.                 k = i - pos
  536.                 pos = i
  537.                 break
  538.             end
  539.         end
  540.         if not k then
  541.             for i=0,pos-1 do
  542.                 if prev_img[i] ~= video.image[i] then
  543.                     k = 8000
  544.                     pos = i
  545.                     break
  546.                 end
  547.             end
  548.         end
  549.     end
  550.    
  551.     if not k then
  552.         -- aucun changement
  553.         completed = 1
  554.         pos = 7999
  555.         k = 8000
  556.     end
  557.    
  558.     if k<=2 then
  559.         -- deplacement trop faible: mise a jour des 4 octets videos suivants d'un coup
  560.         pos = pos - k
  561.         buf[1] = buf[1] + 1
  562.         buf[2] = video.image[pos+0]*16 + video.image[pos+1]
  563.         buf[3] = video.image[pos+2]*16 + video.image[pos+3]
  564.         prev_img[pos] = video.image[pos]; pos = pos+1
  565.         prev_img[pos] = video.image[pos]; pos = pos+1
  566.         prev_img[pos] = video.image[pos]; pos = pos+1
  567.         prev_img[pos] = video.image[pos]; pos = pos+1
  568.     elseif k<=256 then
  569.         -- deplacement 8 bit
  570.         buf[2] = k%256
  571.         buf[3] = video.image[pos+0]*16 + video.image[pos+1]
  572.         prev_img[pos] = video.image[pos]; pos = pos+1
  573.         prev_img[pos] = video.image[pos]; pos = pos+1
  574.     else
  575.         -- deplacement arbitraire
  576.         buf[1] = buf[1] + 2 + math.floor(pos/4096)
  577.         buf[2] = math.floor(pos/16) % 256
  578.         buf[3] = (pos%16)*16 + video.image[pos]
  579.         prev_img[pos] = video.image[pos]; pos = pos + 1
  580.     end
  581.     return string.char(buf[1], buf[2], buf[3])
  582. end
  583.  
  584. function trame_fin()
  585.     local s1 = audio:next_sample()
  586.     local s2 = audio:next_sample()
  587.     local s3 = audio:next_sample()
  588.    
  589.     local t = s1*1024 + math.floor(s2/2)*32 + math.floor(s3/2)
  590.    
  591.     return string.char(math.floor(t/256), t%256)
  592. end
  593.  
  594. io.stdout:write('> '..w..'x'..h..' ('..aspect_ratio..') '..duration..'s at '..fps..'fps\n')
  595. io.stdout:flush()
  596.  
  597. while audio.running do
  598.     if blk:len() < 3*170 then
  599.         blk = blk..trame_video()
  600.         current_cycle = current_cycle + cycles
  601.     else
  602.         blk = blk..trame_fin()
  603.         current_cycle = current_cycle + cycles*3
  604.         OUT:write(blk)
  605.         blk = ''
  606.     end
  607. end
  608. if blk:len()==3*170 then
  609.     OUT:write(blk .. string.char(0,0))
  610.     blk = ''
  611. end
  612. OUT:write(blk)
  613. OUT:write(string.rep(string.char(255),512-blk:len()))
  614. OUT:write(string.rep(string.char(255),512))
  615. OUT:close()
  616. audio:close()
  617. video:close()
  618. io.stdout:write('\n')
  619. io.stdout:flush()
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement