代写实现MIDI播放器,播放音乐。
Introduction
You will write a program to read a song represented as a MIDI file and produce
a sampled signal in WAVE format suitable for playing on a computer. The MIDI
file format represents music as a sequence of events, the fundamental ones
being “note on” and “note off”. Each event also specifies the note, it’s
“velocity”, and the time to the next event. This allows us to encode a single
note as two on/off events separated by a time delay, rather than a single note
and duration, but now we can specify chords as a sequence of on events with
zero time delay between them, followed by off events. There are also events
other than note on/off for specifying the current instrument, the tempo, and
other parameters.
Translating the simple example from milestone 0:
C, 10.0, 2.5
G, 20.8, 5.0
C, 10.0, 3.0
into a sequence of events gives:
NOTE ON, C, 10.0
WAIT 2.5
NOTE OFF, C
WAIT 0
NOTE ON, G, 20.8
WAIT 5.0
NOTE OFF, G
WAIT 0
NOTE ON, C, 10.0
WAIT 3.0
NOTE OFF, C
END TRACK
In the new design a MIDIEvent is any change in the instruments that may affect
the output sound. A Track is a sequence of MIDI events representing a
recording from a single instrument. Each instrument has a specific algorithm
for generating the sound samples from the event stream. In this milestone we
will use a single instrument using an algorithm very similar to milestone 0
(see below) that we will call the default instrument.
In this milestone we will define only three events:
- TempoEvent, the tempo of the music should change
- NoteEvent, a note should be turned on or off with a “velocity”, and
- EndTrackEvent, the track has ended.
The synthesizer reads tracks from the input file and converts it into sound
samples that can be written as a WAVE file (the same way as in milestone 0).
Because there are multiple simultaneous notes this process becomes slightly
more complicated.
Synthesizer Algorithm
The synthesizer can be viewed as a state machine where each state is the
status of all Instruments, e.g. what notes are on, and the output state is the
current sound sample from the instrument. Transitions between synthesizer
states depend on the stream of events that make up the track.
Consider a track as a list of events ordered with the absolute time from the
start of the track. The synthesizer computes the time of the next tick and
processes all events from the list with absolute times less than that. Based
on these events, the Instrument’s state is updated and the sound sample
stored.
In this milestone, the synthesizer converts each track separately to a signal,
one signal per track. We will tackle mixing tracks in the next milestone.
Default Instrument Algorithm
The default instrument algorithm is a state machine whose state is the notes
that are currently on, the time elapsed since each was turned on, and their
velocity.
The instrument output is computed as the sum over each note currently on where
i is the note index, and subscripted t is the elapsed time since note i was
turned on. V(i) is the velocity of note i, f(i) is the frequency of note i,
and E is the value of the envelope function at elapsed time t. The constant
200 simply scale the velocity to represent an amplitude.
The envelope function is a piece-wise function,
The time axis is scaled by a note length in seconds. The default instrument
should use a note length of 0.25 sec (i.e., the envelope is non-zero from 0 to
0.25).
Notes supported are those in the MIDI standard, 128 notes numbered 0-127. Use
the following frequency mapping (in Hz)
- index 60 is middle C = 261.63
- index 61 is C# = 277.18
- index 62 is D = 293.66
- index 63 is D# = 311.13
- index 64 is E = 329.63
- index 65 is F = 349.63
- index 66 is F# = 369.994
- index 67 is G = 392.0
- index 68 is G# = 415.305
- index 69 is A = 440.0
- index 70 is A# = 466.164
- index 71 is B = 493.88
The remaining note numbers follow the same pattern as 60-71, with a change in
octave (multiply or divide by 2): - indices 0-11 are five octaves lower
- indices 12-23 are four octaves lower
- indices 24-35 are three octaves lower
- indices 36-47 are two octaves lower
- indices 48-59 are one octave lower
- indices 72-83 are one octave higher
- indices 84-95 are two octaves higher
- indices 96-107 are three octaves higher
- indices 108-119 are four octaves higher
- indices 120-127 are five octaves higher (stops at G)
The sample time is at regular intervals given by the sample rate. The relative
timing of MIDI ticks is determined by both the MIDI tick-time (the clock rate
per beat), M, and the tempo, T. The MIDI clock rate is in time units of ticks-
per-beat. The tempo T is in units of microseconds per beat. Thus the real-time
between MIDI ticks is
Thus, the real-time t of a MIDI event at tick m, given a MIDI tick-time
(clock) M and tempo T is The sample time and MIDI times both start at zero,
but in general are not aligned. Consider the following figure.
The time axis is real (floating point) time. MIDI events occur on a MIDI clock
tick, but are spaced by the delay time between them (three are shown in the
figure). The samples are taken at regular intervals. For each MIDI event, the
state of the instrument should be updated before the next sample is taken.
That is, suppose a MIDI event occurs between two sampling events. The second
sample should reflect the change in instrument state, but not the first (there
is no time interpolation). If the event is a Tempo event, the tempo should
change at the next sample. If it is an end track event, the track should end
at the next sample. If it is a note-on event the note should start at the next
sample. Thus notes always begin on a sample boundary with elapsed time reset
to zero. A note cannot be turned on again, until it has been turned off.
Module Specifications
Your C++ code implementing the synth program must be divided into the
following modules, consisting of a header and implementation pair (.hpp and
.cpp). You should not define additional source files, but you are free to
modify the header and implementation files as desired as long as you do not
break the module API. See the comments in the header file of each module in
the starter code for detailed documentation.
Input and Output File Specification
A module for reading and parsing (a limited subset of) MIDI files is provided
in midi.hpp and midi.cpp. This module uses your Track class to read and store
events from the file using it’s API. Do not modify these files.
This milestone will use the same WAVE format as milestone 0. A clarification
is that quantization into 16 bits should use rounding to nearest integer and
normalization (scaling magnitude) should only be done if the signal would be
clipped.
Program Specification
The file main.cpp should provide the entry point (main) for the synth program.
The file names to read and write are specified as command line arguments to
milestone 0. However the second argument is the base filename (without the
.wav extension) to use when writing synthesized tracks, one wav file per
track.
For example, the following program invocation reads the file MIDI_sample.mid,
which contains 6 tracks, and converts it to the files
output-0.wav,output-1.wav, …, output-5.wav(Assuming Windows platform, $ as the
command line prompt)
$ .\synth.exe MIDI_sample.mid output
If no file names are specified, or the specified files cannot be opened, the
program should print an appropriate error message to standard error and return
EXIT_FAILURE. If the input file is invalid or the tracks cannot be
synthesized, then an error message should be printed to standard error and the
program should return EXIT_FAILURE. However any previously synthesized and
written tracks should remain. Otherwise it should convert the file(s) and
return EXIT_SUCCESS.
Testing Requirements
The initial git repository has a sub-directory called test which has several
examples of input files (ending in .mid) and corresponding expected output
(ending in .wav). The included CMakeLists.txt file sets up these tests for
you. Just configure the build, run the build, and then run the tests. These
functional tests are used to determine if your overall program works as
expected.
Setup
Accept the GitHub invitation above and then clone the git repository to your
local machine. Implement your program in a source files provided. Do not add
additional files or modify the ones marked DO NOT EDIT at the top. You should
use git to commit versions of your program source (only) as you go along,
demonstrating good incremental programming technique.
Hints and Suggestions
- This project might seem daunting with several modules and files. The first step is to write stubs for each module so that the entire project compiles.
- Work incrementally and cyclically, from the Event module, to the Track module, then using the MIDI module, the Signal module, the Synthesize module, to finally the WAV module. Write the tests as you go, using them to drive your development.
- Work initially on functional test0 as that is the simplest.
- Use a tagged union (see help sessions) to implement the MIDIEvent class.
- Pay close attention to default states and Constructors.
- Leverage the standard library where you can.