view src/melodyTriangle.cpp @ 38:330f2746fedd tip

Added keys help in a smaller font; changed loadness keyboard controls.
author samer
date Mon, 27 Feb 2012 20:09:10 +0000
parents 260cc4f4d70a
children
line wrap: on
line source
#include "melodyTriangle.h"
#include <GLUT/GLUT.h> 

#define BUFFER_ZONE 64 // have to drag this far to snap out of triange.
#define NUM_TEMPI 9
#define NUMEL(a) sizeof(a)/sizeof(a[0])

static int tempi[]={20,30,45,60,90,120,150,180,240};

static ofxOscBundle& operator<<(ofxOscBundle &bundle, ofxOscMessage msg) { 
	bundle.addMessage(msg); return bundle; 
}

static ofxOscSender& operator<<(ofxOscSender &sender, ofxOscMessage msg) { 
	sender.sendMessage(msg);
	return sender;
}

static ofxOscSender& operator<<(ofxOscSender &sender, ofxOscBundle bundle) { 
	sender.sendBundle(bundle);
	return sender;
}
	

melodyTriangle::melodyTriangle(const char *host, int port, int numVoices, 
							   bool enableKeys,int voiceIdOffset,int receivePort): 
	numVoices(numVoices), receivePort(receivePort), snapTruePos(enableKeys),
	enableKeys(enableKeys), randInit(false),allowExit(true), ratio(2), tempoIndex(4),
	display_msg(""), display_frames(0), display_font(NULL)
{
	for (int i=0;i<numVoices;i++) voices[i]=new Voice(i+1+voiceIdOffset);
	
	sender.setup( host,port );
	receiver.setup( receivePort );
	main_font.loadFont("/System/Library/Fonts/LucidaGrande.ttc",24);
	help_font.loadFont("/System/Library/Fonts/Menlo.ttc",16);
	//help_font.loadFont("/System/Library/Fonts/Courier.dfont",16);
}

melodyTriangle::~melodyTriangle() {
	printf("Deleting voice objects...\n");
	for (int i=0;i<numVoices;i++) delete voices[i];
}
	
//--------------------------------------------------------------
void melodyTriangle::setup(){
	ofSetCircleResolution(64);
	ofBackground(0,0,0);
	ofSetWindowTitle("Melody Triangle");
	ofSetFrameRate(40); // caps framerate if vertical sync is off.
	ofEnableSmoothing();

	// Set up triange coordinates. 
	// NB. whatever happens here, the triangle must be
	// isosceles and left-right symmetric around x=x1.
	// Otherwise the clipping won't work
	fitTriangleIn(ofGetWidth(),ofGetHeight());
	
	ofxOscBundle bundle;
	sender << ( bundle 
				<< msg("/tempo",tempi[tempoIndex])
				<< msgCalibrate()
			    << msgReplyTo());
	
	voiceGrabbed=NULL;
}

//--------------------------------------------------------------
void melodyTriangle::update(){
	bool sendStart=false;

	while( receiver.hasWaitingMessages() ) {
		// get the next message
		ofxOscMessage m;
		receiver.getNextMessage( &m );
		handleMessage(m);
	}
	
	constrained=false;
	if (voiceGrabbed!=NULL){
		Voice *vg=voiceGrabbed;
		if (mouseX!=vg->posx || mouseY!=vg->posy){
			int clipx=mouseX, clipy=mouseY;
			bool clipped=clipToTriangle(&clipx,&clipy);
			ofxOscBundle bundle;
			
			if (vg->inTriangle) {
				
				if (clipped) {
					// check how far we clipped
					if (ofDist(clipx, clipy, mouseX, mouseY)>BUFFER_ZONE && allowExit) { 
						// if far enough, we pop out of triangle and send
						bundle << msg("/death",vg->id);
						sender << bundle;
						vg->posx=mouseX;
						vg->posy=mouseY;
						vg->inTriangle=false;
						vg->status=Voice::pending;
					} else {
						// otherwise, we move to clipped point
						constrained=true;
						vg->posx=clipx;
						vg->posy=clipy;
					}
				} else { // not clipped; normal move
					vg->posx=mouseX;
					vg->posy=mouseY;
				}
			} else { // token was outside triangle
				vg->posx=mouseX;
				vg->posy=mouseY;
				if (!clipped){ // ie mouse now in triangle
					bundle << msg("/birth",vg->id)
						   << msgAmplitude(vg->id,vg->amplitude)
					       << ( randInit
					          ?	msg("/randinit",vg->id)
							  : msgOctave(vg->id,vg->octave)
							  );
					sendStart=true;
					vg->inTriangle=true;
					vg->truex=vg->truey=-1; // ie not known yet.
				}
			}
			
			if (vg->inTriangle){
				bundle << msgPosition(vg);
				vg->status=Voice::moved;
				if (sendStart && vg->isActive) bundle << msg("/start",vg->id);
				sender << bundle;
			}
		}
	};
}



//--------------------------------------------------------------
void melodyTriangle::draw(){
	ofSetLineWidth(2);
	ofSetColor(60,60,60);
	ofFill();		
	ofTriangle(x1, y1, x2, y2, x3, y3);

	// draw smooth edge, brighter if a token is constrained
	if (constrained) ofSetColor(255,96,96);
	ofNoFill();		
	ofTriangle(x1, y1, x2, y2, x3, y3);
	
	for (int i=numVoices-1; i>=0; i--){
		voices[i]->draw(enableKeys && voices[i]->isInVoice(mouseX,mouseY));
	}
	
	// display message if any
	if (display_frames!=0) {
		ofRectangle bbox=display_font->getStringBoundingBox(display_msg,0,0);
		ofSetColor(220,220,220);
		display_font->drawString(display_msg,
			(ofGetWidth()-bbox.width)/2, (ofGetHeight()-bbox.height)/2);
		if (display_frames>0) display_frames--;
	}
	if (ratio!=2) {
		ofSetColor(160,160,160);
		display_font->drawString(ofToString(ratio),16,ofGetHeight()-16);
	}
}

bool melodyTriangle::clipToTriangle(int *x, int *y) {
	bool clipped;
	
	if (*y>y2) { // off the bottom
		clipped=true;
		*y=y2;
		if (*x<x2) *x=x2; 
		else if (*x>x3) *x=x3;
	} else { // have to be a bit cleverer
		bool reflect=false;
		if (*x<x1) { // work in reflected coordinates
			reflect=true;
			*x=2*x1-*x;
		}
		
		int dx=(*x-x1), dy=(*y-y1); // deltas from top
		if (dx*DY13 > dy*DX13) {
			// (x,y) must be somewhere right of triangle now
			clipped=true;
			int dp=dx*DX13 + dy*DY13;
			if (dp<0) { *x=x1; *y=y1; } // off the top
			else if (dp>SQLEN13) { *x=x3; *y=y3; } // off the bottom right
			else { // project onto right edge
				*x=x1+dp*DX13/SQLEN13;
				*y=y1+dp*DY13/SQLEN13;
			}
		} else {
			clipped=false;
		}
		
		if (reflect) *x=2*x1 - *x; // reflect back if necessary
	}
	return clipped;
}



//- Keyboard ----------------------------------------------------------

void melodyTriangle::keyReleased(int key){
	if (enableKeys && key>='2' && key<='9') {
		ratio=2;
	}
}

struct command {
	const char *key;
	const char *desc;
};

struct command voice_commands[] = {
//	{ "←", "fractional delay" },
//	{ "→", "fractional advance" },
//	{ "↑", "multiply note rate" },
//	{ "↓", "divide note rate" },
	{ "[right]", "fractional delay" },
	{ "[left]", "fractional advance" },
	{ "[up]", "multiply note rate" },
	{ "[down]", "divide note rate" },
	{ "[", "transpose octave down" },
	{ "]", "transpose octave up" },
	{ "{", "quieter" },
	{ "}", "louder" },
	{ "c", "change to nearby pattern" },
	{ "s", "shuffle notes" },
	{ "h", "this help screen" },
	{ "H", "global keys help screen" },
};

struct command global_commands[] = {
	{ "<", "reduce tempo" },
	{ ">", "increase tempo" },
	{ "I", "toggle randomise on birth" },
	{ "S", "save current pattern" },
	{ "C", "calibrate triangle" },
	{ "F", "toggle full screen" },
	{ "R", "reset token positions" },
	{ "Q", "quit" },
	{ "H", "this help screen" },
	{ "h", "token keys help screen" }
};

void melodyTriangle::keyPressed(int key){
	if (enableKeys){
		if (key>='2' && key<='9') {
			ratio=key-'0';
		} else {
			printf("got key: %d.\n",key);
			switch (key) {
					
				case '<': 
					if (tempoIndex>0) tempoIndex--; 
					sender << msg("/tempo",tempi[tempoIndex]); 
					break;
				case '>': 
					if (tempoIndex<NUM_TEMPI-1) tempoIndex++;
					sender << msg("/tempo",tempi[tempoIndex]); 
					break;
					
				case ' ': sender << msg("/marker"); break;
				case 'S': sender << msg("/save"); break;
				case 'r': sender << msg("/report"); break;
				case 'I': 
					randInit ^= true; 
					display_msg = (randInit?"randomise on birth":"no randomise on birth");
					display_font = &main_font;
					display_frames = 40;
					break;
				case 'C': {
					ofxOscBundle bundle;
					sender << (bundle << msgReplyTo() << msgCalibrate()); 
					break;
				}
				case 'F': ofToggleFullscreen(); break;
				case 'R': reset(); break;
				case 'Q': ofAppGlutWindow::exitApp();
				case '/': display_frames=0; break;
				case 'h': 
				case 'H': 
				case '?':
					display_frames = -1;
					display_msg = help_string(key!='h');
					display_font = &help_font;
					break;
					
				case OF_KEY_ESC: setKeyboardEnable(false); break;

				default: // otherwise, send key to all voices under mouse
					for (int i=0; i<numVoices; i++)
						if (voices[i]->isInVoice(mouseX,mouseY)) 
							voiceKeypress(voices[i],key);
			}
		}
	} else {
		if (key==OF_KEY_ESC) setKeyboardEnable(true);
		else sender << msg("/key",key);
	}
}

void melodyTriangle::voiceKeypress(Voice *v, int key) {
	switch (key) {
		case 'a':
			if (v->inTriangle) {
				sender << msg(v->isActive ? "/stop" : "/start", v->id);
			}
			v->isActive=!v->isActive;
			break;
		case OF_KEY_LEFT:  sender << msgShift(v->id,-1,ratio); break;
		case OF_KEY_RIGHT: sender << msgShift(v->id,1,ratio); break;
		case OF_KEY_UP:    sender << msgPeriod(v->id,1,ratio); break;
		case OF_KEY_DOWN:  sender << msgPeriod(v->id,ratio,1); break;
		case ']': sender << msgOctave(v->id, ++v->octave); break;
		case '[': sender << msgOctave(v->id, --v->octave); break;
		case '}': sender << msgAmplitude(v->id, v->louder()); break;
		case '{': sender << msgAmplitude(v->id, v->quieter()); break;
		case 'c': sender << msg("/change",v->id); v->status=Voice::pending; break;
		case 's': sender << msg("/shuffle",v->id); break;
		default:  printf("unrecognised key: %d.\n",key);
	}
}

inline int max(int x, int y) { return y>x ? y : x; }

string melodyTriangle::help_string(bool global) {
	stringstream buf;
	struct command *cmds;
	int n, maxw=0;
	
	buf << " ( " << (global ? "GLOBAL" : "TOKEN") << " KEYBOARD CONTROLS ) \n\n";
	if (global) { cmds=global_commands; n=NUMEL(global_commands); }
	else        { cmds=voice_commands;  n=NUMEL(voice_commands); }

	for (int i=0; i<n; i++) maxw=max(maxw,strlen(cmds[i].key));
	for (int i=0; i<n; i++) buf << setw(maxw) << cmds[i].key << " - " << cmds[i].desc << "\n";
	
	buf << "\nPress '/' to dismiss.";
	return buf.str();
}


//- Mouse ------------------------------------------------------

void melodyTriangle::mouseDragged(int x, int y, int button){}
void melodyTriangle::mouseMoved(int x, int y ){}
void melodyTriangle::mousePressed(int x, int y, int button){
	// pick up first token under mouse
	for (int i=0; i<numVoices;i++){
		if (voices[i]->isInVoice(x,y)) {
			voiceGrabbed=voices[i];
			break;
		}
	}
}

void melodyTriangle::mouseReleased(int x, int y, int button){ 
	if (voiceGrabbed!=NULL) {
		Voice *v=voiceGrabbed;
		if (v->status==Voice::clear && v->truex>=0) {
			v->posx=v->truex;
			v->posy=v->truey;
			v->truex=v->truey=-1;
		}
		voiceGrabbed=NULL; 
	}
}

//--------------------------------------------------------------

void melodyTriangle::windowResized(int w, int h){
	fitTriangleIn(w,h);
	sender << msgCalibrate();
	reset();
}

void melodyTriangle::reset() {
	voiceGrabbed=NULL;
	for (int i=0;i<numVoices;i++) {
		Voice *v=voices[i];
		v->posx=x2+RADIUS+(numVoices-1-i)*36;
		v->posy=48;
		v->status=Voice::pending;
		v->isActive=true;
		if (v->inTriangle) {
			sender << msg("/death",v->id);
			v->inTriangle=false;
		}
	}
}

// OSC Message handling -----------------------------------------

Voice *melodyTriangle::get_voice(int id) throw(bad_voice_id) {
	for (int i=0; i<numVoices; i++) {
		if (voices[i]->id==id) return voices[i];
	}
	throw bad_voice_id(id);
}

void melodyTriangle::handleMessage(ofxOscMessage &m) {
	string msg_path=m.getAddress();

	try {
		if (msg_path.compare(0,8,"/notify/")==0) {
			string msg=msg_path.substr(8);
			Voice *v=get_voice(m.getArgAsInt32(0));
			
			cout << "Received " << msg_path << "\n";
			if (msg=="requested") 	 v->status=Voice::waiting;
			else if (msg=="pending") v->status=Voice::pending;
			else if (msg=="received") {
				float x=m.getArgAsFloat(1);
				float y=m.getArgAsFloat(2);
				v->status=Voice::clear;
				if (snapTruePos) {
					if (voiceGrabbed==v) {
						v->truex=x;
						v->truey=y;
					} else {
						v->posx=x;
						v->posy=y;
						v->truex=v->truey=-1;
					}
				}
				printf("True position of %d: %4.1f, %4.1f\n",v->id,x,y);
			} else if (msg=="position") {
				int x=(int)m.getArgAsFloat(1);
				int y=(int)m.getArgAsFloat(2);
				v->posx=x;
				v->posy=y;
				v->inTriangle=!clipToTriangle(&x,&y);
				v->status=Voice::clear;
				if (voiceGrabbed==v) voiceGrabbed=NULL;
			} else if (msg=="running") {
				v->isActive  = m.getArgAsInt32(1) ? true : false;
			} else if (msg=="params") {
				v->octave    = m.getArgAsInt32(1);
				v->amplitude = m.getArgAsFloat(2);
			} else if (msg=="visible") {
				v->isVisible = m.getArgAsInt32(1);
				if (voiceGrabbed==v && !v->isVisible) 
					voiceGrabbed=NULL;
			}
		} else if (msg_path=="/display") {
			display_msg=m.getArgAsString(0);
			display_frames=m.getArgAsInt32(1);
			display_font=&main_font;
		} else if (msg_path=="/font") {
			main_font.loadFont(m.getArgAsString(0),m.getArgAsInt32(1));
		} else if (msg_path=="/keyboard") { setKeyboardEnable(m.getArgAsInt32(0)); }
		else if (msg_path=="/reset")	  { reset(); } 
		else if (msg_path=="/fullscreen") { ofSetFullscreen(m.getArgAsInt32(0)); }
		else if (msg_path=="/quit")		  { ofAppGlutWindow::exitApp(); } 
		else if (msg_path=="/allowExit")  { allowExit=m.getArgAsInt32(0); }
		else {
			cout << m.getAddress();
			for (int i=0; i<m.getNumArgs(); i++) {
				cout << " " << m.getArgTypeName(i) << ":";
				switch (m.getArgType(i)) {
					case OFXOSC_TYPE_INT32:  cout << m.getArgAsInt32(i);
					case OFXOSC_TYPE_FLOAT:  cout << m.getArgAsFloat(i);
					case OFXOSC_TYPE_STRING: cout << m.getArgAsString(i);
					default: cout << "unknown";
				}
			}
			cout<< "\n";
		}
	} catch (std::exception &ex) {
		cout << "** Error processing OSC message: " << ex.what() << "\n";
	}
}

void melodyTriangle::setKeyboardEnable(bool en) {
	enableKeys=en;
	snapTruePos=en;
	display_msg=en ? "Keyboard enabled" : "Keybard disabled";
	display_frames=40;
	display_font=&main_font;
}

// OSC Message sending -----------------------------------------

ofxOscMessage melodyTriangle::msgPosition(Voice *v){
	
	ofxOscMessage m;
	///track id x y left right top bottom area  
	m.setAddress( "/track2d" );
	m.addIntArg( v->id );
	m.addIntArg( v->posx );
	m.addIntArg( v->posy );
	// printf("/track2d %i %i %i\n",v->id,v->posx,v->posy);
	return m;
}

ofxOscMessage melodyTriangle::msgCalibrate(){
	ofxOscMessage m;
	m.setAddress( "/calibrate" );
	m.addIntArg( x1 );
	m.addIntArg( y1 );
	m.addIntArg( x2 );
	m.addIntArg( y2 );
	m.addIntArg( x3 );
	m.addIntArg( y3 );
	printf("/calibrate %i %i %i %i %i %i\n",x1,y1,x2,y2,x3,y3);
	return m;
}

ofxOscMessage melodyTriangle::msgReplyTo(){	
	ofxOscMessage m;
	m.setAddress( "/reply_to" );
	m.addIntArg( receivePort );
	printf("sent /reply_to %i\n",receivePort);
	return m;
}

ofxOscMessage melodyTriangle::msgPeriod(int id, int num, int den) { return msg("/period",id,num,den); }
ofxOscMessage melodyTriangle::msgShift(int id, int num, int den) { return msg("/shift",id,num,den); }
ofxOscMessage melodyTriangle::msgOctave(int id, int oct) { return msg("/octave",id,oct); }

ofxOscMessage melodyTriangle::msgAmplitude(int id, float amp){
	ofxOscMessage m;
	m.setAddress("/amplitude");
	m.addIntArg(id);
	m.addFloatArg(amp);
	printf("/amplitude %i %1.3f\n",id,amp);
	return m;
}

ofxOscMessage melodyTriangle::msg(const char *addr, int a, int b, int c) {
	ofxOscMessage m;
	m.setAddress(addr);
	m.addIntArg(a);
	m.addIntArg(b);
	m.addIntArg(c);
	printf("%s %i %i %i\n",addr,a,b,c);
	return m;
}

ofxOscMessage melodyTriangle::msg(const char *addr, int a, int b) {
	ofxOscMessage m;
	m.setAddress(addr);
	m.addIntArg(a);
	m.addIntArg(b);
	printf("%s %i %i\n",addr,a,b);
	return m;
}

ofxOscMessage melodyTriangle::msg(const char *addr, int a) {
	ofxOscMessage m;
	m.setAddress(addr);
	m.addIntArg(a);
	printf("%s %i\n",addr,a);
	return m;
}

ofxOscMessage melodyTriangle::msg(const char *addr) {
	ofxOscMessage m;
	m.setAddress(addr);
	printf("%s\n",addr);
	return m;
}

void melodyTriangle::fitTriangleIn(int width, int height) {
	int triHeight    = height*0.75;
	int triHalfWidth = triHeight/sqrt(3);
	
	x1=width/2;
	x2=x1-triHalfWidth;
	x3=x1+triHalfWidth;
	y1=(height-triHeight)/2;
	y2=y3=height - (height-triHeight)/2;
	
	// used for clipping
	DX13=x3-x1; DY13=y3-y1;
	SQLEN13=DX13*DX13+DY13*DY13;
}

const char *melodyTriangle::bad_voice_id::what() const throw() { 
	std::stringstream out;
	printf("bad_voice_id(%d).\n",id);
	out << "Voice id " << id <<" not present.";
	return out.str().c_str();
}