写给设计师的 OF 编程指南10-媒体加载与事件
Openframeworks 中可以载入许多外部数据。
其中有三类最为常用,分别是图片,音频,视频。
这节我们将结合事件,对音视频的加载进行详细展开。在最后,你可以亲自打造自己的音乐键盘,音乐画板。
读取图片
在开始前,先来回顾一下图片的加载方法。
与图片相关的函数
基本函数事件 | 说明 |
---|---|
.load(string a) | 读取图片,a 为图片路径 |
.draw(float x,float y) | x,y 为图片的横纵坐标 |
.draw(float x,float y,float w,float h) | x,y 为图片的横纵坐标.w,h 为图片的宽高 |
.setAnchorPercent(float a,float b) | a,b 会决定图片绘制中心的的偏移比例,a 值表示中心坐标在 x 轴方向上与图片宽度的比值,b 值表示在 y 轴方向上与图片高度的比值。当不调用此函数,图片的绘制中心默认处于图片左上角。与调用此函数, 并将 a ,b 值设为 0 的效果一样。 若 a,b 值都为 0.5,图片的绘制中心会居中。a,b 值都为 1,图片的绘制中心则会处于右下角 |
前面带.号的函数,都属于 ofImage 对象的成员函数,只有先声明图片对象,才能正常使用。
程序运行前,别忘记在 bin 中的 data 文件夹内放入图片素材
音乐的载入,播放与暂定
下面开始正式介绍音乐的调用方法。与载入图片非常类似,开始需要先声明一个音频对象。具体语法可参看以下例子
代码示例(10-1):
—- ofApp.h 内
#include "ofMain.h"
class ofApp : public ofBaseApp{
public:
void setup();
void update();
void draw();
...
ofSoundPlayer sound;
};
—- ofApp.cpp 内
void ofApp::setup(){
//载入音频
sound.load("1.mp3");
}
void ofApp::update(){
}
void ofApp::draw(){
}
void ofApp::keyPressed(int key){
//播放音频
if(key == 'p'){
sound.play();
}
//暂停音频
if(key == 's'){
sound.stop();
}
}
代码说明:
Openframeworks 本身就自带声音库,无需像 Processing 一样做许多前期的准备。现在只要复制上面代码,点 RUN 即能运行。按 P 键音乐播放,S 键音乐暂停。
ofApp.h 中的 “ ofSoundPlayer sound; ”声明了一个音频对象。ofSoundPlayer 的作用类似于 ofImage。
setup 函数中的 “ sound.load(“1.mp3”); ”,作用是指定音频对象的读取路径,将音乐素材载入其中。
keyPressed() 事件中的 “ sound.play() ” 与 “ sound.stop() ” 分别起播放和暂停的作用。中间的 “.” 表示 play 和 stop 是属于音频对象的成员函数。成员函数可理解为这个对象中包含的函数,属于这个对象,是事先定义好的。当以后需要播放多个音频对象,只要在对应的变量名后加上.play()。
音频素材需放在工程文件目录下的 data 文件夹内
Openframeworks 中支持常用的音频格式,如 mp3,wav,ogg 等。在写路径时记得写上对应的后缀名。
音乐速度控制
下面的例子会开始变得有意思起来,Openframeworks 中提供了一些函数可以控制音乐的播放速度。播放速度改变的同时,音调也会同时发生变化。当用鼠标来控制,会产生非常迷幻的效果。
视频地址(
https://v.qq.com/txp/iframe/player.html?vid=y1308mzqsaq&width=500&height=375&auto=0
代码示例(10-2):
—- ofApp.h 内
#include "ofMain.h"
class ofApp : public ofBaseApp{
public:
void setup();
void update();
void draw();
...
ofSoundPlayer sound;
};
—- ofApp.cpp 内
void ofApp::setup(){
//载入音频
sound.load("1.mp3");
}
void ofApp::update(){
//左右移动控制播放速度
float speed = mouseX/(float)ofGetWidth() * 3;
sound.setSpeed(speed);
//上下移动控制播放音量
float vol = mouseY/(float)ofGetHeight() * 4;
sound.setVolume(vol);
}
void ofApp::draw(){
}
void ofApp::keyPressed(int key){
//播放音频
if(key == 'p'){
sound.play();
}
//暂停音频
if(key == 's'){
sound.stop();
}
}
代码说明:
.setSpeed() 函数用于控制音频的播放速度。括号中的数值决定播放速度的大小。数值为 1 时,播放速度正常。数值大于 1 加速,小于 1 减速。
.setVolume() 函数用于控制音频的音量大小。括号中的数值决定音量值。数值为 1 时,音量值正常。数值大于 1 时音量增大,小于 1 减小。
这里创建了两个局部变量 speed 和 vol 作为参数的传入。因而鼠标的横坐标会改变音乐的音调,纵坐标会改变音量大小。
视频暂停与播放
Openframeworks 中加载视频与加载音频类似,无需加载外部库就能直接调用。
代码示例(10-3):
—- ofApp.h 内
#include "ofMain.h"
class ofApp : public ofBaseApp{
public:
void setup();
void update();
void draw();
...
ofVideoPlayer video;
};
—- ofApp.cpp 内
void ofApp::setup(){
ofSetWindowShape(640,360);
video.load("1.mov");
}
void ofApp::update(){
video.update();
}
void ofApp::draw(){
video.draw(0,0,640,360); // 位置x坐标,位置y坐标,视频宽度,视频长度
}
void ofApp::keyPressed(int key){
// 按键 P 与 按键 D 功能等价
if(key == 'p'){
video.play(); // 播放
}
if(key == 's'){
video.setPaused(true); // 暂停
}
if(key == 'd'){
video.setPaused(false); // 继续(播放)
}
}
视频截图:
代码说明:
第二行 “ ofVideoPlayer video; ” 用于声明视频对象。其中“ ofVideoPlayer ” 作用类似于 PImage。
setup 中的“ video.load(“1.mov”); ”,作用是指定视频对象的读取路径。括号中填写视频素材的地址
.update() 函数用于更新视频,是必须要写的函数。
.draw() 函数用于显示视频。第一第二个参数是视频绘制的横纵坐标。第三第四个参数决定视频显示的长和宽。
.play() 函数表示播放。.setPaused() 可以决定视频是播放还是暂定。括号中可以写 true 或 false。写成 true 时表示暂停。false 则表示继续播放。当参数为 false 时,与.play() 函数的效果等价。
视频速度控制
代码示例(10-4):
—- ofApp.h 内
#include "ofMain.h"
class ofApp : public ofBaseApp{
public:
void setup();
void update();
void draw();
...
ofVideoPlayer video;
};
—- ofApp.cpp 内
void ofApp::setup(){
ofSetWindowShape(640,360);
video.load("1.mov");
video.play()
}
void ofApp::update(){
video.update();
float speed = ofGetMouseX()/(float)ofGetWidth() * 4;
video.setSpeed(speed);
}
void ofApp::draw(){
video.draw(0,0,640,360);
}
.setSpeed() 函数可以控制视频的播放速度。当参数数值为 1 时,播放速度正常。数值大于 1 加速,小于 1 减速
因为创建了局部变量 newSpeed 并传入了 setSpeed() 函数中,鼠标的横坐标便会直接影响视频的播放速度
Openframeworks 常用事件一览
之前只介绍了 keyPressed() 事件,当它在键盘按下后就会触发。下面再解释 Openframeworks 中的其他最常用事件。
基本函数事件 | 说明 |
---|---|
void ofApp::setup() | 最先执行,只运行一次 |
void ofApp::update() | 反复执行,数据更新一般写在update内 |
void ofApp::draw() | 反复执行,绘图函数一般写在draw内 |
键盘相关函数事件 | 说明 |
---|---|
void ofApp::keyPressed(int key) | 键盘按下时执行,key返回键值 |
void ofApp::keyReleased(int key) | 键盘释放时执行,key返回键值 |
鼠标相关函数事件 | 说明 |
---|---|
void ofApp::mousePressed(int x, int y, int button) | 鼠标按下时执行,x,y返回坐标,button返回键值 |
void ofApp::mouseReleased(int x, int y, int button) | 鼠标释放时执行,x,y返回坐标,button返回键值 |
void ofApp::mouseDragged(int x, int y, int button) | 鼠标拖动时执行,x,y返回坐标,button返回键值 |
void ofApp::mouseMoved(int x, int y ) | 鼠标移动时执行,x,y返回坐标,button返回键值 |
void ofApp::mouseEntered(int x, int y); | 鼠标进入窗口时执行,x,y返回鼠标坐标 |
void ofApp::mouseExited(int x, int y); | 鼠标退出窗口时执行,x,y返回鼠标坐标 |
其他函数事件 | 说明 |
---|---|
void ofApp::windowResized(int w, int h) | 窗口尺寸改变时运行,w,h返回窗口的宽高 |
void ofApp::dragEvent(ofDragInfo dragInfo) | 拖动文件到窗口 |
它们的使用方法和 keyPressed 类似,在 ofApp.cpp 中早已定义了这些事件,使用的时候直接在里面添代码即可,非常方便。上面的事件都非常容易理解,只要略微实验一下就能迅速掌握用法。
事件流
我们可以通过一个实例,了解事件的执行顺序。
代码示例(10-5):
—- ofApp.h 内
#include "ofMain.h"
class ofApp : public ofBaseApp{
public:
void setup();
void update();
void draw();
...
};
—- ofApp.cpp 内
void ofApp::setup(){
ofSetFrameRate(1);
cout << 1 << endl;
}
void ofApp::update(){
cout << 2 << endl;
}
void ofApp::draw(){
cout << 3 << endl;
}
void ofApp::keyPressed(int key){
cout << 4 << endl;
}
void ofApp::keyReleased(int key){
cout << 5 << endl;
}
void ofApp::mouseMoved(int x, int y){
cout << 6 << endl;
}
void ofApp::mouseDragged(int x, int y, int button){
cout << 7 << endl;
}
void ofApp::mousePressed(int x, int y, int button){
cout << 8 << endl;
}
void ofApp::mouseReleased(int x, int y, int button){
cout << 9 << endl;
}
代码说明:
setup 函数中通过 ofSetFrameRate() 函数把程序的运行速率设成了 2 帧每秒。降低帧率有助于我们观察控制台的输出。不至于触发事件后就迅速被新数据刷到后面了。
尝试移动鼠标,点击鼠标,释放鼠标。观察输出的结果。通过 cout 来了解事件的执行顺序
值得注意的是,绘图函数一般不能写在除了 draw 函数的其他事件中。否则无法显示。若希望通过 keyPressed 之类的事件来控制图形元件的显示或隐藏,可以考虑创建布尔变量来作为中介
事件会按依序执行,只有当前一个事件中的所有代码执行后,才会执行下一个事件中的代码
综合示例-音乐键盘
结合新掌握的事件,我们就可以给程序添加新的交互。下面只要用几分钟的时间,就能简单地模拟一个音乐键盘。
视频地址:(
https://v.qq.com/txp/iframe/player.html?vid=a130872dluv&width=500&height=375&auto=0
代码示例(10-6):
—- ofApp.h 内
#include "ofMain.h"
class ofApp : public ofBaseApp{
public:
void setup();
void update();
void draw();
...
ofSoundPlayer sound1,sound2,sound3,sound4,sound5;
bool key1,key2,key3,key4,key5;
};
—- ofApp.cpp 内
void ofApp::setup(){
ofSetWindowShape(700, 400);
//载入音频
sound1.load("do.wav");
sound2.load("re.wav");
sound3.load("mi.wav");
sound4.load("fa.wav");
sound5.load("so.wav");
}
void ofApp::update(){
}
void ofApp::draw(){
ofBackground(255,214,79);
ofSetRectMode(OF_RECTMODE_CENTER);
float w = ofGetWidth() * 0.1;
float h = ofGetHeight() * 0.8;
if(key1){
ofSetColor(255);
}else{
ofSetColor(238,145,117);
}
ofDrawRectangle(ofGetWidth()/6 , ofGetHeight()/2, w, h);
if(key2){
ofSetColor(255);
}else{
ofSetColor(246,96,100);
}
ofDrawRectangle(ofGetWidth()/6 * 2, ofGetHeight()/2, w, h);
if(key3){
ofSetColor(255);
}else{
ofSetColor(214,86,113);
}
ofDrawRectangle(ofGetWidth()/6 * 3, ofGetHeight()/2, w, h);
if(key4){
ofSetColor(255);
}else{
ofSetColor(124,60,131);
}
ofDrawRectangle(ofGetWidth()/6 * 4, ofGetHeight()/2, w, h);
if(key5){
ofSetColor(255);
}else{
ofSetColor(107,27,157);
}
ofDrawRectangle(ofGetWidth()/6 * 5, ofGetHeight()/2, w, h);
}
void ofApp::keyPressed(int key){
//播放音频
if(key == 'a'){
sound1.play();
key1 = true;
}
if(key == 's'){
sound2.play();
key2 = true;
}
if(key == 'd'){
sound3.play();
key3 = true;
}
if(key == 'f'){
sound4.play();
key4 = true;
}
if(key == 'g'){
sound5.play();
key5 = true;
}
}
void ofApp::keyReleased(int key){
if(key == 'a'){
key1 = false;
}
if(key == 's'){
key2 = false;
}
if(key == 'd'){
key3 = false;
}
if(key == 'f'){
key4 = false;
}
if(key == 'g'){
key5 = false;
}
}
代码说明:
需要通过创建多个音频对象,来读取对应的声音信息。以便在触发不同键位时,能播放不同声音。
这里用到一个新事件 keyReleased()。它的作用是将键盘的颜色恢复成原始状态。当松开按钮就能出发。
开头声明的 5 个布尔值。用于检测按键状态。
综合示例-音乐画板1
除了键盘事件外,鼠标事件也是个好东西,得把它灵活运用起来。下面的示例可以打造一个音乐画板,其中用到两个和鼠标相关的事件。
视频地址:(
https://v.qq.com/txp/iframe/player.html?vid=b1308noi6wy&width=500&height=375&auto=0
视频截图:
代码示例(10-7):
—- ofApp.h 内
#include "ofMain.h"
class ofApp : public ofBaseApp{
public:
void setup();
void update();
void draw();
...
ofSoundPlayer sound1,sound2,sound3,sound4,sound5;
bool isDragging;
};
—- ofApp.cpp 内
void ofApp::setup(){
ofSetWindowShape(600, 400);
ofBackground(255,214,79);
ofSetBackgroundAuto(false);
//载入音频
sound1.load("do.wav");
sound2.load("re.wav");
sound3.load("mi.wav");
sound4.load("fa.wav");
sound5.load("so.wav");
}
void ofApp::update(){
}
void ofApp::draw(){
if(isDragging){
ofSetColor(107,27,157,100);
ofDrawCircle(mouseX,mouseY,8);
}
}
void ofApp::mouseDragged(int x, int y, int button){
isDragging = true;
if(x > 100 && x < 105){
sound1.play();
}
if(x > 200 && x < 205){
sound2.play();
}
if(x > 300 && x < 305){
sound3.play();
}
if(x > 400 && x < 405){
sound4.play();
}
if(x > 500 && x < 505){
sound5.play();
}
}
代码说明:
我们希望当按住鼠标拖动时,才在屏幕上进行绘图。所以需要创建一个布尔变量 isDragging,来获取当前的状态。
当拖动鼠标时,isDragging 就变成 true 值。Draw 函数中的绘图函数才会执行。所以会在屏幕上留下痕迹。当松开鼠标时,isDragging 会变成 false 值,所以 draw 函数中的绘图函数便会停止执行
在鼠标拖动事件中设计了几个触发条件。比如当鼠标的横坐标为 100 到 105 像素之间,音乐会自动播放。这就让屏幕产生了几根隐形的琴弦。只要经过特定的几个区域,便会触发相应的音乐。
综合示例-音乐画板2(改良版)
上面的例子效果已经很不错了。但仔细观察就会发现有不少问题。比如当鼠标移动速度特别快,屏幕上就会残留一个个圆点,并不是连贯的直线。同时还会导致某些音乐漏播。而当鼠标移动速度特别慢,经过横坐标为 100 到 105 的位置时,便会在极短的时间内播放多次音乐,产生卡顿的感觉。这些问题我们都可以通过以下例子来解决
由于微信有视频数量限制,可到以下地址观看:
(
代码示例(10-8):
—- ofApp.h 内
#include "ofMain.h"
class ofApp : public ofBaseApp{
public:
void setup();
void update();
void draw();
...
ofSoundPlayer sound1,sound2,sound3,sound4,sound5;
bool play1,play2,play3,play4,play5;
bool isDragging;
float lastMouseX,lastMouseY;
};
—- ofApp.cpp 内
void ofApp::setup(){
ofSetWindowShape(600, 400);
ofBackground(255,214,79);
ofSetBackgroundAuto(false);
sound1.load("do.wav");
sound2.load("re.wav");
sound3.load("mi.wav");
sound4.load("fa.wav");
sound5.load("so.wav");
}
void ofApp::update(){
}
void ofApp::draw(){
if(isDragging){
ofSetColor(107,27,157,100);
ofSetLineWidth(10);
ofDrawLine(mouseX,mouseY,lastMouseX,lastMouseY);
}
lastMouseX = mouseX;
lastMouseY = mouseY;
}
void ofApp::mouseDragged(int x, int y, int button){
isDragging = true;
if((mouseX - 100) * (lastMouseX - 100) < 0){
sound1.play();
}
if((mouseX - 200) * (lastMouseX - 200) < 0){
sound2.play();
}
if((mouseX - 300) * (lastMouseX - 300) < 0){
sound3.play();
}
if((mouseX - 400) * (lastMouseX - 400) < 0){
sound4.play();
}
if((mouseX - 500) * (lastMouseX - 500) < 0){
sound5.play();
}
}
void ofApp::mouseReleased(int x, int y, int button){
isDragging = false;
}
代码说明:
这里 Openframeworks 不像 Processing 一样自带 pmouseX 和 pmouseY。所以需要创建两个局部变量来模拟两者的效果。这里用了 lastMouseX 和 lastMouseY 来表示。当它们写在 draw 函数的末尾。就相当于获取上一帧鼠标的横纵坐标
draw 函数中使用了 ofDrawLine() 函数来替换原来的 ofDrawCircle() 函数。将上一帧的坐标和当前帧的坐标用直接连接起来。也就可以绘制出连续的曲线或直线。
mouseDragged 事件中设计了一个新的触发条件。它通过判断上一帧的坐标和当前帧的坐标是否在同一边,来判断是否有越过某个坐标。以这个条件为例“ if ((mouseX - 100) * (lastMouseX - 100) < 0) ”。其中” mouseX - 100 “得出的正负值,可以知道 mouseX 是距离横坐标100 的左侧还是右侧。结果为负数代表左侧,正数代表右侧。” lastMouseX - 100 “ 同理。因此,当前后的两个点不是同侧,一正一负相乘得出的便会是负数。也就满足了执行条件。
以上是一种简化的表达,巧妙地运用了某个数学运算规律 - 负负得正。你也可以分两种情况去分别讨论。但判断条件写起来就繁复多了。” if ((mouseX < 100 && lastMouseX >= 100) || (mouseX > 100 && lastMouseY <= 100)) “ 此判定条件等价于源代码的判定条件。
源文件下载链接
https://github.com/Wenzy--/OpenFrameWorksTest/tree/master/%E7%AC%AC%E5%8D%81%E7%AF%87%EF%BC%8DOF%E5%AE%9E%E4%BE%8B
Openframeworks 系列文章
资源索引