VC实现卡拉OK字幕叠加
一. 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的封装类。这些类的继承关系如下:
图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对象也进行了类封装。这些类的继承关系如下:
图2 GDI对象的MFC封装类的继承关系
CGdiObject——GDI对象的父类,定义了GDI对象封装类的一些公有函数接口;
CBitmap——位图相关操作的封装类,包括位图的装入或创建等;
CBrush——画刷对象的封装类;
CFont——字体属性及相关操作的封装类;
CPalette——调色板的封装类;
CPen——画笔对象的封装类;
CRgn——区域对象以及区域相关操作的封装类。
通过上述介绍,相信读者对GDI编程有了一定的了解。接下去,我们就来讨论卡拉OK字幕叠加的实现原理。
二. 实现原理
字幕叠加,最基本的一种是在静态图像上进行的,一般就是直接在图像上输出标准的字符串,以合成新的图像帧;而视频上的字幕叠加,则是在连续的图像帧序列上进行的,单帧上的叠加与静态图像上的叠加类似。本文所要讲述的卡拉OK字幕叠加,就是一种在视频上进行的字幕叠加。
在视频上进行叠加的字幕,一般可以呈现出多种动态效果,比如滚动、旋转等;卡拉OK字幕需要表达更多的内容,它至少包括:
1.根据进度,显示不同的字幕内容(即歌词);
2.字幕上应该表达出卡拉OK的音乐节奏;
3.对字幕进行勾边或其他效果处理,以突出显示。
以下是卡拉OK字幕效果的演示图:
(图片较大,请拉动滚动条观看)
(图片较大,请拉动滚动条观看)
图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(®ion1, RGN_COPY);
// 1.选入用于画已经唱过字幕的画笔、画刷
// 2.画字幕路径
// ......
pClientDC->SelectClipPath(RGN_AND);
pClientDC->SelectClipRgn(®ion2, 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的一种简单实现,并且提供了完整的示例源代码,希望能够对读者朋友们有所启示。
分享到:
相关推荐
卡拉OK字幕叠加效果,用VC++模仿的,这是源代码,VC6下编译通过,字幕叠加的效果相信都知道吧,两行文字叠加在一起,跟随歌曲的进度与另一种颜色的文字相叠加,文字跟随音乐跑的感觉,虽然说本效果在平时不怎么用,...
CDC::TextOut输出的字体只有加粗和不加粗2种效果,这是微软系统默认的效果,现在项目的需要,设置画笔宽度(使字体变粗变细),...参考:VC实现卡拉OK字幕叠加 http://blog.csdn.net/zhuzhubin/article/details/5218373
在卡拉OK字幕程序中,可能用到CString来存储和处理歌词文本,实现动态更新字幕内容。 接下来,定时器是Windows应用程序中用于定期触发事件的关键组件。在本项目中,定时器可能被用来控制字幕的滚动速度,或者实现...
本文将深入探讨如何使用VC++(Visual C++)这一编程环境来实现字幕叠加的功能,让你如同置身于卡拉OK现场,享受到自定义字幕带来的便捷与乐趣。 首先,理解“字幕叠加”的概念是关键。字幕叠加是指在视频播放过程中...
**VC 卡拉OK MFC 知识点详解** 在计算机编程领域,尤其是涉及到多媒体应用时,`MFC`(Microsoft Foundation Classes)是Windows平台上一个非常重要的开发框架。`MFC`是微软公司为Visual C++提供的类库,它封装了...
内容索引:VC/C++源码,字符处理,字符叠加 卡拉OK字幕叠加效果,用VC++模仿的,这是源代码,VC6下编译通过,字幕叠加的效果相信都知道吧,两行文字叠加在一起,跟随歌曲的进度与另一种颜色的文字相叠加,文字跟随音乐...
在VC++环境中实现视频图像叠加字符是一个涉及到多媒体处理和计算机视觉技术的任务。在这个项目中,开发者使用了经典的VC++6.0集成开发环境以及OpenCV(Open Source Computer Vision Library)1.0版本来完成。OpenCV...
标题中的"VC 字幕叠加滚动仿mtv字幕效果.rar"指的是使用Visual C++(简称VC)编程语言实现的一种特殊字幕效果,该效果模仿了MTV或KTV中的滚动歌词展示方式。在KTV中,歌词会随着音乐节奏同步滚动,给唱歌者提供歌词...
本文将深入探讨一个基于VC++编程语言和Access数据库管理系统构建的卡拉OK点歌系统的实现过程,通过ODBC(Open Database Connectivity)接口进行数据交互,实现歌曲的高效管理和用户友好的交互界面。 首先,VC++,...
【基于MFC的卡拉OK】项目是一个利用Microsoft Foundation Classes (MFC)库开发的应用程序,设计目的是实现类似于卡拉OK的功能,即在播放音频的同时显示相应的歌词或字幕。MFC是微软提供的一种C++类库,它封装了...
在本文中,我们将深入探讨如何使用Visual C++ 6.0(简称VC6.0)来实现字幕滚动效果。字幕滚动是一种常见的视觉特效,常用于电影、电视节目和各种多媒体展示中,以呈现文本信息。在VC6.0中,我们可以利用MFC...
【标题】"模拟卡啦OK字幕变色(VC源码)"所涉及的知识点主要集中在计算机编程、图形处理和多媒体应用领域,特别是使用Microsoft Visual C++ 6.0这一经典开发环境进行C++编程。下面将详细介绍这些内容。 在卡拉OK系统...
在VC++环境中,实现字幕的滚动显示效果主要涉及到Windows编程的基本原理,包括窗口消息处理、GDI(Graphics Device Interface)图形绘制以及定时器的使用。以下是对这一主题的详细阐述: 1. **Windows编程基础**: ...
本项目"VC 窗体实现渐变的字幕滚动.rar"提供了一种方法,利用C++编程语言来创建这种效果。通过分析描述和标签,我们可以深入探讨这个实现涉及的关键技术点。 首先,"VC 源码-文本字符"标签表明这个项目主要关注的是...
用vc实现串口通信的完整代码.rar用vc实现串口通信的完整代码.rar用vc实现串口通信的完整代码.rar用vc实现串口通信的完整代码.rar用vc实现串口通信的完整代码.rar用vc实现串口通信的完整代码.rar用vc实现串口通信的...
vc实现VS开发界面vc实现VS开发界面vc实现VS开发界面vc实现VS开发界面vc实现VS开发界面vc实现VS开发界面vc实现VS开发界面vc实现VS开发界面vc实现VS开发界面vc实现VS开发界面vc实现VS开发界面vc实现VS开发界面vc实现VS...
《VC实现聊天室服务端及客户端程序》 在IT领域,开发一款聊天室程序是学习网络编程和客户端-服务器架构的常见实践。本项目利用Microsoft Foundation Classes (MFC)库,一个C++类库,来构建一个具备基本聊天功能的...
在VC中,字幕滚动通常是通过处理窗口消息和定时器来实现的。字幕滚动程序的核心是文本的动态显示和更新,这通常涉及到以下知识点: 1. **C++基础知识**:C++是一种静态类型的、编译式的、通用的、大小写敏感的、...
在Visual C++(简称VC)环境下,实现多语言程序版本主要涉及到资源文件、MFC框架、国际化和本地化策略等多个方面。下面我们将深入探讨如何在VC中实现这一功能。 1. **资源文件与资源脚本** 多语言支持首先从资源...
总之,通过理解上述步骤,你可以在VC++环境中实现简单的图片叠加和文字添加功能,从而模拟Photoshop的部分功能。这不仅锻炼了你的编程技巧,也让你对图像处理有了更深入的理解。在实践中,不断探索和学习,你将能够...