各位同学好,各位知乎的小伙伴们大家好。今天,我要给大家分享的内容是:“如何在Matlab GUI中播放带声音的视频?”
这个问题有点奇怪,什么叫做带声音的视频?视频一般不都是带声音的吗?
没错,一般视频都是带声音的。而且,Matlab是擅长处理视频的,网上找到的教程也确实能够把视频文件读取,处理成一帧一帧的,然后播放出来。但是,问题出在很少有教程把“声音”问题处理好,视频处理成图像逐帧播放却没有声音,让人非常着急。
“没有声音,再好的戏也出不来!”
这个教程会完整地教会大家怎么把视频文件处理好,既能够播放图像,同时也能够播放声音。
我们把整个教程分为六个部分:
(1)读取视频文件(不带声音);
(2)读取视频文件的声音部分;
(3)搭建播放视频的Matlab GUI框架(表面);
(4)实现播放视频的操作;
(5)实现播放声音的操作;
(6)利用跟随变量在一个按钮上实现播放和暂停。
(1)读取视频文件(不带声音)
读取视频文件的标准操作需要用到VideoReader,我们看下面这句代码:
mp4FileName = 'Friends.mp4';
mVideo = VideoReader(mp4FileName);
读取后,mVideo这个结构体包含如下变量:
Duration - 视频的总时长(秒)
FrameRate - 视频帧速(帧/秒)
NumberOfFrames - 视频的总帧数
Height - 视频帧的高度
Width - 视频帧的宽度
BitsPerPixel - 视频帧每个像素的数据长度(比特)
VideoFormat - 视频的类型, 如 'RGB24'.
Tag - 视频对象的标识符,默认为空字符串''
Type - 视频对象的类名,默认为'VideoReader'.
UserData - 用户定义的数据,指定为任意数据类型的值,一般默认为“[ ]”
NumberOfFrames - 视频的总帧数。
宽和高信息对于确定窗口和坐标轴控件的属性是有用的,所以一般我们会有意识地把这个变量提取出来:
vWidth = mVideo.Width;
vHeight = mVideo.Height;
vFrameRate = mVideo.Framerate;
我们还提取了视频帧速这个变量,它可以用来决定播放速度的。
(2)读取视频文件的声音部分
读取视频文件的声音部分需要用到mmread这个工具包,代码示例如下:
[mVar01 mAudio] = mmread(mp4FileName);
这里的问题是mmread不像VideoReader,是Matlab自带的函数,它需要专门做一次“设置路径”的操作。
这个“设置路径”的按钮就在中间的位置,“布局”按钮的右侧,“预设”按钮的下方。点击这个按钮,会弹出一个对话框:
我们采用“添加并包含子文件夹”的方式添加了当前工作目录下面的mmread文件夹,至此,我们就可以利用这个工具包中的mmread函数来读取.mp4文件的音频部分的信息。
(3)搭建播放视频的Matlab GUI框架(表面)
function y = matlabgui_mp4Player(~)
% prepare
mp4FileName = 'Friends.mp4';
mVideo = VideoReader(mp4FileName);
% get the info of width and height
vWidth = mVideo.Width * 2;
vHeight = mVideo.Height * 2;
% create a figure
hFigure = figure(1);
set(hFigure, 'position',[100 50 vWidth vHeight+100]);
% create an axes
hAxes = axes('parent',hFigure);
set(hAxes, 'units','pixels', 'position',[1 101 vWidth vHeight]);
% create a image object, and set its 'parent' property
hImage = image(readFrame(mVideo), 'parent',hAxes);
%
% % create a pushbutton
hPushbutton = uicontrol(hFigure, 'Style','pushbutton', 'String','Play', 'position',[1 1 vWidth 100]);
y = hFigure;
end
细心的朋友应该注意到了,我把从视频中获取的高度和宽度都乘以了一个系数2,与此同时,我在设置hFigure的时候,在视频高度的基础上增加了一个100,并且坐标轴的高度也上提了100个像素。前者这么操作是因为我们使用的Friends.mp4文件是压缩过的,如果按照正常的比列进行播放的话会非常小,只有220 x 176。后者操作是为了给hPushbutton这个按钮控件留出空间。我们看下最后的布局效果:
大家对看到的黑色有可能产生两种理解,一种是这个黑色是控件默认的颜色,另外一种是这个黑色是读取的视频的第一帧image到坐标轴上的效果。后面这种猜测才是对的!这里关键是对readframe函数的理解。为了更准确地理解这个函数,我们试着用一个循环,假装运行readframe函数200次试试:
这就验证了第二种理解是正确的。同时,我们大概能够理解,readframe这个函数,运行一次,某个类似指针的用来标记frame的变量会跳到下一帧。有了这个理解,我们就可以人为地设置一个Timer控件,按照视频自身的每秒帧数的节奏播放画面,就可以达到播放视频的效果。
(4)实现播放视频的操作
因为运行一次readframe,指针就会跳到下一帧,这个就为我们播放视频提供了便利。配合Timer控件的使用,我们可以做到收放自如。先定义一个Timer:
% timer
global t;
timePeriod = round(1/vFrameRate,3);
t = timer('timerFcn', {@timerFcn, mVideo, hImage}, 'ExecutionMode','Fixedrate', 'Period',timePeriod);
然后要专门写一个它的回调函数timerFcn:
% function --> timerFcn
function timerFcn(obj, ~, v, hIM)
if hasFrame(v)
set(hIM,'cdata',readFrame(v));
else
stop(obj);
end
end
在定义Timer控件的时候,可以利用元胞格式{},往timerFcn函数中传入视频变量(mVideo是实参,v是形参),还有图层句柄(hImage是实参,hIM是形参)。呈现图片用的是set函数,set(hIM,'cdata',readFrame(v))修改hImage句柄的cdata参数就可以把读取的那一帧图像呈现在图层上。
这里用了一个if判断语句,如果还有帧的话,就执行呈现图片的操作;如果没有的话(hasframe函数是用来确定视频变量中的帧是否可供读取),就把obj给停止了。这里的obj就是这个函数的源对象,也就是Timer控件t。因为t是全局变量,所以obj也可以写成stop(t)效果应该是一样的。
(5)实现播放声音的操作
之前通过mmread读取了mp4文件:
[mVar01 mAudio] = mmread(mp4FileName);
但是,这样是不能播放声音的,还需要打造一个mp3player对象:
global mp3player;
mp3player = audioplayer(mAudio.data, mAudio.rate, mAudio.bits);
有了这个对象之后,不管是播放play,终止stop,暂停pause,还是恢复播放resume,都可以直接运用在这个对象上。比如,
play(mp3player);
就可以开始播放当前的mp3,也就是从视频中读取的音频部分。
(6)利用跟随变量在一个按钮上实现播放和暂停
作为标题,这么描述有点过于简单了,实际上整个播放包括一开始的启动play,暂停pause,恢复resume和退出后的停止stop。我们会引入一个全局变量叫做mState,它可以标记当前的状态是初始状态0,播放状态2,抑或是暂停状态1,直接粘贴完整版代码(想象一下这时候是有BGM的):
function y = matlabgui_mp4Player(~)
% prepare
mp4FileName = 'Friends.mp4';
% read the video and audio via VideoReader and mmread
mVideo = VideoReader(mp4FileName);
[mVar01 mAudio] = mmread(mp4FileName);
% get the info. from mVideo
vWidth = mVideo.width * 2;
vHeight = mVideo.height * 2;
vFrames = mVideo.NumberOfFrames;
vFrameRate = mVideo.Framerate;
% prepare mp3player obj
global mp3player;
mp3player = audioplayer(mAudio.data, mAudio.rate, mAudio.bits);
% create a figure
hFigure = figure(1);
set(hFigure, 'position',[100 50 vWidth vHeight + 100]);
%
% create an axes
hAxes = axes('parent',hFigure);
set(hAxes, 'units','pixels', 'position',[1 101 vWidth vHeight]);
% create a image object, and set its 'parent' property
hImage = image(readFrame(mVideo), 'parent',hAxes);
%
% % create a pushbutton
hPushbutton = uicontrol(hFigure, 'Style','pushbutton', 'String','Play', 'position',[1 1 vWidth 100]); % ,
% timer
global t;
timePeriod = round(1/vFrameRate,3);
t = timer('timerFcn', {@timerFcn, mVideo, hImage}, 'ExecutionMode','Fixedrate', 'Period',timePeriod);
%
global mState;
mState = 0; % 0 == original vs. 1 == pause vs. 2 == play
% assign y
y = hFigure;
% % Binding Mechanism
set(hPushbutton,'Callback',@hPushbutton_CallbackFcn);
set(hFigure, 'DeleteFcn',@hFigure_DeleteFcn);
%
% function --> hPushbutton_CallbackFcn
function hPushbutton_CallbackFcn(obj, event, hs)
if mState == 0
start(t);
play(mp3player);
set(obj, 'String','Pause');
mState = 2;
else
if mState == 1
start(t);
resume(mp3player);
set(obj, 'String','Pause');
mState = 2;
else
stop(t);
pause(mp3player);
set(obj, 'String','Resume');
mState = 1;
end
end
end
% function --> timerFcn
function timerFcn(obj, ~, v, hIM)
if hasFrame(v)
set(hIM,'cdata',readFrame(v))
else
stop(obj);
end
end
%
% % function --> hFigure_DeleteFcn
function hFigure_DeleteFcn(obj, event, hs)
stop(t); delete(t);
stop(mp3player);
end
end
补充一些细节:
(1)一旦我们设定了一个Timer控件,就要记得在退出窗口的时候,把这个timer对象停止并删除。
(2)退出窗口的时候,同样要记得停止mp3player对象,否则窗口关闭之后,声音会一直播放感觉很奇怪。
(3)mState实际上是3种状态,一开始为0,点击按钮就开始播放;播放时为2,点击按钮会出现暂停;进入暂停状态之后为1,点击按钮继续播放。
(4)image和imshow都可以用来把图像矩阵显示在坐标轴上,这里用了image是因为两个原因。第一,它在性能方面是优于imshow的,在这种连续播放图像帧的操作中,肯定会选择性能更优的image函数;第二,是因为image有一个自适应的缩放功能,hAxes在定义时用的是图像矩阵的两倍宽和高,image自动把图像矩阵放大了呈现在坐标轴上。
(5)VideoReader类中的readframe这个操作在2018b版本是会运行报错的,原因是我的这个版本的代码中有一句是这么写的:
vFrames = mVideo.NumberOfFrames;
然后就会报错:
错误使用 VideoReader/readFrame (line 99)
不能在查询 NUMBEROFFRAMES 属性或使用 READ 方法后调用
'READFRAME' 方法。
这里有两个建议,一个是采用2019b及以上版本,另外一个是注释掉“vFrames = mVideo.NumberOfFrames;”的语句即可。
(6)还有一个很小的细节,就是我们的mp4文件名不能带中文,不然会报错。
到这里,整个教程就写完了。国庆节&中秋节快乐♥
感谢李昌锦和杨轩两位同学的卓越贡献,mmread工具包就是他们找到的。多亏了他们,要不然也不太可能有这么完整的一个教程,♥谢谢谢谢♥
.