陈义锋,黄陂电视台新闻经济中心技术部主任,工程师;
戴汉军,黄陂广电局网络中心技术部主任,工程师
播音提示器是电视台最常见的设备,其基本原理比较简单,即采用单反玻璃反射镜像显示的上滚文字。老式的提示器大多采用CRT显示器和字幕上滚软件,新型提示器大多使用液晶显示屏输出文字并使用了专用的实时播音提示软件,部分型号还增加了遥控功能。
2007年10月随着我台真三维虚拟演播室的建设,我们更新了一直使用老式DOS版字幕上滚软件的播音提示器,即保留了单反玻璃、机架、用做输出的21寸彩电,重新采购了计算机,重新设计编写了新的提示器软件,设计制作了无线遥控电路和相关的单片机程序。
本文将详细介绍提示器软件的设计、无线遥控电路的设计以及相关的VC++程序、单片机C语言程序、电路原理图。
提纲:
一、 播音提示器的基本原理
二、 使用VC++设计提示器软件
三、 实时高速贴图的实现途径----双缓冲和消息循环
四、 液晶显示字幕与CRT显示字幕的区别
五、 使用无线遥控器,PT2262和PT2272
六、 使用AVR单片机转发信号
七、 按键复用的问题
八、 提示器VC++程序主要源代码
九、 AVR单片机CodeVision C语言源程序
十、 结束语
正文:
一、 播音提示器的基本原理
播音提示器利用单反玻璃单面反射和单面透射的光学特性,使播音员能够看到挡在摄像机前面的单反玻璃上反射的上滚文字,而摄像机拍摄透过单反玻璃并不会受到另一面反射的文字的影响,播音提示器或通过摄像头或通过计算机将文字显示在于单反玻璃呈45度夹角的显示器上,由于镜面反射的缘故,要想看到反射形成的正像,显示器必须输出镜像的画面。镜像画面对于CRT显示器来说很简单,只需要拆开机器交换行扫描输出线即可,对于液晶显示器来说就没有那么简单了,所有的镜像画面全部要通过计算机软件来生成。至于镜像的方法有两种,一种是更改显示输出,使整个显示画面镜像显示,一种是通过实时运算,镜像一个贴图区,本文将介绍后一种实现方法。
二、 使用VC++设计提示器软件
很多字幕上滚软件并不是实时贴图的,即有一个生成上滚文件的过程。随着计算机运算能力的大幅跃升,采用优化的算法、高效率的设计语言也是可以实现实时贴图输出的。VC++6.0是widows程序设计中应用程序执行效率较高的开发平台之一,而且其内置的GDI绘图函数可以很方便地输出图形,相比.net语言中的GDI+输出速度上还有一定优势。
三、 实时高速贴图的实现途径----双缓冲和消息循环
在windows MFC程序设计中,即使是简单的刷新也可能使界面闪烁不已,要实现平滑流畅的文字滚动效果依赖控件和定时器肯定是行不通的。我们只能通过双缓冲的办法,即在内存中作图,在贴图的过程中又画好下一幅图片,如此反复,在通过一定的延时手段,让贴图速度(文字滚动速度)能够得到很好的控制。
下面是一个使用液晶屏做为字幕输出时镜像贴图的函数,演示了内存缓冲贴图的技术。需要指出的是本文所采用的实时贴图算法均衡考虑了一些实时参数修改响应的问题,并不是最精简的贴图算法,实际上现在一般的计算机配置用这个函数也可以滚动得飞快!
void CTishiqiDlg::Onplay2()
{
isPlayMirr=2;
//在内存中建立位图
CDC *pDC=GetDC();
CDC *pDCpic=m_pic.GetDC();
CDC dcMem;
// BITMAP bm;
iExit=0;
CBitmap bmpBuf;
DWORD start,nowTime;
dcMem.CreateCompatibleDC(NULL);
bmpBuf.DeleteObject();
bmpBuf.CreateCompatibleBitmap(pDC,mWidth,mHeight);
CBitmap *poldBitmap=dcMem.SelectObject(&bmpBuf);
dcMem.SetBkMode(TRANSPARENT);
//输出滚动字幕
for (mTi=0;mTi<=mLen;mTi++)
{
start=GetTickCount();
if (mLen<10)
{ //估算文字长度,如不考虑中途修改可去掉以提高速度
CSize size=pDCpic->GetTextExtent(tStr);
int xx= size.cx;
int yy= size.cy;
mLen=(xx/mWidth+1)*yy*172+mHeight;
pos=0;
}
rect.top=rect.top-m_slid2.GetPos();
pos=pos+m_slid2.GetPos();
// m_slid3.SetScrollPos(pos,TRUE);
if (iExit>0) mTi=mLen;
dcMem.SetTextColor(fontColor);
CFont *poldFont=dcMem.SelectObject(&m_font);
dcMem.FillSolidRect(0,0,mWidth,mHeight,0);
dcMem.DrawText(tStr,rect,DT_WORDBREAK); //在内存中输出文字
pDCpic->StretchBlt(0,rect.bottom ,rect.right,-rect.bottom ,&dcMem, 0,0,rect.right,rect.bottom,SRCCOPY); //将内存中的图像镜像旋转并贴到m_pic图片框
DWORD dti=(DWORD)m_slid.GetPos();
do
{
//消息循环
MSG msg;
PeekMessage(&msg,NULL,0,0,PM_REMOVE);
TranslateMessage(&msg);
DispatchMessage(&msg);
nowTime=GetTickCount();
}while((nowTime-start)<dti); //延时函数
}
dcMem.DeleteDC();
ReleaseDC(pDC);
ReleaseDC(pDCpic);
}
延时手段简单的说有两种,一种是使用定时器Timer,一种是通过GetTickCount()函数和循环来进行ms级的延时。但是定时器的精度大约在20-30ms,因为还要执行复杂的作图操作通过定时器并不能满足每秒25帧画面的输出要求;如果单纯使用循环的话,也不可行,因为Windows操作系统是通过消息来响应各种操作的,循环操作会很显著地将系统陷入一种无响应的假死状态,所以还要在循环中加入消息循环机制,及时捕获并将消息传递出去。
下面是一个采用了消息循环机制的延时模块,这段程序和网上流行的延时程序不同,它避免了陷入等待消息输入才能运行的漏洞。
DWORD start,nowTime;
for (;;;)
{
start=GetTickCount();
//这里写你的代码
do
{
MSG msg;
PeekMessage(&msg,NULL,0,0,PM_REMOVE);
TranslateMessage(&msg);
DispatchMessage(&msg);
nowTime=GetTickCount();
}while((nowTime-start)<dti);
}
四、 液晶显示字幕与CRT显示字幕的区别
前面提到液晶显示器无法通过更改电路的方式来实现镜像模式,只有通过软件来实现。软件实现包括驱动层面的镜像输出和贴图的镜像输出。前者会使整个操作界面都变成镜像,很难再对软件进行操作,但在贴图速度上有一定优势;后一种方法只是文字上滚区域镜像贴图,而操控区域不变,有利于使用,但对计算机速度有一定要求。
Windows为我们提供了一个旋转贴图的函数StretchBlt(m_hDC, x, y, nWidth, nHeight,pSrcDC->GetSafeHdc(), xSrc, ySrc, nSrcWidth, nSrcHeight, dwRop)通过设置不同参数可以很方便的镜像图像然后贴到特定的区域。我们可以使用这个函数实现液晶型提示器的显示输出(具体调用方法见第三节例程)。
五、 使用无线遥控器,PT2262和PT2272
要播音员自主控制提示器可以使用笔记本电脑直接操作也可以使用遥控器,遥控器常见的有红外遥控和无线遥控两种,红外遥控因发射距离短、红外光不能被阻挡和发射红外线时可以被摄像机拍到等缺陷不能采用,因此我们采用了无线遥控装置。
常见的无线遥控器大多采用PT2262和PT2272发射接收芯片。PT2262/2272是台湾普城公司生产的一种CMOS工艺制造的低功耗低价位通用编解码电路,PT2262/2272最多可有12位(A0-A11)三态地址端管脚(悬空,接高电平,接低电平),任意组合可提供531441地址码,PT2262最多可有6位(D0-D5)数据端管脚,设定的地址码和数据码从17脚串行输出,可用于无线遥控发射电路。
我们采购了一种有4个按键的PT2262遥控器和使用PT2272 M4芯片的接收模块,无线遥控频率为315MHz,PT2272 M4是不带锁存的接收芯片,对应的PT2272 L4则带锁存。
PT2272 M4接收模块有四个数据线分别为D10、D11、D12、D13以及解码确认端子VT和电源VCC、地GND等7个引脚。当接收到地址编码正确的遥控信号时,VT输出高电平,对应的数据线也为高电平,无信号时均为低电平。
六、 使用AVR单片机转发信号
要使播音提示器软件能够响应遥控操作,还需要能够将遥控信号(即接收模块的VT电平变化、数据线电平变化)传递给计算机,因此我们需要使用单片机来转发这个信号。这里我们使用了ATMEL公司的AVR单片机AT MEGA8L,MEGA8L是一款低电压8位单片机。通过MEGA8单片机可以采集到无线接收模块的数据线和VT端口的电平变化,然后通过串行口转发给计算机,提示器软件在得到串口接收数据的消息后立即调用相关函数响应遥控操作。
单片机可以采用扫描端口的方法也可以将VT端接到单片机中断输入脚通过中断来响应遥控操作,我们在设计电路时同时考虑了上述两种处理方法,通过R13和R15(两个只需焊接一个即可)来选择。
七、 按键复用的问题
实际应用中,仅有四个键(标记为A、B、C、D)的遥控器并不能很好地满足操作的需要,所以我们需要对按键进行复用。按键的复用实际上在上位机软件即提示器软件中进行处理。首先通过D键设置两种操作状态,即滚动状态和调速状态,在滚动状态下,A、B键为前进、后退键,可前进或后退约半屏内容,C键为开始、暂停键,在调速状态下,A、B为速度微调键,加减延时参数。
程序设置有自动参数记忆功能,能够在程序关闭时将现行参数记载下来,下次启动时自动调用。
八、 提示器VC++程序主要源代码:
软件初始化部分
BOOL CTishiqiDlg::OnInitDialog()
{
CDialog::OnInitDialog();
// Add "About..." menu item to system menu.
// IDM_ABOUTBOX must be in the system command range.
ASSERT((IDM_ABOUTBOX & 0xFFF0) == IDM_ABOUTBOX);
ASSERT(IDM_ABOUTBOX < 0xF000);
CMenu* pSysMenu = GetSystemMenu(FALSE);
if (pSysMenu != NULL)
{
CString strAboutMenu;
strAboutMenu.LoadString(IDS_ABOUTBOX);
if (!strAboutMenu.IsEmpty())
{
pSysMenu->AppendMenu(MF_SEPARATOR);
pSysMenu->AppendMenu(MF_STRING, IDM_ABOUTBOX, strAboutMenu);
}
}
// Set the icon for this dialog. The framework does this automatically
// when the application's main window is not a dialog
SetIcon(m_hIcon, TRUE); // Set big icon
SetIcon(m_hIcon, FALSE); // Set small icon
mWidth=GetSystemMetrics(SM_CXSCREEN);
mHeight=GetSystemMetrics(SM_CYSCREEN);
SetWindowPos(NULL,0,0,mWidth,mHeight-20 ,SWP_NOMOVE | SWP_NOZORDER); //调节程序自动适应桌面分辨率
mWidth=mWidth-70;
mHeight=mHeight-50;
rect.top=mHeight-100;
rect.bottom=mHeight;
rect.left=0;
rect.right=mWidth;
ResetParame();
//fontColor=RGB(255,255,0);
fontSize=720;
fontName="黑体 ";
mLen=1;
m_font.CreatePointFont(fontSize,fontName,NULL);
Zmstr1="现在开始调节滚动速度,按遥控器A 键速度加快,B 键速度减慢,D 键退回提示器模式!现在开始调节滚动速度,按遥控器A 键速度加快,B 键速度减慢,D 键退回提示器模式!现在开始调节滚动速度,按遥控器A 键速度加快,B 键速度减慢,D 键退回提示器模式!";
tStr="播音提示器";
m_slid.SetRangeMax(50,FALSE);
m_slid.SetRangeMin(0,FALSE);
m_slid.SetPos(Speed);
m_slid2.SetRangeMax(3,FALSE);
m_slid2.SetRangeMin(1,FALSE);
m_slid2.SetPos(Step);
isPlayMirr=0;
m_pic.SetWindowPos(NULL,0,0,mWidth,mHeight,SWP_NOMOVE | SWP_NOZORDER);
// m_slid3.SetWindowPos(NULL,50,mHeight+10,mWidth+10,20, SWP_NOZORDER);
// TODO: Add extra initialization here
pos=0;
m_comm.SetPortOpen(TRUE); //打开串口
m_comm.SetSettings("9600,n,8,1"); //串口参数设置
m_comm.SetInputMode(0); //设置TEXT缓冲区输入方式
iExit=2;
m_comm.SetRThreshold(1);
inBuffer.bstrVal=new unsigned short[100];
return TRUE; // return TRUE unless you set the focus to a control
}
字幕上滚程序(非镜像模式)
void CTishiqiDlg::Onplay1()
{
// TODO: Add your control notification handler code here
isPlayMirr=1;
CDC *pDC=GetDC();
CDC *pDCpic=m_pic.GetDC();
CDC dcMem;
iExit=0;
CBitmap bmpBuf;
DWORD start,nowTime;
dcMem.CreateCompatibleDC(NULL);
bmpBuf.DeleteObject();
bmpBuf.CreateCompatibleBitmap(pDC,mWidth,mHeight);
CBitmap *poldBitmap=dcMem.SelectObject(&bmpBuf);
dcMem.SetBkMode(TRANSPARENT);
for (mTi=0;mTi<=mLen;mTi++)
{
start=GetTickCount();
if (mLen<10)
{
CSize size=pDCpic->GetTextExtent(tStr);
int xx= size.cx;
int yy= size.cy;
mLen=(xx/mWidth+1)*yy*172+mHeight;
//m_slid3.SetScrollRange(0,mLen,TRUE);
pos=0;
}
rect.top=rect.top-m_slid2.GetPos();
pos=pos+m_slid2.GetPos();
// m_slid3.SetPos(pos);
if (iExit>0) mTi=mLen;
dcMem.SetTextColor(fontColor);
CFont *poldFont=dcMem.SelectObject(&m_font);
dcMem.FillSolidRect(0,0,mWidth,mHeight,0);
dcMem.DrawText(tStr,rect,DT_WORDBREAK);
pDCpic->StretchBlt(0,0 ,rect.right,rect.bottom ,&dcMem, 0,0,rect.right,rect.bottom,SRCCOPY); // 和镜像模式有区别
DWORD dti=(DWORD)m_slid.GetPos();
do
{
MSG msg;
PeekMessage(&msg,NULL,0,0,PM_REMOVE);
TranslateMessage(&msg);
DispatchMessage(&msg);
nowTime=GetTickCount();
}while((nowTime-start)<dti);
}
dcMem.DeleteDC();
ReleaseDC(pDC);
ReleaseDC(pDCpic);
}
串口消息响应程序
void CTishiqiDlg::OnOnCommMscomm1()
{
// TODO: Add your control notification handler code here
if (m_comm.GetCommEvent()==2)
{
inBuffer=m_comm.GetInput();
CString strx;
int i;
strx=_T(inBuffer.bstrVal);
if (strx=="a") i=4;
if (strx=="b") i=3;
if (strx=="c") i=2;
if (strx=="d") i=1;
switch(i)
{
case 1:
{
if (iExit==0) rect.top=rect.top-100;
if (iExit==1)
{
rect.top=rect.top-100;
ShowTxt();
//OnPause();
}
if (iExit==3) m_slid.SetPos(m_slid.GetPos()-2);
break;
}
case 2:
{
if (iExit==0)
{
rect.top=rect.top+100;
}
if (iExit==1)
{
rect.top=rect.top+100;
ShowTxt();
}
if (iExit==3) m_slid.SetPos(m_slid.GetPos()+2);
break;
}
case 3:
{
if (iExit==2)
{
Onplay1();
}
OnPause();
break;
}
case 4:
{
if (iExit==3)
{
OnStop();
Onplay1();
OnStop();
}
if (iExit==2) TestSpeed();
if (iExit<2)
{
OnStop();
iExit=2;
}
break;
}
}
}
}
九、 AVR单片机CodeVision C语言源程序:
下面是使用CodeVision编译器编写的AVR单片机C语言程序,采用扫描端口的方式获取遥控信息,并通过串行口将遥控按键以字符”a”、”b”、”c”、”d”简单地转发给计算机。实际上我们还可以写入一个加密算法,同时将其作为一个加密狗来使用。
/*****************************************************
Chip type : ATmega8L
Program type : Application
Clock frequency : 4.000000 MHz
Memory model : Small
External SRAM size : 0
Data Stack size : 256
*****************************************************/
#include <mega8.h>
// Standard Input/Output functions
#include <stdio.h>
#include <delay.h>
char cc;
void main(void)
{
// Declare your local variables here
// Input/Output Ports initialization
// Port B initialization
// Func7=Out Func6=Out Func5=Out Func4=Out Func3=Out Func2=Out Func1=Out Func0=Out
// State7=0 State6=0 State5=0 State4=0 State3=0 State2=0 State1=0 State0=1
PORTB=0x01;
DDRB=0xFF;
// Port C initialization
// Func6=In Func5=In Func4=In Func3=In Func2=In Func1=In Func0=In
// State6=T State5=T State4=T State3=T State2=T State1=T State0=T
PORTC=0x00;
DDRC=0x00;
// Port D initialization
// Func7=In Func6=In Func5=In Func4=In Func3=In Func2=In Func1=In Func0=In
// State7=T State6=T State5=T State4=T State3=T State2=T State1=T State0=T
PORTD=0x00;
DDRD=0x00;
// Timer/Counter 0 initialization
// Clock source: System Clo