home
***
CD-ROM
|
disk
|
FTP
|
other
***
search
/
Audio Plus 10-94
/
AUDIOPLUS.ISO
/
win3
/
edit
/
muzika
/
muzika.asc
< prev
next >
Wrap
Text File
|
1979-12-31
|
56KB
|
930 lines
Technion - Israel Institute of Technology
Faculty of Electrical Engineering
Laboratory of Computer Music
MUZIKA - A Musical Notes and Scores Editor
Internal Description of the Software
Presented By: Lavy Libman & Yakov Aglamaz
Conducted By: Noam Amir
Contents:
1. Introduction
2. General description of the database
3. General description of the program modules
1. Introduction
This part of the MUZIKA documentation intends to describe the internal
structure of the software. We shall assume that the reader has learnt the
User's Guide and knows how the editor looks and "feels", and how to operate
the different features in it. We also assume the reader to be an experienced
programmer in the C++ language under the Microsoft Windows environment
(preferably using Borland C++ for Windows, as this is the compiler which was
used to compile the software). No general C++ or Windows terms, unless
related specifically to MUZIKA, will be explained herein.
While reading the internal documentation, it should be remembered that the
MUZIKA project is not a finished, commercial-style application. Indeed, from
the very start the intention of the project was to write the minimum amount
of code to implement the features required from a musical score editor,
stressing the ease with which the code can be extended and reused.
The program consists altogether of about 5400 lines of C++ code (not
counting the source code of the class libraries provided by Borland), making
an executable file of about 120K. We hope that this description of the
internal structure of the code, as well as the extensive commenting in the
code itself, will help the reader clarify how MUZIKA functions from the
inside.
2. General description of the database
Before we explain how the software functions from the inside, it is
necessary to explain the structure of the database which keeps the melody in
memory. In light of the database description, it will be easier to explain
the tasks performed by the different functions in the software through the
changes they make to the data in the database. We recommend the reader, as
he goes through the explanations below, to browse at the MUZIKA.H file,
which contains all kinds of global definitions relevant to different modules
of the software, including the definitions of the classes that comprise the
melody database.
2.1. The melody database
At the very top, the melody has a few global parameters, followed by a list
of parts. Each part has its information kept separately from other parts;
the structure of a part will be explained later. This is implemented by
having the Melody class - the topmost class in the database, of which there
is precisely one instance at any given time - inherit its parameters from
the MelodyParameters class, and add to that an IndexedList of parts. The
global parameters of the melody include the staff width (in pixels); this is
global to the entire melody, for this width has to be common to all staves
in the melody if score displays are to be shown and printed. Other
parameters that may be added here can be, for example, the name of the
melody or the text color. On the other hand, the key signature and the time
signature should not be kept as melody parameters, as these can change
throughout the different sections of the music.
The IndexedList class is an extension of the Array class from Borland's
class library, designed specifically for the purpose of holding lists at the
different levels of the MUZIKA database. It is essentially an Array, with
the following functions added:
- insertAt(Object &obj, int index) inserts an object at the given index in
the list, pushing the other objects from this index one forward.
- detachAt(int index) removes an object from the given index in the list,
pulling objects from subsequent indexes to close the gap. The object itself
is not destroyed.
- destroyAt(int index) is similar to detachAt, except that the object is
destroyed (i.e. its destructor is called).
- printOn(ostream &out) writes the objects in the list to the stream by
calling the printOn virtual function once for every object in the list.
Continuing with the description of the MUZIKA database, the same idea of a
few global parameters followed by a list is now repeated at the part level:
the Part class inherits from the PartParameters class a few parameters
global to the part, and follows with an IndexedList of staves. The
parameters of a part in the current implementation are:
- name is the part name.
- multiplicity is the staff multiplicity, i.e. the number of single staves
that are grouped together to create a multiple staff with common bars.
- editYMin is the Y-coordinate of the topmost visible line of the part. This
is not a constant parameter of the part, but rather used for displaying to
remember which staves are visible at the moment. Keeping this parameter for
all parts, instead of just for the currently visible one, ensures that when
the display is flipped from one part to another (via the Layout/Page... menu
selection), the staves initially visible in the new part will be those that
were visible the last time the part was shown.
Other parameters which a part may have can include, for example, the musical
instrument that plays the part.
Next, we come to how a staff is implemented. No surprises here; as the
reader should have guessed by now, the staff is again no more than a few
parameters followed by two lists this time: the list of point objects in the
staff and the list of the continuous objects. These lists were separated to
ease the treatment of the two object type groups, which is subtly different
in most cases. The Staff class inherits its parameters from a
StaffParameters class and has two IndexedList items corresponding to the two
object lists. The staff parameters include, in the current implementation,
only its coordinates: X, Y, and width. The Y coordinate of a staff is
invariant to display scrolling; the actual Y coordinate of a staff on the
screen is calculated as its Y coordinate minus the part's editYMin.
The PointObject and ContinuousObject classes, both derived from
MusicalObject, are the base classes defining the properties of a generic
point
object or continuous object, respectively. Every class defining a specific
musical object type (such as Note, Pause, etc.) is derived from one of these
two base classes. Actually, the MusicalObject class already defines the
properties that every musical object must have, letting the PointObject and
ContinuousObject classes only add the properties specific to the respective
object type groups.
An attribute that every musical object must have is the location of the
object relative to the staff in which it is. The location attribute can take
one of the following values (all defined in MUZIKA.H):
- INSTAFF means that the object can be anywhere in the staff. Therefore, at
the time of creation, the constructor of the object receives both the X and
Y coordinates of the mouse cursor relative to the staff.
- ABOVESTAFF means that the object is always located above the staff.
Therefore, at the time of creation, the constructor of the object receives
only the X coordinate of the mouse cursor.
- BELOWSTAFF means that the object is always located below the staff.
Objects having this attribute value are treated similarly to those which are
ABOVESTAFF.
- ABOVEMULTIPLE means that the object is always located above the multiple
staff. Therefore, at the time of creation, no matter where the mouse was
clicked, the object is inserted in the appropriate list of the topmost staff
of the multiple staff.
- BELOWMULTIPLE means that the object is always located below the multiple
staff. Therefore, at the time of creation, no matter where the mouse was
clicked, the object is inserted in the appropriate list of the bottommost
staff of the multiple staff.
- COMMONMULTIPLE means that a copy of the object will always be in every
staff in the multiple staff. Therefore, at the time of creation, no matter
where the mouse was clicked, a copy of the object is inserted in the list of
every staff comprising the multiple staff.
In addition, the value of ONEPERSTAFF can be added to any of the values
above to indicate that only one such object can exist in a staff. Thus, for
example, a Key object has a location attribute of INSTAFF+ONEPERSTAFF,
meaning that there can be only one key in a staff, and the thick bar limits
at the start or end of a staff have a location attribute of
COMMONMULTIPLE+ONEPERSTAFF, meaning that there can only be one such bar
limit per staff, and in that case there is a copy of it in every staff
comprising the multiple staff.
The MusicalObject class also defines a few member functions that every
musical object should have. These are:
- Draw(HDC hDC) draws the graphic image of the object in the given display
context. This function is used to display the object.
- printOn(ostream &out) saves the object data in the given output stream.
This function is used when the melody is saved in a file.
- clipOn(void far &*clipboard) saves the object data in the given memory
location. This function is used during clipping (i.e. cutting or copying).
The PointObject class, derived from MusicalObject, adds the properties that
every point object must have. These include the X coordinate of the object,
and the following functions:
- Format(int &X) sets the object's X coordinate to the given value. This
function is used when the part is reformatted. If a point object has any
special actions required when its coordinate is changed, they can be added
by having the specific class define its own version of this virtual function.
- Width() returns the object width in pixels. Usually, this is the general
object width defined in the Layout/Page... dialog box and kept in the
pixelsPerObject variable. This function, too, is used in the process of part
reformatting.
- MIDIPlay(ostream &out) writes the MIDI event which the object represents
to a temporary file. This function is used during the first pass of the MIDI
file creation.
- Duration() returns the duration of the MIDI event which the object
represents, in units of 148-th of a full note. This function, too, is used
in the process of creating the temporary file during the MIDI first pass.
The ContinuousObject class, derived from MusicalObject, adds the properties
that every continuous object must have. These include the Xleft and Xright
coordinates of the object, and the following functions:
- FormatLeft(int &Xleft) sets the object's Xleft coordinate to the given
value. This function is used when the part is reformatted. If a continuous
object has any special actions required when its left coordinate is changed,
they can be added by having the specific class define its own version of
this virtual function.
- FormatRight(int &Xright) is similar to FormatLeft, but alters the Xright
coordinate of the object.
Summarizing what has been said about the database so far, we see that it has
a structure of a 4-level tree, at the root of which sits the melody, on the
second level - the parts, on the third level - the staves, and finally, on
the leaves - the musical objects themselves. The object-oriented design of
the database is readily seen. It should be emphasized that none of the
functions, anywhere in the software, does not - in fact, cannot - make any
assumptions about the nature of the objects at the leaves, e.g. test the
type of a specific object and act differently if it is a loudness sign than
if it is a note sign. Whatever actions are specific to the musical object
classes are made in virtual member functions of the classes themselves.
Having said this much, let us turn and see what specific musical object
types have been defined in the current implementation of MUZIKA, remembering
that this is only a minimal set, intended to demonstrate the tremendous
possibilities of the design. No doubt does this set have to be extended to
include many more class types for the application to become a really useful
tool for editing high-featured scores. Anyway, the classes that have been
implemented are:
- Note is the class of note objects. The different note durations (full,
half, etc.) are distinguished by the value of the class's duration
attribute. In addition, the class defines an Y attribute representing the
note height, and eventually its frequency.
- Pause is the class of pause objects. The different pause durations are
distinguished by the value of the class's duration attribute.
- Key is the class of key objects. The different key types are distinguished
by the value of the class's type attribute. An instance of this class always
has a location value of INSTAFF+ONEPERSTAFF.
- Beat is the class of time signatures (i.e. beats per bar). The different
time signatures are distinguished by the value of the class's type attribute.
- Bar is the class of bar limits. The different bar limit types (single,
double, etc.) are distinguished by the value of the class's type attribute.
An instance of this class always has a location value of COMMONMULTIPLE,
with or without the addition of ONEPERSTAFF.
- Loudness is the class of loudness point objects (forte, fortissimo, piano
and pianissimo). The different loudness signs are distinguished by the
class's loudness attribute.
- Crescendo is the class incorporating the crescendo and diminuendo signs.
The two signs are distinguished by the class's direction attribute.
- Text is the class of text signs and instructions above or below the
staves. The text itself (up to 16 characters) is kept in text.
This concludes the description of the part of the database classes which is
defined in the MUZIKA.H and OBJECTS.H header file. To get the impression of
how things operate at the lowest levels of the database, the reader is
invited to catch a glimpse of the member function definitions in
OBJCLASS.CPP.
2.2. The symbols database
The second large part of the database is the database of the symbols. After
all, the user does not have a direct access to the melody database; rather,
he creates the musical objects using symbols on the left of the screen.
The symbols database is conceptually simpler than the melody database. Every
symbol has its own class, from which there is exactly one instance. All the
symbol classes are derived from the generic SymbolClass base class, which
defines the properties that every symbol must have. The symbol class
instances are held in a constant array called symbolArray.
The following properties are defined in SymbolClass:
- symbolID is a unique number that identifies the symbol. The ID is not just
a random unique number; rather, it represents the symbol set where it
belongs and its number within the set. In more detail, the formula to which
a symbolID value must conform is:
symbolID = 16 * (symbol set) + (symbol offset)
where symbol set is the symbol set number (e.g. 1 for Notes, 2 for Keys,
etc.), and symbol offset is the offset of the symbol, counted from 0 upwards
(e.g. 0 for the topmost leftmost symbol, 1 for the topmost rightmost, etc.).
- symbolType is a slightly misleading name for the attribute: it is actually
the type of the object (either POINTOBJECT or CONTINUOUSOBJECT) that is
created by the symbol.
- BitmapName(LPSTR, int) returns the name of the symbol bitmap in the
resource file, without the "B_" prefix. As can be easily verified by
browsing in the resource file (MUZIKA.RES) using any resource editor, all
the bitmaps are named "B_xxxx".
- DrawSymbol(HDC, HDC, BOOL) draws the symbol in the given display context
at the coordinates that are calculated from its own symbolID. This function
uses the previous one to load the symbol bitmap from the resource file. The
bitmap is drawn in reverse video if the symbol is active.
- CreateObject(int, int, int) creates the object corresponding to the
symbol. Sometimes, an single object type (such as Note) is created by
several symbols; in that case, it is the duty of CreateObject to construct
the appropriate version of the object and load its attribute variables with
the proper values.
It is pointless to give here a list of all the symbol classes derived from
SymbolClass. Far better is to direct the reader to the SYMCLASS.CPP file,
which is actually a library of the derived symbol classes and their versions
of the three virtual functions defined in SymbolClass, above. As with the
library of musical classes, we would like to emphasize how straightforward
it is to add a new symbol to the library: just define the new class, derived
from SymbolClass, and write its own versions for the three virtual functions
BitmapName, DrawSymbol, and most important, CreateObject, all after having
designed the symbol bitmap in the resource file (MUZIKA.RES) - and that is
all there is to it.
3. General description of the program modules
The source code of MUZIKA consists of over 5400 lines of C++ lines, not
including the sources of Borland's class library. Naturally, a project of
this size could not have been maintained without some sort of dividing the
code into smaller modules. In fact, MUZIKA contains 16 *.CPP files,
corresponding to logical modules of the software, containing functions that
take care of different tasks. In addition to that, we should mention the *.H
header files, especially MUZIKA.H and OBJECTS.H, that although do not
contain any actual statements, they provide the definitions of classes,
global variables, and function prototypes to be used by all the modules.
In the sequel, we attempt to describe the operation of each module
independently from the others, as much as this proves possible. When it was
not possible, we ordered the module descriptions in such a way that there
would only be references to modules described previously.
In our descriptions, we do not provide an exact list of the functions, their
parameters and return values. We feel that that would be a rather useless
duplication of the source code itself, which is well and extensively
documented as it is. Instead, we concentrate on the duty that the module
fulfills within the overall design, and explain the implemented algorithms
on the conceptual level. Either way, we recommend the reader to browse
through the enclosed source code listings as he reads the description; this
will without doubt help in understanding the logic behind the software
design.
3.1. The main module (MAIN.CPP)
The main module contains several initialization functions and the main
window message-processing and painting functions. The entry point to the
program is at the beginning of this module, in function WinMain, which does
no more than calling the initialization functions and establishing a
message-dispatching loop, as is customary in every Windows application. The
initialization functions register the main window class, create and display
the main and edit windows for the first time. The initialization code is
entirely straightforward and is not much different from what can be found in
any other Windows application.
The main window function processes messages originated by Windows that are
intended for the main window. For example, a WM_COMMAND message, indicating
that the user has triggered an action using the menu, causes it to call the
ProcessMenu function in the menu processing module. A WM_LBUTTONDOWN
message, sent when the left mouse button is clicked, causes a call to the
symbol identification function in the symbols module, to check if the cursor
has been clicked on a symbol and make it active if so. Finally, a WM_PAINT
message, sent when the main window needs repainting (i.e. when it has been
resized, or another window has been removed from over it), triggers a call
to the PaintEditWindow function, described next.
The PaintEditWindow function is in charge of painting the edit window. It
draws the lines separating between the symbols, and then calls the
symbol-drawing functions in the symbols module.
To summarize, then, the main module play a role of a dispatcher; any
messages intended for the main window are, this is true, recognized here,
but the actions taken in response are actually a part of other modules
(specifically the menu processing module and the symbols module).
3.2. The IndexedList class module (INDEXED.CPP)
The IndexedList module is not as important by itself as it is for other
modules, that use the functions defined here constantly. In a previous
chapter we have already explained the use of the IndexedList class, which is
an extension of the Array class from Borland's class library, in the melody
database. The functions that actually extend IndexedList over Array are
defined here. These are the insertAt, detachAt, destroyAt, and printOn
functions, all except the last similar to those of Array but keep the list
elements at contiguous indexes at all times (as opposed to an Array, which
allows for empty slots in the list).
3.3. The database module (DATABASE.CPP)
The name of the database module may be slightly misleading: functions that
make changes in the database are scattered throughout all parts of the
software, without any central database management unit that would take care
of keeping the database intact. What there actually is in the database
module are member functions of classes that comprise the database. Among the
functions defined here for all the class levels (Melody, Part, and Staff)
are LoadFrom, which loads the appropriate class instance data from a file,
and printOn, which does just the opposite - saves the class instance data in
a file. There are also a few class constructor and destructor functions.
Finally, the only Melody class instance is declared at the file end. The
code in this module is straightforward and contains no subtleties; we feel
therefore confident that the commenting of the code in the source file
itself provides a sufficient explanation.
3.4. The display module (DISPLAY.CPP)
The display module is responsible for displaying the information stored in
the melody database in either format supported by MUZIKA (single part or
score, in the current implementation). There are two important functions in
the module, namely EditWindowProc, the edit window function, and
PaintEditWindow, which is responsible for displaying staves and objects in
the edit window. The other two functions, used only during initialization,
are less important. By the way, the edit window is a child of the main
window, containing the main window client area minus the area that is
occupied by the symbols and status line. The edit window is automatically
resized every time the main window changes its size. Staves and musical
signs are drawn only in the edit window.
The EditWindowProc function responds to all messages intended for the edit
window, including cursor movement, mouse button clicks and double-clicks,
and scrollbar changes. Covering this quite complex function's tasks is
impossible without elaborating on the way it responds to each and every
message type, so let's do just that.
A WM_PAINT message, requesting a repaint of the edit window, is treated by
the PaintEditWindow function, described later in this section. A
WM_MOUSEMOVE message does not offer any cause for alarm, either - all that
has to be done is change the cursor shape according to the current edit mode
symbol (pencil, eraser or hand).
The problems begin with the WM_LBUTTONDOWN message, because a simple
clicking of the left button can trigger a variety of actions, according to
what symbol is active at that moment. In any case, the actions which
eventually lead to making the required changes in the database are done in
the edit module (EDIT.CPP), but it is the duty of EditWindowProc to select
the operation to be triggered. First of all, EditWindowProc checks that a
single part is displayed (no editing is allowed on a score display). Then,
it checks what the current symbol is.
- If it is the pencil, a staff creating function in the edit module is
called.
- If it is the eraser, an object deleting function in the edit module is
called.
- If it is a symbol whose symbolType is POINTOBJECT, a point object
inserting function in the edit module is called.
In the remaining cases (the active symbol is the hand or corresponds to a
CONTINUOUSOBJECT), no operation can be triggered just yet, because we need
the point where the mouse button is released as well as where it is clicked.
In that case, the mouse is captured to the edit window and the capture mode
variable is set accordingly, to let the required operation to be completed
after the mouse button is released.
A WM_LBUTTONDBLCLK is processed similarly: if the current symbol is the
eraser, the staff erasing function is called immediately, while if the
current symbol is the hand, the mouse is captured until the time it is
released, and only then will the staff movement operation be completed.
In light of the above, a mouse button release notification message cannot be
just thrown away: an operation may need to be completed at this stage.
Therefore, in response to a WM_LBUTTONUP message, EditWindowProc checks
whether the mouse has been captured to the edit window, indicating that
there is indeed an unfinished operation pending for the mouse button release
point. The nature of the unfinished operation is kept in the capture
variable. Whatever the operation is (inserting a new continuous object or
moving an object or a staff), the appropriate function in the edit module is
called with the mouse button clicking and releasing points.
There is yet another message which EditWindowProc has to process, and that
is WM_VSCROLL, indicating an operation of some kind on the vertical scroll
bar (line up, line down, page up, page down, or direct thumb movement). In
response to the message, the thumb position is set to its new location, and
the screen is refreshed. By the way, the scroll bar range is identical at
all times to the range of Y coordinates of the staves in the current part
(that is, the scroll bar values range from 0 to the Y coordinate of the last
staff).
To summarize, EditWindowProc takes much of the sting out of the editing
process: it makes the relatively difficult decision of which operation is to
be performed, leaving the edit module functions only to update the database
given all the needed coordinates.
The other nontrivial function in the display module, as we have noted, is
PaintEditWindow, whose job is to display a part of the melody database on
the screen. The display algorithm is similar whether it is a single-part
display or a score display. In both cases, the relevant staves are drawn
(using the Staff class Draw function, also included in this module at the
end of the file), and the musical signs are drawn on those staves that
reported that they were not entirely clipped, by going through the point and
continuous object lists, calling the Draw virtual function for every item in
the lists. The difference is only which staves are drawn; obviously, when
displaying a score the parallel staves of all parts take part in the
algorithm, while when displaying a single part only that part's staves are
included. Anyway, after having drawn the staves, the status line in the main
window is updated with information about which staves are visible. Strictly
speaking, having the main window updated in one of its children's window
function is a violation of the Windows "correct" programming style; what
should have been done instead is define the status line area as another
child window and have the edit window send an update message to that child
whenever it needed an update, but the implemented solution was chosen for
the sake of saving space (and even more modules).
3.5. The edit module (EDIT.CPP)
The edit module functions are responsible for updating the melody database
in response to a edit operation initiated by the user. Except for the first
couple, all the functions are called from the display module, after the edit
window function has selected the operation and obtained the cursor screen
coordinates for it. The operations supported here include inserting,
deleting and moving staves and point and continuous objects. To understand
how these operations are taken care of the reader should have a clear view
of the melody database structure, for which he should refer to section 2.1.
Basically, all the edit functions operate similarly: they scan an
appropriate list (either a list of staves in a part or of objects in a
staff) in an attempt to find the list index where the operation takes place,
and then insert or delete at that index, according to the operation that
needs to be done. There are, however, certain subtleties here and there,
that need specific care to be taken (a value of COMMONMULTIPLE for an
object's location attribute, for example). The code of the functions is well
commented, but we still feel that a general explanation of each function's
task here will be helpful.
InsertEmptyStaff inserts a new empty staff in the specified staves list
given the staff Y location and the part multiplicity. The list is scanned
for a staff having a Y coordinate larger that the inserted staff's one, and
the new staff is inserted at that index. All the subsequent staves get their
Y coordinate moved down by a staff height. Therefore, the list is kept
sorted by the Y locations in ascending order. Also, if there is a marked
block past where the staff is inserted, the block marking variables are
incremented too.
NewMultipleStaff creates a new multiple staff, after the user has clicked
the pencil-on-staff symbol. A number of single staves equal to the part's
staff multiplicity is inserted in the part's staff list by calling
InsertEmptyStaff that number of times. The scroll bar range is readjusted to
the new Y coordinate range (recall that the scroll bar range, from Windows'
point of view, is from 0 to the last staff's Y coordinate).
IdentifyStaff finds the index of the staff to which the given Y coordinate
(obtained from the current cursor position) is closest. It does this by
scanning the list of staves, returning one which actually contains the
point, if it exists. Failing that, it returns the staff from which the
point's Y distance is minimal, but in any case not more than half a staff's
total width, as defined in the page layout dialog box and kept in the
pixelsPerStaff global variable.
DeleteMultipleStaff deletes the multiple staff identified by a Y coordinate
(obtained from the current cursor position). Having identified the staff
using IdentifyStaff, the function simply removes all the staves in the same
multiple staff from the list. If the multiple staff contains any objects,
the user is requested to confirm the operation.
NewPointObject is probably one of the most important functions of the
software, though it is not conceptually difficult. First of all, it creates
a point object corresponding to the active symbol at the given cursor
position (using the current symbol's CreateObject function - there is no
idea about what the object is). Next, it checks the just created object's
location attribute to decide where to insert it. If the object has a
location value of INSTAFF, ABOVESTAFF, or BELOWSTAFF, it is simply inserted
in the staff identified by IdentifyStaff. An object with a location value of
ABOVEMULTIPLE or BELOWMULTIPLE is inserted in the top or bottom staff of the
multiple staff, respectively. Finally, in case of a COMMONMULTIPLE object, a
copy of it is inserted in every staff comprising the multiple staff. In
addition to all of the above, if the object has the ONEPERSTAFF bit set, the
staff is checked not to contain another object at the same place, reporting
an error if this condition is not met.
NewContinuousObject is mostly similar to NewPointObject, just described. It
creates a continuous object corresponding to the active symbol at the given
cursor coordinate), and inserts it in one of the staves from which the
multiple staff is constructed, according to the object location attribute.
DeleteMusicalObject may look frightening at first sight, considering the
amount of code in it, but this actually follows from the majority of special
cases that need to be taken care of. Basically, it scans the point and
continuous object lists of the staff identified by IdentifyStaff, deleting
the objects that are within (pixelsPerObject/2) pixels away from the given
cursor position. If any of the deleted objects is COMMONMULTIPLE, the other
staves in the same multiple staff are also scanned and this object is
deleted from them too.
MoveStaff moves a multiple staff from one place to another in the same part.
Before actually moving, the destination is checked to be free from other
staves. In the process of moving (achieved by detaching each staff of the
multiple staff from the staff list and reinserting it at its new index), any
staves that the moving has crossed (and are now on a different side relative
to the moved staff) get their Y coordinate updated, and so is done to the
block marking variables in case there was a marked block in the area.
MoveMusicalObject moves a musical object by using the CutBlock and
PasteBlock functions in BLOCK.CPP. Therefore, to understand the object
moving operation, the user is referred to the description of the block
operations module later in this chapter.
3.6. The menu processing module (MENU.CPP)
The job of this module is to dispatch various requests made by the user
through the main menu to the modules that take care of them. The ProcessMenu
function is thus not more than one big switch statement, interpreting the
menu item codes and either calling the appropriate functions in other
modules directly or by creating a dialog box function instance first.
Another function in this module is InitializeMenu, used during the software
initialization. According to the Boolean parameter it receives, it enables
or grays out the menu items that cannot be used before a melody is loaded in.
3.7. The file operations module (FILE.CPP)
The file operations module is an interface to the functions offered to the
user by the File menu, namely creating a new melody, loading and saving a
melody, and converting a melody to a MIDI file. The functions themselves are
not implemented in this module; what is rather contains is the functions of
the dialog boxes created when the user selects different file operations.
The first function in the source file is AskSave, which does the job of
confirming any operation that may destroy the current melody (such as
loading a new one or quitting the application) when the current melody has
been modified since the last time is was saved. The information about
whether the melody has been modified is kept in the melodyModified global
variable, which is set to TRUE by all the editing functions that modify the
melody, and to FALSE by the saving function.
The rest of the module functions are straightforward dialog box functions,
whose duty is to process typical dialog box messages, such as WM_INITDIALOG
to initialize a dialog box or WM_COMMAND to respond to an action made by the
user. Actually, they differ little except for what they do after the user
selects OK to confirm his selections:
- DialogNew empties the current melody from any parts it might have
contained, and creates a new melody with a single part named UNNAMED, with a
staff multiplicity of 1. It also puts default values into a few global
variables, such as melodyModified and scoreDisplay.
- DialogOpen opens the file whose name the user has given (reporting an
error if that file does not exist) and issues a call to the LoadFrom
function of the Melody class to load the melody.
- DialogSaveAs opens the file whose name the user has given (issuing a
warning if that file already exists) and calls the printOn function of the
Melody class to save the melody.
- DialogCreateMIDI calls the MIDI file creation module to convert the
current melody to a MIDI file.
3.8. The printing module (PRINT.CPP)
The printing module contains the functions that make a hard copy of a melody
or of one of its parts on a printer. In any case, the printer selected as
the Windows default is used. There are two functions, PrintSinglePart and
PrintScore, that take care of the process of printing a single part or the
score of all parts. The interface function to the module is PrintEditWindow,
which creates a printer device context and calls one of the other two
printing functions according to what the current display setting.
Both printing functions operate quite similarly, the main difference being
the source of their staves, taken either from only one part or all parts,
respectively. Both use the staves' and objects' Draw functions to create a
multiple staff image in the printer device context and then flush it to the
printer. The multiple staves are therefore printed one by one.
3.9. The layout module (LAYOUT.CPP)
The layout module implements the layout dialog box functions. Specifically,
it contains three functions:
- DialogParts, the Layout/Parts... dialog box function;
- DialogPage, the Layout/Page... dialog box function;
- DialogNewPart, the Layout/Parts.../New... dialog box function.
The dialog box functions code is tailored to the specific form of the dialog
boxes themselves, designed in the MUZIKA.RES resource file under the names
D_PARTS, D_PAGE and D_NEWPART respectively. To understand the code, the
reader must try using the MUZIKA editor for a while and get the feel of how
the mentioned dialog boxes look, but once that stage is over, the code is as
straightforward as any dialog box function code usually is. All the three
functions respond to messages intended for their dialog boxes, such as
WM_INITDIALOG to initialize a dialog box (which means, for the mentioned
dialog boxes, to fill the list boxes with the lists of part names), and
WM_COMMAND to respond to an action made by the user. After the user selects
OK to confirm his selections, these functions complete their duties by
copying these selections to global variables, such as pixelsPerStaff for the
staff height and pixelsPerObject for the object width, both selected in the
Page dialog box. It is pointless to elaborate on any of these functions
further, as they are commented well enough for the reader to be able to
understand them easily by himself.
3.10. The About dialog box module (ABOUT.CPP)
The About dialog box module is the smallest module of the software. As its
name suggests, it has no more than the function for the dialog box that
appears upon the user's selection of Help/About... The trivial function code
should pose no problem for the reader who has had the experience with the
File and Layout dialog box much more complicated functions.
3.11. The reformatting module (FORMAT.CPP)
The reformatting module implements a part reformatting algorithm. In its
current version, the algorithm reformats each multiple staff by itself; bars
and musical objects do not move from one staff to another. Let the reader,
however, have no illusion; even the implemented algorithm is complicated
enough as it is, which is easily proved at least by the length of the
module. The central function of the module is FormatMultipleStaff, which
adjusts the X coordinates of musical objects on one multiple staff. The
interface function of the module is FormatEntirePart, that does no more than
calling FormatMultipleStaff once per every multiple staff contained in the
part.
Before describing the algorithm itself, a word should be said about the data
structures it uses. At the beginning of FormatMultipleStaff a few lists are
allocated on the heap for later use. The lists are as long as there are
single staves in a multiple staff, i.e. every list item holds a datum about
the single staff corresponding to it. These lists are:
- duration holds the durations left (in units of 148-th of a full note) for
the current object on the staves. Whenever this duration is over, the next
object should be brought in on the corresponding staff.
- index holds the indexes of the current objects for all staves.
- atCommon holds TRUE for a staff who has stopped on a COMMONMULTIPLE
object. It is used to ensure that such objects remain at the same X
coordinate on all staves after the reformatting process.
- staffEnd holds TRUE for a staff that has no objects left to reformat.
The next few lines create two lists combining the continuous object lists of
all the single staves, one sorted by Xleft and one sorted by Xright. This
operation is required because later, when the X coordinates are changed
considering only the point objects, the continuous objects' left and right
coordinates will not be changed simultaneously. Note that during this stage,
the continuous objects appear on three lists at once! However, this does not
contradict in any way the normal usage of the IndexedList class, holding
only pointers to the actual objects instead of copies of the objects
themselves.
Next comes the main loop of the algorithm, which lasts as long as the
multiple staff has not been completely reformatted. At every stage of the
loop the minimum value in the duration list is subtracted from all the
others. In those staves at which the duration reaches zero as a result of
the subtraction, the next object is placed at the current X position,
denoted by the currX variable. If several objects were at the same place
originally (for example, an accord), they are all moved to their new place
together. This includes the continuous objects having one of the coordinates
at the current X position as well as the point objects. To conclude the
loop, the index list of indexes is updated and the current X position is
advanced by the default object width, held in pixelsPerObject.
If the current object in any staff is a COMMONMULTIPLE object, the atCommon
flag is marked for that staff. Later, no objects will be reformatted further
in that staff before atCommon is raised to TRUE at all staves. This ensures
that COMMONMULTIPLE objects remain at a common X coordinate (though possibly
different from the original). As a consequence, objects cannot cross bar
limits; whatever is in a bar before reformatting stays there afterwards,
even if there is a logical error and the object durations do not sum up to
the same value.
3.12. The MIDI file creation module (MIDI.CPP)
The MIDI file creation functions are gathered in this module. They are
activated when the user chooses Create MIDI... from the File menu. The
length of the file witnesses that the problem of creating a MIDI file is far
from being simple. We hope to clarify our approach to the solution in the
sequel.
The conversion of a melody to a MIDI file is performed in two stages, or
passes, as they are called in the program text. During the first pass, a
temporary file is created with information about the MIDI events represented
by the objects in the melody. The second pass adds information that was not
be available during the first translation pass, such as track lengths, and
creates a final MIDI file.
The first pass is implemented by the MIDIFirstPass function, which simply
calls MIDIStaff once per every multiple staff in the score. The algorithm
implemented in MIDIStaff bears some resemblance to that of reformatting a
multiple staff (see function FormatMultipleStaff in the reformatting module
description). In particular, there are the duration and index arrays having
the same purpose.
The main loop of the first-pass translation algorithm lasts as long as the
multiple staff has not been completely reformatted. At every stage of the
loop the minimum value in the duration list is subtracted from all the
others. In those staves at which the duration reaches zero as a result of
the subtraction, the next object's MIDIPlay virtual function is called to
put a description of the MIDI event represented by that object in the
temporary file. If several objects were at the same place originally (for
example, an accord), they are all MIDIPlayed together. To conclude the loop,
the index list of indexes is updated, advancing the current object in the
staves that took part in the play.
Before we can explain the second pass of the algorithm, it is necessary to
elaborate on the format of the temporary file available after the first
pass. Ignoring for a while the meta-events defined by objects other than
notes and pauses, the temporary file contains note-on events written by the
Note objects and note-off events written by the Pause objects. The events
are simply there in the file one after another, each event keeping such
information as part number, note frequency, and most important - the
duration. However, the MIDI file format does not use durations of events but
rather delta times between events. This fact creates a translation problem
from the temporary file format to the final MIDI file format, because each
note event having any duration is translated to two MIDI events: a note-on
at the beginning and a note-off at the end of its duration. This problem is
solved in the second translation pass.
The second-pass translation could be achieved by building a list of events
from the temporary file, adding two events to the list for every note and
keeping the list ordered by the time of the note appearances. This is
precisely how MIDISecondPass works, except for two differences: one is that
there are several such lists, one of each part (kept in the event[] array),
and the other is that it does not build the list in its entirety before
writing it to the MIDI file. Instead, as soon as it becomes clear that a
specific event will not be preceded by another, that event is written to the
MIDI file. Otherwise there would have been a danger of getting out of memory
very quickly during the translation process.
The class that is instantiated in the list(s) described above is MIDIEvent.
To what every list item (of class Object) has, it adds the properties of
event code, frequency, and delta time. In the algorithm implemented in
MIDISecondPass, the lists of MIDIEvent instances are kept as delta lists, in
which the delta time of an event is its actual delta time from the previous
event on the list. Such arrangement eases the insertion and deletion of
events from the lists.
In the main loop of MIDISecondPass, events are read from the temporary file
and inserted in the part lists as described above until no lists remain
empty. At this point, it is clear that one of the events already in the
lists must come before any subsequent ones. Among the first events in the
lists, the one with the minimum delta time is chosen for writing into the
MIDI file. After it is written and removed from the list, the temporary file
is again read until no list is empty, etc. The process is finished at the
end of the temporary file, after which the events already in the lists are
flushed in an appropriate order to the MIDI file.
Of course, the general structure of a MIDI file (its division to a header
and playing tracks) is preserved in the MIDISecondPass function that creates
the final-version MIDI file.
3.13. The block operations module (BLOCK.CPP)
The block operations module defines four functions, corresponding to the
four operations that can be performed on blocks: marking, copying, cutting
and pasting. The functions are executed from the block operation symbol
class instances when the user clicks the mouse with any of these symbols
active.
The block marking function (MarkBlock) code is trivial: it only has to
assign the block marking variables, which are markBeginStaff, markBeginX,
markEndStaff and markEndX, with the proper values. These variables being
global, the display module's PaintWindowProc function can figure out where
to display the musical text in reverse video. Also, any editing operation
that may alter the block marking variables (such as inserting a new staff)
must be sure to update their values whenever a possibility of that happening
exists.
The other functions, however, are not that simple. The operation of CutBlock
and CopyBlock is extremely similar, the only difference being that CutBlock
destroys the objects it clips, whereas CopyBlock doesn't. Both allocate a
memory block in which the clipped objects' information will be written and
that will be attached to the clipboard later on. Then they loop on the
staves' point and continuous object lists, finding those objects contained
in the marked block, and call their clipOn virtual functions, that write the
objects' information in the memory block. CutBlock also removes the clipped
objects from the lists and destroys them. Finally, the memory block handle
is attached to the Windows clipboard. From now on, the information in the
clipboard is independent of the current MUZIKA application instance, and it
can be pasted in any other MUZIKA window.
PasteBlock does the reverse operation of restoring musical objects out of
the information available in a clipboard handle. During pasting, a block
that is entirely contained in one multiple staff is treated differently from
a block scattered among several multiple staves. The difference is expressed
by the fact that a one-staff block can be pasted at any location on an
existing staff, enabling objects to be moved horizontally as well, whereas
pasting a many-staves block recreates the staves with the original X
coordinates of the objects.
The pasting process is performed by reading the object types one by one from
the clipboard handle and reinstantiating them at their new places. Actually,
PasteBlock distinguishes only between a staff object and whatever is not a
staff (i.e. is a MusicalObject). Further separation to the different musical
class types is done by the PasteObject function in the musical class library
contained in OBJCLASS.CPP.
3.14. The symbol operations module
The symbol operations module defines the functions that are common to all
symbols. This is the module that works extra hours when the user chooses a
symbol set from the Symbols submenu, or when he changes the active symbol
within a given set. The functions presented by the module are summarized
below. We recommend the reader to review the symbols database organization,
described in section 2.2.
DisplayEditModeSymbols displays the edit mode symbols on the screen top,
marking the current by reverse video. In the symbols' module terminology,
edit mode symbols are the three constantly appearing symbols at the screen
top: the pencil, the eraser and the hand. The active edit mode symbol, if
any, is marked by the variable activeEditMode. Whether an edit mode symbol
is active at all (a regular symbol from a set on the screen left can be
active instead) is marked in the variable editModeSymbolActive.
IdentifyEditModeSymbol checks if the given cursor position is on one of the
edit mode symbols. If so, it makes that symbol active and refreshes the
display accordingly.
RefreshSymbols displays the current symbol set on the screen left, marking
the current symbol, if any, by reverse video. The active symbol is specified
by the pair of variables activeSymbolSet and activeSymbol. In addition, a
pointer to the current symbol class instance is held in the currentSymbol
pointer.
IdentifySymbol checks if the given cursor position is on one of the symbols
on the screen left. If so, it makes that symbol active and refreshes the
display accordingly, updating the currentSymbol pointer.
Finally, GetActiveSymbol and GetCurrentSymbol return the current symbol, by
ID or as a pointer to the symbol class instance, respectively.
3.15. The symbols class library (SYMCLASS.CPP)
The symbols class library gathers the definitions of the symbol classes. As
the reader will recall from section 2.2, for every symbol there is exactly
one symbol class, derived from the generic base class SymbolClass, with one
instance. In particular, a symbol class must redefine the virtual functions
defined in SymbolClass, which are BitmapName, DrawSymbol, and CreateObject.
Probably the most important of these is CreateObject, that creates a musical
object corresponding to the symbol and returns a pointer to it. Only because
it is defined as virtual for all the symbol classes does it become possible
for the editing process, especially the NewPointObject and
NewContinuousObject functions, to behave in a truly object-oriented manner,
without knowing the types of the objects on which they operate at all.
The symbol class instances - or more precisely, pointers to them - are kept
in a list called symbolList, sorted by their ID code. For information about
what a symbol ID is, refer to section 2.2. This list, declared at the end of
SYMCLASS.CPP, is searched whenever the active symbol or symbol set changes.
Except for that, there is little we can add to the above sentences. The
reader is invited to browse the SYMCLASS.CPP to see the symbol classes
define, in an entirely straightforward manner, their own versions of the
virtual functions listed above.
3.16. The musical class library (OBJCLASS.CPP)
The musical class library contains the definitions of the musical classes'
versions of the pure virtual functions defined in MusicalClass (the base
class for all musical object types). The different musical classes
themselves are defined in the OBJECTS.H header file. More specifically, each
musical class (of which there are currently seven: Note, Pause, Key, Beat,
Bar, Loudness, Crescendo, and Text) has its own versions of the following
functions:
- Three different constructor functions: one that receives the instance data
directly as parameters; one that gets as a parameter the input stream from
which to read the instance data; and one that gets the instance data from a
memory block attached to the clipboard.
- A Draw function, that draws the object in the given display context, using
(if needed) the display module's global variables staffX, staffY, and
staffLoc. This functions is used to draw object images during displaying and
printing.
- A printOn function, that writes the object data to the given output stream
in a way that enables its complete reconstruction. This function is used
when the melody is saved.
- A clipOn function, that writes the object data to the given memory block,
presumably attached to a clipboard handle. This function is used when the
object is inside a clipped (i.e. cut or copied) block.
In addition, point object classes are allowed to redefine the Format, Width,
MIDIPlay, and Duration virtual functions, and continuous object classes can
make their own versions of FormatLeft and FormatRight. This may be needed in
cases where an object has actions to perform during formatting or
translating a MIDI file other than the default ones, defined in MUZIKA.H.