2012年4月18日水曜日

32bitDIBから無圧縮AVIファイル

AVIFileCreateStreamで作成してもいいですが、
あらかじめ動画の長さを指定する必要があったりと地味に使いづらい部分があったりするので、
自力でAVI出力することで好き勝手しようという趣旨です。
もっともAVI1.0フォーマットの都合上2GB以上は記録できない(オーバーフローするため)
ということになってるので注意が必要。


ソースコード
size: 42KB



このような動画が出力されます。



AVIRecorder2 avi;
avi.create( fileName, width, height, fps );
avi.update( dib32Image );
...
avi.close();

といった感じに使います。
好きなタイミングでclose()すれば、update()回数分のフレーム数のAVIファイルを作成できます。


■AVIファイルフォーマット
RIFF - AVI (fileSize - 8)
├─LIST - hdrl (200 byte)
│ ├─avih (56 byte)
│ │ ├─dwMicroSecPerFrame
│ │ ├─dwMaxBytesPerSec
│ │ ├─dwPaddingGranularity
│ │ ├─dwFlags
│ │ ├─dwTotalFrames
│ │ ├─dwInitialFrames
│ │ ├─dwSuggestedBufferSize
│ │ ├─dwWidth
│ │ ├─dwHeight
│ │ └─dwReserved[4]
│ │
│ ├─LIST - strl (124 byte)
│ │ ├─strh (64 byte)
│ │ │ ├─fccType
│ │ │ ├─fccHeader
│ │ │ ├─dwFlags
│ │ │ ├─dwPriority
│ │ │ ├─dwLanguage
│ │ │ ├─dwInitialFrames
│ │ │ ├─dwScale
│ │ │ ├─dwRate
│ │ │ ├─dwStart
│ │ │ ├─dwLength
│ │ │ ├─dwSuggestedBufferSize
│ │ │ ├─dwQuality
│ │ │ ├─dwSampleSize
│ │ │ └─rcFrame
│ │ │
│ │ └─strf (40 byte)
│ │   ├─biSize
│ │   ├─biWidth
│ │   ├─biHeight
│ │   ├─biPlanes
│ │   ├─biBitCount
│ │   ├─biSizeImage
│ │   ├─biXPelsPerMeter
│ │   ├─biYPelsPerMeter
│ │   ├─biClrUsed
│ │   └─biClrImportant
│ │
│ └JUNK (1808 byte)

├ LIST - movi
│ ├00db
│ ├00db
│ └00db...

└idx1



以下プログラム。
#include <windows.h>
#include <vfw.h>
#include <algorithm>
 
 
 
const int WINDOW_WIDTH = 640;
const int WINDOW_HEIGHT = 480;
const int TIMER_ID = 1000;
const int FPS = 30;
 
 
 
class DIB32
{
public:
  /**
   * Point
   */
  struct Point
  {
    LONG x, y;
    LONG u, v;
 
    void set( LONG x, LONG y, LONG u, LONG v )
    {
      this->x = x;
      this->y = y;
      this->u = u;
      this->v = v;
    }
  };
 
  class PutColorAdd
  {
  public:
    static inline void update( DWORD& dest, DWORD src )
    {
      DWORD color = (dest & 0x00fefefe) + (src & 0x00fefefe);
      DWORD mask = color & 0x01010100;
      dest = (dest&0xFF000000) | color | (mask - (mask >> 8));
    }
  };
 
 
 
 
  DIB32()
    : m_pixel( NULL )
  {
    ZeroMemory( &m_bmi, sizeof(m_bmi) );
  }
 
  virtual ~DIB32()
  {
    release();
  }
 
  virtual bool create( LONG width, LONG height )
  {
    if ( width <= 0 || height <= 0 ) return false;
    release();
 
    m_pixel = new DWORD[ width * height ];
    if ( !m_pixel ) return false;
 
    m_bmi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
    m_bmi.bmiHeader.biWidth = width;
    m_bmi.bmiHeader.biHeight = height;
    m_bmi.bmiHeader.biPlanes = 1;
    m_bmi.bmiHeader.biBitCount = 32;
    m_bmi.bmiHeader.biSizeImage = width * height * 4;
    m_bmi.bmiHeader.biCompression = BI_RGB;
    m_bmi.bmiHeader.biXPelsPerMeter = 3780;  //96dpiだと3780らしい。0の場合もあるとのこと
    m_bmi.bmiHeader.biYPelsPerMeter = 3780;
 
    return true;
  }
 
  /** create from HBITMAP */
  bool create( LPCTSTR fileName )
  {
    HBITMAP hBmp = static_cast<HBITMAP>( LoadImage( NULL, fileName, IMAGE_BITMAP, 0, 0, LR_CREATEDIBSECTION | LR_LOADFROMFILE ) );
    if ( !hBmp ) return false;
 
    BITMAP bm = {0};
    GetObject( hBmp, sizeof(BITMAP), &bm );
 
    // create DC
    HDC hdc = CreateCompatibleDC( NULL );
    if ( !hdc ) return false;
 
    // create buf
    if ( !create( bm.bmWidth, bm.bmHeight ) )
    {
      DeleteDC( hdc );
      return false;
    }
 
    // copy
    GetDIBits( hdc, hBmp, 0, bm.bmHeight, m_pixel, &m_bmi, DIB_RGB_COLORS );
 
    DeleteDC( hdc );
    DeleteObject( hBmp );
 
    return true;
  }
 
  virtual void release()
  {
    if ( m_pixel )
    {
      delete [] m_pixel;
      m_pixel = NULL;
    }
    ZeroMemory( &m_bmi, sizeof(m_bmi) );
  }
 
  bool render( HDC hdc, HWND hWnd ) const
  {
    RECT rect;
    GetClientRect( hWnd, &rect );
    return GDI_ERROR != StretchDIBits(
      hdc, 0, 0, rect.right, rect.bottom,
      0, 0, getWidth(), getHeight(),
      m_pixel, &m_bmi, DIB_RGB_COLORS, SRCCOPY );
  }
 
 
 
  template < class T >
  inline void renderLine(
    LPDWORD destLine,
    LONG destMaxWidth,
    LONG startX, LONG endX,
    float startU, float startV,
    float endU, float endV )
  {
    if ( startX > endX )
    {
      std::swap( startX, endX );
      std::swap( startU, endU );
      std::swap( startV, endV );
    }
 
    if ( startX > destMaxWidth || endX < 0 || (endX-startX)==0 ) return;
 
    const float addU = (endU-startU) / static_cast<float>(endX - startX);
    const float addV = (endV-startV) / static_cast<float>(endX - startX);
 
    // cliping-left
    if ( startX < 0 )
    {
      startU += (addU * static_cast<float>(-startX));
      startV += (addV * static_cast<float>(-startX));
      startX = 0;
    }
    // cliping-right
    if ( endX > destMaxWidth )
    {
      endX = destMaxWidth;
    }
 
    float u = startU;
    float v = startV;
    for (LONG x=startX; x<endX; x++)
    {      
      T::update( *(destLine + x), getPixel(static_cast<LONG>(u), static_cast<LONG>(v)) );
 
      u += addU;
      v += addV;
    }
  }
 
  template < class T >
  bool triangle( DIB32& dest, const Point& point1, const Point& point2, const Point& point3 )
  {
    Point p1( point1 ), p2( point2 ), p3( point3 );
 
    // 手動ソート
    if ( p1.y > p2.y ) std::swap( p1, p2 );
    if ( p1.y > p3.y ) std::swap( p1, p3 );
    if ( p2.y > p3.y ) std::swap( p2, p3 );
 
    const float height1_2 = static_cast<float>( p2.y-p1.y ? p2.y-p1.y : 1 );
    const float height1_3 = static_cast<float>( p3.y-p1.y ? p3.y-p1.y : 1 );
    const float height2_3 = static_cast<float>( p3.y-p2.y ? p3.y-p2.y : 1 );
    
    const float destAddX1_2 = static_cast<float>(p2.x-p1.x) / height1_2;
    const float destAddX1_3 = static_cast<float>(p3.x-p1.x) / height1_3;
    const float destAddX2_3 = static_cast<float>(p3.x-p2.x) / height2_3;
    
    const float addU1_2 = static_cast<float>(p2.u-p1.u) / height1_2;
    const float addU1_3 = static_cast<float>(p3.u-p1.u) / height1_3;
    const float addU2_3 = static_cast<float>(p3.u-p2.u) / height2_3;
    const float addV1_2 = static_cast<float>(p2.v-p1.v) / height1_2;
    const float addV1_3 = static_cast<float>(p3.v-p1.v) / height1_3;
    const float addV2_3 = static_cast<float>(p3.v-p2.v) / height2_3;
    
    // 1~2
    float startX = static_cast<float>( p1.x );
    float endX = static_cast<float>( p1.x );
    float startU = static_cast<float>( p1.u );
    float startV = static_cast<float>( p1.v );
    float endU = static_cast<float>( p1.u );
    float endV = static_cast<float>( p1.v );
    LPDWORD destLine = dest.getPixelAddr( 0, p1.y );
 
    for (LONG y=p1.y; y<p2.y&&y<dest.getHeight(); y++)
    {
      // render line
      if ( y >= 0 )
      {
        renderLine<T>( destLine, dest.getWidth(), static_cast<LONG>(startX), static_cast<LONG>(endX), startU, startV, endU, endV );
      }
 
      startX += destAddX1_2;
      endX += destAddX1_3;
      startU += addU1_2;
      endU += addU1_3;
      startV += addV1_2;
      endV += addV1_3;
 
      destLine -= dest.getWidth();
    }
    // 2~3
    startX = static_cast<float>( p2.x );
    startU = static_cast<float>( p2.u );
    startV = static_cast<float>( p2.v );
    for (LONG y=p2.y; y<p3.y&&y<dest.getHeight(); ++y)
    {
      if ( y >= 0 )
      {
        renderLine<T>( destLine, dest.getWidth(), static_cast<LONG>(startX), static_cast<LONG>(endX), startU, startV, endU, endV );
      }
 
      startX += destAddX2_3;
      endX += destAddX1_3;
      startU += addU2_3;
      endU += addU1_3;
      startV += addV2_3;
      endV += addV1_3;
 
      destLine -= dest.getWidth();
    }
 
    return true;
  }
 
  
  LONG getWidth() const { return m_bmi.bmiHeader.biWidth; }
  LONG getHeight() const { return m_bmi.bmiHeader.biHeight; }
  const LPDWORD getPixelAddr() const { return m_pixel; }
  const LPDWORD getPixelAddr( LONG x, LONG y ) const { return m_pixel + ((getHeight()-1)-y)*getWidth() + x; }
  LPDWORD getPixelAddr() { return m_pixel; }
  LPDWORD getPixelAddr( LONG x, LONG y ) { return const_cast<LPDWORD>(static_cast<const DIB32&>(*this).getPixelAddr(x, y)); }
 
  DWORD getPixel( LONG x, LONG y ) const
  {
    if ( x >= 0 && x < getWidth() && y >= 0 && y < getHeight() )
    {
      return *getPixelAddr( x, y );
    }
    return 0;
  }
  
 
protected:
  LPDWORD m_pixel;
  BITMAPINFO m_bmi;
};
 
 
 
class AVIRecorder2
{
public:
#pragma pack(push, 2)
  struct LIST
  {
    DWORD dwList;
    DWORD dwSize;
    DWORD dwForrCC;
  };
 
  struct CHUNK
  {
    DWORD dwFourCC;
    DWORD dwSize;
  };
 
 
  struct Header
  {
    LIST aviList;
    LIST hdrlList;
    CHUNK avih;
    MainAVIHeader mainAVIHeader;
    LIST strlList;
    CHUNK strh;
    AVIStreamHeader streamHeader;
    CHUNK strf;
    BITMAPINFOHEADER streamFormat;
    CHUNK junk;
    BYTE junkData[ 2048 - 240 ];  // moveList含めて2048Byteになるように調整。240=sizeof(Header)-sizeof(junkData)
 
    LIST moviList;
 
    void setData( DWORD width, DWORD height, DWORD fps = 60, DWORD frame = 0 )
    {
      ZeroMemory( this, sizeof(*this) );
 
      aviList.dwList = mmioFOURCC('R','I','F','F');
      aviList.dwSize = 2048-8 + (sizeof(CHUNK)+width*height*4)*frame + sizeof(CHUNK) + sizeof(AVIINDEXENTRY)*frame;
      aviList.dwForrCC = mmioFOURCC('A','V','I',' ');
 
      hdrlList.dwList = mmioFOURCC('L','I','S','T');
      hdrlList.dwSize = 4 + sizeof(avih) + sizeof(mainAVIHeader) + 
        sizeof(strlList) + sizeof(strh) + sizeof(streamHeader) + sizeof(strf) + sizeof(streamFormat);
      hdrlList.dwForrCC = mmioFOURCC('h','d','r','l');
 
      avih.dwFourCC = mmioFOURCC('a','v','i','h');
      avih.dwSize = sizeof(mainAVIHeader);
 
      mainAVIHeader.dwMicroSecPerFrame = 1000000 / fps;
      mainAVIHeader.dwMaxBytesPerSec = width*height*4*fps;
      mainAVIHeader.dwPaddingGranularity = 0;
      mainAVIHeader.dwFlags = 2064;
      mainAVIHeader.dwTotalFrames = frame;
      mainAVIHeader.dwInitialFrames = 0;
      mainAVIHeader.dwStreams = 1;
      mainAVIHeader.dwSuggestedBufferSize = width * height * 4 + sizeof(CHUNK);
      mainAVIHeader.dwWidth = width;
      mainAVIHeader.dwHeight = height;
 
      strlList.dwList = mmioFOURCC('L','I','S','T');
      strlList.dwSize = 4 + sizeof(strh) + sizeof(streamHeader) + sizeof(strf) + sizeof(streamFormat);
      strlList.dwForrCC = mmioFOURCC('s','t','r','l');
 
      strh.dwFourCC = mmioFOURCC('s','t','r','h');
      strh.dwSize = sizeof(streamHeader);
 
      streamHeader.fccType = mmioFOURCC('v','i','d','s');
      streamHeader.fccHandler = mmioFOURCC('D','I','B',' ');
      streamHeader.dwFlags = 0;
      streamHeader.wPriority = 0;
      streamHeader.wLanguage = 0;
      streamHeader.dwInitialFrames = 0;
      streamHeader.dwScale = 1;
      streamHeader.dwRate = fps;
      streamHeader.dwStart = 0;
      streamHeader.dwLength = frame;
      streamHeader.dwSuggestedBufferSize = width * height * 4 + sizeof(CHUNK);
      streamHeader.dwQuality = -1;
      streamHeader.dwSampleSize = width * height * 4;
 
      strf.dwFourCC = mmioFOURCC('s','t','r','f');
      strf.dwSize = sizeof(streamFormat);
 
      streamFormat.biSize = sizeof(streamFormat);
      streamFormat.biWidth = width;
      streamFormat.biHeight = height;
      streamFormat.biPlanes = 1;
      streamFormat.biBitCount = 32;
      streamFormat.biCompression = 0;
      streamFormat.biSizeImage = width * height * 4;
      streamFormat.biXPelsPerMeter = 3780;
      streamFormat.biYPelsPerMeter = 3780;
      streamFormat.biClrUsed = 0;
      streamFormat.biClrImportant = 0;
 
      junk.dwFourCC = mmioFOURCC('J','U','N','K');
      junk.dwSize = sizeof(junkData);
 
      moviList.dwList = mmioFOURCC('L','I','S','T');
      moviList.dwSize = 4 + sizeof(CHUNK)*frame + width*height*4*frame;
      moviList.dwForrCC = mmioFOURCC('m','o','v','i');
    }
  };
#pragma pack(pop)
 
  /** constructor */
  AVIRecorder2()
    : m_file( INVALID_HANDLE_VALUE )
    , m_frame( 0 )
    , m_width( 320 )
    , m_height( 240 )
    , m_fps( 60 )
  {
  }
 
  /** desturctor */
  ~AVIRecorder2()
  {
    clear();
  }
 
  void clear()
  {
    close();
    m_frame = 0;
    m_fps = 60;
    m_width = 320;
    m_height = 240;
  }
 
  /** isOpen */
  bool isOpen() const { return m_file!=INVALID_HANDLE_VALUE; }
 
  /** create */
  bool create( LPCTSTR fileName, DWORD width, DWORD height, DWORD fps )
  {
    m_file = CreateFile( fileName, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL );
    if ( isOpen() )
    {
      m_width = width;
      m_height = height;
      m_fps = fps;
 
      Header header;
      header.setData( width, height );
 
      DWORD writeSize;
      WriteFile( m_file, &header, sizeof(header), &writeSize, NULL );
 
      return true;
    }
 
    return false;
  }
 
  void update( const DIB32& image )
  {
    if ( isOpen() )
    {
      CHUNK chunk;
      chunk.dwFourCC = mmioFOURCC('0','0','d','b');
      chunk.dwSize = m_width * m_height * 4;
 
      DWORD writeSize;
      WriteFile( m_file, &chunk, sizeof(chunk), &writeSize, NULL );
      WriteFile( m_file, const_cast<LPDWORD>(image.getPixelAddr()), chunk.dwSize, &writeSize, NULL );
 
      m_frame++;
    }
  }
 
  /** close */
  void close()
  {
    if ( isOpen() )
    {
      DWORD writeSize;
 
      // write index header
      CHUNK index;
      index.dwFourCC = mmioFOURCC('i','d','x','1');
      index.dwSize = sizeof(AVIINDEXENTRY) * m_frame;      
      WriteFile( m_file, &index, sizeof(index), &writeSize, NULL );
 
      // write index chunks
      AVIINDEXENTRY entry;
      entry.ckid = mmioFOURCC('0','0','d','b');
      entry.dwFlags = AVIIF_KEYFRAME;
      entry.dwChunkLength = m_width * m_height * 4;
      for (DWORD i=0; i<m_frame; ++i)
      {
        entry.dwChunkOffset = 4 + sizeof(CHUNK)*i + m_width*m_height*4*i;
        WriteFile( m_file, &entry, sizeof(entry), &writeSize, NULL );
      }
 
      // write header again
      SetFilePointer( m_file, 0, NULL, FILE_BEGIN );
      Header header;
      header.setData( m_width, m_height, m_fps, m_frame );
      WriteFile( m_file, &header, sizeof(header), &writeSize, NULL );
 
      // close
      CloseHandle( m_file );
      m_file = NULL;
    }
  }
 
private:
  HANDLE m_file;
  DWORD m_frame;
  DWORD m_width, m_height, m_fps;
};
 
 
 
struct ObjPoint
{
  float x, y, vx, vy;
};
 
 
 
LRESULT CALLBACK wndProc(
  HWND hWnd,
  UINT msg,
  WPARAM wParam,
  LPARAM lParam )
{
  static DIB32 back, image;
  static ObjPoint point[ 4 ];
  static AVIRecorder2 avi;
  switch (msg)
  {
  case WM_DESTROY:
    avi.close();
    ShowWindow( hWnd, SW_HIDE );
    PostQuitMessage(0);
    break;
  case WM_CREATE:
    back.create( WINDOW_WIDTH, WINDOW_HEIGHT );
    image.create( TEXT("image.bmp") );
 
    point[0].x = 100;
    point[0].y = 100;
    point[1].x = 100;
    point[1].y = 200;
    point[2].x = 200;
    point[2].y = 100;
    point[3].x = 200;
    point[3].y = 200;
    srand( GetTickCount() );
    for (int i=0; i<4; ++i)
    {
      point[i].vx = (static_cast<float>( std::rand() & 1023 ) / 1023.f) * 32 - 16;
      point[i].vy = (static_cast<float>( std::rand() & 1023 ) / 1023.f) * 32 - 16;
    }
    avi.create( TEXT("output.avi"), back.getWidth(), back.getHeight(), FPS );
    break;
  case WM_PAINT:
    {
      PAINTSTRUCT ps = {0};
      HDC hdc = BeginPaint( hWnd, &ps );
      back.render( hdc, hWnd );
      EndPaint( hWnd, &ps );
    }
    break;
  case WM_TIMER:
    ZeroMemory( back.getPixelAddr(), back.getWidth() * back.getHeight() * 4 );
    for (int i=0; i<4; ++i)
    {
      point[i].x += point[i].vx;
      point[i].y += point[i].vy;
      if ( point[i].x < 0 || point[i].x > WINDOW_WIDTH ) point[i].vx = -point[i].vx;
      if ( point[i].y < 0 || point[i].y > WINDOW_HEIGHT ) point[i].vy = -point[i].vy;
    }
 
    {
      DIB32::Point p1, p2, p3, p4;
      p1.x = point[0].x;
      p1.y = point[0].y;
      p1.u = 0;
      p1.v = 0;
      p2.x = point[1].x;
      p2.y = point[1].y;
      p2.u = 0;
      p2.v = 32;
      p3.x = point[2].x;
      p3.y = point[2].y;
      p3.u = 32;
      p3.v = 0;
      p4.x = point[3].x;
      p4.y = point[3].y;
      p4.u = 32;
      p4.v = 32;
      image.triangle< DIB32::PutColorAdd >( back, p1, p2, p3 );
      image.triangle< DIB32::PutColorAdd >( back, p2, p3, p4 );
 
      InvalidateRect( hWnd, NULL, FALSE );
      avi.update( back );
    }
 
    break;
  default:
    return DefWindowProc( hWnd, msg, wParam, lParam );
  }
 
  return 0;
}
 
 
 
int WINAPI WinMain(
  HINSTANCE hInstance,
  HINSTANCE, PSTR, int )
{
  LPCTSTR WINDOW_NAME = TEXT("sample");
 
  WNDCLASSEX wc;
  wc.style    = CS_HREDRAW | CS_VREDRAW;
  wc.lpfnWndProc  = reinterpret_cast<WNDPROC>( wndProc );
  wc.cbClsExtra  = 0;
  wc.cbWndExtra  = 0;
  wc.cbSize    = sizeof( WNDCLASSEX );
  wc.hInstance  = hInstance;
  wc.hIcon    = NULL;
  wc.hIconSm    = NULL;
  wc.hCursor    = LoadCursor( NULL, IDC_ARROW );
  wc.hbrBackground= reinterpret_cast<HBRUSH>( GetStockObject(WHITE_BRUSH) );
  wc.lpszMenuName  = NULL;
  wc.lpszClassName= WINDOW_NAME;
  if ( !RegisterClassEx(&wc) ) return 0;
 
  LONG winWidth = WINDOW_WIDTH
    + GetSystemMetrics(SM_CXEDGE)
    + GetSystemMetrics(SM_CXBORDER)
    + GetSystemMetrics(SM_CXDLGFRAME);
  LONG winHeight = WINDOW_HEIGHT
    + GetSystemMetrics(SM_CYEDGE)
    + GetSystemMetrics(SM_CYBORDER)
    + GetSystemMetrics(SM_CYDLGFRAME)
    + GetSystemMetrics(SM_CYCAPTION);
  HWND hWnd = CreateWindowEx(
    0, WINDOW_NAME, NULL, WS_OVERLAPPEDWINDOW ^ WS_THICKFRAME,
    CW_USEDEFAULT, CW_USEDEFAULT, winWidth, winHeight,
    NULL, NULL, hInstance, NULL);
  if ( !hWnd ) return -1;
 
  ShowWindow( hWnd, SW_SHOWNORMAL );
  UpdateWindow( hWnd );
 
  SetTimer( hWnd, TIMER_ID, 1000/FPS, NULL );
  
  MSG msg;
  for (;;)
  {
    if ( !GetMessage(&msg, NULL, 0, 0) ) break;
    TranslateMessage( &msg );
    DispatchMessage( &msg );
  }
 
 
  UnregisterClass( WINDOW_NAME, hInstance );
  return msg.wParam;
}


640x480なら30秒程度で2GBに達する。
ゲームのPV程度なら30秒も回さないからこれでも十分ではある。
2GB以上保存可能な、AVI2.0のフォーマットを勉強中ではあるのだけど
肝心のAVI2.0の動画ファイルが手に入らないので実態が不明瞭で困っている。
仕様通りに作ると動かないらしいが。

0 件のコメント: