25 Years of Programming
An open source source for C, C++, OWL, BASIC, MDB, XLS, DOT, and more...
Home   Projects   Up   Sitemap   Search   Blog   Forum+Chat   About Us   Privacy   Terms of Use   Feedback   FAQ   Images   Services   Payments   Humor   Music

Generate and play random music through the PC speaker, Microsoft Visual C++

A C++ class (Scale) and a program that generates random music played through the PC speaker. It builds musical scales in 12 keys and a variety of musical modes. Converted from the earlier Borland version, this was written in Microsoft Visual C++ 2005 using its STL (Standard Template Library).

In addition to the code on this page, the program requires my.h and mylib.cpp.

The program places restrictions on the sequences of notes generated, in an attempt to make the output sound as musical as possible:

  1. Restriction to key and mode. The program selects one of the 12 musical keys; surprisingly, even on a computer, different keys sound different, probably due to the PC speaker's response characteristics. It then selects one of several musical modes (major, minor, etc.), and determines which notes of the 88 available are legal for that key and mode. From that point on, it only plays notes that are legal for that key and mode.
     
  2. Limitation of interval size using a normal distribution with a specified standard deviation. If you merely select random notes from the set of legal ones, the result will sound terrible because a note from the middle of the keyboard followed by a note at the top end followed by a note at the bottom end does not have enough continuity to sound musical. Given a starting note, the next note must usually be close enough to it to provide continuity, but it must also be somewhat unpredictable to avoid being boring.

    Thus, this program selects each next note using a normal distribution centered on the current note, with a standard deviation that the user specifies. For example, using a starting note of Middle C, a C Major scale, and a standard deviation of 4, the next note in the sequence is most likely to be Middle C again (a double-duration note), slightly less likely to be 1 legal note higher or lower than Middle C (B or D), even less likely to be 2 notes higher or lower (A or E), and so on. About 2/3 of the time, the next note will be within the 4-notes-lower to 4-notes-higher range, but occasionally larger intervals might be used. Thus, most of the time the music is continuous, but there are occasional larger intervals to create variety.

These strategies seemed to work quite well, and there were times when the program created short melodies that I wished could be captured to further develop manually. The program was working as a musical motive and theme generator! That's why the option was added to save the note sequence to a file. I never wound up using it that way, but the potential is definitely there.

Interesting enhancements would be:

  1. Give it MIDI output to get it away from the PC speaker. I believe the correct way to do this is with Microsoft's DirectMusic SDK, but I haven't learned how to use it yet.
  2. Allow selecting which MIDI instruments to use.
  3. Give it the ability to generate multiple simultaneous notes, polyphony.

A continuation of this project that I'm experimenting with in Ubuntu Linux implements the above features and is producing some strange music.

musiclib.h

/*	musiclib.h			5-20-07
	MS Visual C++ / STL version.
	Copyright (C)2003, 2007 Steven Whitney.
	Initially published by http://25yearsofprogramming.com.
	Published under GNU GPL (General Public License) Version 3, with ABSOLUTELY NO WARRANTY.

	Platform independent classes and methods used by the random music application.

*/
#ifndef __MUSIC_H
#define __MUSIC_H

#include "my.h"

using namespace std;

//////////////////////////////////////////////////////////////////////////////
// globals
//----------------------------------------------------------------------------
// the various modes.  There is a matching list of names in .CPP: ensure they always match.
//       0         1      2      3		  4			  5		    6	    7	     8
enum {CHROMATIC, MAJOR, MINOR, BLUES, PENTATONIC, WHOLETONE, MMINOR, HMINOR, MIXOLYDIAN };

//////////////////////////////////////////////////////////////////////////////
//
class Scale
{
public:
	Scale();
	virtual ~Scale();

	friend ostream& operator << (ostream& os, const Scale& s);	// write
	friend istream& operator >> (istream& is, Scale& s);		// read

	// functions
	bool IsLegal(int note);		// whether the given note is legal for the current key and mode
	int TranslateToNote(double x, double bottom, double top); 		// maps value into 0-87
	int TranslateToScaleNote(double x, double bottom, double top);	// maps value into NoteArray
	uint TranslateToSound(double x, double bottom, double top);		// maps value into a linear scale
	void Randomize();         	// randomize all the random note generation variables
	int GetNextRandomNote();	// returns next random note in the current sequence
	string GetRandomInfo();		// text info about the random set
	int GetFreq(int note);		// the frequency corresponding to the given note number
	int CurrentFreq();			// frequency of the note just randomly generated.
	int FillNoteArray();		// fill the array with notes

	// variables
	int CurrentKey;		// the key now in use; A = 0...
	int CurrentMode;	// the mode now in effect

						// THESE VARIABLES USED FOR RANDOMIZING KEYS AND NOTES
	double StdDev;		// standard deviation of distribution next random note is chosen from
						// 4 seems best, has wide variety.  3 is also good.
						// 2 is too stable.  5 is too wild.
	int NoteLength;		// the time value in milliseconds of the shortest note you use.
	int CurrentNote;	// the note just randomly generated.
	int PrevNote;		// the note just previous.

	string Major;		// lists of notes legal for the named mode, as chars; root = 0.
	string Minor;
	string Blues;
	string MMinorUp;
	string HMinor;
	string Pentatonic;
	string WholeNote;
	string Mixolydian;

	vector<long> NoteArray; 	// currently holds a major scale of notes

protected:

};
//----------------------------------------------------------------------------
// 							end class Scale
//////////////////////////////////////////////////////////////////////////////
#endif			// __MUSIC_H

musiclib.cpp

/*	musiclib.cpp		5-20-07
	MS Visual C++ / STL version.
	Copyright (C)2003, 2007 Steven Whitney.
	Initially published by http://25yearsofprogramming.com.
	Published under GNU GPL (General Public License) Version 3, with ABSOLUTELY NO WARRANTY.

*/
#include "musiclib.h"

using namespace std;

//////////////////////////////////////////////////////////////////////////////
// Globals
//----------------------------------------------------------------------------
// make sure these always match the enums in music.h!
char* ModeNames[] =
{"CHROMATIC","MAJOR","MINOR","BLUES","PENTATONIC","WHOLETONE","MMINOR","HMINOR","MIXOLYDIAN"};

//////////////////////////////////////////////////////////////////////////////
// class Scale code
//----------------------------------------------------------------------------
// constructor
Scale::Scale() : NoteArray(88)
{
	CurrentKey = 3;			// default is C Major
	CurrentMode = MAJOR;
	PrevNote = 0;			// a definite starting value, but arbitrarily chosen
	CurrentNote = 0;
	StdDev = 4.;
	NoteLength = 200;		// ms

	int major[] = 	 	{0, 2, 4, 5, 7, 9, 11, 255};
	int minor[] = 	 	{0, 2, 3, 5, 7, 8, 10, 255};
	int blues[] = 	 	{0, 3, 5, 7, 10, 255};
	int mminorup[] = 	{0, 2, 3, 5, 7, 9, 11, 255}; 	// mminordown is same as normal minor
	int hminor[] = 	 	{0, 2, 3, 5, 7, 8, 11, 255};
	int penta[] = 	 	{0, 2, 4, 7, 9, 255};
	int whole[] = 	 	{0, 2, 4, 6, 8, 10, 255};
	int mixolydian[] =	{0, 2, 4, 5, 7, 9, 10, 255};

	int i;
	for(i = 0 ; major[i] != 255			; i++) Major 		+= (char)(major[i]);
	for(i = 0 ; minor[i] != 255 		; i++) Minor 		+= (char)(minor[i]);
	for(i = 0 ; blues[i] != 255 		; i++) Blues 		+= (char)(blues[i]);
	for(i = 0 ; mminorup[i] != 255		; i++) MMinorUp 	+= (char)(mminorup[i]);
	for(i = 0 ; hminor[i] != 255 		; i++) HMinor 		+= (char)(hminor[i]);
	for(i = 0 ; penta[i] != 255 		; i++) Pentatonic 	+= (char)(penta[i]);
	for(i = 0 ; whole[i] != 255 		; i++) WholeNote 	+= (char)(whole[i]);
	for(i = 0 ; mixolydian[i] != 255 	; i++) Mixolydian	+= (char)(mixolydian[i]);
	// it might be interesting to add the other Greek modes

	FillNoteArray();			// fill array with notes
}                       		//constructor
//----------------------------------------------------------------------------
// destructor
Scale::~Scale()
{
}                       	//destructor
//----------------------------------------------------------------------------
// fill NoteArray with all the legal notes in the current key and mode
// set the key and mode you want before calling.
int Scale::FillNoteArray()
{
	NoteArray.clear();
	for(int i = 0 ; i < 88 ; i++)
		if(IsLegal(i))
			NoteArray.push_back((long)i);
	return NoteArray.size();
}                      		//FillNoteArray
//----------------------------------------------------------------------------
// the frequency corresponding to the given note number, rounded to the nearest whole number.
int Scale::GetFreq(int note) { return round(27.5 * pow(ROOT12OF2, note)); }
//----------------------------------------------------------------------------
// frequency of the note just randomly generated, rounded to the nearest whole number.
int Scale::CurrentFreq() { return GetFreq(CurrentNote); }
//----------------------------------------------------------------------------
// text info about the random set
string Scale::GetRandomInfo()
{
	ostrstream os;
	os	<< "Key = " << CurrentKey  << ",  Mode = " << CurrentMode
		<< " (" << ModeNames[CurrentMode] << "),  StdDev = "
		<< StdDev << ends;
	string s = os.str();
	delete[] os.str();
	return s;
}         					//GetRandomInfo
//----------------------------------------------------------------------------
void Scale::Randomize()
{
	// use whatever values prove interesting. Minimum allowable duration is 1 ms.
	NoteLength = 40 * random(11) + 1;
	CurrentKey = random(12);
	CurrentMode = random(9);
	StdDev = random(5) + 1;		// 1 to 5
}							//Randomize
//----------------------------------------------------------------------------
// whether the given note is legal for the current key and mode
bool Scale::IsLegal(int note)
{
	bool legal = false;
	if((note >= 0) && (note <= 87))
	{
		// (note % 12) normalizes it down to the base octave.
		// (note - CurrentKey) normalizes it down to the base scale of A (note 0).
		// so if it is legal NOW in A, then the real note is also legal in its key.
		int i = (note % 12) - CurrentKey;
		if(i < 0)				// but the normalizing can take it to the octave below
			i += 12;			// so bring it back up.
		char base = (char)i; 	// separate assignment here because chars are unsigned.
		switch(CurrentMode)
		{
			case MAJOR: 		legal = (Major.find(base) != string::npos); break;
			case MINOR: 		legal = (Minor.find(base) != string::npos); break;
			case BLUES: 		legal = (Blues.find(base) != string::npos); break;
			case PENTATONIC:	legal = (Pentatonic.find(base) != string::npos); break;
			case WHOLETONE: 	legal = (WholeNote.find(base) != string::npos); break;
			case HMINOR:		legal = (HMinor.find(base) != string::npos); break;
			case MIXOLYDIAN:	legal = (Mixolydian.find(base) != string::npos); break;
			case MMINOR:
				if(note > PrevNote)
					legal = (MMinorUp.find(base) != string::npos);
				else
					if(note < PrevNote)
						legal = (Minor.find(base) != string::npos);
					else
						legal = ((MMinorUp.find(base) != string::npos) || 
								(Minor.find(base) != string::npos));
				break;
			case CHROMATIC:
			default:
				legal = true;	// true to prevent accidental hangups
				break;
		}
	}
	return(legal);
}								//IsLegal
//----------------------------------------------------------------------------
// returns next random note in the current sequence
int Scale::GetNextRandomNote()
{
	PrevNote = CurrentNote;  	// archive it
	do
	{
		// this method has a different character, keeping notes near keyboard center.
		// CurrentNote = NormalRandom(39,7);
		// for a truly uniform distribution (should sound terrible)
		// CurrentNote = random(88);

		CurrentNote = NormalRandom(PrevNote,StdDev);	// normal generation method.
		if((CurrentNote < 0) || (CurrentNote > 87))  	// ran out of bounds
			CurrentNote = CurrentKey + 36;				// approx. mid-keyboard and surely legal
	}
	while(!IsLegal(CurrentNote)); 	// just an arg: CurrentNote won't change
	return CurrentNote;
}          						//GetNextRandomNote
//----------------------------------------------------------------------------
// returns a note value 0-87 proportional to where x stands between bottom and top.
// it is a chromatic note, not restricted to any scale.
// typical (and anticipated) use is for Bif.exe where raw values are 0 to 1:
// TranslateToNote(x, 0, 1);
int Scale::TranslateToNote(double x, double bottom, double top)
{
	if(bottom > top)
		swap(bottom,top);
	if((x == bottom) || (bottom == top))	// at bottom, OR range is undeterminable (an error)
		return 0;
	if(x == top)
		return 87;
	return round((x - bottom)/(top - bottom) * 87);
}                 				//TranslateToNote
//----------------------------------------------------------------------------
// returns a note value taken from NoteArray that is
// proportional to where x stands between bottom and top.
// returns the note number, NOT an index into NoteArray.
int Scale::TranslateToScaleNote(double x, double bottom, double top)
{
	int N = NoteArray.size();
	if(!N)
		return 0;
	if(bottom > top)
		swap(bottom,top);
	if((x == bottom) || (bottom == top))	// at bottom, OR range is undeterminable (an error)
		return (int)NoteArray[0];
	if(x == top)
		return (int)NoteArray[N - 1];

	// #error unchecked and sloppy
	int index = round((x - bottom)/(top - bottom) * (N - 1));
	if((index < 0) || (index >= N))
	{
		logerror("index out of bounds");
		return (int)NoteArray[0];
	}
	return (int)NoteArray[index];
}                 				//TranslateToScaleNote
//----------------------------------------------------------------------------
// maps value to a sound in a linear scale with a pleasing range,
// proportional to where x stands between bottom and top.
// the range 50 to 2550 Hz is copied from bif.cpp where it was found to work well.
uint Scale::TranslateToSound(double x, double bottom, double top)
{
	if(bottom > top)
		swap(bottom,top);
	if((x == bottom) || (bottom == top))	// at bottom, OR range is undeterminable (an error)
		return 0;
	if(x == top)
		return 2550;
	return round((x - bottom)/(top - bottom) * 2500. + 50.);
}
//----------------------------------------------------------------------------
// 						end class Scale
//////////////////////////////////////////////////////////////////////////////

music.cpp

/*	music.cpp			5-20-07
	Copyright (C)1995, 2003, 2007 Steven Whitney.
	Initially published by http://25yearsofprogramming.com.
	Published under GNU GPL (General Public License) Version 3, with ABSOLUTELY NO WARRANTY.

	MS Visual C++ CLR Console App target.
	This originally combined several experiments with sending sound to the PC speaker 
	and random music generation,
	so it has functions that are only barely related to each other. The most interesting
	is the random music generation, the output of which is surprisingly musical. There are
	several combinations of mode, tempo, and standard deviation that work well.
	It has a few runtime options, but I never got them organized, and usually just experimented
	with the source code and recompiled.

------
To Do:

Random generation:
--if it doesn't allow rests, add it.
--Try to create the ability to use the "87" development techniques on a motive;
  randomly generate the motive; then repeat it using the various alterations
  in randomly selected combinations. Allow real-time user input according to how much they
  like what they're hearing. Then genetically crossbreed the parameters, like
  musical "genetic sandpainting".
--See MS DirectMusic and DirectMusic Producer, which appear to have capabilities 
  similar to this. 
--Output to the sound card would be preferable to the PC speaker.
  DirectMusic appears to be the correct way to do it, but I haven't figured out how.  

------
Notes:

--7/7/04: Somewhere I described ideas about other random generation methods.
--For fractal music, and a bunch of other stuff, see ArtSong 5.05 by Digital Expressions (at CNet).
  (That was a note to myself. I haven't looked at it yet.)

*/
#include "stdafx.h"
#include "..\..\mylib\mylib.cpp"
#include "..\..\mylib\musiclib.cpp"
//#include <dmusici.h> // for DirectMusic experiments (complicated!)

using namespace System;
using namespace std;

//----------------------------------------------------------------------------
// This fn was broken out because the method used to render the sound might change.
void playsound(int frequency, int duration)
{
	System::Console::Beep(range(frequency, 37, 32767), range(duration, 1, 30000));
}						
//----------------------------------------------------------------------------
// Has all the code from the old heartest.cpp. Plays sounds through pc speaker.
// The speaker's range seems to be about 20 to 15,000
// randomwalk option was inspired by the question of what a stock price series would sound like.
int HearingTest()
{
bool randomwalk = false;
int frequency = 440;
string helpline("<H>igher, <L>ower, (octave: <U>p or <D>own) <ESC>=quit <P>ause <R>andom");
cout << helpline << endl;

userinput("Enter starting frequency",frequency);
while(1)
{
	if(randomwalk)
		switch(random(3))
		{
			case 0: break;
			case 1: frequency = max(frequency - 1, 0); break;
			case 2: frequency = min(frequency + 1, 30000); break;
		}
	cout << frequency << " ";
	// the officially supported range is 37 to 32767
	playsound(frequency, 20);
	if(kbhit())
		switch(tolower(getch()))
		{
			case 'u':
				frequency *= 2;
				break;
			case 'd':
				frequency /= 2;
				break;
			case 'h':
				frequency++;
				break;
			case 'l':
				if(--frequency < 0)
					frequency = 0;
				break;
			case 'p':
				cout << helpline << endl;
				presskey();
				break;
			case 'r':
				randomwalk = !randomwalk;
				break;
			case 27:
				//nosound();
				return(0);
		}
}
}							// HearingTest
//----------------------------------------------------------------------------
int PlayScale()
{
int i, frequency;
int sequence[] = {0,2,4,5,7,9,11,12,0,12,0,12,0,12,255};
//              a  a#   b   c  c#   d  d#   e   f  f#   g  g#   a
int scale[] = {220,233,247,262,277,294,311,330,349,370,392,415,440,0 };
//              0   1   2   3   4   5   6   7   8   9   10  11  12
puts("<ESC> quits");
puts("Any other key restarts");
do
{
	i = 0;
	while(sequence[i] != 255)
	{
		frequency = scale[sequence[i]];
		playsound(frequency, 500);
		i++;
	}
}
while(getch() != 27);
return(0);
}						// PlayScale
//----------------------------------------------------------------------------
// play raw frequencies from data file. 
// The program currently does not output freqs to any file automatically.
int PlayFileFreqs()
{
int frequency;
ifstream infile("freqs.dat");
while(infile >> frequency)
{
	frequency *= 20;
	if(kbhit())
	{
		getch();
		break;
	}
	cout << frequency << endl;
	playsound(frequency, 200);
}
return 0;
}
//----------------------------------------------------------------------------
// play the note numbers from data file.
int PlayFileNotes()
{
int note, prevnote = 0, frequency;
ifstream infile("notes.dat");
while(infile >> note)
{
	if(kbhit())
	{
		getch();
		break;
	}
	cout << note << endl;

	if((note < 1) || (note > 88))
		note = 40;

	frequency = (int)(27.5 * pow(ROOT12OF2, note));
	playsound(frequency, 200);
	prevnote = note;
}
return 0;
}
//----------------------------------------------------------------------------
// generates random notes. This is the option that generates random music.
int RandomNotes()
{
int frequency, duration;
Scale scale;

string helpline("<H>elp, <N>ew set, <I>nfo, <R>andomize periodically, <ESC>=quit");
cout << "While running, press:" << endl << helpline << endl;
presskey();

bool autorandomize = false;     	// start false for the file output
ofstream outfile("notes.dat");
cout << scale.GetRandomInfo() << endl;
for(int j = 0 ; j < 10000 ; j++) 	// plays 10000 notes unless user stops it
{
	if(kbhit())
		switch(tolower(getch()))
		{
			case 'h':						// show help line
				cout << helpline << endl;
				presskey();
				break;
			case 'n':   					// new set (start using new key, mode, and std dev)
				scale.Randomize();			// falls thru to info
			case 'i':						// info
				cout << scale.GetRandomInfo() << endl;
				break;
			case 'r':                		// toggle periodic key/mode/stddev randomization
				autorandomize = !autorandomize;
				cout << "AutoRandomize = " << (autorandomize ? "ON" : "OFF") << endl;
				break;
			case 27:
			default:
				//nosound();
				return(0);
		}

	if(autorandomize && !random(200))	// selects new key, mode, and std. dev. about every 200 notes
	{
		scale.Randomize();
		cout << scale.GetRandomInfo() << endl;
	}
	scale.GetNextRandomNote();
	// send the first 1024 notes generated to a data file
	if(j < 1024)								// limit to max for Fourier analysis in Excel
	{
		outfile << scale.CurrentNote << endl;
		cout << "*";							// indicates it was sent to file
	}
	cout << scale.CurrentNote << endl;

	frequency = scale.CurrentFreq();
	//manually set the value to select the options below
	switch(2)
	{
		case 0:
			//1) this option unconditionally plays the note even if the same as previous,
			// simulating longer duration notes.
			playsound(frequency, scale.NoteLength);
			break;
		case 1:			
			//2) this option prevents the same note twice.
			if(scale.CurrentNote != scale.PrevNote)		
			{
				playsound(frequency, scale.NoteLength);
			}
			break;
		case 2:
			//3) This option explicitly varies the duration
			if(scale.CurrentNote != scale.PrevNote)		
			{
				duration = (1 + random(2)) * scale.NoteLength;
				playsound(frequency, duration);
			}
			break;
	}
}
return 0;
}							//RandomNotes

//----------------------------------------------------------------------------
int main(array<System::String ^> ^args)
{
srand((unsigned)time(NULL));
while(1)
{
	char choice;
	do
	{
		cout << "-----------------------------------------------------------" << endl;
		cout << "0  Exit." << endl;
		cout << "1  Generate random tones (random music)" << endl;
		cout << "2  Play FREQUENCIES from file notes.dat" << endl;
		cout << "3  Play NOTES  from file notes.dat" << endl;
		cout << "4  Play scales" << endl;
		cout << "5  Hearing test" << endl;
		cout << endl << "Enter choice: ";
		choice = tolower(getche());
		cout << endl << endl;
	}
	while(string("012345").find(choice) == string::npos);
	if(choice == '0')
		break;				// Done.  break from while(1)
	switch(choice)
	{
		case '1': RandomNotes(); break;
		case '2': PlayFileFreqs(); break;
		case '3': PlayFileNotes(); break;
		case '4': PlayScale(); break;
		case '5': HearingTest(); break;
	}
}				// while(1)

Console::Write("Press <Enter> to close the window...");
Console::ReadLine();
return 0;
}

 

Valid HTML 4.01 Transitional Valid CSS
Yahoo! Search
Search the web Search this site
View content labeling at ICRA.