Ben Davis XMAS: an open MIDI and sample-based music system Computer Science Tripos Robinson College May 9, 2004 Proforma Name: College: Project Title: Examination: Word Count: Originator: Supervisor: Ben Davis Robinson College XMAS: an open MIDI and sample-based music system Computer Science Tripos 2004 11302 Ben Davis Neil Johnson Original Aims of the Project To provide a good solution by which a composer can write and distribute music to be played by a machine, particularly as part of a downloadable computer game. .mid files depend heavily on the hardware or software available at the destination; .mp3, .ogg and similar files are too large in many cases; Amiga-based module files (e.g. .mod, .s3m, .xm and .it) are difficult to compose and the playback behaviour is not well defined. This project aimed to produce an open source system that would avoid all of these problems. Work Completed An XML-based structure that allows a .mid author to build his or her own instrument sounds using .wav files was designed. A software library for parsing the structure and rendering the music to a mono or stereo PCM sample stream was written. In particular this incorporated a real-time resampler with cubic interpolation and a MIDI player. The project is fairly mature and will soon be available at http://xmas.sf.net/. Special Difficulties None. i Declaration I, Ben Davis of Robinson College, being a candidate for Part II of the Computer Science Tripos, hereby declare that this dissertation and the work described in it are my own work, unaided except as may be specified below, and that the dissertation does not contain material that has already been used to any substantial extent for a comparable purpose. Signed Date ii Contents 1 Introduction 1.1 The Technology . . . . . . . . . . . . 1.1.1 MIDI . . . . . . . . . . . . . . 1.1.2 Samples and Streamed Audio 1.1.3 Amiga mod-based Files . . . . 1.2 The Problem . . . . . . . . . . . . . 1.3 The Solution . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2 Preparation 2.1 Requirements . . . . . . . . . . . . . . . . . . . . 2.2 Initial Analysis of Requirements . . . . . . . . . . 2.2.1 Using Industry Standards . . . . . . . . . 2.2.2 Compression and Kolmogorov Complexity 2.2.3 Structure and Tweaks . . . . . . . . . . . 2.2.4 Same Output Everywhere . . . . . . . . . 2.3 Project Layout . . . . . . . . . . . . . . . . . . . 2.4 Further Analysis . . . . . . . . . . . . . . . . . . 2.4.1 Real-Time Playback . . . . . . . . . . . . 2.4.2 Third-Party Players . . . . . . . . . . . . . 2.5 Choice of Programming Language . . . . . . . . . 2.6 Refining the File Structure . . . . . . . . . . . . . 2.7 Final Preparations . . . . . . . . . . . . . . . . . 2.7.1 Core Requirements . . . . . . . . . . . . . 2.7.2 Extensions . . . . . . . . . . . . . . . . . . 2.7.3 Work Plan and Timetable . . . . . . . . . 2.7.4 Libraries and Code Used . . . . . . . . . . 2.7.5 Documentation Used . . . . . . . . . . . . 2.7.6 Code Management and Back-ups . . . . . iii . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1 1 2 2 3 4 5 . . . . . . . . . . . . . . . . . . . 7 7 7 7 7 8 9 9 10 10 10 10 10 11 11 11 11 12 13 13 3 Implementation 3.1 Overview . . . . . . . . . . . . . . . . . . 3.1.1 Digital Signal Processing Modules 3.1.2 The State Tree . . . . . . . . . . 3.1.3 Generating the Music . . . . . . . 3.2 The XML . . . . . . . . . . . . . . . . . 3.3 Variables . . . . . . . . . . . . . . . . . . 3.4 Parameter Tweaks . . . . . . . . . . . . 3.5 The Modules . . . . . . . . . . . . . . . 3.5.1 Samples . . . . . . . . . . . . . . 3.5.2 Volume Envelopes . . . . . . . . . 3.5.3 Multiplexers . . . . . . . . . . . . 3.5.4 MIDI Mappings . . . . . . . . . . 3.5.5 Variable Compute Blocks . . . . . 3.6 The MIDI Playback Algorithm . . . . . 3.6.1 Overview . . . . . . . . . . . . . 3.6.2 State . . . . . . . . . . . . . . . . 3.6.3 Notes . . . . . . . . . . . . . . . 3.6.4 Algorithm . . . . . . . . . . . . . 3.6.5 Noteworthy Features . . . . . . . 4 Evaluation 4.1 Goals Achieved . . . . . . . . . . . 4.2 Evolution of the Plan . . . . . . . . 4.2.1 Generalisation . . . . . . . . 4.2.2 Filtering Whole Channels or 4.2.3 Reference Counting . . . . . 4.3 Milestones . . . . . . . . . . . . . . 4.4 Testing . . . . . . . . . . . . . . . . 4.4.1 General . . . . . . . . . . . 4.4.2 The Resampler . . . . . . . 4.5 Profiling . . . . . . . . . . . . . . . 4.6 Comments . . . . . . . . . . . . . . 4.6.1 Samples . . . . . . . . . . . 4.6.2 Volume Envelopes . . . . . . 4.6.3 MIDI Playback . . . . . . . 4.6.4 Flexibility . . . . . . . . . . 4.7 Problems Encountered . . . . . . . 4.7.1 STL Containers . . . . . . . iv . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Tracks . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15 15 15 16 16 17 17 19 20 20 22 24 25 25 25 25 26 27 28 28 . . . . . . . . . . . . . . . . . 31 31 32 32 32 33 33 34 34 34 34 38 38 38 39 39 39 39 4.7.2 4.7.3 XML Files . . . . . . . . . . . . . . . . . . . . . . . . . . . Code Size . . . . . . . . . . . . . . . . . . . . . . . . . . . 40 40 5 Conclusions 41 Bibliography 43 A The Cubic Interpolation Function 45 B Some Example XML B.1 volenv.xml . . . . B.2 compute.xml . . . B.3 clarinet.xmi . . . B.4 pizz.xmi . . . . . B.5 general.xmi . . . B.6 Example Music . . 47 47 48 48 49 50 50 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . C Demo CD Track Listing 52 D Project Proposal 54 v List of Figures 3.1 3.2 3.3 3.4 3.5 An example piece of music . . . . . A state tree, with variables . . . . . How variables are implemented . . The history buffer . . . . . . . . . . Two examples of volume envelopes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15 18 19 21 22 4.1 4.2 4.3 4.4 How to filter a set of channels . . The visual resampler test. . . . . The three interpolation functions. Some profiling results . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33 35 36 37 vi . . . . Acknowledgements Many thanks are due to Neil Johnson, my Project Supervisor, for the guidance he offered right from inception up until the final deadline. Thanks also go to Dr Alan Mycroft, my Director of Studies, for his assistance with this dissertation. The Dissertation was written inside the skeleton structure provided by Dr Martin Richards’ How to write a dissertation in LATEX [8]. vii viii Chapter 1 Introduction Electronic music is an exciting field. Many people will insist that it is no substitute for conventional acoustic music, and they are right. Acoustic instruments— and human performance—are an enormous challenge to emulate. Electronic music is not a substitute: it is a complement. It is a whole new world, populated with many synthesisers and filters, each with its own distinctive character, and free of such constraints as the span of a pianist’s hand. Moreover, it can all be done in software inside any reasonably modern computer equipped with a sound card and speakers. I am a proficient pianist and have composed a great deal of professional quality music. This strong musical background enabled me to hear bugs in my project’s output and reason about sound quality. 1.1 The Technology The best known example of an electronic musical instrument is an electronic keyboard. This features a piano-like keyboard, a pair of speakers and a range of buttons, and is capable of producing many different instrument sounds, from approximations of acoustic instruments to sounds unlike anything heard in the acoustic or natural world. There are other types of electronic musical instrument too: MIDI controllers feature just the keyboard, while MIDI modules 1 feature just the synthesiser. These devices have to be connected together to function. Sequencers allow music to be recorded or programmed, and can then play it back if a synthesiser is connected. 1 The word ‘module’ has different meanings in different contexts. The intended meaning will always be clear in this dissertation. 1 2 1.1.1 CHAPTER 1. INTRODUCTION MIDI Enter the MIDI Specification [1]. It was created in 1983 by Sequential Circuits, Roland and several other major synthesiser manufacturers as a protocol to allow instruments to communicate with one another. There are 16 channels, numbered from 1 to 16; a device can respond to events on some channels and not others, or assign different instruments to different channels. Events such as the following can be encoded in a byte stream and sent between devices: 9c nn vv Note On 8c nn vv Note Off Cc pp Program Change Start note at pitch nn on channel c + 1 with velocity vv, a measure of how hard the key on the keyboard was hit. Stop note at pitch nn on channel c + 1. The velocity vv here is a measure of how quickly the key was lifted. Assign program pp to channel c + 1. A program is typically an instrument sound, but some devices use it for other purposes such as rhythm selection. The first byte is known as the status byte. There are many other commands, but the status byte is always in the range 80–FF, and data bytes are always in the range 00–7F. If the same command is used repeatedly, the status byte only need be specified once, and it becomes the running status. MIDI commands can also be saved with time stamps in a Standard MIDI File. Such a file has the extension .mid. XMAS uses the .mid file as a major component in a piece of music. General MIDI, an addition made to the standard in 1991, designates a standard set of instrument names for the 128 integers that can be used in a Program Change command. It further allows a single channel to be used for unpitched percussion (such as cymbal crashes), with standard percussive instrument names assigned to many of the 128 note values. On multi-part synthesisers, it is usual for Channel 10 to contain percussion. 1.1.2 Samples and Streamed Audio When we hear sounds, our ears are detecting longitudinal oscillations, or pressure waves, in the air. These waves cause our eardrums to vibrate. A microphone uses a diaphragm to detect the waves and convert them into a varying electric current. This current can be sampled at discrete intervals, a common rate being 44100 Hz, and stored in a .wav file. The individual samples—or sample points—usually 1.1. THE TECHNOLOGY 3 have 16-bit values. Many recordings have two channels, one for each speaker in a stereo set-up;2 here, the sample points are interleaved, and each left-right pair is called a sample frame. This system for encoding sound as a series of samples is called Pulse Code Modulation, or PCM . It is possible to store an arbitrary number of channels in a .wav file, but it is rare for more than two to be stored. A sample, in one sense of the overappropriated word, is a recording of a sound effect, a note, or a short, repeatable sequence of notes. It is typically stored in a .wav file. XMAS uses samples as a key component in instrument design. Lossy compression algorithms exist for PCM data. The best known is MPEG-1 Layer 3, or .mp3; others are Ogg Vorbis (.ogg) and Windows Media Audio (.wma). These typically achieve compression ratios of 12:1. It is possible to stream such compressed PCM data over a channel3 (e.g. for Internet radio), so these schemes are often referred to as streamed audio schemes. Supporting these in addition to .wav files was made an extension for reasons to be given later. Adjusting the speed of a sample—stretching or compressing it in the time axis—results in a shift in the frequencies present in the spectrum, perceived as a change of pitch. XMAS uses this to generate different notes from a single sample. It is not the best way to change the pitch of a note, but it is quick to do, and the same algorithm can enable playback at an arbitrary sampling frequency. 1.1.3 Amiga mod-based Files The Commodore Amiga was a pioneer with its ability to play up to four samples at once, at varying frequencies, without using the CPU. In 1987, Karsten Obarski released SoundTracker, a program for writing music to play on this hardware. Programs of its ilk (along with the composers who used them) became known as trackers, and the names of the files produced began with mod., short for module. This ‘extension’ moved to the end when the files were transferred to the IBM PC. As the PC’s audio and processing capabilities grew, trackers emerged for it, featuring more channels4 and more features and each with its own file format. The three best known are Scream Tracker 3, Fasttracker II and Impulse Tracker, saving files with extensions .s3m, .xm and .it respectively. The creators of these trackers have all documented their file formats. 2 Note that the term channel has been overloaded. It may refer to parts in a MIDI performance or to speakers as in this case. 3 A third meaning of channel ! 4 Analogous to MIDI channels. Conventionally, each channel can play one sample at a time, though Impulse Tracker gets around this. 4 CHAPTER 1. INTRODUCTION I have co-authored DUMB, a library for playing these four mod-based formats [5]. The experience, both positive and negative, from working on DUMB proved very useful in specifying, planning and implementing this project. 1.2 The Problem As a game developer by hobby and soon by profession, my main interest is in using a standard home computer to generate background music for computer games. However, doing this in such a way that the music can be distributed is quite a challenge. Every existing solution has a prohibitive disadvantage. .mid files. Most computers are built with the ability to play .mid files, in software if not in hardware. Since MIDI has so much industrial support, there is good hardware and software available, and .mid files are easy to produce. However, owing to the nature of MIDI, the output varies vastly from one system to another. Even General MIDI does not specify exact instrument sounds, only names: from experience I know that a change of MIDI device can be utterly devastating to a piece of music. Compressed audio streams. Why not produce MIDI files for one’s own MIDI set-up, record the output and encode to a compressed audio scheme such as .mp3? Many people do this, and for boxed games it is a fair solution. However, it is not a good choice for games made available for download on the Internet (including demos of commercial games): even a small selection of .mp3-format music could take a dial-up modem user hours to download. These formats have the further limitation that they are unstructured, and the game cannot make adjustments, e.g. to speed, on the fly. Amiga mod-based formats. These seem like a good solution, but there are two problems. The first is that producing mod-based music is very difficult. The second is that you can never be sure your music will play correctly. All the original tracker software was closed-source. Third-party players have been developed, but most of them misinterpret the data, some of them very severely. We made a serious effort to get it right in DUMB, but there are still errors. The player shipped with the popular Winamp media player is one of the least accurate, which poses a real problem to anyone releasing modbased music. Furthermore, there is a major third-party tracker, ModPlug Tracker, which differs significantly in several ways from the original tracker programs. As a result, there is no single correct way to play mod-based formats. 1.3. THE SOLUTION 1.3 5 The Solution This project set about creating a new solution to allow game developers to include music in their games. It defines a compact5 new file format in such a way that no new editor software needed to be written. It incorporates a library, libxmas, capable of loading and playing the music in real time. The file format is XML-based, so I decided upon the following file name extensions: .xmi: XML Instrument Definition. This defines one or more instrument sounds, most likely referring to .wav files or other .xmi files in the process. .xmm: XML Music. This specifies a .mid file, along with an XML Instrument Definition defining the instrument sounds to play it with. The definition may either be embedded or consist of a reference to an .xmi file. .xma: XML Music Archive. This contains zero or more .xmm files and all files they depend on, using appropriate compression for each part. It allows music to be consolidated into a single file. In addition, it is a nice take on Microsoft’s .wma format! Since .xma is the ideal final format for music, the project as a whole is called the XMA System, or XMAS for short. 5 Modulo the rather verbose XML glue, which is highly compressible. 6 CHAPTER 1. INTRODUCTION Chapter 2 Preparation 2.1 Requirements • It must be easy for a composer to produce music. • It must be possible for the composer to keep file sizes down to a minimum. • The music must be structured so as to allow tweaks on the fly. Such tweaks might include speed variation, muting of some instruments and instrument substitution. • The playback library must be capable of producing the same output on every computer. • It must be able to do so comfortably in real time on a typical home computer for a piece of music of average complexity. 2.2 2.2.1 Initial Analysis of Requirements Using Industry Standards Using industry standard file formats gives the user a choice as to what software and hardware to use. This is by far the best way of meeting the first requirement, since different people find different tools easy to use. It also enables me to use existing files for testing and demonstration. 2.2.2 Compression and Kolmogorov Complexity Fundamental to keeping file sizes down is Kolmogorov complexity. The Kolmogorov complexity of some data is the length of the shortest program capable 7 8 CHAPTER 2. PREPARATION of generating the data. This shortest program is known as the minimal description. In the worst case it will be a PRINT statement followed by the data to be output, but in the best case it can be extremely concise. The Kolmogorov complexity gives a lower bound for the size of losslessly compressed data (assuming the decompression algorithm is simple). In general, it is very difficult to meet this lower bound in any automatic process. The minimal description is usually a representation of the structure of the file, so to create it requires a good understanding of this structure. Music is highly structured. Some of the structure can be articulated. For instance, we think of a piece of music as a sequence of notes, and this is how music has always been written down, whether for live performers or for a computer. This is in fact just one of the many structures found in music. Themes and rhythms recur, harmonic progressions are often predictable, and even the scale itself is riddled with frequency ratios such as 2:3 (perfect fifth) and 3:5 (major sixth). Much of the structure is lost when the music is written down.1 However, the Western twelve-note scale, including its ratios, is implicit and can be implemented in the library, and the idea of a sequence of notes is preserved. This is enough to bring the file size down well below that of an .mp3 file, if simple instrument definitions are used. What if complicated instruments are used? I have an .xm file (mod-based) that is over 10 MB in size. An .mp3 version of typical quality would be smaller. It could still be beaten if the instrument samples were compressed, but this should be done with care. Samples are often looped, so that when playback reaches the end of a sample, it jumps back to a specified point (see Section 3.5.1). To avoid clicks, the point is chosen so that the resultant curve is continuous. .mp3 and friends are lossy, and there is no guarantee that such a loop would be preserved. In light of this consideration, providing instrument compression has been left to the extensions. 2.2.3 Structure and Tweaks Allowing the musician to specify the structure is the ideal solution. The musician, in collaboration with the other game developers, will know what will need to be tweaked and can structure the music accordingly. 1 This is not necessarily a bad thing. Live performers will never repeat a pattern precisely, and an unstructured MIDI stream will be able to capture the variation. Of course, much electronic music—particularly club music—sounds best when precise! 2.3. PROJECT LAYOUT 2.2.4 9 Same Output Everywhere Much computer music technology is unsuitable for use in this project. Examples are the DSP (digital signal processing) chip on the Sound Blaster Live! cards and VST plug-ins in Windows. The DSP chip allows a programmer to write an algorithm that does some DSP and download the algorithm into the sound card, where it will be applied to the output. This is unsuitable because it is specific to one sound card series. VST stands for Virtual Studio Technology, a standard created by Steinberg to allow effect plug-ins conforming to the standard to be used by any VSTcompatible program. It is very useful when the music is to be mastered and for example put on CD. However, it is specific to Windows, and a user will have a personal collection of plug-ins that other users may not have. If a composer used these plug-ins, the goal of being able to distribute music in a structured, compact form would not be met. There is a second, related, consideration. It must not be possible for an arbitrary music program to load a piece of music designed for XMAS and play it back incorrectly. This precludes, for example, the possibility of using a .mid file for the whole piece of music. The standard for .mid files allows proprietary data to be stored in units called chunks, but states that any unrecognised chunk should be skipped over; if XMAS used .mid files with proprietary chunks, all standardcompliant programs would succeed in loading music designed for XMAS and would proceed to play it using the wrong instrument sounds. 2.3 Project Layout The considerations so far led me to decide on a system that loads XML-formatted data containing references to .wav and .mid files. The majority of a musician’s work goes into producing the .wav and .mid files, while writing the top-level XML structure is trivial by comparison. However, since the top-level file is in a newly defined format, no existing software will inadvertently be able to load it and generate the wrong output. The component .mid files can still be loaded into any program, but this is dwarfed by the problem of having the music in many separate files; the .xma format will solve both problems (see Section 1.3). 10 2.4 2.4.1 CHAPTER 2. PREPARATION Further Analysis Real-Time Playback The existing mod-based players and software MIDI synthesisers give a good idea of what a typical computer can do in real time. Predominantly sample-based players have no trouble keeping up. Even simple filters and reverberation are no problem nowadays. However, it is important to be able to apply any such effects to multiple MIDI channels in one go. Applying them to one channel at a time, when the same output could be achieved with a blanket effect over the sum of all channels’ output, would be an unacceptable waste of processor time. Since this engine will be used in games, which must do their own processing, it is important to get the processor usage as low as possible. 2.4.2 Third-Party Players Those who wish to develop third-party players for XMAS’s files should not be made to guess, probably wrongly, how to play the files (recall the comments about Amiga mod-based formats in Section 1.2). The playback code I develop in this project will be freely available, and will serve as a reference. 2.5 Choice of Programming Language The software is required to run quickly. However, it should also be flexible and extensible. C++ has been designed to meet both of these goals, providing for highly structured programming while still allowing the programmer to sacrifice some internal safety and structure in order to gain speed. As an industry standard language, it is a good choice for this project. Most of my experience before this project was with C. Whilst I was not greatly familiar with the syntax of C++, I knew the language’s capabilities well enough to plan this project and see it through. 2.6 Refining the File Structure XMAS will use a tree structure for the music. At the leaves of the tree will be samples. Above these will be instruments, specified in XML and responsible for such activities as choosing different samples for different notes, controlling the fade-out when a note stops, and applying any desired effects. At the root of the 2.7. FINAL PREPARATIONS 11 tree will be a node, also specified in XML, parenting a set of instruments and containing a reference to a .mid file. Furthermore, the above types of node will be unified so that they can be strung together effortlessly in any layout. The term ‘DSP module’ will refer to any node, since a node’s purpose is typically to do some digital signal processing. In particular, this enables short MIDI sequences to be used as instruments in larger ones. 2.7 2.7.1 Final Preparations Core Requirements The project needs to show that it can do the job it is designed for. It may not fulfil all the requirements listed in Section 2.1, but it must be evident that the requirements have been considered and could be fulfilled with a small amount of work. By the end, I expect to have the project playing a piece of music reliably, accurately, and fairly efficiently. 2.7.2 Extensions The following two features will have to be consigned to extensions simply because of the amount of work they would involve: • Compressed samples are an extension. As mentioned in Section 2.2.2, lossy compression should not be done blindly. Doing it properly could develop into a project in its own right. Lossless compression is of limited benefit, and is not important enough to be a core requirement. • The .xma format will take some careful planning, and so has been left as an extension. Other possible extensions include extra DSP modules (such as filters, distortion and echo), click removal for when samples start, stop and loop (not for clicks in the actual sample data), support for surround sound, a GUI for editing and testing .xmi and .xmm files, a stand-alone player, and XMMS and Winamp plug-ins. 2.7.3 Work Plan and Timetable Having planned the project to the extent that I felt ready to begin writing code, I decided upon the following timetable. A spiral development model was adopted, 12 CHAPTER 2. PREPARATION with an aim to complete a design-implement-test cycle within each work package. All dates are Fridays. 24 Oct – 7 Nov 7 Nov – 28 Nov – 19 Dec – 9 Jan – 30 Jan – 27 Feb – 2.7.4 Preliminary research. In particular, read up on XML and find suitable documentation and libraries. 28 Nov Specify DSP module interface. Implement reference-counted .wav loader and sample player. 19 Dec Implement volume envelope module. Specify .xmi format. Implement reference-counted loader. Create an .xmi file for testing. 9 Jan Implement reference-counted .mid loader. Specify .xmm format. Implement reference-counted .xmm loader. Create an .xmm file for testing. 30 Jan Write the Progress Report. 27 Feb Implement MIDI sequence player and .xmm player. 19 Mar Implement command-line player. Create a more involved piece of music for testing and, later, demonstration. Libraries and Code Used XML Parsing XML 1.0 XML 2.0 C C++ libxml libxmlpp libxml2 xmlwrapp As XML is backwards-compatible and the project is using C++, I investigated xmlwrapp [3]. It had clear documentation and a good API, so I decided upon it. Expression Parsing and Evaluation Three solutions for parsing expressions were considered: • Ollivier’s Mathematical expression parser in C++ (mathexpr) [6]. • The L math processor (lmp) [7]. • Constructing my own with flex and bison. lmp is written in C, and lacks object-oriented structure. Parsing an expression consists of setting global variables to point to the expression and calling a function. Furthermore, there is a single table of variables, stored in global variable. 2.7. FINAL PREPARATIONS 13 This kind of API is not conducive to the object-oriented structure I want, and it is certainly not thread-safe. The same problem arises with flex and bison: global variables are heavily used. By contrast, mathexpr is written in C++ and has a good object-oriented structure. The site presented a worrying description and example of the parser’s behaviour, but a test proved that these were incorrect and the parser behaved as one would expect. mathexpr treats concatenation of variable names as multiplication (e.g. xy is x × y), so I performed another test to see if variable names longer than one character would be accepted. They were, with the restriction that a name could not consist of an existing name with a suffix added (so ‘note’ and ‘notevelocity’ could not coexist). I considered this an acceptable limitation and decided to use mathexpr. Since mathexpr is not a proper library with an installation procedure, I incorporated it into libxmas’s code tree. (By contrast, a user who wishes to compile libxmas will have to obtain and install xmlwrapp first.) 2.7.5 Documentation Used The .wav and .mid Formats Files documenting the .wav and .mid file formats were found at http://www. wotsit.org/. The .wav documentation was very thorough. The .mid covered only the skeleton file structure including how to load a MIDI byte stream for each track, but did not contain sufficient documentation on the contents of the byte stream. MIDI http://www.borg.com/~jglatt/tech/miditech.htm covers two important parts of the MIDI Specification in great detail. The first is the MIDI messages (e.g. Note On) that may be sent between devices or stored in .mid files. The second is the .mid file and the meta-events that are stored in it but are not MIDI messages per se (more on this later). 2.7.6 Code Management and Back-ups Since I have experience with CVS, I set up a CVS server on my system. I also wrote a script to archive the repository and upload it to Pelican, the University’s 14 CHAPTER 2. PREPARATION back-up service, keeping one old copy each time. Finally, I set up a cron job so that the script would run every day at 4:00 a.m. Chapter 3 Implementation 3.1 Overview 3.1.1 Digital Signal Processing Modules Figure 3.1 shows an example of a piece of music as defined by XMAS. Sample cymbals.wav MIDIMapping duet.mid Instruments Sample GeneralMultiplexer LookupMultiplexer channel=10,note=57 70 . . . channel=2 note 50 . . . 69 channel=1 . . . 49 harphigh.wav Sample harpmed.wav Sample harplow.wav VolumeEnvelope Sample Sustain point flute.wav Subject loop="on" Figure 3.1: An example piece of music Each node in the tree is a digital signal processing module or DSP module, and holds music data but no playback state. I refer to this tree as the data tree. In the library, the base class DSPModule abstracts all types of node. 15 16 3.1.2 CHAPTER 3. IMPLEMENTATION The State Tree When the music is to be played, a state tree is constructed alongside the data tree. The DSPModule class has a getPlayer method which constructs and returns a player object of a type derived from DSPModulePlayer. This object keeps a pointer to the DSPModule, and plays the music, single note or other sound the DSPModule represents. Building the state tree involves the construction of a player for the data tree’s root node. In this case a MIDIMappingPlayer is constructed. There is an important difference between the data tree and the state tree. A player does not necessarily construct one child for each corresponding child in the state tree. It may construct zero or many children, and it may construct and destroy children dynamically. A MIDIMappingPlayer has, at any given moment, one child for each note that is playing. Some modules are simpler; the VolumeEnvelope always constructs a single child, and the two multiplexers will construct either one or none. 3.1.3 Generating the Music Once the state tree is set up, we play the music by requesting PCM data from the root. The root MIDIMappingPlayer will request PCM data from its children, the currently playing notes, and generate output in which all the notes can be heard. Each child player will do something similar. The VolumeEnvelopePlayer will request data from its child and provide a processed version as its output. The SamplePlayers will generate their output from the PCM data stored in the Sample objects. When multiple sounds occur at the same time, the pressure waves from the individual sources are added together at each moment. Mixing PCM streams therefore involves one addition operation per sample point. Since this is so common in music, it was decided that a DSP module would always add into a buffer passed to it. The DSPModulePlayer class has a method called mixSamples(). It takes a pointer to a buffer of floats, a count indicating how many sample frames (recall Section 1.1.2) are requested, and a reference to a StreamParameters struct containing the sampling rate and the number of channels (speakers). At present, the number of channels is always 1 or 2. The API allows for the possibility of more channels in the future. All DSP modules are expected to be able to work with an arbitrary sampling rate. This is not usually done in music production, mainly because filters are dependent on the sampling rate. However, it is common in real-time situations, 3.2. THE XML 17 since lowering the sampling rate reduces the processor power required. It is possible to design filters to work with an arbitrary rate. The mixSamples() method returns the number of sample frames generated. Generally this will be the same as the number requested. However, many DSP modules are designed only to generate a finite quantity of data, and when a player has generated them all, it will use the return value to tell the parent player—or the user of the library, who manages the root player—that it has finished. 3.2 The XML Classes derived from DSPModule generally have a constructor or an initialiser that takes a (root) XML element and sets up a module tree from it. The base class DSPModule contains a static member function readModule() for identifying an XML element by its name and calling the appropriate constructor. It also recognises the element <external>, which causes a module to be read from another XML file. This mechanism is used by all modules that want to load children. The library does not distinguish between .xmi and .xmm files (defined in Section 1.3). The distinction is left to the user. As an example, the MIDIMapping in Figure 3.1 on page 15 might be stored in an .xmm file that refers to an .xmi file for the GeneralMultiplexer. This .xmi file may defer to more .xmi files for the individual instruments. 3.3 Variables Instruments have to be able to play at arbitrary pitches and velocities1 . Many filters have cut-off frequencies, resonance levels and the like, and these need to be able to be controlled by the MIDI sequence. MIDI has a plethora of parameters that could be used for this. It would also be nice if we could make a filter’s parameters, or perhaps the speed of a volume envelope, depend on pitch or velocity. Finally it would be nice to have mechanisms to control the tempo (speed) at which a MIDI sequence is played, or transpose the sequence. The list goes on, and clearly a great deal of flexibility is desired. XMAS uses a system of variables to achieve this flexibility. The system is illustrated in Figure 3.2. The MIDIMappingPlayer passes a set of variables to the constructor for each child. Three variables are shown in the diagram, but 1 Note velocities, or how fast a key was depressed; used to effect what classical musicians know as dynamics, and often just interpreted as volume. 18 CHAPTER 3. IMPLEMENTATION MIDIMappingPlayer channel = 10 note = 35 velocity = 127 channel = 2 note = 72 velocity = 127 channel = 10 note = 57 velocity = 127 GeneralMultiplexerPlayer Match: none; no child constructed GeneralMultiplexerPlayer Match: channel=2 GeneralMultiplexerPlayer Match: channel=10,note=57 LookupMultiplexerPlayer Look-up index: note=72 SamplePlayer SamplePlayer Playing: cymbals.wav Playing: harphigh.wav Figure 3.2: A state tree, with variables many more exist in reality. The GeneralMultiplexerPlayers use the variables to decide what child to construct, if any. They pass the variables on to the child constructor. This is important since the LookupMultiplexerPlayer needs to consult them to decide which SamplePlayer to construct and the SamplePlayers need to know what frequency to play the samples at. Figure 3.3 shows how variables are set up. Each variable is encapsulated in a Variable object, which incorporates a reference to a value of type double. A Variables object manages the list of Variable objects required by a player, and also keeps a pointer to the Variables object passed down by the parent. For modules that do not need to create any variables of their own, a Variables object need not be constructed. A constructor will keep a pointer to each double it needs. For instance, VolumeEnvelopePlayer will store a pointer to rate. Pointers to Variable or Variables objects are not stored, so, conveniently, these objects can safely be destroyed on exit from the constructor. The use of pointers enables the MIDIMappingPlayer to vary the pitch of a note, or any other variable, over time. The various mixSamples() methods simply dereference such pointers each time they are called. The VariableComputeBlockPlayer (see Section 3.5.5) recalculates rate before every operation involving its child VolumeEnvelopePlayer, in case the note variable has changed. 3.4. PARAMETER TWEAKS 19 MIDIMappingPlayer Variable channelVar; double channel; Name: "channel" Value: 2.0 Variables variables; Parent variables Value Our variables double note; Variable noteVar; Value: 72.0 Name: "note" Value double velocity; Value: 127.0 Variable velocityVar; Name: "velocity" Value VariableComputeBlockPlayer Variable rateVar; Name: "rate" double rate; Value: 2^((note-60)/12) Value Variables variables; Parent variables Our variables VolumeEnvelopePlayer Parent variables Figure 3.3: How variables are implemented 3.4 Parameter Tweaks Many modules have built in the ability to adjust their output volume. All modules will be able to cope with the arbitrary sampling frequency, and this capability can also be used to vary the pitch (though crudely). There are situations in which a parent module would like to be able to tap in to these capabilities: for example, a MIDI player will want to tell notes (the child module players) to respond to something like MIDI volume, but ideally we want the note generator modules not to be unnecessarily specific to MIDI, in case an alternative to MIDI is added one day. All DSP modules have a method pushParameterTweak() which takes a parameter name and a double. The name will be something like “volume” or “delta”2 . The double will be multiplied with the current value for the given parameter, after the current value is saved on a stack. Later, a call to popParameterTweak() will restore the old value. 2 I use the term delta to refer to a frequency ratio. Its meaning, and the choice of terminology, will be clarified in Section 3.5.1 on samples. 20 CHAPTER 3. IMPLEMENTATION 3.5 3.5.1 The Modules Samples The first module implemented for libxmas was the Sample module. It encapsulates a mono or stereo sample loaded from a .wav file, which can be played forwards or backwards. A section of the sample can be looped, or played repeatedly. Two types of loop are available: straight loops, in which the position pointer jumps from one end of the loop to the other, and bidirectional loops, in which the direction changes each time the position hits an endpoint. Bidirectional loops double the period of a sample loop without doubling the size of the data; the longer the period, the less likely it is that the listener will detect the repetition. They are also very useful for effects that sweep up and down periodically. The output from the SamplePlayer consists of the sample played at any volume and any speed. Adjusting the volume is simple: we multiply each sample with the volume value. The real art of this module is in the code for adjusting the speed. This process is known as resampling. A value named delta specifies the change in frequency. If delta is 1, the sample plays as it was recorded. If delta is 2, the sample will play twice as fast and be heard an octave higher. If we assume the sampling rates of the sample and the output are equal, then delta specifies how many samples to advance in the source for each sample in the destination. It is added to the position pointer each time around the resampling loop. This is why it is called “delta”. The name is used throughout XMAS in the more abstract sense of speed/frequency adjustment. The sampling theorem states that when data are sampled, no frequency at or above half the sampling rate can be represented. This cut-off point is known as the Nyquist frequency 3 . If we attempt to represent a frequency x Hz above the Nyquist frequency, it will become a frequency x Hz below the Nyquist frequency in a phenomenon known as aliasing. Since this is an arithmetic transformation and music is based on frequency ratios, aliasing will pollute the spectrum and reduce quality. Although it is important to realise that resampling is a huge discipline and the term often suggests a thoroughly researched algorithm, the methods XMAS applies are relatively crude and well known. Interpolation is used, cutting down on aliasing to an extent sufficient for most applications. 3 Named after Harry Nyquist, author of the sampling theorem. 3.5. THE MODULES 21 Most real-time resamplers keep a pointer into the sample data and effect interpolation by looking at samples before and after the current one. They have to take care not to overrun, and they cannot see transparently across flow changes such as loop points. This can create an audible click each time a sample loops, even when the continuity across the loop points is perfect. Loop mode: bidirectional Loop start +5 5 0 0 1 2 3 4 8 6 9 10 11 Loop end 12 7 -5 #9 #10 #11 1. 0 0 0 4. 1 Initial history buffer pos = 0 #0 2. 0 0 1 -4 -5 -5 -3 Just before looping pos = 12 #10 #11 #12 #12 5. 4 -5 -5 -3 -3 A bit later pos = 1 3. 0 Just looped! pos = 12 #0 #1 1 4 #11 #12 #12 #11 5 6. Later still pos = 2 #10 #9 7. #8 -5 -4 -1 -5 -3 -3 -5 A bit later after looping pos = 11 1 Playing backwards pos = 7 Figure 3.4: The history buffer The XMAS library uses a history buffer, which holds the last three samples seen before the one pos points to. The concept is illustrated in Figure 3.4, which shows the state of the history buffer at several points during playback. Between them, the history buffer and the sample indicated by pos constitute a run of four samples, and the current playback position is considered to be between the second and third. A subpos variable holds the fractional part of the position, a value indicating how far between the second and third samples we are. When it reaches 1, it is reset to 0, pos is incremented and the history buffer is updated. This method provides perfect continuity in all cases, but as presented it is hardly efficient. The library seamlessly switches to a conventional algorithm shortly after starting and after each change of flow. Three interpolation functions are provided. One of them is, ironically, the non-interpolating function, which always takes the second sample verbatim. The output is coarse and suffers from aliasing, but it can be done quickly and is 22 CHAPTER 3. IMPLEMENTATION reminiscent of sounds from old, dearly loved computer systems such as the Commodore Amiga. The second function does linear interpolation between the second and third samples. This is a fair compromise, doing only a little more work than the first function in exchange for considerably less aliasing. The third function does cubic interpolation. All four samples are taken into account. The tangent to the curve at the second sample is parallel to a line joining the first and third samples, and a similar property holds at the third sample. This ensures that the curve and its first derivative are continuous, providing optimum sound quality for a function of this complexity. Appendix A derives the equations and presents an optimisation that uses look-up tables to eliminate much of the computation. The library provides a global variable via which the programmer can set a default interpolation function. The instrument designer can override this for a specific sample by specifying a minimum and maximum quality. 3.5.2 Volume Envelopes Volume Volume 1 1 0 0 1 Sustain point Time seconds 0 0 0.05 Loop start 0.10 0.15 Time seconds Loop end Figure 3.5: Two examples of volume envelopes Behaviour A volume envelope is a graph of volume against time. The VolumeEnvelope module models this graph as a series of linearly connected volume-time pairs with time increasing monotonically, and each VolumeEnvelope object has one child, known as the subject. The VolumeEnvelopePlayer constructs one player for the subject, and applies the envelope to the player’s output. In the case of the right-hand envelope in Figure 3.5, the VolumeEnvelopePlayer’s output will be silence initially, full volume at 0.05 seconds, and silence again between 0.1 and 0.15 seconds. 3.5. THE MODULES 23 A VolumeEnvelope can also manage two loops, which are each given in terms of a starting node and an ending node. These can be the same node if it is desired that the envelope freeze at that node (see the left-hand example). One of the loops is the sustain loop, and is obeyed only as long as the note is held.4 The other loop is obeyed at all times. In Figure 3.5, the left-hand envelope fades a note in quickly, holds the note at full volume, and then fades it out pseudo-exponentially; this is quite usual, and is used by the envelope applied to flute.wav in Figure 3.1. The right-hand example is a lot more unusual, and potentially rather annoying! When a volume envelope terminates at zero volume (as happens after one second in the left-hand example if the note is released immediately), the VolumeEnvelopePlayer will terminate its output (recall Section 3.1.3). This is important. The flute.wav Sample in Figure 3.1 is set to loop indefinitely, but the VolumeEnvelope above it can terminate the output when the note has faded out, telling the MIDIMapping that the player can be destroyed. If this did not happen, the note would persist in memory and waste resources. The VolumeEnvelopePlayer is influenced by a variable called rate. If rate is 1, the output is as expected. If rate is 2, the position in the envelope will advance twice as fast, so the first envelope would elapse in half a second for notes released immediately. It is sometimes useful to compute rate from note or delta using a variable compute block (Section 3.5.5). Implementation The parameter tweak system allows a module to request of a child an adjustment that is constant for a while, but does not allow for gradual changes. Correspondingly, the VolumeEnvelopePlayer will try to use tweaks only when the volume is not changing (as while sustaining in the left-hand example). In this case, it can ask the subject to mix samples into the buffer that was passed to itself. However, if the volume is changing (or if a tweak fails), the following steps are taken: • a temporary sample buffer is allocated; • the buffer is filled with zeros; • the subject player is asked to mix its samples into the buffer; • the VolumeEnvelope mixes the contents of the temporary buffer into its own output buffer, applying the gradual change in the process; 4 There is a variable to indicate when a note is held. See Section 3.6.3. 24 CHAPTER 3. IMPLEMENTATION • the temporary buffer is freed. While this produces perfect output, it is not very efficient. I shall return to this in the Evaluation. 3.5.3 Multiplexers Multiplexers are used to select an instrument sound according to the program variable, and to distinguish Channel 10, the percussion channel, from other channels by using the channel variable.5 They are also used to select a sample according to the note variable, since the method libxmas uses to create different notes from one sample is crude and only works well over small note ranges. Review Figure 3.1 on page 15 for some examples of multiplexers. A multiplexer object manages several subject modules. Each time a player is constructed, one subject module is chosen and a single player is constructed. All subsequent operations on the multiplexer player are deferred to the subject player. There are two types of multiplexer: GeneralMultiplexers and LookupMultiplexers. They differ in how they choose a subject module. GeneralMultiplexers scan the modules in reverse order and the first matching module found is used. Each module is given with a set of variable ranges—for instance, one subject might be given with the two ranges 50 ≤ note ≤ 63 and 0 ≤ velocity ≤ 9—and the module matches if all range variables are defined and within the ranges. The extremes are always integers and the variables are rounded to the nearest integer before the comparisons take place. This is a linear search and will not scale well, so a large number of subject modules is not recommended. LookupMultiplexers specify an index variable and manage a table of subject modules. The index variable is rounded to the nearest integer and used as an index into the table. In addition to the table, there is a pointer to a module to be used for values below the table’s lower bound, and another for values above the table’s upper bound. LookupMultiplexers are more limited than GeneralMultiplexers, but the look-up is a constant-time operation. They are perfect for selecting an instrument using the program variable. 5 The example in Figure 3.1, page 15, chooses instruments according to channel instead of program. This was done so the choice could be combined with the step of identifying the percussion channel, but it is not recommended in real applications. Appendix B.5 shows the more usual approach. 3.6. THE MIDI PLAYBACK ALGORITHM 25 Both types of multiplexer can define one or more variables for use in making the decision. These are computed for the selection process only, and are not passed down to the child constructor. 3.5.4 MIDI Mappings MIDIMapping objects are very simple. A MIDIMapping manages the contents of a .mid file, which consists of a few values and a set of byte arrays (the tracks). A subject module defines all the instrument sounds. The MIDIMapping also stores some playback control parameters that are not part of the .mid file, such as extensive looping information and information on what to do when a Note On event is received for a note that is already playing. The MIDIMappingPlayer, comprising an entire MIDI playback algorithm, is a lot more involved! It is described in full in Section 3.6. 3.5.5 Variable Compute Blocks A VariableComputeBlock module has one child. It allows new variables to be defined in terms of existing ones, and these are made available to the child. The mathexpr package is used to evaluate the expressions corresponding to the variables. The new variables are set up and computed when the VariableComputeBlockPlayer is constructed. They are calculated again every time the VariableComputeBlockPlayer is used. As such they are updated along with the variables they depend on. It is possible to override an existing variable, at the same time using the existing variable to compute the replacement. The main use for the VariableComputeBlock at the moment is to control the rate variable for a VolumeEnvelope. 3.6 3.6.1 The MIDI Playback Algorithm Overview There are two common types of .mid file. The first, Type 0, contains one track. The second, Type 1, contains a number of tracks which are to be played simultaneously and synchronously. There is a third type with independent tracks, but it is uncommon and libxmas does not support it. To keep it simple, libxmas treats Type 0 as a special case of Type 1. A track is merely a sequence of MIDI events and meta-events. Each is prefixed by a delta-time representing the amount of time separating the event from the 26 CHAPTER 3. IMPLEMENTATION last. In a conventional MIDI set-up, a sequencer does the timing and sends the MIDI events to the synthesisers while interpreting the meta-events itself. .mid files adopt the classical concept of beats and subdivide them into deltatime ticks. The number of ticks per beat can be specified in the file. By default, there are 120 beats per minute, but a meta-event can override this, specifying the tempo as a number of microseconds per beat (though the value presented to a user is usually in beats per minute). Tracks are a logical subdivision of music. It is up to the author of a .mid file to decide what to put in each track. The sequencer will process all tracks simultaneously and dispatch events to the synthesisers, but information about which track an event came from is lost. Most events specify a MIDI channel (see Section 1.1.1). There is often a correspondence between tracks and channels, but they are distinct concepts not to be confused. The tracks exist in the sequencer, and the channels are distinguished by the synthesisers. XMAS’s MIDIMappingPlayer behaves like a sequencer connected to a multipart synthesiser. Rather than using hardware timing, it does timing by emulating the synthesiser for precise amounts of time and changing state in between runs of emulation. In more concrete terms, it effects an elapsed time by requesting an appropriate number of samples from the synthesiser. There is no asynchronous behaviour, and the process is deterministic. The algorithm described herein is simplified for conciseness, though some of the extra complexity is alluded to. 3.6.2 State For each track, the MIDIMappingPlayer maintains three values: • a position counter for the track, which points to the event bytes (after the delta-time) for the next event to be processed or holds the value -1 for tracks that have finished playing; • the number of delta-time ticks to wait before the next event should be processed (the wait value); • the running status byte (recall Section 1.1.1). For each channel, the MIDIMappingPlayer stores a list of all the notes currently playing. A note consists of a pointer to a DSPModulePlayer along with some pertinent variables (more on this later). Some variables that are global to the channel are also stored. These include 3.6. THE MIDI PLAYBACK ALGORITHM 27 • the pitch wheel position, used on many devices to bend all notes up or down in pitch; • the channel aftertouch, a measure of the pressure being applied to the keys on an electronic keyboard, averaged over all depressed keys; • the current program, generally used to select an instrument; • a multitude of MIDI controller 6 values, such as the channel volume, the stereo pan (left-right positioning) and the modulation wheel position. Most of these variables are made available to the instruments, but a few are processed in the MIDIMappingPlayer itself. In particular, the channel volume is applied to all notes using volume parameter tweaks, and the MIDIMappingPlayer takes it upon itself to calculate the final frequency for each note, incorporating pitch bend and other factors into the computation. As stated in Section 3.5.4, a single DSPModule is used for all the notes. It is likely that the DSPModule will include a LookupMultiplexer switching on the program variable, but it may choose to use the program variable for something else, or not to use it. Finally, the MIDIMappingPlayer also stores some global state, such as the tempo, the number of times the music has left to loop, and a measure of how much output to generate before the tracks’ wait values will be correct. This last measure is henceforth referred to as the global wait value, and is given in extremely fine units of 232 per second. 3.6.3 Notes As stated, a note consists of a DSPModulePlayer and some pertinent variables. Some of the variables are note, velocity, and held. The held variable is 1 initially and goes to 0 when the Note Off event is encountered. Each instrument should be designed to respond to the held variable in an appropriate manner. The MIDIMappingPlayer never cuts notes off, so DSPModulePlayers should terminate themselves to avoid a build-up of old notes. At present, VolumeEnvelope is the only module that responds to held. An instrument could incorporate a VolumeEnvelope designed to take the volume down to 0 after the note is released, or it might consist of a sample configured to play once without looping. 6 This is distinct from the MIDI controllers mentioned in Section 1.1. This kind of MIDI controller is simply a playback control parameter that can be set by a MIDI event. 28 CHAPTER 3. IMPLEMENTATION 3.6.4 Algorithm The playback algorithm is essentially a form of discrete event simulation. When the MIDIMappingPlayer is constructed, all the variables are initialised and the track pointers are set up. The tracks’ wait values are set to 0, and then the initial delta-time for each track is processed. Processing a delta-time involves adding the delta-time to the track’s wait value and then advancing the track pointer to the following event bytes. Finally, the processMIDI() method is called. For each track whose wait value is 0, processMIDI() processes MIDI events until it finds a nonzero delta-time tick. Then it determines how long to wait before another MIDI event will be due on any track, subtracts that amount of time from all tracks’ wait values, and adds it to the global wait value, scaling as necessary and factoring in the current tempo. Each time the MIDIMappingPlayer’s mixSamples() method is called, the following steps are undertaken. (It may be helpful to refer back to the description of mixSamples() in Section 3.1.3, page 16.) 1. First, we use the global wait value and the sampling rate to determine how many samples to generate. If this number is greater than the count passed to mixSamples(), we reduce it accordingly. 2. Each note (on each channel) is asked to generate that many samples. 3. The global wait value is reduced in accordance with the number of samples generated. If it reaches zero or goes negative, we call processMIDI() until it goes positive again. (It would always go positive straight away unless there were many delta-time ticks to a sample, which is very unlikely, but the while loop does no harm.) 4. If we have not yet generated all the samples that were requested by the caller, we advance the buffer pointer and return to Step 1. 3.6.5 Noteworthy Features Looping The following loop control parameters are specified in the MIDIMapping: • How many times to play the music. If this is 0, the music will loop indefinitely. 3.6. THE MIDI PLAYBACK ALGORITHM 29 • Where to loop back to. This can be used to avoid playing an introduction after the first time. • Whether to stop outstanding notes, reset variables or both at the end of the music. • An optional delay to be inserted at the end before looping. A period of silence at the end is an important part of some music, and it is often omitted in .mid files. • A flag indicating whether to wait for a whole beat to elapse before looping. Some .mid files end as soon as the last Note Off event is seen, which may be a little too early to loop. Looping on a beat is most likely to sound correct. Duplicate Note Handling The MIDIMapping lets the musician specify a duplicate note policy. This comes into play when two Note On events are received for the same note on the same channel without an intervening Note Off event. The following options are available. Except in the case of preempt, a second Note On will not have any effect on the first, and the notes will play together. stack (default). Each Note Off will stop the most recently started note that was started before the current delta-time tick. If no such notes exist, it will stop the last note from the current tick. This is useful when one track starts a note at the same time another track stops it, but the former track is processed first. strictstack. Each Note Off will stop the most recently started note, including any started on this delta-time tick. queue. Each Note Off will stop the note that was started earliest. preempt. A second Note On will stop the first note (but allow it to fade out). stopall. Notes can accumulate, but each Note Off will stop all notes. Portamento The MIDI Specification provides for a feature called portamento, but I have found that neither my Creative Labs Sound Blaster Live! card nor the Yamaha Portatone PSR-550 electronic keyboard obeys the relevant MIDI controller values. 30 CHAPTER 3. IMPLEMENTATION Portamento is loosely defined as sliding pitch, as exemplified by the clarinet at the beginning of Gershwin’s Rhapsody in Blue. XMAS implements it by keeping a single note playing and having this note slide to the new pitch every time a Note On event is seen. Note Off events are registered but not acted upon until portamento is disabled. In order to effect the slide, libxmas bisects the buffer recursively until a ‘granularity’ measure becomes small enough. The threshold was chosen aurally. The granularity measure was defined as the product of the step length and the size of the step in semitones, since increasing either of these will make the steps more noticeable. Chapter 4 Evaluation 4.1 Goals Achieved Here I comment on the requirements listed in Section 2.1. • “It must be easy for a composer to produce music.” I am able to use my favourite .mid and .wav editors. Writing the XML itself was painless. This goal was met. • “It must be possible for the composer to keep file sizes down to a minimum.” No compressed audio file formats are supported, so this goal was not met. However, support for compressed audio could be added with no major redesigning, and as explained in Section 2.2.2, doing it properly would have taken more time than was available. • “The music must be structured so as to allow tweaks on the fly. Such tweaks might include speed variation, muting of some instruments and instrument substitution.” No tweaks have been implemented for the MIDIMappingPlayer; I chose instead to put the available time towards supporting a good selection of MIDI events. The structure is there, so technically the goal was met. • “The playback library must be capable of producing the same output on every computer.” As far as I know, it does! I took headphones to the computer lab when my computer was out of order, and the output was the same. • “It must be able to do so comfortably in real time on a typical home computer for a piece of music of average complexity.” Overall, this goal has 31 32 CHAPTER 4. EVALUATION been met. Sections 4.1 and 4.6.2 discuss this further. See Section 4.5 for some measurements. While not all goals have been met at this stage, the project would not need any major redesigning to meet any of them. I consider the project a success. 4.2 4.2.1 Evolution of the Plan Generalisation The original project proposal (Appendix D) began by emphasising a specific structure in which a MIDI mapping appeared at the root of a tree and had modules called instruments as children. The MIDI mapping would act as a multiplexer on the MIDI program (instrument), and an instrument would multiplex on the note. An instrument would only have samples (or perhaps synthesisers) as children. Any effects such as filters or volume envelopes would be defined within the instrument modules, in the form of embedded effect trees. An effect tree would look much like the state tree from Figure 3.1, but there would be a missing leaf where a sample would be plugged in. As an afterthought, the project plan mentioned that MIDI mappings, instruments, samples and effects would actually be generalised into a unit known as a DSP module. In the time following the submission of the plan, it began to become clear that the proposed structure could be simplified. As the structure stood, it was not clear what should happen if an instrument (properly part of the main state tree) were defined as part of an effect tree. There would be many missing leaves, and it would not be clear which one a sample should be plugged into. Apart from managing the overcomplicated effect trees, the only job an instrument module performed was selecting a sample according to the note being played. I soon realised that this would be better off in dedicated modules called multiplexers (Section 3.5.3). First, this allowed multiplexing to be done on any variable, not just the current note. Secondly, it became trivial for a musician to put the effects anywhere they were required, above or below any multiplexer. Effect trees were therefore no longer necessary in defining instruments. 4.2.2 Filtering Whole Channels or Tracks Some functionality has been lost as a result of the changes described in Section 4.2.1. In addition to the instruments, the MIDI mapping was going to 4.3. MILESTONES 33 manage some effect trees whose job would be to filter one or more whole channels or tracks. It is still possible to filter whole channels. Figure 4.1 applies a filter to Channels 4, 5 and 6. This is rather involved, and the filter’s parameters must be controlled in control.mid. Sometimes it would be preferable for cool.mid to control them, especially when filtering single channels. A composer might well reject this idea in favour of filtering every note individually, clearly a waste of processor time. MIDIMapping MIDIMapping cool.mid control.mid Instruments Instruments LookupMultiplexer LookupMultiplexer 1 1 channel 2 channel Filter Subject 2 3 MIDIMapping cool.mid Instruments LookupMultiplexer 4 channel 5 6 Figure 4.1: How to filter a set of channels I believe the best way to fix this would be to split the MIDI mapping so that ‘channel player’ or ‘track player’ modules could appear as descendants with effects in between as desired. Since no such effects were actually implemented as part of the core work, it seemed appropriate to leave splitting the MIDI mapping as an extension. 4.2.3 Reference Counting I planned to implement reference counting for .wav, .mid, .xmi and .xmm files. When it came to doing it, I wanted to make my code reusable and could not see immediately how to achieve this. It was not an essential part of the project, so I left it as an extension. 4.3 Milestones I did not anticipate the amount of time it would take to do the second work package, consisting of the volume envelope and other components that can be 34 CHAPTER 4. EVALUATION used to define an instrument. A large part of this work was the system of variables discussed in Section 3.3. However, the subsequent three weeks’ work collapsed to a few days, as most of the functionality the .xmm format was going to implement already existed. In summary, the structure of the project changed to such an extent that the milestones were no longer a good subdivision of the work to be done. Nevertheless, they did their job of providing short-term goals and keeping the project moving. 4.4 Testing 4.4.1 General Most testing was performed aurally. To aid this, files were set up to check that newly added features were working properly. Test programs designed to call the mixSamples() method for varying numbers of samples at a time were written. The one part of the project that required more than aural testing was the resampler. 4.4.2 The Resampler I wrote a visual test for the resampler. Figures 4.2 and 4.3 present six screen shots from the test program. The program accepts the name of a .wav file on the command line. Keystrokes change the volume and delta parameters, select an interpolation function, adjust the looping settings, and switch between mono and stereo. The test calls mixSamples() repeatedly; the number of sample frames requested each time is a random number from 1 to 8. This test proved invaluable in the construction of the resampler. Allowing many cases to be verified in a short space of time, it found many bugs that may otherwise not have been known. 4.5 Profiling Profiling was done using gprof, after the code was compiled and linked with g++’s -pg switch and run on my AthlonXP 1800+ running at 1145 MHz, a typical modern configuration. The jou5cred.xmm file featured on the Demo CD was played, and the audio output was piped into ALSA’s aplay command. Figure 4.4 shows the results for the three different interpolation modes. The resampler uses just over half the processor time. Considerable proportions go towards the volume ramping code in the VolumeEnvelopePlayer, discussed in 4.5. PROFILING 35 Initial display: volume = 1, delta = 1. delta reduced to 14 . Cubic interpolation at work. A straight loop. Note how the curve is smooth everywhere. Figure 4.2: The visual resampler test. 36 CHAPTER 4. EVALUATION A bidirectional loop with cubic interpolation. The same loop with linear interpolation. The same loop with no interpolation. Figure 4.3: The three interpolation functions. 4.5. PROFILING 37 % cumulative self self total time seconds seconds calls ms/call ms/call name 54.46 11.00 11.00 243660 0.05 0.05 void SamplePlayer::doResample...InterpCubicF... 12.33 13.49 2.49 141864 0.02 0.02 void VolumeEnvelopePlayer::applyVolumeRamp... 9.01 15.31 1.82 main 6.63 16.65 1.34 179704 0.01 0.03 VolumeEnvelopePlayer::mixSamples... 4.90 17.64 0.99 1161462 0.00 0.00 std::_Rb_tree<...ParameterTweak*...>::find... 1.53 17.95 0.31 894780 0.00 0.00 DSPModulePlayer::PassDownTweak::applyTweak... 1.44 18.24 0.29 1161462 0.00 0.00 DSPModulePlayer::pushParameterTweak... 0.79 18.40 0.16 22656000 0.00 0.00 std::floor(float) 0.79 18.56 0.16 1417 0.11 12.67 MIDIMappingPlayer::mixSamples... Profiling results with the cubic resampler. % cumulative self self total time seconds seconds calls ms/call ms/call name 50.32 9.51 9.51 243660 0.04 0.04 void SamplePlayer::doResample...InterpLinearF... 14.07 12.17 2.66 141864 0.02 0.02 void VolumeEnvelopePlayer::applyVolumeRamp... 7.83 13.65 1.48 main 7.41 15.05 1.40 179704 0.01 0.03 VolumeEnvelopePlayer::mixSamples... 5.87 16.16 1.11 1161462 0.00 0.00 std::_Rb_tree<...ParameterTweak*...>::find... 1.96 16.53 0.37 22656000 0.00 0.00 std::floor(float) 1.16 16.75 0.22 36 6.11 7.50 Sample::readSample... 1.11 16.96 0.21 1417 0.15 11.84 MIDIMappingPlayer::mixSamples... 1.01 17.15 0.19 1161462 0.00 0.00 DSPModulePlayer::pushParameterTweak... Profiling results with the linear resampler. % cumulative self self total time seconds seconds calls ms/call ms/call name 51.81 8.61 8.61 243660 0.04 0.04 void SamplePlayer::doResample...InterpNoneF... 13.72 10.89 2.28 141864 0.02 0.02 void VolumeEnvelopePlayer::applyVolumeRamp... 6.50 11.97 1.08 main 6.44 13.04 1.07 179704 0.01 0.03 VolumeEnvelopePlayer::mixSamples... 6.08 14.05 1.01 1161462 0.00 0.00 std::_Rb_tree<...ParameterTweak*...>::find... 1.68 14.33 0.28 1417 0.20 10.66 MIDIMappingPlayer::mixSamples... 1.50 14.58 0.25 22656000 0.00 0.00 std::floor(float) 1.32 14.80 0.22 1161462 0.00 0.00 DSPModulePlayer::pushParameterTweak... 1.20 15.00 0.20 894780 0.00 0.00 DSPModulePlayer::PassDownTweak::applyTweak... Profiling results with the non-interpolating resampler. Figure 4.4: Some profiling results 38 CHAPTER 4. EVALUATION Section 4.6.2, and the code in main that converts to 16-bit integers and outputs them, which is not a concern since it is merely part of the test program and has not been optimised. Additionally, a noteworthy amount of time is spent processing parameter tweaks; this would deserve investigation given more time. Surprisingly, the choice of interpolation function does not make much difference to the amount of processor time used by the resampler. (The ‘self seconds’ column is the most appropriate measurement for this comparison.) I suspect the code generated by the compiler is sub-optimal, and the overhead per sample is greater than the cost of the interpolation function. Despite the above concerns, when compiled without the profiling overhead, the test program used 32.640 seconds of processor time to play jou5cred.xmm through aplay with cubic interpolation, as reported by Linux’s time command. The real time reported was 4 minutes and 20.481 seconds. This equates to an average of 12.5% CPU usage, which is comfortable. 4.6 4.6.1 Comments Samples Refer back to Figure 3.4 and observe how the history buffer begins filled with zeros. This ensures that the curve makes a smooth departure from the centre line as a sample starts. Unfortunately, the end of playback is another matter. If the sample in Figure 3.4 were set not to loop, and instead ended where the loop end is marked, then the output from the SamplePlayer would terminate after state 4. Ideally, the contents of the history buffer should be allowed to phase out and be replaced by zeros before the output terminates. Luckily, this is rarely a problem. Most samples are set to loop and are faded out by an envelope. Those samples that are not set to loop will usually include their own fade-out, however brief, so the output that is not generated would be very close to silence anyway. 4.6.2 Volume Envelopes As mentioned in Section 3.5.2, the volume envelope implementation is not particularly efficient. An alternative implementation would be to use parameter tweaks and adjust the volume in small steps. I rejected this idea during implementation because it would create some clicking. 4.7. PROBLEMS ENCOUNTERED 39 However, the MIDI protocol itself cannot vary a parameter smoothly over time. If a channel is faded in or out, the fade will have to be done in steps. A better implementation would use steps and have generators endowed with the ability to remove clicks themselves. The SamplePlayer could do this by including the volume ramping functionality in the resampling loop, where it would cost considerably less. 4.6.3 MIDI Playback It would be unreasonable to expect every MIDI event or feature to be handled, and quite a few are missing from libxmas. However, all the commonly occurring ones have been implemented, and all the .mid files I have tested play correctly. As evidenced in Section 3.6.5, libxmas sometimes outperforms commercially available MIDI players! 4.6.4 Flexibility I am exceptionally pleased with the flexibility XMAS offers. As I was preparing music, I felt that some of the instruments were too loud on the high notes and too quiet on the low notes. No problem; XMAS allowed me to compensate by adjusting the velocity variable. I wanted one instrument to decay more slowly for low notes. No problem; the volume envelope will respond if I set the rate variable. This is leagues ahead of what an Amiga mod-based file or a SoundFont (an instrument definition for the MIDI player on a Creative Labs Sound Blaster) can do. 4.7 4.7.1 Problems Encountered STL Containers Early in the project’s development, having used g++ to compile the code so far, I decided to try Intel’s icc and see if the code would run faster (on my AMD processor). The difference in execution time was incredible. Investigating the cause of the immediate segmentation fault, I discovered behaviour on the machine code level that suggested a compiler bug. When the project had progressed further and the code exhibited the same problem with g++, I knew something was wrong. After a while I realised what the problem was. The Standard Template Library, providing containers such as vectors and linked lists, often reallocates mem- 40 CHAPTER 4. EVALUATION ory and has to move the objects to a new location. If pointers to the objects exist anywhere, those pointers will become invalidated. The solution was to construct containers only of pointers to objects, so the pointers would be moved and the objects would not. This hitch cost me a couple of days. It did not throw the project off track. 4.7.2 XML Files xmlwrapp seemed unable to read XML files unless they were in the working directory. This could pose problems for composers who want to use directories to organise their instruments. I did not have time to investigate this problem. 4.7.3 Code Size A generic resampling algorithm, including the support for the history buffer described in Section 3.5.1, was written once in the form of C++ templates. It is instantiated with three different interpolation functions. Versions exist to play forwards and backwards. There are versions for mono source and stereo source, and versions for mono destination and stereo destination. This results in a large explosion in executable code size. Compiled with optimisation and stripped of all symbols, the MIDI playback test is 357 kB. Compressed with UPX [4] it is 120 kB, which is still a lot for just the music playback code. It is likely to continue to grow exponentially when surround sound support is added to XMAS. I do not currently have a solution to this problem. Changing the template parameters into variables would likely cause an unacceptable performance hit. Building code on the fly would tie me to a specific architecture and is prohibited on some machines for security reasons. Chapter 5 Conclusions I am extremely pleased with the outcome of this project. While a few problems are outlined in the Evaluation, they are minor and it is easy to forget how much of the project went well. I have learnt a lot from the project, particularly in terms of instrument design and C++ experience, and I shall certainly use XMAS for games I write in the future. A Demo CD is enclosed. It includes some aural test results and two complete pieces of music. A track listing is given in Appendix C. After doing a little more work on XMAS, I intend to release the library as an open source project at http://xmas.sf.net/. Please visit this site if you are interested in XMAS. 41 42 CHAPTER 5. CONCLUSIONS Bibliography [1] The MIDI Manufacturers Association. The complete MIDI 1.0 detailed specification. http://www.midi.org/about-midi/specinfo.shtml, 1996. [2] B. N. Davis. Rock ‘n’ Spin. rockspin, 2000. [3] P. Jones. xmlwrapp. 2001–2003. http://bdavis.strangesoft.net/?page= http://pmade.org/pjones/software/xmlwrapp/, [4] Markus F. X. J. Oberhumer and L´aszl´o Moln´ar. UPX, the Ultimate Packer for eXecutables. http://upx.sf.net/, 1996–2002. [5] B. N. Davis, R. J. Ohannessian and J. Cugni`ere. DUMB, Dynamic Universal Music Biblioth`eque. http://dumb.sf.net/, 2002, 2003. [6] Y. Ollivier. Mathematical expression parser in C++. http://www.eleves. ens.fr/home/ollivier/mathlib/mathexpr.html, 1997–2000. [7] B. Pietsch. The L math processor. http://lmp.sf.net/, 2000. [8] M. Richards. How to prepare a dissertation in LATEX. http://www.cl.cam. ac.uk/users/mr/demodiss.tar, 2001. 43 44 BIBLIOGRAPHY Appendix A The Cubic Interpolation Function The cubic interpolation function is based on the following formula, where x is the interpolated value, t is the fractional part of the sample position, and a, b, c and d are given in terms of four existing samples, x0 , x1 , x2 and x3 . We are interpolating between samples x1 and x2 . x = at3 + bt2 + ct + d (A.1) dx = 3at2 + 2bt + c (A.2) dt At t = 0 we desire x to evaluate to x1 , and at t = 1 we desire x to evaluate to x2 . Substituting these values into Equation A.1 gives us the following equations: d = x1 (A.3) a + b + c + d = x2 (A.4) At t = 0, we desire the curve’s gradient to be parallel to a line joining samples 0 and 2, so dx = 21 (x2 − x0 ). Likewise, the gradient at t = 1 should be parallel to a dt line joining samples 1 and 3, so dx = 12 (x3 − x1 ). Substituting into Equation A.2 dt gives the following. 1 (x2 − x0 ) = c (A.5) 2 1 (x3 2 − x1 ) = 3a + 2b + c (A.6) Equations A.3, A.4, A.5 and A.6 can be solved simultaneously, giving the following matrix equation: a b c d 1 = 2 −1 3 −3 1 x0 2 −5 4 −1 x1 −1 0 1 0 x2 0 2 0 0 x3 45 (A.7) 46 APPENDIX A. THE CUBIC INTERPOLATION FUNCTION The formula for x can also be expressed in matrix form: x= ³ t3 t2 t 1 ´ a b c d (A.8) Substituting A.7 into A.8 gives x= ´ 1³ 3 2 t t t 1 2 x0 −1 3 −3 1 2 −5 4 −1 x1 −1 0 1 0 x2 x3 0 2 0 0 (A.9) Since matrix multiplication is associative, we can elect to do the multiplication by powers of t first. The result is a vector of four values, each dependent on t alone. It is therefore possible to use four look-up tables, each indexed by t, to construct this vector, after which the only necessary operation is a four-dimensional dot product. T0 (t) = −t3 + 2t2 − t x0 x1 1 T1 (t) = 3t3 − 5t2 + 2 x= · (A.10) x 2 T2 (t) = −3t3 + 4t2 + t 2 T3 (t) = t3 − t2 x3 Furthermore, observe the following results: T0 (1 − t) = −(1 − t)3 + 2(1 − t)2 − (1 − t) = t3 − t2 = T3 (t) (A.11) T1 (1 − t) = 3(1 − t)3 − 5(1 − t)2 + 2 = −3t3 + 4t2 + t = T2 (t) (A.12) Only two look-up tables are required. Equation A.10 becomes, x= 1 2 T0 (t) T1 (t) T1 (1 − t) T0 (1 − t) and this is the formula used by libxmas. · x0 x1 x2 x3 (A.13) Appendix B Some Example XML B.1 volenv.xml This is the test for the VolumeEnvelope module. A <volenv> element contains a subject and a list of nodes. The first node is assumed to be at time zero. This example plays a sample of a harpsichord, applying an envelope (the inner one) that begins at full volume, fades to silence, immediately fades to five times the full volume, and then fades down to twice the full volume before sticking there (end of envelope). The output does not terminate since the final node is nonzero. A second envelope, much like the right-hand example pictured in Figure 3.5, is applied to the result. The output can be heard on the enclosed Demo CD. <?xml version=’1.0’?> <volenv> <subject> <volenv> <subject> <sample filename="harpsi.wav" /> </subject> <node value="1" /> <node time="0.15" value="0" /> <node time="0.4" value="5" /> <node time="0.7" value="2" /> </volenv> </subject> <node value="1" loopstart="" /> <node time="0.025" value="0" /> <node time="0.035" value="0" /> <node time="0.06" value="1" loopend="" /> </volenv> 47 48 B.2 APPENDIX B. SOME EXAMPLE XML compute.xml This is the test for the VariableComputeBlock module. The same harpsichord sample is used, but this time the note it was recorded at is specified. In the test, notes with numbers ranging from 48 to 72 are generated in quick succession. For the musicians, this constitutes a chromatic scale covering the octaves below and above middle C (60). The compute block assigns a value to velocity that starts at (72−48)∗5+7 = 127 and decreases linearly to (72 − 72) ∗ 5 + 7 = 7. The result, a scale that starts loud and fades out, can be heard on the Demo CD. <?xml version=’1.0’?> <compute> <variable name="velocity" value="(72-note)*5+7" /> <subject> <sample filename="harpsi.wav" note="A4" /> </subject> </compute> B.3 clarinet.xmi This is an instrument definition for a clarinet. Three samples are used for different note ranges, and a GeneralMultiplexer (<multiplexer> chooses between them. The note at which each sample was recorded at is given. The samples are not quite at the right pitch; the frequency is overridden to correct this. The samples are set to loop, and the loop start point is given. The loop end point defaults to the end of the sample. clarinetl.wav is enclosed in a volume envelope with a constant amplification of 70%. It sounded too loud against the other samples, so I added the envelope to compensate. Around the multiplexer, there is a VariableComputeBlock. Its purpose is to reduce the note velocity for high notes and increase it for low notes. This was judged necessary aurally, but a scientific explanation would be that higher frequency waves transmit greater power. The use of the velocity variable is a hack; we want to adjust the volume, and SamplePlayers simply interpret the note velocity as a variable. The outermost volume envelope simply applies a rapid, pseudo-exponential fade-out when the note is stopped. <?xml version="1.0"?> <volenv> <subject> <compute> B.4. PIZZ.XMI 49 <variable name="velocity" value="velocity*2^((60-note)/24)" /> <subject> <multiplexer> <generator> <subject><volenv> <subject><sample filename="clrinetl.wav" note="C5" frequency="44127.51" loop="on" loopstart="6044" /></subject> <node value="0.7" /></volenv></subject> </generator> <generator> <range variable="note" low="63" /> <subject><sample filename="clrinetm.wav" note="G5" frequency="44127.49" loop="on" loopstart="4160" /></subject> </generator> <generator> <range variable="note" low="75" /> <subject><sample filename="clrineth.wav" note="E6" frequency="44096.84" loop="on" loopstart="4409" /></subject> </generator> </multiplexer> </subject> </compute> </subject> <node value="2" sustainpoint="" /> <node time="0.05" value="1" /> <node time="0.10" value="0.4" /> <node time="0.15" value="0.14" /> <node time="0.20" value="0.04" /> <node time="0.25" value="0" /> </volenv> B.4 pizz.xmi pizz.xmi defines string instruments (the violin family) played pizzicato, where the performer plucks the strings instead of drawing a bow across them. The definition includes another example of a multiplexer, and a volume envelope. Note that this envelope has no sustain point; the pseudo-exponential fade-out happens immediately. Here, a VariableComputeBlock sets the rate variable, which the envelope obeys. The result is a long decay for low notes and a short decay for high notes. Once again, the output is on the Demo CD, this time generated by the MIDI player using a simple scale.mid file that covers the entire range of a piano (88 notes). This is a greater range than the instruments represented can manage! <?xml version="1.0"?> <compute> <variable name="rate" value="2^((note-60)/12)" /> <subject> <volenv> <subject> <multiplexer> <generator> <subject><sample filename="pizzl.wav" note="E4" frequency="11000" loop="on" loopstart="8403" /></subject> </generator> 50 APPENDIX B. SOME EXAMPLE XML <generator> <range variable="note" low="58" /> <subject><sample filename="pizzh.wav" note="E5" frequency="11000" loop="on" loopstart="6372" /></subject> </generator> </multiplexer> </subject> <node value="2" /> <node time="0.10" value="1" /> <node time="0.25" value="0.4" /> <node time="0.45" value="0.18" /> <node time="0.70" value="0.10" /> <node time="1.00" value="0.06" /> <node time="1.35" value="0.03" /> <node time="2.00" value="0" /> </volenv> </subject> </compute> B.5 general.xmi This defines a whole set of instruments, referring to separate files for the individual definitions. It also accounts for percussion as defined by General MIDI (see Section 1.1.1). There are two LookupMultiplexers. The outer one switches on the channel variable: all notes on Channel 10 are rendered using the percussion.xmi definition, which chooses samples according to the note variable. For all other channels, the inner multiplexer uses the program variable to select an instrument definition. <?xml version="1.0"?> <lookup variable="channel"> <generator> <range /> <subject> <lookup variable="program"> <generator> <range /> <generator> <range value="13" /> <generator> <range value="27" /> <generator> <range low="40" high="55" /> <generator> <range value="45" /> <generator> <range value="46" /> <generator> <range value="47" /> <generator> <range value="56" /> <generator> <range value="57" /> <generator> <range value="60" /> <generator> <range low="68" high="69" /> <generator> <range value="70" /> <generator> <range value="71" /> <generator> <range value="73" /> </lookup> </subject> </generator> <generator> <range value="10" /> <subject><external filename="percussion.xmi" </generator> </lookup> B.6 <subject><external <subject><external <subject><external <subject><external <subject><external <subject><external <subject><external <subject><external <subject><external <subject><external <subject><external <subject><external <subject><external <subject><external filename="piano.xmi" filename="xylophon.xmi" filename="bass.xmi" filename="strings.xmi" filename="pizz.xmi" filename="harp.xmi" filename="timpani.xmi" filename="trumpet.xmi" filename="trombone.xmi" filename="horn.xmi" filename="oboe.xmi" filename="bassoon.xmi" filename="clarinet.xmi" filename="flute.xmi" /></subject> /></subject> /></subject> /></subject> /></subject> /></subject> /></subject> /></subject> /></subject> /></subject> /></subject> /></subject> /></subject> /></subject> </generator> </generator> </generator> </generator> </generator> </generator> </generator> </generator> </generator> </generator> </generator> </generator> </generator> </generator> /></subject> Example Music Here is jou5cred.xmm, a simple MIDI mapping. By default, looping is off. <?xml version="1.0"?> B.6. EXAMPLE MUSIC 51 <midimapping midifilename="jou5cred.mid"> <external filename="general.xmi" /> </midimapping> rockspin-piece10.xmm is set to loop. The outer volume envelope serves the purpose of fading the music out once it starts repeating. This was done for demonstration purposes, but it shows how a MIDIMapping is no different from any other module! <?xml version=’1.0’?> <volenv> <subject> <midimapping midifilename="rockspin-piece10.mid" loop="on"> <external filename="general.xmi" /> </midimapping> </subject> <node value="1" /> <node time="170" value="1" /> <node time="180" value="0" /> </volenv> Both these pieces may be heard on the Demo CD. Appendix C Demo CD Track Listing All tracks were generated using cubic interpolation in the resampler unless otherwise stated. 1. The output from the sample player test, playing harpsi.wav. The sample was recorded at 22050 Hz and the output is at 44100 Hz, so resampling is taking place. 2. The output from the volume envelope test described in Section B.1. 3. The result of the variable compute block test presented in Section B.2. 4. This track first shows the outcome of using a single piano sample for the whole range of the instrument, illustrating the need for multiple samples. Next, recordings of twelve notes spanning the entire range of the instrument are all adjusted to Middle C and played in sequence, showing how different they are. 5. This track contains a scale covering every note on the piano. The astute listener will hear each change of sample, confirming that the multiplexer is at work. It is hoped that a casual listener can ignore the changes, especially in real music where they are usually less noticeable. 6. The same scale is played using the strings pizzicato definition from Section B.4. Note how the decay rate varies with the pitch. Some aliasing can be heard on the high notes, but such high notes are rare. 7. The scale from the last track is played again with linear interpolation. Some unwanted high frequencies can be heard on some notes, but it is subtle. 52 53 8. The scale is played with the non-interpolating resampler. The difference is very noticeable, particularly on low notes. Sometimes this effect is desired, and XMAS does indeed allow a musician to request it for a specific sample! 9. jou5cred.xmm, the first example of a complete piece of music (Section B.6). The underlying .mid file was my contribution to a game called Jou 5, which sadly the author has no further interest in distributing. 10. rockspin-piece10.xmm, the second example. The music comes from the final three levels of my game Rock ‘n’ Spin [2]. It loops, and Section B.6 shows how even the fade-out was able to be done by libxmas. c 2004 Ben Davis. The material on the Demo CD is Copyright ° Appendix D Project Proposal Computer Science Tripos Part II Project Proposal XMAS: an open MIDI and sample-based music system B. N. Davis, Robinson College Originator: B. N. Davis 22 October 2003 Special Resources Required My own computer (if it breaks down I can use the computer room) Project Supervisor: N. E. Johnson Director of Studies: Dr A. Mycroft Project Overseers: Dr I. Pratt & Dr G. Winskel 54 55 Introduction The MIDI protocol is very useful in the production of music. Devices may use it to communicate performance events (such as when a note is pressed or released) between each other. It is an industry standard with widespread software and hardware support. Unfortunately, it has been misused. Most software-based music editors can dump MIDI data to standard .mid files. These files store the aforementioned performance events, but not much else. MIDI module manufacturers have collaborated to implement a scheme called General MIDI, which specifies a standard instrument mapping (so a piano will be a piano everywhere), but synthesisers still vary wildly and a piece that sounds great on one device is likely to sound unbalanced on another (for example the string section may be too loud). This poses a problem for their distribution. The Amiga gave birth to ‘music modules’, which are files capable of storing samples in addition to the sequence data. The PC has expanded them beyond the Amiga’s limitations, and there are now several editors (‘trackers’) and players of varying quality. While not properly standardised, modules can be trusted to sound correct on any system if you are careful which software you use. However, they are limited, and the trackers are not very user-friendly. Nowadays, music can be distributed using lossy compression. This is satisfactory in many situations, but not all; dial-up Internet users have to wait a long time to download them, which is especially a problem if for example a game developer wants to offer a product for download and include one music track for each level. There are also people who can hear the degradation that results from the lossy compression. This project will produce a solution that has the advantages of both MIDI and Amiga modules without necessarily the large size or quality loss of generalpurpose streamed audio. Lossy compression may be used if small files are required, otherwise lossless compression may be used if sound quality is paramount. That said, forms of compression will be considered extensions to this project, and I will not mention them again until the ‘Extensions’ section. Description A musician may produce one or more sequence files (.mid for the purposes of this project) using any existing software and hardware, and produce or obtain a set of samples (.wav for this project). Instruments may be specified in .xmi (XML Instrument) files; these are a layer above samples and may for example specify volume envelopes and different samples for different note ranges. Then a .xmm 56 APPENDIX D. PROJECT PROPOSAL (XML Music Mapping) file ties the samples, instruments and sequences together. These two XML formats will be specified by this project. They will both allow author information and other human-readable notes to be embedded. DSP trees are used at various points. These are trees of DSP modules, which are filters capable of generating, modifying or combining PCM data. Modules may have parameters to control them, and a tree will contain expressions for evaluating the parameters; a simple expression parser will be used here, and MIDI’s continuous controls will be accessible as variables. Volume envelopes and the sample and sequence players will be implemented as DSP modules. The sample player will support stereo and offer three different interpolation options: none, linear and cubic. The user chooses a preferred algorithm, but instruments may override this. Where a tree is used to modify sound, it may have a ‘missing leaf’, at which the input will be generated. When it is used to generate sound, it may not. A tree may never have multiple missing leaves. An instrument file specifies a generator DSP tree for each note of the scale. It may also specify modifier DSP trees to apply to note ranges and to all notes. A typical instrument will use a volume envelope at the very least. In a mapping file, one sequence is designated the root. This is the one that will be played. Each MIDI instrument is assigned an XML instrument, or another mapping to use as a sub-sequence; either of these may be a separate file or a nested XML block. Sub-mappings will inherit their parents’ instrument mappings, but these may be overridden. A mapping file may assign a modifier DSP tree to each track or each MIDI channel (but not both since these are two different ways of subdividing the same set), and to the whole. Since samples, instruments, sequences and mappings may be reused, they will be loaded once and reference-counted. It is worth reiterating that all the components that have been described will be treated as DSP modules. There will be a simple command-line playback tool that writes raw PCM data to stdout; this can be piped into ALSA’s aplay command. I will use C++ for this project. Extensions Perhaps the two most important extensions are support for other sample formats, particularly those using lossy compression such as Ogg Vorbis, and support for 57 a ready-to-play archive format to make the music easier to distribute. The .xma (XMM Archive) format will allow for this; note that it is not XML since it needs to store binary data compactly. It will be able to store any combination of samples, sequences and the two XML formats; typically it will be used for a whole piece of music, a shared sample database and files that refer to this database, or a whole album. There will be support for author information and human-readable notes, typically to be used to describe the collection as a whole since there are already human-readable notes for individual pieces and instruments. Lossless compression will be used, ideally with algorithms optimised for the various types of data. The name of the project comes from the ultimate ideal of being able to distribute music as .xma files (a nice take on .wma files). ‘XMAS’ is short for ‘XMA System’. Other possible extensions include extra DSP modules (such as filters, distortion and echo), click removal for when samples start, stop and loop (not for clicks in the actual sample data), support for surround sound, a GUI for editing and testing instrument and mapping files, a stand-alone player, and XMMS and Winamp plug-ins. Finally, the product could be developed into a complete music authoring environment, with facilities for recording and editing sample and MIDI data in the GUI, but this could become a whole project in itself. Work that has to be done The core implementation work breaks down into the following sections:1. Specify DSP module interface. Implement reference-counted .wav loader and sample player. 2. Implement volume envelope module. Specify .xmi format. Implement reference-counted loader. Create a .xmi file for testing. 3. Implement reference-counted .mid loader. Specify .xmm format. Implement reference-counted .xmm loader. Create a .xmm file for testing. 4. Implement MIDI sequence player and .xmm player. 5. Implement command-line player. Create a more involved piece of music for testing and, later, demonstration. 58 APPENDIX D. PROJECT PROPOSAL Note that I have already written plenty of pieces of music that can be exported to .mid, so I will work with these. Creating example music will not use up a disproportionate amount of time. Each of these work packages takes the project to a new level of complexity. First it will be able to play .wav files. Then it will support instruments, then, two work packages later, whole pieces of music. I will be able to perform tests at the end of each work package and thus fix most of the bugs as I go along. Success Criteria By the end of the project I will have a piece of music in .xmm (or .xma) format that takes advantage of multi-sample instruments and volume envelopes. The software will be able to play this music reliably and accurately. The specifications for the .xmm and .xmi file formats will indicate what constitutes accurate playback. If the above paragraph is true, the project will be considered a success. At the time of the progress report, I expect to have a program that plays a simple hard-coded tune using a .xmi file loaded from disk, in addition to textual output to verify the integrity of loaded .xmm and .mid files. Difficulties to Overcome The following main tasks will have to be undertaken before the project can be started: • Learn about XML, select a library for XML parsing, and familiarise myself with the library. The library must be able to read XML from an arbitrary stream. • Find a suitable mathematical expression parser. • Secure documentation on the .wav and .mid formats, and on the MIDI protocol itself. Starting Point I have worked with MIDI before, and am reasonably familiar with its main features. I have written a player for Amiga module-based formats, which incorporates a sample player with cubic interpolation; its code will serve as a reference. The player, including source code, is available at http://dumb.sf.net/. 59 Resources All development work will be carried out on a Linux PC equipped with a standard PCM sound interface. I will be using my machine primarily, but if it breaks down, I will bring headphones and use the machines in the William Gates Building. I will be using CVS to manage my source code, and the repository will be archived and uploaded nightly to Pelican (one old copy will be kept each time). Work Plan All dates listed here are Fridays. 24 October 2003 – 7 November 2003 (two weeks) Preliminary work. Do the tasks listed in ‘Difficulties to Overcome’. In addition, set up a CVS server on my system and a script for making regular back-ups of the repository. Lay out the project and set up a Makefile system. 7 November 2003 – 28 November 2003 (three weeks) Do the first work package listed in ‘Work that has to be done’. The project should be able to play samples. 28 November 2003 – 19 December 2003 (three weeks) Do the second work package. This is the .xmi support. Hard-code a test that loads an instrument and plays several notes in sequence. 19 December 2003 – 9 January 2004 (three weeks) Do the third work package. This is support for loading .mid and .xmm files, but not for playing them. Generate text output for the purposes of verifying loaded data structures. 9 January 2004 – 30 January 2004 (three weeks) Do the progress report and prepare for the presentation. Include test results so far. 60 APPENDIX D. PROJECT PROPOSAL 30 January 2004 – 27 February 2004 (four weeks) Do the fourth work package. This is the music playback code and is liable to take longer. Test aurally. 27 February 2004 – 19 March 2004 (three weeks) Do the fifth and final work package. This is the command-line player, the music for demonstration, and final bug-fixes. 19 March 2004 – 14 May 2004 (eight weeks) This time will be spent on the dissertation. I will work on some extensions if I finish the dissertation early. 14 May is the final deadline.
© Copyright 2024