<menu id="w8yyk"><menu id="w8yyk"></menu></menu>
  • <dd id="w8yyk"><nav id="w8yyk"></nav></dd>
    <menu id="w8yyk"></menu>
    <menu id="w8yyk"><code id="w8yyk"></code></menu>
    <menu id="w8yyk"></menu>
    <xmp id="w8yyk">
    <xmp id="w8yyk"><nav id="w8yyk"></nav>
  • 網站首頁 > 物聯資訊 > 技術分享

    VC實現卡拉OK字幕疊加

    2016-09-28 00:00:00 廣州睿豐德信息科技有限公司 閱讀
    睿豐德科技 專注RFID識別技術和條碼識別技術與管理軟件的集成項目。質量追溯系統、MES系統、金蝶與條碼系統對接、用友與條碼系統對接

    一. GDI編程基礎

     字幕疊加,應當是屬于圖形、圖像處理的范疇。在Windows平臺上,圖形、圖像處理的方法當然首選GDI(Graphics Device Interface,圖形設備接口)。GDI是什么?GDI其實是一套API函數;它們功能豐富,使用起來簡單、靈活。下面,我們首先來介紹一些GDI編程的基礎知識。

      GDI函數有很多,我們大致可以把它們分成如下幾類:

      · 設備上下文(Device Context,簡稱DC)函數,如GetDC、CreateDC、DeleteDC等;

      · 畫線函數,如LineTo、Polyline、Arc等;

      · 填充畫圖函數,如Ellipse、FillRect、Pie等;

      · 畫圖屬性函數,如SetBkColor、SetBkMode、SetTextColor等;

      · 文本、字體函數,如TextOut、GetTextExtentPoint32、GetFontData等;

      · 位圖函數,如SetPixel、BitBlt、StretchBlt等;

      · 坐標函數,如DPtoLP、LPtoDP、ScreenToClient、ClientToScreen等;

      · 映射函數,如SetMapMode、SetWindowExtEx、SetViewportExtEx等;

      · 元文件(MetaFile)函數,如PlayMetaFile、SetWinMetaFileBits等;

      · 區域(Region)函數,如FillRgn、FrameRgn、InvertRgn等;

      · 路徑(Path)函數,如BeginPath、EndPath、StrokeAndFillPath等;

      · 裁剪(Clipping)函數,如SelectClipRgn、SelectClipPath等。

      上述這些函數可以完成繪制用戶界面中的各個部分,包括我們在Windows平臺上司空見慣的窗口、菜單、工具條、按鈕等。除了完成顯示操作功能外,GDI還提供了一些繪圖對象,用以渲染顯示。這些GDI對象包括:

      設備上下文(DC)——具有如顯示器或打印機等輸出設備的繪圖屬性信息的數據結構;

    畫筆(Pen)——用于繪制線條;

      畫刷(Brush)——用于圖案的填充;

      字體(Font)——用于確定文本字符的樣式;

      位圖(Bitmap)——用于存儲圖像;

      調色板(Palette)——屏幕上畫圖時可以使用的一些顏色的集合。

      DC在GDI中是一個非常重要的概念。在MSDN上查看各個GDI函數的使用說明,我們會發現大部分GDI函數都有一個HDC類型的參數;HDC就是DC句柄。Windows應用程序進行圖形、圖像處理的一般操作步驟如下:

      1. 取得指定窗口的DC;

      2. 確定使用的坐標系及映射方式;

      3. 進行圖形、圖像或文字處理;

      4. 釋放所使用的DC。

      為了進一步簡化GDI函數的使用,或者說為了適應面向對象的程序設計風格,微軟的MFC類庫提供了幾個DC的封裝類。這些類的繼承關系如下:

    VC實現卡拉OK字幕疊加

      圖1 關于DC的幾個MFC類的繼承關系

      我們知道,絕大部分MFC類都是從CObject類派生的,CDC類也不例外。我們看到,CDC類是最基本的DC封裝類;它幾乎對應封裝了所有的GDI函數。另外,CDC類的各個派生類各有專門的用途:

      CClientDC——在窗口的客戶區畫圖的DC;

      CMetaFileDC——用于操作Windows元文件的DC;

      CPaintDC——響應WM_PAINT消息時畫圖使用的DC,多見于MFC程序的OnDraw函數中;

      CWindowDC——在整個窗口范圍(包括框架、工具條等)中畫圖的DC。

      MFC除了對DC進行類封裝外,對其它GDI對象也進行了類封裝。這些類的繼承關系如下:

    VC實現卡拉OK字幕疊加

      圖2 GDI對象的MFC封裝類的繼承關系

      CGdiObject——GDI對象的父類,定義了GDI對象封裝類的一些公有函數接口;

      CBitmap——位圖相關操作的封裝類,包括位圖的裝入或創建等;

      CBrush——畫刷對象的封裝類;

      CFont——字體屬性及相關操作的封裝類;

      CPalette——調色板的封裝類;

      CPen——畫筆對象的封裝類;

      CRgn——區域對象以及區域相關操作的封裝類。

      通過上述介紹,相信讀者對GDI編程有了一定的了解。接下去,我們就來討論卡拉OK字幕疊加的實現原理。

      二. 實現原理

      字幕疊加,最基本的一種是在靜態圖像上進行的,一般就是直接在圖像上輸出標準的字符串,以合成新的圖像幀;而視頻上的字幕疊加,則是在連續的圖像幀序列上進行的,單幀上的疊加與靜態圖像上的疊加類似。本文所要講述的卡拉OK字幕疊加,就是一種在視頻上進行的字幕疊加。

     在視頻上進行疊加的字幕,一般可以呈現出多種動態效果,比如滾動、旋轉等;卡拉OK字幕需要表達更多的內容,它至少包括:

      1.根據進度,顯示不同的字幕內容(即歌詞);

      2.字幕上應該表達出卡拉OK的音樂節奏;

      3.對字幕進行勾邊或其他效果處理,以突出顯示。

      以下是卡拉OK字幕效果的演示圖:

     (圖片較大,請拉動滾動條觀看)

    RFID設備管理軟件

      (圖片較大,請拉動滾動條觀看)

      圖3 卡拉OK字幕效果圖

      簡單的字幕疊加我們就可以通過GDI函數來實現。我們知道,字符的輸出可以使用TextOut函數;但是,如何輸出空心字,如何填充空心字呢?我們這里要用到路徑。字符路徑的繪制過程參考如下:

    CClientDC * pClientDC = new CClientDC(mTargetWnd);
    // ......
    pClientDC->BeginPath();
    pClientDC->TextOut(x, y, szSubtitleLine);
    pClientDC->EndPath();
    // pClientDC->StrokePath();
    pClientDC->StrokeAndFillPath();

      我們看到,在TextOut函數調用前后分別調用了BeginPath函數和EndPath函數,以記錄字符輸出的路徑(實際上就是字符的輪廓);然后調用StrokePath函數將路徑勾勒出來,或者調用 StrokeAndFillPath函數在勾勒路徑的同時進行填充。需要注意的是,路徑勾勒的顏色由DC中當前選入的畫筆決定,填充的顏色由DC中當前選入的畫刷決定。

      那么,我們如何在字幕上表示演唱進度呢?根據音樂的節奏,我們需要為每個字符確定開始填充的時刻,并且指定該字符完成填充需要的時間。比如上述“真的好想你”一句歌詞,我們從時刻0開始填充,讓“真”顯示1500毫秒,“的”顯示300毫秒,“好”顯示1600毫秒, “想”顯示500毫秒,“你”顯示1000毫秒。于是,我們可以從開始播放時進行計時,并且以一定的頻率刷新當前播放到的時間點;表現在卡拉OK字幕上,就是不斷地更新已經唱過的字幕和尚未唱過的字幕之間的分界線。從視覺效果上,我們看到的是填充色隨著音樂從左到右地行進;并且單個字符的行進速度,也因該字符上分配的總的填充時間不同而不同,從而體現出應有的節奏感。

     另外,我們從上述卡拉OK字幕效果圖中不難看出,已經唱過的字幕和尚未唱過的字幕的畫法是不一樣的:前半部分是藍色填充、白色勾邊,后半部分是黑色勾邊的空心字。而且,這兩部分之間的分界線有可能位于某個字符中(不會總是剛好在相鄰字符的間隙中)。那么,如何準確地畫出這兩部分字幕呢?我們這里可以使用GDI的區域、路徑裁剪操作。首先,根據當前進度,將窗口分成左右兩個矩形區域:

    // xStart, yStart為字幕行第一個字符顯示的(x, y)坐標
    // pregress為當前進度坐標(已經唱過的寬度)
    // sz為SIZE類型的變量,記錄整行字幕的寬、高
    CRgn region1, region2;
    region1.CreateRectRgn(xStart, yStart,
    xStart + pregress,
    yStart + sz.cy);
    region2.CreateRectRgn(xStart + pregress, yStart,
    xStart + sz.cx,
    yStart + sz.cy);

      在畫兩部分字幕的路徑之前,分別調用SelectClipRgn函數選入各自的區域;等到字幕路徑畫完之后,再調用SelectClipPath函數跟先前選入的區域進行“與”操作,即提取兩者的公共部分。整個過程參考如下:

    pClientDC->SelectClipRgn(&region1, RGN_COPY);
    // 1.選入用于畫已經唱過字幕的畫筆、畫刷
    // 2.畫字幕路徑
    // ......
    pClientDC->SelectClipPath(RGN_AND);
    pClientDC->SelectClipRgn(&region2, RGN_COPY);
    // 1.選入用于畫尚未唱過字幕的畫筆、畫刷
    // 2.畫字幕路徑
    // ......
    pClientDC->SelectClipPath(RGN_AND);

      三. 關鍵實現

      我們使用VC生成一個基于對話框的程序來演示卡拉OK字幕疊加的實現。程序界面如下:

     

    (圖片較大,請拉動滾動條觀看)

      圖4 演示程序界面

      為了使字幕疊加的過程更加清晰,我們設計了一個邏輯控制類 CSubtitleController。在進行真正的字幕疊加之前,我們必須首先調用CSubtitleController類的 SetTargetWindow函數設置字幕的顯示窗口,隨后調用SetSubtitleLine函數設置字幕行的內容、填充時間等屬性。具體實現中,我們在主對話框類CKaraokeDemoDlg中定義一個CSubtitleController類的實例mController,并且在對話框的初始化函數OnInitDialog中進行了如下的調用:

    BOOL CKaraokeDemoDlg::OnInitDialog()
    {
    CDialog::OnInitDialog();
    // TODO: Add extra initialization here
    mController.SetTargetWindow(&mKaraokeWnd);
    mController.SetSubtitleLine(mSubtitleArray, mDurationArray, 0, 5);
    // ......
    return TRUE;
    }
      其中,mKaraokeWnd表示字幕顯示窗口,是一個CStatic類的對象實例;mSubtitleArray是CString類型的數組,用于存儲字幕內容(注意,應將字幕行中的各個字符單獨存儲);mDurationArray是int類型的數組,用于存儲字幕行中各個字符填充需要的時間。 mSubtitleArray和mDurationArray可以在CKaraokeDemoDlg類的構造函數中做如下的初始化:
    mSubtitleArray = new CString[5];
    mDurationArray = new int[5];
    mSubtitleArray[0] = "真";
    mSubtitleArray[1] = "的";
    mSubtitleArray[2] = "好";
    mSubtitleArray[3] = "想";
    mSubtitleArray[4] = "你";
    mDurationArray[0] = 1500; // 以毫秒為單位
    mDurationArray[1] = 300;
    mDurationArray[2] = 1600;
    mDurationArray[3] = 500;
    mDurationArray[4] = 1000;
      主對話框類中還使用了一個定時器,定時間隔是40毫秒,即以每秒25幀的頻率刷新字幕疊加的進度。我們在開始播放(即當用戶按下“Play”按鈕)時記下系統時間(存儲到DWORD類型的變量mStartTime中),然后在每次定時到達的時候再次讀取系統時間,與mStartTime做差值運算,得到當前播放到的時間點(我們暫且稱之為流時間)。在定時器消息響應函數CKaraokeDemoDlg::OnTimer中,我們會調用 CSubtitleController類的DrawSubtitle函數來完成實際的卡拉OK字幕輸出,這個函數的參數就是這個流時間。
      在CSubtitleController類中,我們看到DrawSubtitle函數的具體實現如下:
    BOOL CSubtitleController::DrawSubtitle(DWORD inStreamTime)
    {
    ASSERT(mClientDC);
    DWORD timeInChar = 0; // 相對于當前字符填充的開始時間的時間
    LONG sungLength = 0; // 已經唱過的字幕寬度
    // LocateChar為CSubtitleController類的一個私有函數
    // 根據當前播放到的時間點,定位到當前進度中的字符,
    // 并且得到播放時間點在當前字符中的相對時間
    int currentChar = LocateChar(inStreamTime, timeInChar);
    if (currentChar != -1) // 定位成功
    {
    // 計算已經唱過的字幕寬度
    // mFromToArray數組記錄各個字符的屬性,包括開始、結束時間、尺寸等
    sungLength = mFromToArray[currentChar].size.cx * timeInChar;
    sungLength = sungLength / mFromToArray[currentChar].duration;
    for (int i = 0; i < currentChar; i++)
    {
    // 累加上當前進度中的字符以前的所有字符的寬度
    sungLength += mFromToArray[i].size.cx;
    }
    }
    else
    {
    // 如果無法定位到任何一個字符,則畫出整行
    sungLength = mTotalWidth;
    }
    // 將字幕字體選入目標窗口的DC中
    CFont * pOldFont = (CFont *) mClientDC->SelectObject(&mTextFont);
    mClientDC->SetBkMode(TRANSPARENT); // 設置輸出時背景透明
    // 生成已經唱過的和尚未唱過的兩塊窗口區域
    // mSungRegion和mSingingRegion均是CRgn類對象實例
    mSungRegion.CreateRectRgn(mStartPoint.x, mStartPoint.y,
    mStartPoint.x + sungLength, mStartPoint.y + mFromToArray[0].size.cy);
    mSingingRegion.CreateRectRgn(mStartPoint.x + sungLength, mStartPoint.y,
    mStartPoint.x + mTotalWidth, mStartPoint.y + mFromToArray[0].size.cy);
    // 畫出第一部分:已經唱過的字幕(藍色填充,白色勾邊)
    int ret = mClientDC->SelectClipRgn(&mSungRegion, RGN_COPY);
    mClientDC->SetPolyFillMode(WINDING);
    HPEN pOldPen = (HPEN) mClientDC->SelectObject(mSungBoundaryPen);
    HBRUSH pOldBrush = (HBRUSH) mClientDC->SelectObject(mSungTextBrush);
    mClientDC->BeginPath();
    mClientDC->TextOut(mStartPoint.x, mStartPoint.y, mSubtitleLine);
    mClientDC->EndPath();
    mClientDC->StrokeAndFillPath(); // 畫出字符路徑并填充
    mClientDC->SelectClipPath(RGN_AND);
    // 恢復以前的畫筆和畫刷
    mClientDC->SelectObject(pOldPen);
    mClientDC->SelectObject(pOldBrush);
    // 畫出第二部分:尚未唱過的字幕(黑色勾邊空心字)
    pOldPen = (HPEN) mClientDC->SelectObject(mSingingBoundaryPen);
    pOldBrush = (HBRUSH) mClientDC->SelectObject(mSingingTextBrush);
    mClientDC->SelectClipRgn(&mSingingRegion, RGN_COPY);
    mClientDC->BeginPath();
    mClientDC->TextOut(mStartPoint.x, mStartPoint.y, mSubtitleLine);
    mClientDC->EndPath();
    mClientDC->StrokePath(); // 畫出字符路徑(不填充)
    mClientDC->SelectClipPath(RGN_AND);
    // 恢復以前的畫筆和畫刷
    mClientDC->SelectObject(pOldBrush);
    mClientDC->SelectObject(pOldPen);
    mSungRegion.DeleteObject();
    mSingingRegion.DeleteObject();
    // 恢復目標窗口為“全區域”
    RECT bounds;
    mTargetWnd->GetClientRect(&bounds);
    CRgn rgn;
    rgn.CreateRectRgn(bounds.left, bounds.top, bounds.right, bounds.bottom);
    ret = mClientDC->SelectClipRgn(&rgn, RGN_COPY);
    // 恢復以前的字體
    mClientDC->SelectObject(pOldFont);
    // 如果無法定位到任何一個字符,則返回一個錯誤值
    return (currentChar != -1);
    }
    // 根據當前播放到的時間點,定位到當前進度中的字符
    int CSubtitleController::LocateChar(DWORD inStreamTime, DWORD & outTimeInChar)
    {
    // mCharCount為整個字幕行的字符個數
    for (int i = 0; i < mCharCount; i++)
    {
    if (inStreamTime >= mFromToArray[i].from &&
    inStreamTime < mFromToArray[i].to)
    {
    outTimeInChar = inStreamTime - mFromToArray[i].from;
    return i;
    }
    }
    return -1;
    }

     

     四. 性能優化

      我們在演示中發現,頻繁地直接在窗口DC中畫圖會帶來一定的閃爍感。對此,我們可以進行一下優化,即首先創建一個與目標窗口DC兼容的內存DC,在這個內存DC中畫好字幕后,再將字幕位圖從內存DC拷貝到目標窗口DC中去。

      我們可以參考CSubtitleController類的DrawSubtitle2函數的實現:

    BOOL CSubtitleController::DrawSubtitle2(DWORD inStreamTime)
    {
    ASSERT(mClientDC);
    RECT bounds;
    mTargetWnd->GetClientRect(&bounds);
    int wndWidth = bounds.right - bounds.left;
    int wndHeight = bounds.bottom - bounds.top;
    CDC memDC;
    // 創建與目標窗口DC兼容的內存DC
    memDC.CreateCompatibleDC(mClientDC);
    // 創建與目標窗口DC兼容的位圖
    HBITMAP membmp = CreateCompatibleBitmap(mClientDC->GetSafeHdc(),wndWidth,wndHeight);
    // 將位圖選入內存DC
    HBITMAP oldbmp = (HBITMAP) memDC.SelectObject(membmp);
    FillRect(memDC.GetSafeHdc(), &bounds, (HBRUSH)GetStockObject(LTGRAY_BRUSH));
    /*----------------- 以下字幕操作都在內存DC中進行 ----------------*/
    DWORD timeInChar = 0;
    LONG sungLength = 0;
    int currentChar = LocateChar(inStreamTime, timeInChar);
    if (currentChar != -1)
    {
    sungLength = mFromToArray[currentChar].size.cx * timeInChar;
    sungLength = sungLength / mFromToArray[currentChar].duration;
    for (int i = 0; i < currentChar; i++)
    {
    sungLength += mFromToArray[i].size.cx;
    }
    }
    else
    {
    sungLength = mTotalWidth;
    }
    CFont * pOldFont = (CFont *) memDC.SelectObject(&mTextFont);
    memDC.SetBkMode(TRANSPARENT);
    mSungRegion.CreateRectRgn(mStartPoint.x, mStartPoint.y,
    mStartPoint.x + sungLength, mStartPoint.y + mFromToArray[0].size.cy);
    mSingingRegion.CreateRectRgn(mStartPoint.x + sungLength, mStartPoint.y,
    mStartPoint.x + mTotalWidth, mStartPoint.y + mFromToArray[0].size.cy);
    // Draw the first part which has been sung
    int ret = memDC.SelectClipRgn(&mSungRegion, RGN_COPY);
    memDC.SetPolyFillMode(WINDING);
    HPEN pOldPen = (HPEN) memDC.SelectObject(mSungBoundaryPen);
    HBRUSH pOldBrush = (HBRUSH) memDC.SelectObject(mSungTextBrush);
    memDC.BeginPath();
    memDC.TextOut(mStartPoint.x, mStartPoint.y, mSubtitleLine);
    memDC.EndPath();
    memDC.StrokeAndFillPath();
    memDC.SelectClipPath(RGN_AND);
    memDC.SelectObject(pOldPen);
    memDC.SelectObject(pOldBrush);
    // Draw the second part which is waiting for being sung
    pOldPen = (HPEN) memDC.SelectObject(mSingingBoundaryPen);
    pOldBrush = (HBRUSH) memDC.SelectObject(mSingingTextBrush);
    memDC.SelectClipRgn(&mSingingRegion, RGN_COPY);
    memDC.BeginPath();
    memDC.TextOut(mStartPoint.x, mStartPoint.y, mSubtitleLine);
    memDC.EndPath();
    memDC.StrokePath();
    memDC.SelectClipPath(RGN_AND);
    memDC.SelectObject(pOldBrush);
    memDC.SelectObject(pOldPen);
    mSungRegion.DeleteObject();
    mSingingRegion.DeleteObject();
    memDC.SelectObject(pOldFont);
    // 將內存DC中的位圖拷貝到目標窗口DC中
    mClientDC->BitBlt(0, 0, wndWidth, wndHeight, &memDC, 0, 0, SRCCOPY);
    // 刪除內存DC及使用的資源
    memDC.SelectObject(oldbmp);
    DeleteObject(membmp);
    memDC.DeleteDC();
    return (currentChar != -1);
    }

      五. 結束語

      本文介紹了卡拉OK字幕疊加的一般原理以及VC上使用GDI的一種簡單實現,并且提供了完整的示例源代碼,希望能夠對讀者朋友們有所啟示。

    RFID管理系統集成商 RFID中間件 條碼系統中間層 物聯網軟件集成
    最近免费观看高清韩国日本大全