Project: Music Composition
Introduction
When developing a song, a composer may have an initial idea for a melody, including pitches (frequencies) and durations (length of the pitch). After writing down some first ideas, the composer likely will want to edit the melody: altering some notes, adding new notes, removing others, reordering notes, etc.
Of course, a full melody-composition program would require extensive development and is far beyond the scope of what might be done for a CSC 161 project. Instead, this project provides some basic capabilities for developing a melody and suggests ideas for a more extensive system.
Picking Data Structures
In considering how to store a single note, the natural choice is
a struct with fields for pitch and frequency. The
choice for an entire melody requires some analysis.
- A melody may contain any number of pitches.
- The composer may want to insert, delete, or modify notes anywhere within the melody.
- The composer may want to transpose a melody from one key to another (e.g., adjust the melody by moving all notes up 3 steps in the scale).
The need for flexibility suggests a linked structure be used. To use an array, we would need to determine how long the array should be. Further, insertion within the array would likely require moving many existing notes over to make room for the new addition. Deletion of a note would likely require moving many existing notes toward the start to take up the place of the deleted note. Linked lists do not have size or ordering constraints, so linked lists seem better suited to the storage of note information than arrays.
For this project, the simplest approach for storing a melody would be a singly-linked list, as discussed in several labs in this module. Other, more sophisticated list structures are beyond the scope of this project.
Specifying Notes
When writing a melody, a composer likely would want to identify the name of a note and its octave. For example, see Piano key frequencies on Wikipedia for some details.
Following this approach,
file pitch.h defines a pitch as
a struct with name, octave, and pitch information.
- As a complication, the "black" keys on a piano normally have two names. For example, an "A flat" or "Af" is the same pitch as a "G sharp" or "Gs".
-
For this reason, the
struct pitchcontains both anamefield and an alternative name fieldaltName.
Specifying Notes on the Piano
Using the struct pitch
in file pitch.h, it is
natural to define the notes on a piano as an array with an entry for
each of the 88 keys on the piano. Further, we only need one copy of
this array. For this reason, we utilize
file scale.c to define the
array of pitches corresponding to all keys on a piano. This array can
be compiled once and then used where necessary.
When using the scale array, a single index gives a structure with
full information about any note on the piano. For example, here are
several entries from the array:
| index | note name |
alternative name |
octave | frequency | comment | |
|---|---|---|---|---|---|---|
| 39 | "C" | "" | 4 | 262 | (middle C) | |
| 40 | "Df" | "Cs" | 4 | 277 | (D flat or C sharp) | |
| 48 | "A" | "" | 4 | 440 | ("Concert A" used for tuning) |
As this table suggests, given an array index, one can quickly retrieve name, octave, and frequency information.
With this notation, the first 5 notes of Spirit Song (from the Getting Started Module) might be represented as follows:
| index | name | octave | frequency | duration |
|---|---|---|---|---|
| 62 | "B" | 5 | 988 | 0.75 |
| 60 | "A" | 5 | 880 | 0.25 |
| 58 | "G" | 5 | 783 | 1 |
| 55 | "E" | 5 | 659 | 0.75 |
| 56 | "F" | 5 | 698 | 0.75 |
Because the index provides information about name and octave, we need store only the array index to obtain the rest of the note information.
Technical Note
Within a music composition program, there is no need to have multiple
copies of the scale array, but we may want to refer to
that array in several places. Thus, the main declaration of the
array will be in a file
scale.c which we will
define and compile separately.
In order to reference this array in other parts of the program, we place the line
extern struct pitch scale[88];
in any implementation files that may need to use this array. This
will allow the compiler to know that a scale array will be
supplied separately whereever it is needed.
Storage of a Note
Putting the above pieces together in the context of a composer creating a melody, a single note will contain just an array index and a duration. For a linked list of melody notes, a node could use these declarations:
typedef struct node noteNode;
struct node {
int scaleIndex;
double duration;
noteNode* next;
};
In reviewing this framework, a noteNode is
a struct with three fields: a scale array index, a
duration, and a pointer to the next node in the melody. For
convenience, these declarations are contained in the header
file noteNode.h.
Referencing a Note within a Melody
For this project, we will simply number the notes in a melody, with the first note having index 1. Thus, the first five notes in a melody would be notes 1, 2, 3, 4, and 5. Also, once a note is inserted within a melody, numbering will start fresh with the first number being 1. For example, suppose a melody has 5 notes and we insert 3 notes before the last one. The new melody will have 8 notes, and the last note (formerly number 5 in the original melody) now has number 8.
Musical Composition Functions for this Project
Altogether, this project involves the following operations:
-
Editing the melody.
- Setting the sequence of notes to null
- Adding a sequence of notes at the end of the current melody
- Changing any single note
- Deleting a single note
-
Displaying/playing a melody
- Printing a table of the notes (i.e., sequence number, note name, octave, frequency, duration)
- Playing the melody on the Scribbler 2
-
Modifying a melody (extra credit)
- Transposing each note in the sequence up or down by a specified number of half steps.
- Inverting each note in a sequence, so that if the original melody goes up a certain number of half steps, the inverted melody goes down the same number of half steps.
As described below, parts of the outline in blue are implemented in the code below.
Project Organization
Work on this project falls into approximately three main categories:
- Information about pitches
-
The data and operations for linked lists of notes
-
Definition of a node for the linked list
(
noteNode.h) -
Logical operations on a linked list (e.g., insert notes at end,
change node, delete node)
(
noteSeq.h) -
Implementation of the logical list operations
(
noteSeq.c)
-
Definition of a node for the linked list
(
-
Main program that uses pitch and note-sequence information to
perform operations for the composer
(
composerMain.c)
Following the reading on program management, these elements could be placed within the following file structure:
As illustrated in this diagram, noteNodes are used for
the linked list (noteSeq.h). Similarly, information
about note pitches in pitch.h is used in the
user's composerMain program and within linked list
processing (in noteSeq.c). However, information about
note pitches in pitch.h may or may not be used in the
definition of various linked-list operations
(in noteSeq.h). An array of notes on a piano
(in scale.c) is used in the implementation of note
operations (in noteSeq.c and
maybe noteSeq.h) and possibly in the
user composerMain program. Linked list operations
(defined in noteSeq.h) are used by the
user composerMain.c program, but the details of
implementation of those operations (given in noteSeq.c)
are not needed by the user.
Getting Started
To begin this project, code is available in the various files mentioned above to create a simple melody and to print the table of notes for the melody. (These are operations 1a, 1b, and 2a in the outline above. For further clarification, the outline displays these operations in blue.)
Existing files:
Work for this Project
Expand the initial files to complete the operations identified above (steps 1c, 1d, 2b in the outline; 3a and 3b may be completed for extra credit).
-
For each operation, you will need to consider what parts relate
specifically relate to list processing (and thus should be defined
in
noteSeq.hor implemented innoteSeq.c). -
For each operation, you will need to consider what is largely a
user-application matter and thus should be handled
in
composerMain.c.
Define a Makefile to compile and link the various parts
of your program. To make use of the MyroC library, you may need to
-
Copy
the
CPPFLAGSandLDFLAGSvariables from your standardMakefile, -
include
CPPFLAGSin the command line whenever you compile a C file into an object file, and -
include
LDFLAGSin the command line when you link object files into an executable.
