95% found this document useful (21 votes)
7K views916 pages

Audio Programming

Audio Programming

Uploaded by

hhakim32
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
95% found this document useful (21 votes)
7K views916 pages

Audio Programming

Audio Programming

Uploaded by

hhakim32
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 916

THE AUDIO

PROGRAMMING BOOK
EDITED BY

RICHARD BOULANGER AND VICTOR LAZZARINI

FOREWORD BY

MAX MATHEWS

nclude
<stdio.h> #include <stdio.h> define SAMPLING_R
#include <stdio.h> #include <stdio.h> define SAMPLING_RATE 44100 #define NUM_SECONDS 3 #define NUM_SAMPLES

NUM_SECONDS * SAMPLING_RATE)

e <stdio.h>
#include <stdio.h>
SECONDS * SAMPLING_RATE)

define SAMPLING_RATE 44100

#define NUM

The Audio Programming Book

The Audio Programming Book

edited by Richard Boulanger and Victor Lazzarini

foreword by Max V. Mathews

The MIT Press


Cambridge, Massachusetts
London, England

( 2011 Massachusetts Institute of Technology


All rights reserved. No part of this book may be reproduced in any form by any electronic or mechanical
means (including photocopying, recording, or information storage and retrieval) without permission in
writing from the publisher.
For information about quantity discounts, email [email protected].
Set in Stone Serif and Stone Sans on 3B2 by Asco Typesetters, Hong Kong. Printed and bound in the
United States of America.
Library of Congress Cataloging-in-Publication Data
The audio programming book / edited by Richard Boulanger and Victor Lazzarini; foreword by Max
Mathews.
p. cm.
Includes bibliographical references and index.
ISBN 978-0-262-01446-5 (hardcover : alk. paper) 1. Computer sound processing. 2. Signal processing
Digital techniques. 3. MusicComputer programs. I. Boulanger, Richard Charles, 1956. II. Lazzarini,
Victor, 1969.
ML74.3.A93 2011
006.5dc22
10 9 8 7 6 5 4 3 2 1

2010001731

This book is humbly dedicated to our teachers, mentors, and friends Max V. Mathews,
F. Richard Moore, and Barry Vercoe. They paved the way and they showed the way. They
were great explorers who freely and passionately shared their discoveries, their treasures
their source code. Moreover, they invited every one of us to join them on the journey, to follow their example, and to nd ways to become a part of their great adventure and to build
upon the solid foundation that they laid down for us. All the contributors to this book stand
on the shoulders of these three giants of computer music. It is our hope that the book will
help the next generation to fully appreciate the great gifts of Mathews (MUSIC V), Moore
(cmusic), and Vercoe (Csound), and that it will help them nd their own unique and inspiring way to take the world one step further on this extraordinary audio adventure.

Contents

Foreword by Max V. Mathews


Preface xiii
Acknowledgments xv
Introduction xxi

xi

C Programming Basics
0 An Overview of the C Language with Some Elements of CBB
Victor Lazzarini and Richard Boulanger
1 Programming in C
Richard Dobson

55

Audio Programming Basics


2 Audio Programming in C
Richard Dobson

185

3 Working with Audio Streams


Gabriel Maldonado

329

4 Introduction to Program Design


John ftch

383

Audio Programming Essentials


5 Introduction to Digital Audio Signals
Victor Lazzarini
6 Time-Domain Audio Programming
Victor Lazzarini

431
463

viii

Contents

Spectral Audio Programming


7 Spectral Audio Programming Basics: The DFT, the FFT, and Convolution
Victor Lazzarini
8 The STFT and Spectral Processing
Victor Lazzarini

539

9 Programming the Phase Vocoder


Victor Lazzarini

557

Programming Csound Opcodes


10 Understanding an Opcode in Csound
John ftch
11 Spectral Opcodes
Victor Lazzarini

581

617

Algorithmic Synthesis and Music Programming


12 A Modular Synthesizer Simulation Program
Eric Lyon
13 Using C to Generate Scores
John ftch

629

655

14 Modeling Orchestral Composition


Steven Yi

677

Appendixes
A Command-Line Tools Reference
Jonathan Bailey

697

B Debugging Software with the GNU Debugger


Jonathan Bailey
C Soundles, Soundle Formats, and libsndle
Victor Lazzarini

719
739

D An Introduction to Real-Time Audio IO with PortAudio


Victor Lazzarini
E MIDI Programming with PortMIDI
Victor Lazzarini

783

771

521

ix

Contents

F Computer Architecture, Structures, and Languages


John ftch

797

G Glossary 823
John ftch with Richard Dobson, Victor Lazzarini, and Richard Boulanger
H An Audio Programmers Guide to Mathematical Expressions
John ftch
Contents of the DVD
References 873
About the Authors
Index 881

869

879

855

Foreword

This is not just a book; it is an encyclopedia of mathematical and programming techniques


for audio signal processing. It is an encyclopedia focused on the future, but built upon the
massive foundations of past mathematical, signal processing, and programming sciences.
It is clearly written and easy to understand, by both human readers and computers. It
gives complete information, from the basic mathematics to the detailed programs needed
to make sound. It is the essential library, not only for computer musicians, but also for all
computer scientists, including those who work in the elds of communication and articial
intelligence.
Today the dominant language in which to write programs is C (including C). A halfcentury ago, sound synthesis programs for music were written in assembly language. The
resulting music had few voices and uninteresting timbres. Programs were tedious to write.
Block diagram compilers, including MUSIC V, cmusic, and Csound, greatly expanded the
musical possibilities by giving composers and sound designers tools with which to create
their own timbres from blocks of codeoscillators, envelopes, lters, mixers, etc. These
blocks performed tasks that electronic musicians were familiar with and that they could
understand. Block diagram compilers were a great step forward, but they imposed limits on
what the computer was allowed to do, in many ways because of their limited library of audio
modules or opcodes.
These limits have now been swept away. This book makes it practical to write a new C program for each new piece of music. The composition is the C program. This is the great step
forward.
A half-century ago, computer sound processing was limited by the speed and expense of
existing hardware. Today those limits are gone. Affordable laptop computers are from
10,000 to 100,000 times as powerful as the roomful of equipment in a typical 1960s computer center. And this book sweeps away the programming limits and makes practical musical use of the great power of laptops.
Early computers could not be used to perform a piece of music in real timethey took
many seconds to compute a single second of sound. But today real-time performance is possible, and practical real-time programming is a big part of this book. Thus, laptops can join

xii

Foreword

with chamber groups and orchestras and thereby add rich new timbres to the already beautiful timbres of acoustic instruments.
What now is the musical challenge of the future? I believe it is our understanding of the
power and limitations of the human brain, and specically discovering which sound waves,
sound patterns, timbres, and sequences humans recognize as beautiful and meaningful
musicand why. This book holds the key to copiously producing the software, sounds, and
music we need to truly and deeply explore these many and hidden dimensions of our
musical minds.
Max V. Mathews

Preface

But how does an oscillator really work? My 40-year journey to The Audio Programming Book
began with that question. Some of the answers came from Tom Piggott (my rst electronic
music teacher, and the one who got me started with analog synthesizersan EML200 and
an Arp2600).
More answers came from Alan R. Pearlman, founder and president of the ARP Synthesizer
Company, the man who commissioned my rst symphony, Three Soundscapes for Synthesizers
and Orchestra.
Still more answers came from Dexter Morrill, who offered me a Visiting Composers Residency at Colgate University, where I made my rst computer music. It took days back then,
but I rendered Happy Birthday in MUSIC 10 and played the rendered soundle for my dad
over the phone on his birthday.
And more answers came from Bruce Pennycook, who was also in residence at Colgate. (We
would work through the night and end our sessions with breakfast at a local diner; I picked
his brain every spare minute; he taught me how to do stereo panning and gave me his subbass oscillator instrument, LOW.)
I started to really answer the question in Barry Vercoes summer workshop at the Massachusetts Institute of Technology, in which I learned music11. (I will never forget Barry lling
whiteboard after whiteboard, and revealing, one morning, that an oscillator consisted of a
phasor and a table.)
I made my way from MIT to the Center for Music Experiment at the University of California at San Diego, where I learned about cmusic from Dick Moore, Gareth Loy, and Mark
Dolson. My rst cmusic composition, Two Movements in C, featured a new trick that they
taught me to do with two oscillators: FM synthesis.
Life brought me back to Boston, and Barry invited me to continue my work at MITs new
Media Lab, where I got to explore and beta-test his new language, Csound. By his side, I was
able to further my understanding and to share some of the answers I had found along the
way through The Csound Book.
Overlapping with my time at the Computer Audio Research Lab in San Diego and the MIT
Media Lab, I got to know and work with Max V. Mathews. He invited me to work in his

xiv

Preface

studio at Bell Labs. (He would sleep in the recording booth there so that I could compose and
program.) We have worked together for more than 25 years now, touring, performing, lecturing, and sometimes sailing. It was from him that I learned the programming language C. We
would spend days and days going over every single line of his Conductor and Improv programs, his Scanned Synthesis program, his PhaserFilters program, and his MUSIC V program.
(Imagine my surprise upon discovering that an oscillator is also an envelope generator, and
then the mind-bending fact that if you scan a mechanically modeled wavetable, an
oscillator can be a lter.)
But still, it took John ftch, Richard Dobson, Gabriel Maldonado, and Victor Lazzarini to
teach me to actually program an oscillatorright here in The Audio Programming Book.
Yes, for me The Audio Programming Book answers my rst question and many others. I
think you will agree that ftch, Dobson, Maldonado, and Lazzarini are wonderful teachers,
and that the other contributors are their young apprentices. I hope the book will answer
all your questions and will raise new ones too.
Richard Boulanger

Acknowledgments

This book has had a long period of gestation, and while we have been at it the world has
changed; some things have disappeared, and some new things have been invented. But the
basic elements of what we wanted to teach are still as relevant today as they were when we
started. Along the way, we have also been helped and supported by many others, and
we would like to give our thanks to them here.
from Victor Lazzarini
I would like to acknowledge the support of my parents. Their support throughout my education years and beyond, both affective and nancial, was essential to my developing career
and life. Even though they might at some point have doubted the wisdom of embarking on
a music degree instead of medical school, they have never been anything but supportive.
I would also like to give thanks to the An Foras Feasa research institute in Ireland, which
provided the funds for my sabbatical at a time when I was heavily involved in nishing this
book. In particular, I would like to say thanks to its director, Margaret Kelleher, and to John
Keating, whose ofce I was able to use in that period. At the NUI Maynooth Music Department, I would like to thank all my colleagues for their friendship and support, Fiona Palmer
for allowing my research leave at a busy time for the department, and Barra Boydell for his
advice, guidance, and example in all things academic. I would also want to acknowledge the
help and support of my research colleagues Joe Timoney and Tom Lysaght, with whom I
have worked for many years and who taught me a lot about signal processing.
My involvement in this book would not have happened had I not made a decision one
morning in 2005 to join the Csound users discussion list. A Csounder since 1993 and an
audio developer since 1996, I had always been a retiring sort of person who was not involved
in the community (although I had contributed to Csound privately by sending things to
John ftch). But that morning, for some reason, I decided to join the list. and this whole new
world suddenly appeared, almost by extension. So I would like to extend my gratitude to
fellow Csounders for the years of interesting conversation and bug reports. Of these new
friends, I would in particular like to acknowledge my new friends Steven Yi and Oeyvind

xvi

Acknowledgments

Brandtsegg. I would also want to thank John ftch and Richard Dobson, who have been
great mentors to me on the art of programming and making noise with it. Last, but not least,
I need to thank my colleague and co-editor Dr. B. for providing the opportunity rst to
contribute to this book and then to co-edit it. His support and guidance have contributed
substantially to my development as a researcher and a composer.
Another group of people I would like to acknowledge and thank is the Linux Audio Developers family, like-minded individuals who are in the business of teaching, learning, and
sharing computer music code as members of the great brotherhood of free and open-source
software. We would not be where we are now in our eld without the efforts of countless
people contributing to FOSS projects, in many guisesa practice that dates back to the
beginnings of computer music.
Finally, I have to thank my three kids, Danny, Ellie, and Chris, for their love and for keeping me on rm ground at times when my mind was about to take off. Another special dedication goes to my wife Alice, who has put up with my work-related bad moods and my
computer-bound disappearances. Her love and support have helped me through a lot of
bad weather, for which I am very grateful.
from Richard Boulanger
Over the years, there have been many brilliant and dedicated students who have helped me
work on, work out, and work through the chapters, the code, and the ideas of the contributing authors. Not only have they worked on this project with me, but they have also been
a huge part of my compositions, my concerts, my lectures, and my research. Their spirit is
in the pages of this book and is channeled through all of my creative work. To date, the
gifted and dedicated students who have served as my musical assistants and collaborators
have included Paris Smaragdis, Eric Singer, Jacob Joaquin, Young Choi, Luigi Castelli, Greg
Thompson, and Kathryn Bigbee. I am eternally grateful to them.
A number of my students wrote chapters for this book. I want to recognize and thank
them for their contributions to my classes and my teaching. When you read and study their
contributions here, I am sure that you too will recognize the dedication, commitment, and
brilliance of Joo Won Park, Barry Threw, Tae Min Cho, Andrew Beck, Federico Saldarini, and
Johannes Bochmann, and you will know how lucky I have been to work with them.
A number of undergraduate and graduate students read specic chapters and offered feedback that helped to focus the authors and signicantly improve the nal content of this
book and the associated DVD. The student reviewers from the Berklee College of Music
were Bobby Pietrusko, David Buchs, Jason McClinsey, Victor Magro, Matthew Thies,
Gautam, Paul Marinaro, Danny Patterson, Brian Baughn, Nicolas Villa, Tom Owen, and
Sean Meagher, those from New York University were Won Lee and Nicholas Barba, the
reviewer from McGill University was Huey How, and the reviewers from the MIT Media Lab

xvii

Acknowledgments

were Noah Vawter, Victor Adan, and Adam Boulanger. Thanks to all of you for making the
time and making a difference.
From the very beginning, I had one student who not only worked through all the chapters
but also found the time to put his talent for graphic design to work; he contributed most
of the gures that appear in this book. His name is Robert Gerard Pietrusko. This talented
musician, engineer, programmer, and designer was able to translate some murky code and
concepts into transcendent and illuminating illustrations.
As the manuscript approached the nish line, two students offered to help me: Jonathan
Bailey and Tim Lukens. The three of us put together a solid Audio Programming class at
Berklee, and they helped me to write strong tutorial chapters to support that class. In addition to making the time to write some very musical code, both of these guys found the time
to write signicant and important chapters of their own for the bookJonathan, in fact,
wrote two. There is no possible way that I can thank Jonathan Bailey and Tim Lukens
enough for all the work that they did to make this book happen, and to make our chapters
what they are. Their musical code is at the very core of my writing and my teaching, and
for that I am most grateful to them both.
Clearly, my students have been a profound and deep source of inspiration to me, but at
Berklee the real support for my research and my development has come from my department chairmen, David Mash and Kurt Biederwolf, and from my deans, Don Puluse and
Stephen Croes. For more than 22 years they have supported my travel, my studies, my
courses, and my compositions; moreover, they have brought my collaborators to the college
to work with my students and to work with me. They continue to support my growth and
development, and I will be forever grateful to them for this wonderful gift.
A number of colleagues read and reviewed the preliminary chapters and code for this book.
Their insights helped to focus this book on the target audience. I would like to thank Javier
A. Garavaglia, Michael Rhoades, Luis Jure, Sal G. Soa, Josep M. Comajuncosas, Mark Ballora,
David L. Phillips, and Steven Yi.
Of all the comments and the constructive criticism that I received over the years, perhaps
the most important wake-up call came from my closest colleague, collaborator, and friend,
Lee Ray. About two years ago, I asked Lee to check out the nal draft. He read every
pageat the time, all 3,000. And after two months, Lee probably made one of the most difcult phone calls of his life. He let me know everything that was broken, missing, dull,
out of place, out of date, redundant, inferior, and in some cases inappropriate. Time had
clouded my judgment and my ability to see what was there and, more important, what I
was missing. Lees insights opened my eyes again and helped to get me back on the right
track.
In the beginning, the number of contributing authors was slightly larger than the nal
group. They began this journey with me, but for one reason or another some contributions
do not appear in the book. Still, I want to thank the contributors for helping to move the

xviii

Acknowledgments

project forward and for reading, reviewing, suggesting, and inspiring. On behalf of all the
contributing authors I would like to thank Michael Gogins, Javier Garavaglia, Micha Thies,
IOhannes m zmoelnig, Craig Stuart Sapp, Nick Didkovsky, and Matt Ingalls. These are the
most productive, prolic, and inspiring developers on the computer music scene today, and
I am grateful for their help.
Clearly, I am grateful to all my contributing authors for their brilliant contributions, but
there are four contributing authors who have been working with me on this project from
the very beginning, almost 10 years ago. Over this period, computer technologies, compilers,
processors, and platforms changed dramatically, and most times the amazing technological breakthroughs broke everything that we had built (and trashed everything I had tested
and edited). There were so many times that we had to set out in an entirely new direction
that any normal programmer or professor would have just given up. Still, when everything
was broken, these four were determined enough and committed enough to pick up the
pieces, rewrite their code, and start all over. Time and time again, they forged ahead. I
learned a great deal from each of them, and I stand in awe of all the creative possibilities
they have manifest through the software that they have written. A small glimpse of that
dynasty is now a part of this book. Richard Dobson, Gabriel Maldonado, John ftch, and Eric
Lyon: on behalf of all of your future and current students (and I consider myself to be one
among them), I thank you all for everything you have taught me over these past 10 years.
One other contributor deserves special mention: Victor Lazzarini. Victor came along when
we were well into the project, and yet, with his code and his words, he raised the book up to
a new level of relevance and excellence. Where there was a hole, he would ll it. Where there
was a missing piece, he would nd it (in his brilliant mind) and then write it up (and code it
up) in what seemed like no time at all. And when all his chapters were written, and all his
code was fully tested and running on all platforms, he offered to help edit and test the
mountain of words and code. Before I knew it, I had a brilliant, passionate, and dedicated
partner on this project. Together we would tag team around the world and around the
clock, and we managed to get two years work done in a matter of months.
I asked Doug Sery at the MIT Press if I could ofcially offer Victor the title of co-editor. (He
was already doing the job.) I was prepared to argue that he had become indispensable, that
he had spent six months making one signicant contribution after another, and that he
brought an essential and substantive level of expertise to the project (as was evident from
the content of his chapters and his code). But I didnt need to present my case at all. Doug
had noticed the difference in the quality and content of the product. He had noticed the
accelerated rate of production too. In fact, in less than a minute, Doug gave me the thumbs
up. Because of Victors expert editorial contributions, this book is now in your hands. I am
honored and proud to have him as a major contributing author and as my co-editor.
I cant end these acknowledgments without thanking my parents, Dick and Claire
Boulanger. Its sad because I work hard to impress them, but when they look at any single
page of this book they both shriek with horror and wish I would just slow down and

xix

Acknowledgments

smell the roses and spend some time enjoying life with them by the pool. Oh well. Even if
there isnt a single page in here that makes one bit of sense to you, know that its all for you
and because of you, mom and dadwith respect, gratitude, and love. And now that its
done, there might be a bit more pool time.
And I cant end these acknowledgements without apologizing to my three wonderful sons,
Adam, Philip, and Adrien. I hope you can forgive me for my absence over the past 10 years,
and for the all-too-frequent short-fuse verbal explosions when I was struggling at the computer to nd a word and you would have liked me to listen, for a minute or two, to some of
your words.
When I started this book, Adam was graduating from high school; now he is married and
graduating from MIT with a Ph.D. Where did that time go? Philip is playing classical cello
professionally in Chicago and pursuing his second masters degree. When I started this project, he was practicing to get a decent chair in the Massachusetts Southeast District Orchestra.
Adrien was in elementary school (second grade, I think), and we were both obsessed with
Pokemon. As the text was delivered to the MIT Press, Adrien was starting college.
I hope Adrien will use this book in one of his CS classes someday. I hope Adam (Dr. B. v2.0)
will teach one of his classes from this book someday, or his daughter Anas. And I hope Philip and I can get back to performing and touring and making new music togetherworking
on our Book of Dreams.
This book is about making new music by discovering new ways of transforming and
transguring sounds. I hope it will open a door to tomorrows concert hall and online classroom for my sons and for you, your students, and your children.
Finally, I offer my deepest and most heartfelt thanks to my wife Colette. This beautiful,
caring, and loving cardiac-rehab nurse put all the pieces of this quite literally broken and
broken-hearted man back together. I had a heart attack along the way, and Colette brought
me back to life and back to this project. Without her love, her understanding, and her sacrice, neither the book nor I would have seen the light of this day.
But in the end, it was the faith, support, and encouragement of Doug Sery that kept this
project alive, in me and at the MIT Press. Everyone interested in audio programming truly
has him to thank for this fantastic bookand we sincerely do thank you, Doug.

Introduction

In chapter 0, we provide a complete overview of the C language and some essential elements
of C. Using examples written from a musicians perspective, we guide the reader through
the elements of these two languages that will be used in the audio programming chapters of
this book.
In chapter 1, Richard Dobson introduces readers with no prior experience to the basics of
C programming. All the examples employ a music-oriented focus, from MIDI-frequency calculations to the textual breakpoint le and a tuning-fork emulation including the creation of
raw binary soundles. It also introduces primary tools such as Audacity and Gnuplot.
In chapter 2, Richard Dobson introduces the specially written PortSF soundle library. This
is used to develop a number of useful programs designed to demonstrate the generation and
processing of basic envelopes and waveforms (including examples of additive synthesis). In
the course of introducing further important C language elements, it explores the development of the table-lookup oscillator and other basic tools.
In chapter 3, Gabriel Maldonado shows us how to send audio data to the operating systems standard output, to a raw binary le, to a RIFF-Wave le, and directly to the audio interface in real time by using different libraries: the Tiny Audio Library, PortSF, and PortAudio.
In chapter 4, John ftch introduces important software engineering principles such as
object-oriented design. These are articulated through the construction of a simple wave-table
playback program. The description includes debugging the resulting program and some additional notes on the thought processes of program construction.
In chapter 5, Victor Lazzarini introduces more formal aspects of digital signal processing
theory, including some basic mathematical concepts. Programming examples demonstrate
some essential operations on audio signals. The chapter concludes with a look into the
implementation of FM synthesis.
In chapter 6, Lazzarini explores several basic elements of waveform processing, including
synthesis and the transformation of audio signals. These processes are presented as components implemented as C functions, exposing the details of their operation. The chapter
concludes with an introduction to plug-in development, with the VST architecture used as
an example.

xxii

Introduction

In chapter 7, Lazzarini introduces the theory and implementation of basic operations


in the spectral domain. The chapter takes an intuitive approach, but it still covers the very
important mathematical descriptions of each operation. It concludes with a discussion of a
fully functional convolution program.
In chapter 8, Lazzarini goes a step beyond chapter 7 by looking into time-varying spectral
analysis and resynthesis. Here he discusses the implementation of a basic Short-Time Fourier
Transform operation and a few examples of basic manipulation in the frequency domain.
Chapter 9, also by Lazzarini, complements the study of spectral processing (featured in
chapters 7 and 8) by introducing a basic tool for the job: the phase vocoder. It also demonstrates how the phase vocoder can be used in time stretching, ltering, and pitch scaling. It
concludes by looking at an alternative method of frequency estimation and its use in additive synthesis.
In chapter 10, ftch studies and analyzes a number of simple Csound opcodes to reveal the
mechanisms by which they are integrated into the overall system. This leads to some practical advice concerning the creation of opcodes.
Chapter 11, by Lazzarini, complements the extensive exploration of time-domain Csound
opcodes in chapter 10 with a short look at the processing of spectral audio. Two examples are
provided, showing the separate manipulation of amplitude and frequency aspects of input
signals.
In chapter 12, Eric Lyon develops a specication language based on the model of patchable analog synthesizers. Patch descriptions written in this language are transformed into
working Csound code.
In chapter 13, ftch presents a case study of an algorithmic composition (actually an algorithmic remixer) to show how C can be used to generate Csound scores and to perform
simple analytic calculations. He demonstrates that small programs are not difcult to write,
and that, despite all the teaching of advanced software engineering, there are times when a
little program can prove very powerful, musical, and beautiful.
In chapter 14, Steven Yi offers observations about the orchestra as a performance ensemble
and surveys some of the techniques composers have used in writing for it. Yi derives a model
for music making from these observations, and he presents a C class library implementing this model, and a program demonstrating how the library can be used.
Appendix Awritten for those new to UNIX-compatible environmentscovers basic
techniques for navigating a Bash command shell, for building code using the GNU compilation tools gcc and make, and for editing les with the emacs text editor.
Appendix B is a debugging tutorial for beginning developers. It covers the basics of using
gdb to debug audio programs. Techniques for preparing your programs for debugging, for
slowing down and stepping through code line by line, and for viewing application state
and memory are reviewed.
Appendix C, an overview of soundle formats from the raw to the self-describing, focuses
on the use of libsndle.

xxiii

Introduction

Appendix D covers the basics of real-time audio programming using the PortAudio library.
It demonstrates the two modes of operation supported by the library, blocking and asynchronous, with comparative programming examples.
Appendix E introduces MIDI programming using the PortMIDI library. It features three
examples covering output, input, and the combination of these two.
Appendix F is a short introduction to how computers work and to the functioning of their
main components. Further, this appendix shows how computer languages are compiled into
a form that can be executed.
Appendix G is a glossary that provides support for the entire book. It includes entries that
range from simple denitions, to more extended ones, with some tutorial elements. This
appendix offers a concise introduction to a range of words and topics that readers may
encounter in this book or in other resources. The readers are presumed to have music as their
primary eld of knowledge; accordingly, the glossary concentrates on subjects relating to
programming, audio engineering, and computer architectures. Computing has created a
very large body of jargon words. This appendix explains those you are most likely to meet
as a computer musician.
Appendix H presents and explains the most important mathematical concepts for the
audio programmer and computer musician, and other mathematical concepts that audio
programmers and computer musicians might encounter.

The Audio Programming Book

C Programming Basics

An Overview of the C Language with Some Elements of CBB

Victor Lazzarini and Richard Boulanger

The programming languages C and C are used throughout this book, and yet the details
of the languages are often secondary to the more important issues of audio programming.
After all, this is The Audio Programming Book, and programming audio is the main goal. However, it is appropriate to reserve a little space here at the beginning for a general discussion of
the languages themselves. C is often described as a small language, and therefore we believe
it possible to present, discuss, and review the whole of it in a very compact way.
0.1

C and CBB
Most people get somewhat confused when learning about the programming languages C and
C. Sometimes the two are confused into one; sometimes C is taken to be C and vice
versa. They are, effectively, two separate and different languages, so they should not be confused. In practice, they are very similar and share many elements, but we should be careful
not to mistake one for the other. As a rule of thumb, just think of C as a C with extra
elements. While not the complete picture, this is often all you need to know.
Sometimes C is taught with some elements of C that simplify the coding. While this
might be reasonable for someone who knows what is C and what is C in the code, for
beginners this is very confusing. A secondary issue here is that C allows a more sophisticated programming approach, known as object-oriented programming, which is complex to
tackle for the novice. In this chapter, we will only deal with C at rst; at the end, we will introduce some features of C.

0.2

Building C Programs
0.2.1

Compiled and Interpreted Languages

C is what we call a compiled language. This is because programs, written with C code in a text
le, are generated by a package of programs called the compiler. These programs transform

Chapter 0

the text code into binary codes that are understood by the machine and the operating system. The process of compilation/program building is discussed below.
Interpreted languages, such as Python, are generally higher-level languages that are dependent on an interpreter program, which translates the text code into machine actions directly.
Their advantage is that the result of the programming can be seen immediately. On the
other hand, generally speaking, the overhead of interpreting the code can make these
languages very inefcient for intensive tasks (such as calculating the output of a synthesis
routine, in the case of a music system).
0.2.2

Compiling

After opening your console/terminal window, you can invoke the compiler program can be
called by the simple command
cc mysource.c
where cc is the C compiler and mysource.c is the name of your source le. (gcc is a very
widespread compiler, if you are using it, just change the command for gcc.) The output of
this command will be a binary executable le called a.out, which you can rename (using mv)
if you want. This le will sit in the directory you are working on.
For more practical uses, you can output the binary executable to a different le, using
-o . . . (just as in Csound -o means output to):
cc -o myprogr mysource.c
On Windows machines your program lename will have the extension .exe. On other
systems no extensions are used.
0.2.3

Source Files

Source les are text (ASCII) les containing the source code written in C, which will be used
to build the program. There can be just one or many of these needed to build a program.
0.2.4

Header Files

Header les (usually with extension .h) are les with denitions and parts of code which are
included in (i.e. they will be copied to) the source les. The compiling process will use the
code in these header les in the compilation process.
0.2.5

Libraries

C is a very lean language. It has a number of built-in denitions, rules, etc.; the rest is done
by elements (functions, data denitions, etc.) existing in libraries. These are pre-compiled

An Overview of the C Language

(i.e. they are already in binary) and are added to the executable at the last phase of the build
of the program. In this phase, called linking, references to libraries are linked to the actual
binary code.
There are libraries for virtually anything you want to do in C: math, sorting, comparing
things, controlling processes, playing audio and MIDI, etc. When using different libraries,
you will have to inform the compiler which one(s) you are using. This can be done with
the relevant command-line options. (For more details, see appendix A.)
0.2.6

The Build Process

The program build has three main phases:


1. Pre-processing. This expands all include header les (by copying them into the source
code) and other preprocessor statements (which usually start with the character #) found in
the source le(s).
2. Compiling. This reads the source code and transforms it into binary code for the machine
you are targeting (i.e. the machine for which youre building the program).
3. Linking. This links all references to binary code that is not in the source code (i.e. in libraries), as well as putting together all source binaries (if there are multiple source les).
These three stages can be done in one go, or if you like, separately. Usually, when building
large pieces of software it is common to do steps one and two at one time and then leave
the linkage process for later. The source les are compiled into binary les called object les
(extension .o), which can later be combined (linked) to make the program. This is usually
the case when there are many source les in the project. This procedure enables the programmer to compile the different source les only when they have been modied, saving
time and resources. Utilities such as make or scons can be used to maintain a build project
and will automatically call the compiler and the linker to build from sources that have been
updated.
0.3

The Structure of C Programs


0.3.1

Program Format and Layout

A C executable program requires that at least one function be present in it. This is what we
call the entry point of the program: where it starts executing and also providing the exit point,
i.e. when the function ends, the program also nishes. By default this function is called
main(), but this can be changed by a compiler ag (although we will not normally do it).
This function can take two forms:
int main( );
and

Chapter 0

int main(int argc, char* argv);


The rst form does not take any arguments and, being the simplest, is the one we will use
rst. Both forms are supposed to return (i.e. answer with) an integer number, which is
passed as a code to the operating system (generally 0 means OK). The second form will be
discussed later in section 0.15. In addition to main(), the program can also have other functions, although they are not absolutely required.
0.3.2

Character Set and Reserved Symbols

The C Programming Language uses the following characters:


A-Z
{}

a-z
0-9
[]
()
<

.
>

,
|

:
+

;
-

'
/

$
*

"
=

#
%
&
tab space

There are only 32 keywords (which are reserved symbols) in the C language. They are combined with the formal syntax to form the language proper. These symbols cannot be used for
any other purpose:
auto
double
int
struct
break
else
long
switch
case
enum
register
typedef
char
extern
return
union
const
float
short
unsigned
continue
for
signed
void
default
goto
sizeof
volatile
do
if
static
while
0.4

An Example Program
Our rst example is the one traditionally given in C courses:
#include <stdio.h> /* header file for IO */
int main()
/* main function */
{
printf("Hello World\n"); /* print message */
return 0; /* function returns 0 (0K) */
}
Here we have all the typical components of a program:
n
n
n
n
n
n

A main function matching one of the forms given above.


A header le, in this case for the standard input/output library.
An executable statement, to do something (print to the screen).
Since the function is said to return an int, a return statement.
Statements are always terminated with a semicolon.
Comments, between /* */, which will not be seen by the compiler.

If you build this program with the commands shown above and then run it, you will get
Hello World printed to the terminal.

0.5

An Overview of the C Language

Data Types
The rst element of C programming is based on the concept of a variable. Basically, variables
are memory slots that are reserved for you to stored numeric values or characters in. In C, we
need to declare these at the start of a function before we can use them, by stating the variable type and its name. The basic data types for variables are the following:
intan integer (a whole number)
floata oating-point value (with a decimal part)
doublea double-precision oating-point value
chara single character.
In addition, we will also see the type void, which is used to indicate no type or any type,
depending on its use. As for names, all variables must begin with a letter or an underscore.
0.5.1

Integer Data Types

An int variable, in most systems, can store a value in the 32-bit range from 2,147,483,648
to 2,147,483,647, but this is system dependent. For example,
int a;
creates a memory slot for an int and calls it a. Other types of integers also exist: a short
integer or short is stored as a 16-bit number (two bytes), and its range is from 32768 to
32767. Integers can be signed (the default) or unsigned. The latter are positive only and
have twice the positive range of a signed integer (from 0 to 4,294,967,295). The type long
can be used to mean 32-bit integers.
unsigned int ua; /* an unsigned integer */
unsigned long ulb; /* an unsigned long integer */
short sample; /* a 16-bit integer */
0.5.2

Floating-Point Data Types

Floating-point data types are so named because they store a decimal-point number in two
parts: an exponent (which tracks the point position) and a mantissa (which holds the actual
numbers over which the point oats).
float: A oating-point number has about seven digits of precision and a range of about
1.E36 to 1.E36. A float takes four bytes to store. Example: float result;
double: A double precision number has about 13 digits of precision and a range of about
1.E303 to 1.E303. A double takes eight bytes to store. Example: double value;

Chapter 0

0.5.3

Characters

The type char holds a single character, stored in one byte. Example:
char c;
This type is most often used to store ASCII characters (which are themselves seven-bit codes),
but in fact can be used for any single-byte numerical use.
0.5.4

Assignment and Arithmetic Operations

You can store a value in a variable using


name = value;
For instance,
a = 10;
stores the value 10 in the int variable a. Note that = is the assignment operator and does
not mean equality (which is ==, as we will see later). The arithmetic operators are the
following:
addition: a+b
subtraction: a-b
multiplication: a*b
division: a/b
Care must be taken with situations like a = 10.0/3. If a is an integral type (int, char,
etc.), the result will be truncated to 3. If it is a oating-point number, it will be expanded up
to the type precision (single or double), e.g., 3.3333333. This means (1) that integer division
truncates the result and (2) that if a float is involved in the calculation, integers will be
converted into float.
0.5.5

Casting

Data types can be converted one into another by using a cast:


int a;
float b;
. . .
a = (int) b;
b = (float) a;
Conversions between integral and oating-point types will cause truncation, as was discussed above. It is also important, when converting types, that the recipient have enough
range to hold the data; if it doesnt, overow may occur.

An Overview of the C Language

Table 0.1
Order of operations in C and C.
Operator

Associativity

( ) [ ] -> . expr++ expr++

Left to right

! ~ ++expr --expr (type) * & sizeof

Right to left

* / %

Left to right

+ << >>

Left to right
Left to right

<

<=

> >=

Left to right

== !=

Left to right

&

Left to right

Left to right

Left to right

&&

Left to right

||
?:

Left to right
Right to left

= += -= *= /= >>= <<= &= ^= |=

Right to left

Left to right

0.5.6

Arithmetic Ordering

Expressions are calculated using a certain precedence order, shown in table 0.1. If in doubt of
the precedence of operations, use parentheses to group them.
0.5.7

Variable Declaration and Initialization

A variable has to be declared before it is used, so that the compiler can create the memory
slot for it. You can also assign an initial value to a variable when you declare it. For example,
int a, i=1;
declares two variables (separated by commas) and initializes the second to 1.
0.5.8

Variable Scope

Variables declared within a program block are valid, and in existence, only inside that
block (and enclosed blocks). A program block is delimited by braces, so a function is a program block. These variables are called local to separate them from variables declared outside
any block, which are global and are seen by all functions within the source-code le. It is best
to avoid global variables.

10

Chapter 0

0.5.9

Constants

Constants are numeric values that cannot be changed throughout a program. Integer constants are normally written in base-10 format (decimal system): 1, 2. For long integer
constants an L is added: 2L, 10L. Floating-point constants will have two forms: with an f
at the end, for oats and just with a decimal point somewhere for doubles (2.f is a float;
2.0 is a double).
Integer constants can also be written as either hexadecimals (base 16) or octals (base 8). An
octal constant is preceded by 0 and a hexadecimal by 0x.
The decimal 31 (0001 1111 in binary) can be written as an octal (037) or as a hexadecimal: 0x1F.
Octal units will range from 0 to 7. Hexadecimal ones will range from 0 to F, with AF representing the decimals 1015. For instance, F in hexadecimals represent (1-valued) set bits,
so a 16-bit bitmask 0xFFFF is a series of 16 set bits (1111 1111 1111 1111).
Macros can be used to give constant names. The pre-processor statement #define will do
this for you, and so
#define END 10000
will substitute 10000 for any instances of the word END, so you can use END as a constant in
your code. The preprocessor takes care of this for you.
0.6

Standard Input and Output


In order for a program to communicate with the outside world, we need some sort of data
input and output. The simplest type of IO is provided by the standard C library console IO
functions. Since all IO is handled by libraries, more sophisticated ways of doing this are
implemented by different systems, but the standard IO is the one that is present everywhere,
so it is very useful in many situations.
0.6.1

printf and Format Speciers

The printf function is usually of the form


printf(format-string,...)
where ... means a variable number of arguments (which can be zero). The format string
determines how many parameters it will have. If it contains format speciers in the form of
% characters, it will call for one or more extra arguments. In the case of
printf("Hello World");

11

An Overview of the C Language

Table 0.2
The ANSI C format speciers.
Specier

Type

Print

%c

char

Single character

%d (%i)

int

Signed integer

%e (%E)

float or double

Exponential format

%f
%g (%G)

float or double
float or double

Signed decimal
Use %f or %e as required

%o

int

Unsigned octal value

%s

string (char array)

Sequence of characters

%u

int

Unsigned decimal

%x (%X)

int

Unsigned hex value

we only have a format string and, as this contains no % characters it results in Hello World
being printed without anything extra. As an example of a format specier, %d means print a
value as a signed decimal integer, so
printf("Total = %d", total);
will print Total = and then the value of the variable total.
Here are some important notes on format speciers:
1. The specier following % indicates the type of variable to be displayed as well as the
format in which that the value should be displayed.
2. If you use a specier with the wrong type of variable, you will see some strange things on
the screen, and the error often propagates to other items in the printf list.
3. You can also add l in front of a specier to mean a long form of the variable type, and h
to indicate a short form. Short integers are 16-bit (two bytes) whereas long ones are 32-bit
(four bytes). For example, %ld means a long integer variable (usually four bytes), and %hd
means a short int. Notice that there is no distinction between a four-byte oat and an
eight-byte double. The reason is that a oat is automatically converted to a double precision
value when passed to printf, so the two can be treated in the same way.
0.6.2

More on Format Speciers

The % speciers that you can use in ANSI C are given in table 0.2. A modier that will determine how the value will be printed can precede each specier. The most general modier is
of the form
flag width.precision

12

Chapter 0

Table 0.3
The ANSI C format modiers.
Flag

Use

Left justify.

Always display sign.

space

Display space if there is no sign.

0
#

Pad with leading zeroes.


Use alternate form of specier.

Table 0.4
Examples using the # modier.
Specier

Display

%#o

Adds leading 0 to octal value

%#x

Adds leading 0x to hex value

%#f or %#e

Ensures decimal point is printed

%#g

Displays trailing zeros

where width species the number of characters used in total to display the value and
precision indicates the number of characters used after the decimal point. The format
modiers are shown in table 0.3.
For example, %10.3f will display a oat using ten characters with three digits after the
decimal point. (The ten characters would consist of the decimal point and a minus sign,
if there were one.) The specier %-1Od will display an int left justied in a ten-character
space. The specier %+5d will display an int using the next ve character locations and
will add a plus sign or a minus sign to the value. The # modier can be used as shown in
table 0.4.
Finally, ASCII control codes can be used in a string as shown in table 0.5. These escape
sequences are used to display non-printing or special characters. They essentially control
how the text is positioned on the screen.
0.6.3

scanf

The scanf function works in much the same way as the printf, as it has the general form
scanf(controlstring,...)
In this case the control string species how strings of characters, usually typed on the keyboard, should be converted into values and stored in the listed variables. To store values,
the scanf function has to have the addresses of the variables rather than just their values.

13

An Overview of the C Language

Table 0.5
Escape sequences and their results.
Escape sequence

Special character or behavior

\b

Backspace

\f

Formfeed

\n

Newline

\r
\t

Carriage return
Horizontal tab

\v

Vertical tab

\'

Single quote

\"

Double quote

\0

Null character

\a

Alert (beep)

So we will pass the address of the variable (&variable) instead of just the variable. In general, all of the relevant format speciers listed in connection with printf can be used with
scanf. The scanf function processes the control string from left to right, and each time it
reaches a specier it tries to interpret what has been typed as a value. If you input multiple
values, then these are assumed to be separated by white space (i.e. blanks, newline, or tabs).
For example,
scanf("%d %d",&i,&j);
will read in two integer values into i and j. The integer values can be typed on the same line
or on different lines as long as there is at least one white space between them. Note, however, that in the case of the %c specier scanf does not skip white space and it will read
the next character typed, regardless of what it is.
The width modier can be used in scanf to limit the number of characters accepted to
width. For example,
scanf("%lOd",&i)
would use, at most, the rst ten digits typed as the new value for i.
0.6.4

A Simple Example

Here is a simple addition program to demonstrate the use of IO and arithmetic in C:


#include <stdio.h>
int main()
{
int a,b,c;

14

Chapter 0

printf("\n Please enter a number: ");


scanf("%d",&a);
printf(" Please enter a second number: ");
scanf("%d",&b);
c=a+b;
printf("%d + %d = %d \n",a,b,c);
return 0;
}
This program will prompt the user for two numbers, using printf(), take them in with
scanf(), then print out the resulting sum. Notice that each line is executed in sequence
and that scanf() blocks waiting for input. Also notice that we could have saved using a
third variable (c) by performing the sum as one of the arguments to the last printf(), as in
printf("%d + %d = %d \n",a,b,a+b);
0.7

Control of Flow
C provides a number of ways of controlling the ow of execution of a program, so that it
does not always have to proceed statement by statement as in the previous example. Before
we examine the different forms of controlling the ow, we have to introduce ways of checking the truth of conditions and statements: logical expressions.
0.7.1

Logical Expressions

A logical expression is an expression used to test a certain condition in a ow control statement. For example, a > 0 is true if a contains a value greater than zero, and b < 0 is true if b
contains a value less than zero. The concept of truth in C is very simple: 0 if not true and
non-0 if true. So a logical expression generally evaluates to 0 or non-0 (say, 1). The following
relational operators are used in logical expressions:
>

<

>

<=

==

!=

Note that the equality operator is == and not simply =. The last operator means NOT equal
to. Because truth conditions are zero or non-zero, a zero-valued variable can also be used to
indicate falsehood, and a non-zero variable to indicate truth.
0.7.2

Conditional Execution

One of the programming devices use to control the ow of a program is called conditional
execution. This is performed by an if statement:
if (a > 0) printf("%d is positive", a);

15

An Overview of the C Language

If the condition is false, the program continues with the next instruction. In general, the if
statement is of the form
if (condition) statement;
The statement above can be a single statement or a program block. In addition, it is also
possible to select between two statements, one to be obeyed when the condition is true and
one to be obeyed when the condition is false. You can do this using
if (condition) statement1;
else statement2;
This is a variation of the if statement. In this case statement1 is carried out if the condition is true and statement2 if the condition is false. Notice that both parts of the if-else
statement can be blocks or single statements terminated by a semicolon:
if (f2 ==0) printf("division by zero !!!\n");
else printf("%f/%f = %f\n",f1, f2, f1/f2);
A third form of if can be used for selecting several options:
if (condition) statement1;
else if(condition2) statement2;
. . .
else if(conditionN) statementN;
else else-statement;
This can be used if several conditions need to be checked, and the last else is used to catch
the case where none of the above are true.
0.7.3

Conditional Operator

A condensed form of if-else can be found within the construction


a = b > c ? b : c;
which means the following: if b is bigger than c, then a = b; else a = c. The general form is
condition ? true-expression : false-expression;
The result of the expression is the true-expression if the condition is true; otherwise the result
is the false-expression.
0.7.4

Switch

An alternative to select from several options is the switch statement, a multiple selection
statement. A variable is successively tested against a list of integral constants. When a match

16

Chapter 0

is found, the program executes from the matched label onwards. The general form of the
switch statement is
switch(expression)
{
case constant1: statement sequence; break;
case constant2: statement sequence; break;
. . .
case constantN: statement sequence; break;
default: statement sequence; break;
}
Each case is labeled by one or more integer-valued constants. The default label is reached
if no matches are found. The default is optional. If all matches fail and default is absent,
no action takes place. When a match is found, the program switches execution to the statement sequence associated with that case. The use of break is important, because the execution will continue forward within that program block, possibly executing more than one of
the cases. Using break ensures that the program block is exited after the relevant statements have been executed. For example:
switch (i)
{
case 1:
printf("one");
break;
case 2:
printf("two");
break;
case 3:
printf("three");
break;
case 4:
printf("four");
break;
default:
printf("out of range");
}
This program fragment recognizes the numbers 14 and prints the value of a variable in
English. The switch statement differs from if in that switch can only test for equality,
whereas the if conditional expression can be of any type. Also, switch will work with
only int and char types.

17

An Overview of the C Language

0.7.5

A Music Theory Example

Here we demonstrate the three forms of conditional execution introduced above. This is a
little music theory program that calculates the interval between two notes and prints its
semitone length and name. We ask the user to input two notes, which we take in as characters and then translate into numerical pitch classes (011). The reason for the dummy variable is that the user is expected to separate the two note names by a carriage return (or
enter) character, or even a blank space. As we are scanning characters, these have to be
accounted for, so the dummy variable is used as a placeholder. (Though we could have used
note2, for the sake of clarity we have not.)
#include <stdio.h>
int main()
{
char note1,note2, dummy; /* note names, dummy char */
int pc1, pc2, interval; /* pitch classes, interval */
printf("Please enter two natural notes.\nfirst note: ");
scanf("%c%c",&note1, &dummy);
printf("second note: ");
scanf("%c",&note2);
switch(note1){ /* translating from note name to pitch class */
case 'C': case 'c':
pc1 = 0;
break;
case 'D': case 'd':
pc1 = 2;
break;
case 'E': case 'e':
pc1 = 4;
break;
case 'F': case 'f':
pc1 = 5;
break;
case 'G': case 'g':
pc1 = 7;
break;
case 'A': case 'a':
pc1 = 9;
break;
case 'B': case 'b':
pc1 = 11;
break;

18

Chapter 0

default:
printf("error: %c is not a natural note\n",note1);return 1;
}
switch(note2){
case 'C': case 'c':
pc2 = 0;
break;
case 'D': case 'd':
pc2 = 2;
break;
case 'E': case 'e':
pc2 = 4;
break;
case 'F': case 'f':
pc2 = 5;
break;
case 'G': case 'g':
pc2 = 7;
break;
case 'A': case 'a':
pc2 = 9;
break;
case 'B': case 'b':
pc2 = 11;
break;
default:
printf("error: %c is not a natural note\n",note2);return 1;
}
/* calculate the interval and keep it modulo 12 */
interval = pc2 - pc1;
if(interval < 0) interval += 12;
else if(interval > 11) interval -= 12;
/* print the number of semitones. The special case of
unison (0) has to be handled correctly, so we use the
conditional operator for this */
printf("%d semitones up or %d semitones down\n", interval,
interval ? 12-interval : 0 );
/* now we print out the interval name */
switch(interval){
case 1:
printf("minor 2nd up or major 7th down\n");
break;
case 2:

19

An Overview of the C Language

printf("major 2nd up or minor 7th


break;
case 3:
printf("minor 3rd up or major 6th
break;
case 4:
printf("major 3rd up or minor 6th
break;
case 5:
printf("perfect 4th up or perfect
break;
case 6:
printf("augmented 4th \n");
break;
case 7:
printf("perfect 5th up or perfect
break;
case 8:
printf("minor 6th up or major 3rd
break;
case 9:
printf("major 6th up or minor 3rd
break;
case 10:
printf("minor 7th up or major 2nd
break;
case 11:
printf("major 7th up or minor 2nd
break;
default:
printf("unison \n");

down\n");

down\n");

down\n");

5th down\n");

4th down\n");

down\n");

down\n");

down\n");

down\n");

}
return 0;
}
As we will see later, this program is longer than it has to be if we use all the tools that the
C language offers. However, it demonstrates what is possible with the elements introduced
so far.
0.8

Loops
Until now we have been using programs that have a certain number of instructions that are
read and executed, line after line depending on control ow statements (if, else, or

20

Chapter 0

switch). If we want some parts of the program to be repeated, we will have to use a neat
programming device: the loop.
0.8.1

The while and do while Loops

You can repeat any statement using either the while loop:
while(condition) { . . . }
or the do while loop:
do { . . . }while(condition);
The condition is just a test to control how long you want the program block to carry on
repeating. In the case of the while loop, before the code block is carried out the condition
is checked, and if it is true the block is executed one more time. If the condition turns out to
be false, the loop is exited. In the case of the do while loop, it will always execute the code
within the loop at least once, since the condition controlling the loop is tested at the bottom
of the loop.
One of the typical ways of controlling a loop is to employ a counting variable, which will
be incrementing every time the loop repeats:
a = 0;
While( a < 10){
. . .
a=a+1;
}
The increment is such a common operation that we have a special operator for it:
++a; or a++;
In this case, ++a increments a before using its value, whereas a++ means use the value in a,
then increment the value stored in a. Decrement operators (--) also work in the same way.
In addition, += and -= are used to increment or decrement by more than 1.
Other ways of stopping a loop include querying the user with scanf() and examining the
value of an arithmetic expression.
0.8.2

The for loop

The loop controlled by a counting variable is so common that a special form is designed to
provide a compact way of doing it. This is implemented by the for loop
for (counter=start; counter < end; counter++) statement;
which is equivalent to

21

An Overview of the C Language

counter=start;
while (counter < end)
{
statement;
counter++;
}
Here is an example creating the classic Fahrenheit-to-Celsius conversion table:
for (fahr = 0 ; fahr <= 300 ; fahr = fahr + 20)
printf("%4d %6.1f\n", fahr, (5.0/9.0)*(fahr-32));
0.8.3

Another Music Theory Example

Here we show the use of a loop to implement a repetitive task. In this program, we want to
print the notes of a major scale in ascending steps. This involves adding two semitones to a
starting note at every repetition, except after the third note, when we have just one semitone. In order to print the note names, we translate the pitch class numbers by bringing
them to the range 011 (modulo 12, we can use the % operator since all notes are now
non-negative).
#include <stdio.h>
int main()
{
int note, i;
printf("Please enter the key (in pitch-class number, 0-11): ");
scanf("%d",&note);
/* make sure start note is not negative */
while (note < 0) note += 12;
/* build the scale */
for (i=0; i < 7; i++){
/* translate pitch-class to note name */
if(note%12==0) printf("C ");
else if(note%12 == 1) printf("Db ");
else if(note%12 == 2) printf("D ");
else if(note%12 == 3) printf("Eb ");
else if(note%12 == 4) printf("E ");
else if(note%12 == 5) printf("F ");
else if(note%12 == 6) printf("Gb ");
else if(note%12 == 7) printf("G ");
else if(note%12 == 8) printf("Ab ");
else if(note%12 == 9) printf("A ");
else if(note%12 == 10) printf("Bb ");

22

Chapter 0

else printf("B ");


/* find the next pitch class, jump
a semitone only after 3rd step */
if(i != 2) note += 2;
else note++;
}
printf("\n");
return 0;
}
0.8.4

Break and Continue

The break statement allows you to exit a program block from anywhere within it (we saw
how it can be used in a switch block). So it can be used to exit a loop from inside it, without
using the loop conditional test. The continue statement is used to jump directly to the test
from anywhere in the loop, skipping any remaining statements.
0.9

Bitwise Operations
C permits a set of low-level operations, known as bitwise, that add some useful functionality
to the language. These operators, as the name implies, are dened to work at the bit level,
on the individual zeroes and ones that make up the binary information. They are dened
for integer use.
0.9.1

Bitwise Logic

A number of operators are dened for bitwise operations; they compare each bit of one
operand with the corresponding bit of the other operand:
&
|
^
~

bitwise AND
bitwise inclusive OR
bitwise exclusive OR
ones complement (unary operator).

Bitwise AND (&) returns a set bit (1) only when both sides of the operation have that bit set.
It is often use with bitmasks to lter bytes off an integer:
short mask = 0xFF00, value, masked;
value = 0x0111;
masked = mask & value;
In the example above, the mask will only let the higher byte pass, ltering off the lower
one. Thus the value of masked will be 0  0100:

23

An Overview of the C Language

0000 0001 0001 0001


&
1111 1111 0000 0000
----------------------------0000 0001 0000 0000
The bitwise OR (|) returns a set bit when either operands have a set bit. It is used to turn
bits on (and to combine bytes).
masked = mask | value;
will turn the higher-order byte to 0  FF, resulting in 0  FF11:
0000 0001 0001 0001
1111 1111 0000 0000
----------------------------1111 1111 0001 0001
The bitwise exclusive-OR returns a set bit when only one operand has a set bit, otherwise it
will return a zero. The unary ones complement operator (~) converts each set bit into a zero
and vice versa.
The bitwise logic operators can be combined in short-hand expressions with the assignment operator, for the updating of variables. For example:
value &= mask; // same as value = value & mask;
0.9.2

Bitshift Operators

Two operators can be used to shift bits in an integer:


<< left shift
>> right shift
They shift bits by a number of positions specied by the right-hand operand: x << 1 shifts all
bits by one position to the left, and x >> 2 shifts all bits by two positions to the right.
Left-shifts ll the vacated bits with 0-bits. Right-shifts will depend on the type of the
operand: for unsigned types, bits will be lled with zeroes; for signed types, the sign bit is
preserved and the shifted bits will be lled with sign bits (the rst bit). This is platform
dependent, but in the major systems we use this is the norm. They employ a representation
for signed integers called twos complement. In it, the rst bit (sign) is 1 for negative
numbers and 0 for positive numbers.
Left shifts are equivalent to multiplication (a fast way of doing it):
x << n

multiplication by 2 n .

Right shifts are equivalent to division (with rounding):


x >> n

multiplication by 2 n .

24

Chapter 0

Thus, a fast way of multiplying or dividing by 2 is to left or right shift a number by one
position. The division will be rounded down to an integer.
0.10

Functions
In C, functions are also known as subroutines or procedures. A function is simply a block of
code grouped together and given a name. This block of code can then be invoked just by
using its name in a statement. For instance a function can be declared to print a message
and exit:
void message()
{
printf("This is my message\n");
}
Now that it is dened, you can use it:
int main()
{
message();
return 0;
}
As the function does not return anything, it is dened with the return type void, meaning
nothing. It also does not take any arguments, so there is nothing inside the parentheses.
Note that any variables declared inside a function (a code block) are only valid and seen
inside that block. These variables are local and they will disappear when function exits. In
order to pass in values to local variables and to get the answers out of functions, we will use
arguments (parameters) and return values.
0.10.1

Arguments and Return Types

Passing values to a function is done via its arguments. These are dened within the parentheses, each one with its type and separated by commas. Arguments declared like this also
behave as local variables for the function. In order to pass the answer (if any) that the function provides, we use the keyword return:
int sum(int a, int b)
{
int result;
result=a + b;
return result;
}

25

An Overview of the C Language

This denes a function (called sum) with two parameters, a and b, both integers, returning
an integer, and can be used as
a = sum(2,2);
This is a call to the sum function with a set to 2 and b set to 2, and so result is set to 4.
You can also initialize parameters to the results of expressions such as
a = sum(x+y,z*w);
This will set a to the result of x+y and b to z*w. The values of the arguments are copied into
them as the function is called. C passes copies of variables as arguments, not the variables
themselves. In summary, a function has the general form
type FunctionName(arguments)
{
statements
}
Notice that the function will always exit when it reaches a return statement. The rest of the
code beyond it will be skipped. The return statement can return a value or (in the case of
void return types) nothing at all.
0.10.2

Prototypes

Before a function is used, the compiler must be given the return type and any arguments
for it. Thus, in order to use it, we will rst have to declare it. We need not completely dene
(implement) it before it is used; we need only declare it. In that case, we can provide the
function prototype: its type, name, and arguments. For instance,
int sum(int, int);
is the prototype for the sum function dened above. The function denition can then go
elsewhere in the source codeafter main(), or even in a different source le. By default, if a
function is not declared before use, it is assumed to be an int functionwhich is not always
correct.
0.10.3

A More Compact Version of the Interval Program

Now we can use functions to implement tasks that might be repeated with different inputs,
so we can have a more compact program. Here we put the translation code of our rst music
theory example program into a function, then call it when we need pitch classes for the
interval calculation:

26

Chapter 0

#include <stdio.h>
/* this function implements translation */
int nameToPc(char name){
switch(name){
case 'C': case 'c':
return 0;
case 'D': case 'd':
return 2;
case 'E': case 'e':
return 4;
case 'F': case 'f':
return 5;
case 'G': case 'g':
return 7;
case 'A': case 'a':
return 9;
case 'B': case 'b':
return 11;
default: /* error code */
return 100;
}
}
int main()
{
char note1,note2, dummy;
int interval;
printf("Please enter two natural notes.\nfirst note: ");
scanf("%c%c",&note1, &dummy);
printf("second note: ");
scanf("%c",&note2);
/* to calculate the interval, we call nameToPc() to translate */
interval = nameToPc(note2) - nameToPc(note1);
if(interval > 20 || interval < -11) {
printf("either %c or %c are invalid notes\n", note1, note2);
return 1;
}
if(interval < 0) interval += 12;
else if(interval > 11) interval -= 12;
printf("%d semitones up or %d semitones down\n", interval,
interval ? 12-interval : 0 );
return 0;
}

27

An Overview of the C Language

0.10.4

The Standard Library

The Standard C Library provides a great number of functions that perform very useful tasks,
including input and output, math, string and character manipulation, and memory allocation. The advantage of using this library is that it has more or less the same behavior across
platforms and is generally guaranteed to work everywhere. Table 0.6 shows some of the functions in the Standard C Library.
0.11

Arrays
So far we have been able to create memory slots only for single variable types, but in many
applications we will require whole blocks of memory for storing values contiguously. This
can be done using arrays. For example,
int a[5];
declares an array called a with ve elements. The rst element is a[0] and the last a[4], so
all the arrays are zero-based. In general you have
type array[size]
which declares an array of the specied type and with size elements. The rst array element is array[0] and the last is array[size-1]. Arrays can be initialized as follows:
int a[5] = {1,2,3,4,5};
The for loop and arrays go together very well. The for loop can be used to generate a
sequence of values to pick out and process each element in an array in turnfor instance,
for (i=0; i<5; i++) a[i] = i+1;
to ll the array with 1, 2, 3, 4, 5.
0.11.1

Two-Dimensional Arrays

In C it is possible to create arrays of two dimensions, sometimes also called two-dimensional


matrices. Such an arrays numbers of rows and columns dene a two-dimensional matrix.
Thus, a 2-D array can be declared as follows:
a[rows][columns]
int a[10][10]; /* a 10x10 matrix */
A two-dimensional array is actually an array of arrays; it can be initialized, just as any other
array, by placing the columns in row order:
int a[3][2] = { {0, 1},
{2, 3},
{4, 5} };

28

Chapter 0

Table 0.6
Some of the more popular functions from the standard C library.
stdio.h

I/O functions

getchar()

Returns next character typed on keyboard

putchar()

Outputs a single character to screen

printf()

Outputs to stdout

scanf()

Reads data from stdin

string.h

String functions

strcat()
strcmp()

Concatenates a copy of str2 to str1


Compares two strings

strcpy()

Copies contents of str2 to str1

ctype.h

Character functions

isdigit()

Returns non-0 if arg is digit from 0 to 9

isalpha()

Returns non-0 if arg is a letter of alphabet

isalnum()

Returns non-0 if arg is a letter or digit

islower()

Returns non-0 if arg is a lowercase letter

isupper()

Returns non-0 if arg is an uppercase letter

math.h

Mathematics functions

acos()

Returns arc cosine of arg

asin()
atan()

Returns arc sine of arg


Returns arc tangent of arg

cos()

Returns cosine of arg

sin()

Returns sine of arg

exp()

Returns natural logarithm e

fabs()

Returns absolute value of num

sqrt()

Returns square root of num

time.h

Time and date functions

time()

Returns current calendar time of system

difftime()
clock()

Returns difference in secs between two times


Returns number of system clock cycles since program execution

stdlib.h

Miscellaneous functions

malloc()

Provides dynamic memory allocation

rand()

Returns a pseudo-random number

srand()

Used to set starting point for rand()

29

An Overview of the C Language

0.11.2

Strings: Arrays of Characters

Strings are arrays of characters, and the C programming language uses the convention that
the end of a string of characters is marked by a null character (ASCII code 0). To store the
null character in a character variable, you can use the notation \0. However the compiler
will automatically add a null character and store each character in a separate element
when you use a string constant. A string constant is indicated by double quotation marks, a
character constant by single quotation marks): "A" is a string constant and 'A' is a character
constant. It is important to realize and remember that strings are always arrays and so cannot
be assigned directly, as in
char string[40];
string="name"
However, you can print strings using printf and read them into character arrays using
scanf. You can also initialize a string with a string constant:
char name[40] = "hello";
But to manipulate strings we will need functions that deal with the fact that they are arrays
(and that they are terminated with a null character). Standard library string functions, such
as strcmp() and strcat(), are designed to operate on such arrays of characters.
0.11.3

A More Elegant Version of the Scale Program

We can now make our scale program more elegant by using an array of strings as a translation table. First, we get the key as a string, use a loop to look it up on the table, and obtain
the index to it, which is our pitch class (from 0 to 11). Then we can use the table again to
print out the note name. We use the strcmp() library function to match the strings. It
returns 0 if the strings are the same.
#include <stdio.h>
#include <string.h>
int main()
{
int note, i;
char key[3];
char* scale[12]={"C","Db","D","Eb",
"E","F","Gb","G",
"Ab","A","Bb","B"};
printf("Please enter the key(capitals only, "
"use b for flats,
eg. Eb):");
scanf("%s",key);
/* use table to translate note name to pitch class */

30

Chapter 0

for(i=0; i < 12; i++)


if(strcmp(scale[i],key)==0){/* found the note */
note = i; /* pitch-class is array index */
printf("== %s major scale ==\n", key);
break;
}
else note = -1; /* note not found */
if(note >= 0){
for (i=0; i < 7; i++){
/* use table to translate pitch-class to note name */
printf("%s ", scale[note%12]);
if(i != 2) note += 2;
else note++;
}
printf("\n");
return 0;
}
else{
printf("%s: invalid key\n", key);
return 1;
}
}
0.12

Pointers
As we mentioned earlier, a variable is a memory slot that has been given a name. For example, int x; is an area of memory that has been given the name x, and x=10; stores the data
constant 10 in memory slot x. The computer accesses its own memory by using a memory
map with each location of memory uniquely dened by a number called an address. A pointer
is a variable that holds this location of memory, the address of a variable. We declare it just
like any other variable, but we use an asterisk to mark it. For example,
int *pa;
is a pointer to an integer. To use a pointer, we will employ two special operators: & and *. We
have already seen that the & operator returns the address of a variable. For example,
int *pa, a;
declares pa, a pointer to int, and an int, and the instruction
pa=&a;
stores the address of a in pa. We say that pa is pointing at a.

31

An Overview of the C Language

The operator * is the indirection operator. A * in front of a pointer variable gives the
value stored in the variable pointed at. That is, pa stores the address of a variable, and *pa
is the value stored in that variable.
a = 10;
b = *pa; /* b is now also 10 */
*pa = 12; /* a is now 12 */
In summary:
n
n
n

A pointer is declared by a * in front of its name.


The address of a variable is given by a & in front of its name.
To obtain or store the value of a variable (pointed at), use * in front of a pointer.

0.12.1

Pointers and Arrays

Pointers and arrays are closely connected. If you declare an array as


int a[10];
you are also declaring a pointer a to the rst element in the array. Here, a is equivalent to
&a[0]. The only difference between a and a pointer variable is that the array name is a constant pointer and cannot be used as a variable. In this sense, a[i] and *(a+i) are equivalent, which makes possible what is called pointer arithmetic, adding integers to pointers to
step through a memory block. The compiler will know how many bytes to move forward
when you add a certain number of memory slots to it. If it is an int, it will jump four bytes
(system-dependent of course) each step, if it is a double, then it will jump eight bytes. This
is one of the reasons why the compiler needs to know the type of a variable when you
declare it.
Finally, when you pass an entire array to a function then by default a pointer is passed.
This allows you to write functions that process entire arrays without having to pass every
single value stored in the array, just a pointer to the rst element:
int randarray(int *pa, int n)
{
int i;
for (i=0; i < n; i++)
{
*pa = rand()%n + 1;
pa++;
}
}
The loop can in fact be reduced to
for(i=0; i<n; i++) *(pa+i)=rand()%n+1;

32

Chapter 0

or
for(i=0; i<n; i++) pa[i]=rand()%n+1;
If you dene pa as a pointer, you can use array indexing notation with it as well as pointer
arithmetic.
0.12.2

Strings, Arrays, and Pointers

We now see that a string is just a character array with the end of the valid data marked by
an ASCII null character \0. Manipulation of strings is based on pointers and special string
functions. For example, the strlen(str) function returns the number of characters in the
string str. It counts the number of characters up to the rst null in the character array, so
it is important to use a null-terminated string.
We need a function to copy strings, because simple assignment between string variables
does not work. For example,
char a[l0],b[10];
b = a;
does not copy characters. It just makes pointer b set point to the same set of characters that a
points to, but a second copy of the string is not created (in fact, this code will not even compile). What we need is strcopy(a,b), which copies every character in a into the array b up
to the rst null character. Similarly, strcat(a,b) adds the characters in b to the end of
the string stored in a, and strcmp(a,b) compares the two strings, character by character,
and returns 0 if the results are equal.
Notice that, to assign a character array to a string, you cannot use
a = "hello";
because a is a pointer and "hello" is a string constant. However, you can use
strcopy(a,"hello");
because a string constant is passed in exactly the same way as a string variable, i.e. as a
pointer. And remember, a string can always be initialized. For example:
char a[6]="hello";
0.12.3

A Transposition Program Using Pointer Arithmetic

As an example of the use of pointer arithmetic (i.e. incrementing and decrementing memory
addresses), we present a program that calculates the result of transposing a note by any number of semitones (positive or negative). This program uses an array that is a table of note
names. We set a pointer to the start address of that array and then increment it until we
nd the base note. We then add to this pointer the interval in semitones (after bringing

33

An Overview of the C Language

this to the range 011), then make sure the address is in the correct range (otherwise we
wrap it around). This will result in the pointer being set to the address in the table where
the correct note name is:
#include <stdio.h>
#include <string.h>
int mod12(int note){
while(note < 0) note += 12;
while(note >= 12) note -=12;
return note;
}
int main() {
char note[3], **p1, **p2,
*table[12] = {"C","C#","D","D#",
"E","F","F#","G",
"G#","A","A#","B"};
int interval;
printf("Enter base note (capitals, use # for sharps, eg. A#): ");
scanf("%s", note);
printf("Enter interval in semitones: ");
scanf("%d", &interval);
/* point p1 to the beginning of the array and p2 to its end */
p1 = table;
p2 = table+11;
/* now find the base note position,
incrementing the pointer until we find it */
while(strcmp(*p1,note)){
p1++;
if(p1 > p2) { /* if we're past the end */
printf("could not find %s\n", note);
return 1;
}
}
/* add the interval to the address of the base note */
p1 += mod12(interval);
/* if beyond the end of the table, wrap it around */
if(p1 > p2) p1 -= 12;
/* print result */
printf("%s transposed by %d semitones is %s\n",
note, interval, *p1);
return 0;
}

34

Chapter 0

0.12.4

Pointers to Functions

In C it is possible not only to have pointers to built-in data types, but also to have pointers to
functions. With this facility, we will be able to manipulate functions as pointers, pass them
to other functions, put them in arrays, and so on. It is quite an advanced principle, but very
useful. For instance,
void (*pf)();
is a pointer to a function that takes no parameters and does not return anything. In that
case, we can assign
void message(){ printf("my message\n"); }
pf = message;
and call the function using a pointer:
(*pf)(); /* will call message() */
or, even simpler,
pf();
This is because (*pf)() and pf() are equivalent, both being the function that is pointed at.
In general, we have this form for function pointer declarations:
return-type (*pointer-name) (arguments);[
Why do we need something like this? Well, function pointers are useful for callbacks, i.e.
functions not invoked directly by you but by existing code. For instance, suppose we have
the function
void message_printer(int times, void (*callback)(char *msg),
char *user_mess){
int i;
for(i=0; i < times; i++) callback(user_mess);
}
This invokes a user-dened function to print out a user-supplied message, a callback. The
user would then supply a callback for message-printer() to call it, as in the following
example:
void my_important_message(char *mess){
printf("VERY IMPORTANT: %s \n," mess);
}
void my_warning_message(char* mess) {
printf("WARNING: %s\n," mess);
}

35

An Overview of the C Language

int main() {
message-printer(10, my_important_message, "functions can be pointers");
message-printer(1, my_warning_message, "but be careful");
return 0;
}
This advanced concept can be much more useful than the trivial example above, but this
gives you an idea of what a callback is. These types of programming constructs are often
seen in audio systems, where the programmer supplies a callback that will be used by the
system to perform a particular task (such as sending audio samples to the soundcard).
0.13

Structures
The array is an example of a data structure made up of a single data type. If you need something that holds a variety of types together in a single block, you will need a single data structure using a single name, provided in the C language by struct.
0.13.1

User-Dened Data Types

With C structures we have a new, user-dened, data type. To use them, rst we need to
dene what the new type looks like, using struct:
struct note
{
char name[3];
int duration;
char intensity[5];
};
Then we can declare a variable of that type, to be used in the program:
struct note first;
Notice that the new variable is called first and is of the type note. This can be considered
as valid a data type as any of the built-in types, such as float or int. In general, you can
dene a structure using
struct name
{
list of member variables
};
and you can have as many member variables as you need. Once dened you can declare as
many examples of the new type as you like, using
struct name list of variables;

36

Chapter 0

For example,
struct note first, second, third;
and so on. If you want to avoid using struct for variable declaration, you can use typedef.
For instance,
typedef struct _note
{
char name[3];
int duration;
char intensity[5];
} note;
note first;
denes the structure, and a new type based on it called note, then uses the newly created
data type directly. To use the data type, you can access each member by name, using a period
to link the variable name and the requested structure member:
first.name
first.duration
Once you have used a qualied name to get down to the level of a member variable, it
behaves like a normal variable of the type dened in the structure. For example,
first.duration = 120;
is a valid assignment to an int, and
strcpy(note.name, "C");
is a valid statement. Notice that to access the member we use the structure variable name
and not the structure type name. You can also dene a structure that includes another structure as a component and of course that structure can contain another structure and so on.
Structures can be initialized using syntax that is similar to array initialization:
note first = { "Ab", 80, "mf" };
As another example of the use of structures, we would like to dene a type to hold complex numbers. A complex number is composed of two partsa real part and an imaginary
partthat can be implemented as single or double precision values. This suggests dening
a new struct type:
typedef struct comp
{
float real;
float imag;
} complex;

37

An Overview of the C Language

After this, you can declare new complex variables using


complex a,b;
The new complex variables cannot be used as if they were simple variables. C does allow
you to assign structures as in
a = b;
Being able to assign structures and operate on them is even more useful when they are
bigger. What we can do is to dene a function to operate on a structure. You can pass entire
structures as parameters and return entire structures. As with all C parameters, structures are
passed by value; thus, if you want to allow a function to alter a parameter you have to
remember to pass a pointer to a struct. For instance, multiplication of complex numbers
can be dened as follows:
complex mult(complex a, complex b)
{
complex c;
c.real=a.real*b.real-a.imag*b.imag;
c.imag=a.real*b.imag + a.imag*b.real;
return c;
}
You can then write a complex multiplication as
x=mult(y,z)
The following program example demonstrates the use of structures and how to operate on
them:
#include <stdio.h>
typedef struct comp
{
float real;
float imag;
} complex;
complex mult(complex a,complex b)
{
complex c;
c.real=a.real*b.real - a.imag*b.imag;
c.imag=a.real*b.imag + a.imag*b.real;
return c;
}
int main()

38

Chapter 0

{
float re,im;
complex a,b;
get two complex numbers as inputs */
printf("Please enter the first complex number (re, im): ");
scanf("%f%f", &re, &im);
a.real = re; a.imag = im;
printf("Please enter the second complex number (re, im): ");
scanf("%f%f", &re, &im);
b.real = re; b.imag = im;
/* multiply them */
a = mult(a,b);
printf("Their product is %f + %fi \n", a.real, a.imag);
return 0;
}
Finally, notice that passing a struct by value (to a function) can use up a lot of memory
in the case of large data structures (which is not the case in the example above). This is
because a copy of the structure is made for the function argument. To avoid this, you can
use pointers to structures.
0.13.2

Pointers to Structures

You can declare a pointer to a structure in the same way as any pointer. For example,
person *ptr
denes a pointer to a person. You can use a pointer to a struct in the same way as any
pointer, but the typical means of structure access makes it look rather awkward. For example,
(*ptr).age
is the age component of the person structure that ptr points ati.e. an int. You need the
brackets because a period has a higher priority than an asterisk. The use of a pointer to a
struct is so common that you can use
prt->age
to mean the same thing as (*ptr).age. The notation gives a better idea of what is happening: prt points (->) to the structure and age selects which component of the structure we
want.
0.13.3

Structures and Functions

Structures can only contain built-in data types, so a function cannot be directly dened
inside a structure. However, as we can declare pointers to functions, we could use them to

39

An Overview of the C Language

hold functions, which in some cases might be useful. For instance, it would be nice to be
able to have an increment function for the complex data type. First, we could declare a structure containing a function pointer
typedef struct comp {
double real, imag;
void (*incr)(struct comp *p);
} complex;
Then we could dene a function to operate on the data (well use a pointer, as it is more
efcient):
void incr1(complex *p){ p->real++; p->imag++; }
To use this structure and its associated function, we can write the following code:
complex a = { 0, 0, incr1 };
a.incr(&a);
0.13.4

Structures and CBB

Dening functions to work on data structures is very useful. In C, there will be an easier
way to do it. Structures with functions associated with them are in fact a point of connection
between C and C, as they become classes in C. In simple terms, a C class is a structure in which you can dene members that are functions (not only function pointers). You
will also be able to control access to the data members of a class. Moreover, the same .
access system applies to the class and the use of pointers and the -> operator. These elements will be further discussed in later sections of this chapter.
0.14

Dynamic Memory Allocation


So far we have not bothered with how memory is set aside for data you use in a program. In
the usual variable declaration, the compiler works out how to allocate storage. For example,
when you declare
int a;
the compiler sorts out how to allocate memory to store the integer, and with int a[256] ;
it allocates space for 256 ints and sets the name a to point to the rst element. This is static,
automatic storagestorage that is allocated at compile time. But another way to do this is
to use dynamic allocation, with the malloc function:
pa = malloc(size);
Here malloc allocates a certain number of bytes (size) and returns the address of that location. Since we can retrieve the size of any data type with the sizeof(...) function, we can
allocate storage using

40

Chapter 0

pa = malloc(sizeof(int)*N);
where N is the number of ints you want to create. But because malloc is used to allocate an
arbitrary memory block, it returns a void pointer, which must then be cast to the right type:
pa = (int *) malloc(sizeof(int)*N);
This makes the value returned by malloc a pointer to int. After this denition, you can use
ptr as normal. For example,
pa[n]
is the nth element of the array. When you are nished with the memory, youll have to use
free(pa); to free it.
0.15

Command-Line Arguments
Now let us look at how to get arguments off a command line. This uses the second form of
main() shown at the beginning of the chapter. Command-line arguments are passed to a
program through two arguments to the main() function. The parameters, are called argc
and argv, are seen in the following main prototype:
int main(int argc, char **argv)
The argc parameter holds the number of arguments on the command-line and is an integer. It will always be at least one because the name of the program is the rst argument. The
argv parameter is an array of string pointers. All command-line arguments are passed to
main() as strings. For example:
#include <stdio.h>
int main(int argc, char *argv[])
{
int i;
for (i=1; i<argc; i++) printf("%s", argv[i]);
return 0;
}
This program will print out all of its arguments, starting with the program name.
0.15.1

A Program for Analyzing Serial Music

We will now use many of the C language tools learned in this chapter to create a fully functional program for analyzing serial music. Suppose we want to apply the rules of twelve-tone
music to create a matrix with all the serial forms that derive from a single tone row. The rst
row of the matrix will receive the original twelve notes, from left to right. The inversion of

41

An Overview of the C Language

the series will ll the rst column of the matrix, from top to bottom. To complete the matrix,
the other rows will receive transposed versions of the original series, each one starting with
the note in the rst column. When the matrix is complete, we will be able to read all the
transposition forms of the original series in the rows from left to right, the retrograde forms
in the same rows from right to left, the inversions in the columns from top to bottom, and
the retrograde of inversion forms in the opposite sense. Here are the steps that we will have
to take to achieve this:
1. As in previous examples, we will take pitch classes from 0 to 11, which form the original
series, from the command-line arguments, and place this series in the rst row of the matrix.
2. We will use two arithmetic formulae for the inversion and transposition of the original
series. The inverse form, appearing in the rst column, is given by
Pm,1 [(Pm1,1 (P1,m1  P1,m )] mod12

(1)

and, the subsequent transpositions are given by


Pm,n [(P1,n (Pm,1  P1,1 )] mod12.

(2)

3. We will then use a translation table to print the serial matrix.


Using many of the principles introduced earlier in the chapter, we can create a very compact
program. Note that we will not check for correct inputs (in the range of 011, but we will
apply mod12 to all input pitch classes) or repetition of notes (required by certain types of
serial music). Here is the program:
#include <stdio.h>
#include <stdlib.h>
int mod12(int note) {
while(note >= 12) note -= 12;
while(note < 0) note += 12;
return note;
}
int
main(int argc, char** argv)
{
int series[12][12], offset;
int n, m, i;
char* table[12]={"C","Db","D","Eb",
"E","F","Gb","G",
"Ab","A","Bb","B"};
if(argc != 13) {
printf("usage: %s note1 note2 ... note12\n", argv[0]);
return -1;

42

Chapter 0

}
/* loop until all available notes are entered*/
for(n = 0; n < 12; n++)
series[0][n] = mod12(atoi(argv[n+1]));
/* create inversion in column 1 */
for(m = 1; m < 12; m++)
series[m][0] = mod12(series[m-1][0] + series[0][m-1]
- series[0][m]) ;
/* create all transpositions */
for(m = 1; m < 12; m++)
for(n = 1; n < 12; n++)
series[m][n] = mod12(series[0][n] + series[m][0]
- series[0][0]);
for(m = 0; m < 12; m++){
/* print the pitch classes, row by row, using the
translation table */
for(n = 0; n < 12; n++) printf(" %s ", table[series[m][n]]);
printf("\n");
}
return 0;
}
This concludes our tour of C, which demonstrates how compact yet expressive and efcient it can be. As a complement, let us look briey at some elements of C.
0.16

Moving to CBB
The C programming language is a super-set of C. As such, it supports and allows all the C
coding we have done so far, but it introduces new elements and additional resources, which
hopefully make life easier for the programmer. There are many new features in C (in fact,
C is anything but a small language), only a few of which we will introduce here. We must
remember that if strict C is required, then we cannot use any C features in a program.
When writing pure C code, we should use the le extension .c; when writing C code,
we should use .cpp. This will avoid confusion and make sure that the building tools call
the right compiler (for C or for C).
0.16.1

Variable Declaration Anywhere in the Code

The C programming language allows variables to be declared only at the start of a code block
({ }), whereas C allows variables to be declared anywhere. For instance:

43

An Overview of the C Language

/* C example */
void func(){
int a,b,c;
a = g();
b = f();
c = a+b;
}
// C++ example
void func(){
int a;
a = g();
int b;
b = f();
int c = a + b;
}
0.16.2

Default Value for Arguments in Functions

In C it is possible to declare functions with default arguments:


float func(int a=0, int b=1, float f=0.5f);
float func(int a, int b, float f){
return a+b*f;
}
When called as in
a = func();
this function will return 0.5.
In order to use this facility, in the declaration, we dene which arguments are to have
default values by initializing them. In the function denition, we just implement the code
as before.
0.16.3

Memory Management: new and delete

C provides a simpler mechanism for memory allocation, which is perhaps less awkward
than malloc() and friends. This mechanism is implemented with two C operators: new
and delete.
The former is used to allocate memory for any built-in or user-dened variable type, as in
// memory allocation for float pointer variable a

44

Chapter 0

float *a
// ditto
int *b =
// ditto
MyStruct

= new float;
for int pointer variable b
new int;
for struct Mystruct pointer variable s
*s = new MyStruct;

Memory can be allocated for arrays, as in


int size = 512;
float *array = new float[size]; // 512 floats.
As this is a dynamic memory allocation, a variable can be used to determine how much
memory will be allocated. Only one dimension of a multi-dimensional array can be allocated
at a time. To free the memory, we use delete:
delete a;
For arrays, we use a slightly different form:
delete[] array;
0.16.4

Structures and Data Types

In C, when dening a structure, we are also creating a new data type, so we do not need
the typedef keyword to dene it. In fact, a struct is a special case of a programming structure called a class, which can also contain constructors and methods (functions that are members of a class). For the moment, it is sufcient to know that we can write
struct MyStruct {
int a, b;
float f;
};
// no need for typedef...
MyStruct obj;
obj.a = 1; // etc...
0.16.5

Line Comments

C also supports a single-line comment, using // as the rst characters in a line of code.
We have been using this type of comments in the examples above. This turns all the text on
the rest of the line into a comment (until a new line character is seen).
0.17

Data Abstraction
Data abstraction is another important element of programming that is enabled in a more
consistent way by C than by C. It consists of dening new data types that would model

45

An Overview of the C Language

somethingperhaps signal-processing components or unit generators. We will dene a data


type by deciding what it is made of (its member variables) and what it can do (its operations).
C structures provide good support for this type of programming. For example, we can
dene a structure to hold an oscillators data (the dataspace) and functions or methods (the
methodspace) as follows:
struct Osc {
// dataspace
float *table; // ftable
float phase; // phase offset
float ndx; // ndx
int length; // ftable length
int vecsize; // vector size
float rate; // sampling rate
float *output; // output audio block
// methodspace
Osc(float *tab, float ph=0.f, int len=def_len,
int vsize=def_vsize, int sr=def_sr);
~Osc() { delete[] output; }
float *Proc(float amp, float freq);
};
The structure holds all the data members that make up the oscillator, the data that needs to
be persistent (i.e. present in memory) between invocations of the process function. The
methodspace is made up of the bare minimum necessary: an initializing function (the constructor), the deallocating function (the destructor), and the oscillator function proper. The
constructor will always have the same name as the structure, and the destructor will have a
~ before the structure name. This is called automatically when the instance of the structure
is deallocated. In order to use this data type, all we need do is create one (using all the default
parameters):
Osc bing(table);
and the process function will generate audio, as in
output(fp_out, bing.Proc(0.5f, 440.f));
Since Proc() is a member of osc, it is accessed means of a .. Notice that in the present
design the function returns the audio block, which has been allocated inside the data type
by the constructor. Member functions such as those declared above can be dened inside
the struct denition (as in ~osc()) or externally. The syntax for the latter is

46

Chapter 0

struct_name::member_func()
For example, the constructor will be dened as
Osc::Osc(float *tab, float ph, int len, int vsize, int sr){
table = tab;
phase = ph;
length = len;
vecsize = vsize;
ndx = 0.f;
rate = sr;
output = new float[vecsize];
}
and the processing function will be
float *Osc::Proc(float amp, float freq) {
float incr = freq*length/rate;
for(int i=0; i < vecsize; i++){
output[i] = amp*table[(int)ndx];
ndx += incr;
while(ndx >= length) ndx -= length;
while(ndx < 0) ndx += length;
}
return output;
}
The above code demonstrates that functions belonging to a struct have access to their
local variables and to the member variables of the structure they belong to. This is very useful because table indexes and wave tables can be kept outside the function and separate from
the rest of the program. In addition, each instance of the Osc data type will have its own
individual member variables, and the user need not supply them to the processing function.
0.17.1

Function Overloading

Function overloading is another feature of C. It is possible to have functions with same


name in a class, as long as their arguments differ in type or in number. Function selection,
then, depends on argument types at the function call. It is possible to supply more than
one processing method to the structure, say to take in audio vectors for the amplitude and/
or frequency arguments (for audio rate modulation of these parameters). Because of overloading, it is possible to use the same name (Proc()) for these functions:

47

An Overview of the C Language

struct Osc {
// dataspace
float amp; // amplitude
float freq; // frequency
float *table; // ftable
float phase; // phase offset
float ndx; // ndx
int length; // ftable length
int vecsize; // vector size
float rate; // sampling rate
float *output; // output audio block
// methodspace
Osc(float amp, float freq, float *tab, float ph=0.f,
int len=def_len,int vsize=def_vsize, int sr=def_sr);
~Osc() { delete[] output; }
// fixed amp & freq
float *Proc();
// variable control-rate amp & freq
float *Proc(float amp,float freq);
// audio-rate amp
float *Proc(float *amp,float freq);
// audio-rate freq
float *Proc(float amp,float *freq);
// audio-rate amp & freq
float *Proc(float *amp,float *freq);
};
0.17.2

Usage Examples

xed parameters
Osc zing(0.5f,440.f,wtable);
for(...) {
sig = zing.Osc(); // 440 Hz 6dB sound
}
FM synthesis
Osc mod(ind*fm, fm, sintab);
Osc car(amp, fc, sintab);

48

0.18

Chapter 0

Data Hiding and Encapsulation


Another feature of C that can be useful for systems design is the support for data hiding
and encapsulation. This allows us to make certain bits of the data structure available only for
member functions, and not for functions that are external to the data structure. One reason
this might be useful is that we can keep things in a structure completely separate from the
rest of the program. We can then provide an interface for accessing only certain elements of
our data types, the ones we want to grant access to. This will provide a more robust data
type, which will be able to behave only in ways dened by us.
In a structure, all members are accessible from the outside: they are public by default. If we
use the keyword private, we can keep them from being accessed by external code:
struct Osc {
// dataspace is now private
private:
float *table; // ftable
float phase; // phase offset
float ndx; // ndx
int length; // ftable length
int vecsize; // vector size
float rate; // sampling rate
float *output; // output audio block
// methodspace needs to be accessible
public:
Osc(float *tab, float ph=0.f, int len=def_len,
int vsize=def_vsize, int sr=def_sr);
~Osc() { delete[] output; }
float *Proc(float amp,float freq);
};
We can still create an instance of our data type, because the constructor is public:
Osc ping(table);
But we cannot read or write to the member variables:
ping.ndx = 1; // not allowed, ndx is private
0.18.1

Classes

In C, a version of struct exists in which all members are private by default. It is called a
class:

49

An Overview of the C Language

class Osc {
// dataspace is private by default
float *table; // ftable
(...)
// methodspace needs to be accessible
public:
Osc(float *tab, float ph=0.f, int len=def_len,
int vsize=def_vsize, int sr=def_sr);
(...)
};
Classes and structures are basically the same thing, although the former is more commonly
used. They provide the support for a programming style called object-oriented programming
(OOP), in which the ideas discussed above in relation to data abstraction and data hiding
play an important part. In OOP parlance, we call the instances of a data type objects.
0.18.2

A CBB Program for Analyzing Serial Music

In this subsection, we will present a version of our serial analysis program using the concept
of data hiding and encapsulation. We will start by modeling the concept of a twelve-tone
series and the basic operations that we can apply to it:
class Dodecaphonic {
protected:
int series[12]; /* the tone row, hidden from outside */
int mod12(int note) { /* the modulus as an internal method */
while(note >= 12) note -= 12;
while(note < 0) note += 12;
return note;
}
public:
Dodecaphonic() { /* default constructor */
for(int i=0; i < 12; i++) series[i] = 0;
}
Dodecaphonic(int *notes) { /* constructor from an array */
for(int i=0; i < 12; i++) series[i] = mod12(notes[i]);
}
int get(int index){ return series[mod12(index)]; /* get & set
notes */
void set(int note, int index) {
series[mod12(index)] = mod12(note); }

50

Chapter 0

Dodecaphonic transpose(int interval); /* the three basic


operations */
Dodecaphonic invert();
Dodecaphonic retrograde();
};
/* defining the operations. All of them return a Dodecaphonic object */
Dodecaphonic Dodecaphonic::transpose(int interval){
Dodecaphonic transp;
for(int i=0; i < 12; i++)
transp.set(mod12(series[i]+interval), i);
return transp;
}
Dodecaphonic Dodecaphonic::invert(){
Dodecaphonic inv;
inv.set(series[0],0);
for(int i=1; i < 12; i++)
inv.set(mod12(inv.get(i-1) + series[i-1] - series[i]), i);
return inv;
}
Dodecaphonic Dodecaphonic::retrograde(){
Dodecaphonic retr;
for(int i=0; i < 12; i++) retr.set(series[i], 11-i);
return retr;
}
Notice that in this simple class we did not have to allocate new memory or do any extensive
initialization. Because of that, we do not have to dene a destructor (the compiler will provide a default one), and assignment (=) will work by default. In more complicated cases, a
copy constructor and an assignment operator would have to be dened for this to work
correctly. These issues, however, are beyond the scope of this chapter. Also, transpose()
and other manipulation methods do not modify the state of the object, but return a new
object with the desired state (this is a design decision; state-modifying methods could also
have been implemented).
With this class in hand, it is very simple to create a little program that will give us one of
the four versions of an original series (transposed, inverted, retrograde, inverted retrograde).
Because each operation returns a dodecaphonic object, we can operate directly on the
returned value, without the need to use a variable to hold it. For example,
b = a.invert(1); c = b.transpose();
can be written as

51

An Overview of the C Language

b = a.invert().transpose(1);
Here is a program that uses this class (the ellipsis is a placeholder for the class code dened
above):
#include <stdio.h>
#include <stdlib.h>
. . .
int main(int argc, char** argv)
{
Dodecaphonic row, res;
int interval, n;
if(argc != 14 || argv[1][0] != '-'){
printf("usage: %s [-oN | -rN | -iN | -irN] "
"note1 note2 ... note12\n",
argv[0]);
return -1;
}
for(n = 0; n < 12; n++) /* initialize the row object */
row.set(atoi(argv[n+2]), n);
switch(argv[1][1]){
case 'o': /* original transposed */
interval = atoi(argv[1]+2);
res = row.transpose(interval);
break;
case 'r': /* retrograde */
interval = atoi(argv[1]+2);
res = row.retrograde().transpose(interval);
break;
case 'i': /* inverted */
if(argv[1][3] != 'r'){
interval = atoi(argv[1]+2);
res = row.invert().transpose(interval);
}
else { /* inverted retrograde */
interval = atoi(argv[1]+3);
res = row.invert().retrograde().transpose(interval);
}
break;
default:
printf("unrecognized option \n");
return -1;
}

52

Chapter 0

for(n = 0; n < 12; n++)


printf("%d ", res.get(n));
printf("\n");
return 0;
}
0.18.3

Inheritance

Classes (and structures) can also be created from other classes by inheritance. This allows the
re-use of code ( rather than rewriting it) and the denition of a common interface for various
interrelated objects (all derived classes will share some code, especially functions). The syntax for deriving a class from an existing one looks like this:
class Osci : public Osc {
(...)
};
This makes Osci a derived class from Osc. All public members in Osc are also made public
in Osci. Thus Osci can access all the public members in Osc, but not the private ones. In
order to make private members accessible to derived classes, we must use the keyword
protected, which means private to the class and all its subclasses.
0.18.4

Virtual Functions

C has also a mechanism that allows functions to be specialized in subclasses. These are
called virtual functions. By using the keyword virtual for a function on the superclass, we can
make these functions overridable; that is, we can supply new code for their implementation:
class Osc {
(...)
virtual float *Proc(float amp,float freq);
};
The Proc() function can be then re-declared and re-implemented in a derived class (Osci,
say) to implement an interpolated lookup (instead of truncation):
class Osci : public Osc {
(...)
virtual float *Proc(float amp,float freq);
};
This allows a system to provide an interface for processing functions, which can be implemented by derived classes. The OOP term for this is polymorphism. An example of the use of

53

An Overview of the C Language

such a mechanism is found in the application programming interface for VST plug-ins,
which is written in C and is based on the derivation of classes from a base class.
0.18.5

Overloaded Operators

Similarly to overloaded functions, it is possible to dene overloaded forms of specic operators for a certain class denition. The operators =, +, -, *, /, <<, and >> can be overloaded.
For instance, we could set up the overloading of operator + for our Osc class as follows:
class Osc {
(...)
float *operator+(float val) {
// adds val to every sample in the output block
for(int i=0; i < vecsize; i++) output[i] += val;
// returns the audio block
return output;
}
};
Now using + with an Osc object and a float has a denite meaning: add that number to
every sample in the output audio block of the Osc object (and return a block of samples).
Here is an example of how it can be used:
Osc oscil(...);
float a = 1000.f;
float *buffer;
(...)
for(...) {
(...)
buffer = oscil + a;
(...)
}
0.19

Final Words
In this chapter we have focused on the aspects of C that should be useful for audio programming, and on almost the whole of the C language. We hope the chapter has served as a
quick refresher course if you are an experienced programmer and as an inspiring appetizer if
you are a beginner.
Clearly, C is much more extensive than C. C has its own powerful and robust standard library, the C Standard Template Library, which is beyond the scope of this chapter
but is well worth exploring.

54

Chapter 0

In the chapters that follow, you will nd both slow-cooked and fast-food audio
recipes in C and C that will sustain, nourish, and inspire the development of your own
unique audio and musical applications regardless of your level of expertise. Along the way,
you will learn to program in both C and C; at the same time, you will learn the fundamentals and secrets of digital signal processing, software synthesis, computer music, algorithmic composition, and digital audio.

Programming in C

Richard Dobson

1.1

Why C?
This section concentrates, as any rst section on programming must, on the essential features of the language used. However, unlike general books on programming, this book has a
specic focus, from the outset, on music and audio applications. A useful advantage of this is
that it gives an opportunity to address in very practical ways the question of why you need
to know this, or do that. We have all seen instruction manuals with statements such as to
decouple the bifurcated sprocket ange, use the long-handled dingbatwithout explaining
why, and in what circumstances, you would want or need to do it.
In each of the examples of C code that follow, some audio or music aspect is incorporated
or developed. This gives a clear focus, and I hope it will give at least partial answers to the
why question, but a pure presentation of the language runs the risk of concealing issues
that a more thorough theoretical approach might consider important. So the approach here
is based on the desire to get results, rather than on a foundation of theory; theoretical principles will arise rather as an emergent feature, on which a stronger emphasis is placed in later
sections. Nevertheless, the question Why C? is worth addressing at the outset.
The most obvious problem we face in programming audio processes is speed of computation. Many techniques, such as spectral transformation, have been known about for decades,
but only with computer processing speeds at the level they have reached in the last few years
has it been possible to use them in real time. Even now, ensuring that the code is as efcient
as possible is still of great importance, especially for real-time performance. It translates into
more of everything (more oscillators, more lters, more channels) and higher quality (better
lters, better reverbs, and so on). Fortunately for the purposes of this book, computers are
fast enough now that we can in many cases afford the luxury of code that is arguably inefcient to a degree, but demonstrates a process clearly. Nevertheless, throughout the book,
hints and tips on optimizing working code for maximum performance will be given; and
as your understanding develops, these techniques will gradually become part of your stock in
trade. They are typically based on knowledge of how compilers work, or how computers
work (e.g. how data is moved around in memory), or both. C programming does tend to
demand that, sooner or later, this knowledge will be essential.

56

Chapter 1

The C language has often been described as close to the hardware. Expressions in the
language can be compiled to very efcient machine code, exploiting the facilities of each
processor. Some idioms in C, such as the auto-increment and decrement operators and the
address-of and pointer operators, match machine-level instructions exactly. As a result, C
programs may run as fast as programs written in low-level assembler code. The complexity
of modern processors, coupled with a powerful code optimizer (an essential component of
any C compiler), can even mean that a compiled C program may be more efcient than a
skilled programmer can achieve working in assembler. However, these idioms presuppose
an understanding of how the processor handles instructions and data, and even of exactly
how that data is stored in memory. C does little to hide such things from the programmer,
and does even less to prevent you from writing code that can crash the program (and maybe
even the computer). Thus, C is a permissive languageit allows you to do things that it
would be a very good idea not to do.
One aspect of C that facilitates closeness to the hardware is, curiously enough, the complete absence, within the language itself, of anything to do with input and output. Text
input from a keyboard and output to a screen or printer depend on hardware, and usually
on the facilities of a supporting operating system. However, an embedded processor may
have neitherperhaps it is connected to transducers and a set of LEDs. All such facilities
are external to the language and have to be supplied by libraries, which now are highly
standardized and which a complete compiler package will always include.
This closeness to the hardware also has made the C language pre-eminent for the writing
of operating systems. C is associated primarily with UNIX and its variants. Many people
today maintain that the ideal platform for the development of audio applications is one or
other avors of UNIX ( including Linux and now Macintosh OS X) and its native C compiler.
That compiler will include a set of libraries supporting all aspects of the operating system,
including support for input and output devices of all kinds.
In view of the importance of speed for audio programming (and the need to be able to talk
directly to hardware such as A to D and D to A converters, MIDI ports, and so on), it is easy
to understand why C has become popular. As a result of its widespread use, a lot of expertise
and a lot of help are available, and there is a great mass of published source code (covering
the range from simple tools to advanced large-scale applications) to draw upon.
Despite being close to the hardware, C also has the characteristics of a high-level language. (High-level means close to the user; low-level means close to the hardware.) It
provides a variety of powerful and exible facilities for creating and managing large applications, while making it easy to write simple yet useful tools. Much that is very useful can be
done without the need for advanced programming skills. This power comes only partly
from the language itself, which supports a building-block approach to programming. The
rest comes from the development environment supporting the compiler itselfcopious messages from the compiler to help track down errors, and a powerful text pre-processor that,
though in principle external to the language, is in practice inseparable from it.

57

Programming in C

The popularity of C has stimulated the development of very sophisticated development


environments supporting many visual tools. Perhaps the most important tool is the
source-level debugger, which makes it possible to run a program a line at a time, watching
how the data changes. The debugger will run a program for you, and if the program crashes
it will automatically show you the line of code that triggered the crash, and give some indication of what the cause is (such as reading from non-existent memory). It is also possible to
make a program stop at a particular place in the code (setting a breakpoint), where again
you can inspect the state of all relevant data, and even change it before resuming execution.
But the compiler cannot tell you what to do to x a bug. Thus, knowing how to x problems (debugging) and, better, knowing how to avoid them in the rst place (defensive
programming) soon become central skills for the programmer to master. Audio programs
that depend on running at full speed for correct operation (e.g. because a process has to
respond to signals from the hardware) offer special challenges in this respect.
Other why questions arise from specic aspects of the language, or from the chosen
application domains of music and audio. These will be addressed within the context of each
topic or each code example. The reasons for coding one way rather than another may arise
not only from the requirement for speed, but also from the desire for audio quality, or simply
from the need for clarity of design, so that the code can be read six months later and still be
understood. Programmers rarely like to reinvent the wheel, so whenever they come up
with some code that might be of general use they may put a bit of extra work into it to
make it as reusable as possible. At other times, code is written a particular way to facilitate
testing and debugging. One may also encounter discussions about ugly code and beautiful codethere is a strong aesthetic element to programming, as is true of most technical
disciplines.
In short, we want to write code that works well, that makes sense when we read it, that is
easy to modify and extend, and that we may be able to use again in other projects. You will
nd that documenting your code becomes at least as important as writing it. Even excellent
code is of little use if you dont know how to use it.
The main point to carry with you as you read this chapter concerns the use of libraries. For
writing audio applications, we cannot avoid the use of libraries; indeed, we will be very grateful for their existence, as they remove from us a great deal of hard work, and make possible
what would otherwise be impossible. This will become very much clearer as you work
through the chapters in this book. The question What is a library, exactly? has not been
answered yet, but it will be answered soon. Indeed, you will nd, almost by accident, that
as you work through the book you are in a real sense creating your own library; and all it
takes to complete that process is use of the standard library-maker tool (not a technical
term) that is part of the compiler. All you need know at this stage is that a library stores sections of code (those building blocks mentioned above) that you expect not to change and
you expect to use frequently. As you will soon discover, a modern C compiler includes a
large number of standard libraries, and in order to use them to the full you will have to be
able to read and understand the documentation.

58

Chapter 1

1.1.1

Your First C Program

Before proceeding, it is worth looking at appendix A (which deals with the compiler and
command-line tools) so that you have an idea of the programming environment we will be
using. These early examples assume a traditional command-line environment. While all the
example programs presented here are available on the DVD as complete projects, it is preferable that you practice typing in the code by hand, especially if you are unpracticed at typing.
The text editor, used to write and edit programs, is of supreme importance, so the sooner you
get familiar with it the better.
Listing 1.1 is a faintly musical version of the classic Hello World program that everyone
traditionally writes rst when learning a new language. Although it is extremely short, it
illustrates many of the fundamental aspects of C described above. Listing 1.1 includes line
numbers (starting from 1) in the left margin for convenience. These are not actually part of
the program codeall text editors used for writing programs will display, somewhere on the
screen, the line containing the typing cursor.
Listing 1.1
1
2
3
4
5
6
7
8
9
10

/* wonder.c:displays "What a Wonderful World!" in the terminal */


#include <stdio.h>

int main()
{
printf("What a Wonderful World!\n");
return 0;
}

The rst thing to notice about this example is that there is a lot of what is called white
space: there are empty lines, and some lines are indented. As far as a C program is concerned, all this white space is almost always unimportant. There are situations where the
presence or absence of white space is meaningful (syntactically signicant), but a lot of
the time we can use it freely, to lay out the code as we wish.
Line 1 comprises a text comment. You could delete this whole line and the program would
still compile. Comments are ignored by the compiler; they are there purely for the benet of
the human reader. This comment simply identies the name of the le and its purpose. A
comment in C is anything placed between the /* and */ character pairs (there must be no
space between the * and the /). These are characters that cannot legally occur together as C
code. There is no requirement for the comment to t on a single line. The lines below
equally form a legal comment:

59

Programming in C

/* This is a comment,
running over
several lines.
*/
Apart from their primary use to add annotations to source code, comments are often used
to prevent lines of code from being compiled, without having to delete them. There are
other, arguably better ways of doing this, but the practice is widespread. Depending on
your text editor, text inside a comment may be drawn in a special color, to make it easy to
see where the comment starts and ends. If this is the case, try deleting the closing */ and see
what happens.
Some programmers like to write elaborate titles and descriptions at the head of a source
le, as in the following example:
/********************* WONDER.C
/*
*
* Version 1.0 RWD 10/1/2002.

***************/

********* DESCRIPTION *********


*
*This program writes a friendly and affirmative message to the user.
* TODO: Add more lines from the song.
*/
/**** BUGS AND LIMITATIONS *****
* No bugs found.
* It would be good to have the computer sing the text.
*
*/
There is no need to type this in (hardly necessary for a program as simple as this); but make
sure you can see how many separate comments there are, by looking for the matching /* */
pairs. Note that you are not allowed to nest comments (putting one /* */ pair inside another
one). Such descriptive blocks are often used to carry copyright or licensing information. The
rest of the text comprises C code to be compiledthis is where we have to get everything
right.
Skipping line 2 for a moment, we see a block of code extending from line 5 to line 9. This
block denes a function. The function is the primary building block used in computer programs. Here we have just one function, called main. This is the one function a C program
must contain in order to be compiled as an executable program. You are not allowed to
have duplicate functions (i.e. with the same name) in C. The term function comes from
mathematics and is used to describe some procedure that takes an input (consisting of one
or more numbers) and produces an output (also consisting of one or more numbers). The

60

Chapter 1

denition of a function must allow for the possibility of either or both input and output. In
this case, the main function outputs a single integer number, indicated by the type specier
word int in line 5.
In the code, int is an example of a C keyword, a word that belongs to the language, and is
therefore reserved (you cant use it as the name for anything else). It is one of a collection of
keywords that dene basic numeric types (see section 1.2). Another keyword in this program
is return, used in line 8.
After the function name main there is a pair of parentheses. These provide a space for any
input data. In this case there is no input, so we can just write the parentheses by themselves.
Later, we will see alternative ways of dening the main function to enable it to receive input
from the user.
So far, the main function looks as follows:
int main()
But this is incompleteit does not dene what the function does. This is the job of the
functions body. The body of a function is dened by code placed between braces, as in this
example:
{
return 0;
}
Now the complete structure of the main function looks as follows:
int main()
{
return 0;
}
You can, in fact, replace all of lines 59 with the above, and the program will compile. It just
wont appear to do anything. However, it is actually performing one task. The main function
is dened as producing an integer result; and the one line of code shown above does this: it
causes main to output the value 0. To do this we are using the C keyword return, followed
by the required value. In the jargon of C programming, the return value of main() is 0.
Overall, this line of code forms a C statement. As was shown above, a C statement is terminated by a semicolon. Where to put a semicolon is one of the aspects that most often confuse beginning C programmers. As you study the code examples in this book, take careful
note of where a semicolon is required and where it is not. You will see that the code inside
the main function comprises one or more statements, each ending in a semicolon (look back
at listing 1.1), but the function body (dened by braces) is not followed by a semicolon.
What happens to the value returned by main()? In the majority of cases it is just ignored;
however, where programs are run remotely, from a UNIX shell script or from a DOS batch
le, it is possible to read the return value, and to take action accordingly. Typically, a return

61

Programming in C

value of 0 means that the program terminated without errors, and anything else means
that some error occurred. For example, if a program requires a le to be opened, but the
name given is incorrect (e.g. the le doesnt exist), you can trap this in the program and
have main() return some other value, such as 1. You will see as you work through later sections of this chapter that this principle is entirely generalthe return value from a function
may be something you use elsewhere, or it may be an indication of success or failure.
The substance of listing 1.1 is found in line 7:
printf("What a Wonderful World!\n");
This is the line of code (a single C statement, ending in a semicolon) that does something
useful: it writes a message to the console. The presence of a matching pair of parentheses
looks very like the format of the main() function just describedand indeed this statement
does contain a function, whose name is printf, which is shorthand for print with formatting. Inside the parentheses is the text to be displayed (referred to by programmers as a
string, or sometimes a character string), within double quotation marks. In the jargon of programming, this string is the argument to the function printf, and overall the statement constitutes a function call.
The nal two characters in the message string may be unfamiliar. They relate to the formatting aspect of the printf function. The rst of the two characters is a backslash. In the
context of a character string, this is called the escape character. It signies that the following
character is not to be written literally to the console, but is instead a control instruction to
arrange the text in a particular way. In this case the control character is n, which signies
new line (sometimes also called carriage return and line feed). To see how this works,
you can delete these two characters, re-compile and run the program, and see what happens.
In line 7, the control characters are at the end of the line. However, they could be placed
anywhere in the string. You can even write a string that contains only the control characters,
and you can write them as many times as you like. For example:
printf("\nThe Hills are alive,\nWith the Sound of Music!");
printf("\n");
We will see in later sections that printf is even more versatile than this. It can print numbers too, and control characters are available to organize numbers with respect to spacing,
number of digits, and even to write them in technical formats such as hexadecimal.
1.1.2

Library Functions and the #include Directive

Two questions remain unanswered. Previously, it was stated that the C language does not
contain a print command, yet here we are using one, called printf. Where does it come
from? And what is the meaning of line 2, which we so casually skipped over earlier?
The printf function is an important example of a C library function, in this case supporting text output to the console. Most important, it is a standard library function that you

62

Chapter 1

can expect any C compiler to include. But as it is not part of the C language itself. The compiler has to be given information about itits name, its inputs, and its outputs. This information takes the form of a declaration. We will discover how a declaration is written in a
later section. The important thing to grasp at this stage is that the declaration of printf is
contained in a text le called stdio.h (the le extension .h is used by convention to show
that the le is a C header le), and is incorporated into your program by a preprocessor
command (directive). This is the purpose of line 2 in listing 1.1. Your code le is passed
to this preprocessor before being passed to the compiler itself. The preprocessor strips out
all the comments, and can also be asked to perform text substitutions, or to add text from
another le, using the preprocessor directive include. All preprocessor directives are indicated by the initial #, followed by the name of the commande.g. #include. The whole
directive is then followed by the name of the required le, enclosed in angle brackets:
#include <stdio.h>
Although by convention the command immediately follows the character # without an
intervening space, this is not mandatory. The spacing
# include <stdio.h>
is legal.
When the compiler is installed on your system, a large number of header les are stored in
a special include directory. The compiler thus knows about this directory, and will automatically search it to nd the requested le. The use of the angle brackets, as shown above,
ensures that this directory is searched rst for les. An alternative notation using double quotation marks asks the preprocessor to search the directory containing the current le, before
searching further:
#include "myfuncs.h"
This would be used typically for local include les that are specic, and usually private, to
a project. You will see plenty of examples of this in later sections, especially in projects
involving multiple source les.
When your program is compiled, the compiler also looks for the code for printf in a
library le in a lib directory, and links that code with yours to create the nal executable. In
subsequent sections, you will be introduced to many more standard C functions that are
required to be supported by all C compilers.
1.1.3

Exercises

Exercise 1.1.1
Create a new program, perhaps called music.c (if you are using an IDE, you will also have
to create a project for it called music), and write a new message of your choice. Add some
informative text inside a comment block at the top of the le.

63

Programming in C

Exercise 1.1.2
Find out what happens (i.e. what messages the compiler reports) if you
(a) leave out #include <stdio.h> (you can do this without deleting text, by commenting
out the line using /* and */)
(b) leave out the angle brackets
(c) swap the braces and the parentheses
(d) leave out one or both of the semicolons
(e) replace the rst semicolon with a comma
(f) leave out the return statement
(g) leave out the nal brace.
1.2

Adding Something of Your OwnAn Introduction to Variables and Types


1.2.1

Basic Arithmetic in C

In this section we will see how to perform basic arithmetical calculations in C. To do this, we
need a means of representing numerical values, an idea of how the computer stores them,
and a means of performing arithmetical operations on them. This may already sound complicated, but fortunately the way arithmetical expressions are written in C is very similar to
how they might be written down on paper. Whether using paper and pencil or writing a
computer program, in order to work anything out we need to be able to represent numbers
by name. For example, you probably will recognize this simple calculation for nding the
area of a rectangle:
area = width * height
By convention, an asterisk is used to signify multiplied by or times. We replace width
and height with numbers and perform the calculation, the result of which is area. The line
above is a simple arithmetical statement. To make it a complete C statement, we must add a
semicolon:
area = width * height;
Note the use of appropriate words, rather than arbitrary letters or symbols. The names make
it completely clear what the task is, and thus the code is self-documenting.
1.2.2

Expressions and Statements

It is very important to understand the difference between expressions and statements. An


expression in C is anything that can be evaluated (i.e. that has a numerical result)for

64

Chapter 1

Table 1.1
Binary operators.
Operator

Example

Description
Assign the value of b to c.

Assignment

c = b

Addition

c = b + a

Add a to b and assign this value to c.

Subtraction

c = b - a

Subtract a from b and assign this value to c.

Multiplication *
Division
/

c = b * a
c = b / a

Multiply a and b and assign this value to c.


Divide b by a and assign this value to c.

Modulo

c = 12%11

Result after integral division. The value of c is 1.

example, the right-hand side of the calculation above, here enclosed in parentheses to make
the point clearer:
(width * height)
Expressions can contain expressions, for example
(100 + (width * height))
Ultimately, even a single number is an expression, evaluating to itself. A statement is any
legal expression, or combination of expressions, possibly combined with one or more C keywords, and terminated by a semicolon. Indeed, the semicolon by itself is a legal C statement,
and this fact commonly gives rise to errors when a semicolon is used by mistake. Normally, a
statement changes something; typically, a variable acquires a new value, or the ow of control through the program is changed.
An expression can also be a statementfor example,
width * height;
This is, however, useless, as the value of the expression is ignored.
1.2.3

Operators

As was noted above, an asterisk is used to indicate multiplication. In C terminology, it is an


operator. More specically, it is a binary operator, as it operates on two numbers (or expressions). C possesses a large number of operators, including ones for the basic arithmetic operations, identical to the usual algebraic symbols (see table 1.1).
Both + and - can also be use as unary operators, though in practice only - has to be
used explicitly:
val = -64; /* unary negation */
Subsection 1.2.9 describes the way C decides in what order these operations are carried out
when an expression incorporates several of them.

65

Programming in C

1.2.4

Numeric Types

We are already remarkably close to performing a calculation in C, but some information the
compiler needs is missing. It needs to know what sorts of numbers we are using so it can
store them correctly and so it can perform the correct kind of processing on them. The two
most important classes of numbers are integers (1, 0, 1, 2, 3, . . .) and oating-point numbers
(e.g., 3.14159, 0.707, 1.414).
C includes a wide range of integer types (described in subsection 1.2.5), but the most frequently used has the keyword int. There are two oating-point types: a float occupies four
bytes and is described as a single-precision number, and a double usually occupies eight bytes
and, as its name suggests, is a double-precision number.
To complete the statement above in C code, we must dene each of our variables in terms
of a numeric type:
double area;
double width;
double height;
These declarations tell the compiler to set aside some memory to hold each variable, and
dene what type each one is. In the next line, we put some numbers into the variables:
width = 10.0;
height = 21.5;
The semicolon is ubiquitous: a variable declaration is a statement, as is an assignment
a statement using the operator =, properly called the assignment operator. In programming
jargon, we would say that the value 10.0 is assigned to the variable width. It is also common
to refer to the right-hand side of an assignment statement (the right-hand side has to be
an expression, therefore) signifying a possibly elaborate calculation that produces a result
that can be stored on the left-hand side.
Why call area, width, height, and so on variables? We can assign a value as often as
we like to a variable, like so:
width = 10.0;
width = 0.0;
width = 1000.3;
However, all that is remembered is the last assignment. So literally, the contents of the variable width can vary over time. We can also assign the value of one variable to another:
height = 21.5;
width = height;

/* a square! */

Most important, we can perform the full calculation, which is where we started1:
area = width * height;

66

Chapter 1

1.2.5

IntegersA Special Case

The examples above used double variables, which ensure a precise result for our calculation. It would also be possible to dene all three variables above as integers instead, by using
the type keyword int:
int area;
int width;
int height;
However, we cannot store fractional (oating-point) numbers in such variables, only whole
numbers:
width = 20;
height = 7;
area = 21.5;

/* compiler will issue a warning here! */

The particular quality of integer calculations emerges when we try to perform division:
area = 26;
height = 10;
width = area / height;
Here width receives the value 2, whereas the use of oating-point variables would result in a
more accurate fractional value of 2.6.
Historically, the integer is the oldest computer data type, and oating-point calculations
had to be performed by means of much arcane code and were therefore extremely slow.
Even today, many digital signal processors used for audio use integer calculations, which
can be performed very rapidly and which offer the highest precision for a given storage size.
1.2.6

Sizes of C Numeric Types

One other important aspect of integers that the programmer has to consider is that there is a
limit to the size of the number that can be represented, depending on the number of bits
used. A 16-bit integer can store numbers from 0 to 65,535 (65,536 numbers in all). Therefore,
no negative numbers can be represented at all. However, a long-established system called
twos complement enables signed numbers to be stored by treating the most signicant
(leftmost) bit as a sign bit. In the case of a 16-bit integer, the signed range is from
32,768 to 32,767; this is how 16-bit audio samples are stored.
A 32-bit integer can similarly be signed or unsigned. The numbers are, of course,
much larger: the signed 32-bit range is from 2,147,483,648 to 2,147,483,647.
So important are integers that a C compiler supports several type (shown in table 1.2,
which assumes a typical 32-bit platform). Note that both signed and unsigned forms

67

Programming in C

Table 1.2
Integer types in C. *The int type is dependent on the CPU integer size.
Integer

Number

type

Size

of values

char

1 byte

28

Minimum
Signed
Unsigned

short

2 bytes

16

long

4 bytes

2 32

int

Maximum
128

127

256

32,768

32,767

Unsigned

65,536

Signed
Unsigned

2,147,483,648
0

2,147,483,647
4,294,967,265

Signed

Signed

Unsigned

*
0

are explicitly supported. Signed is the default for all numeric types, so it is only necessary
to use the unsigned qualier when such a form is required. For 16-bit DOS and Windows
(8086 CPU family), an int is 2 bytes (equivalent to short), whereas on 32-bit architecture
(MC68000, PPC, Pentium) an int is 4 bytes (equivalent to long). Thus, using the int type
is unsafe if a specic numeric range is required. In this case long or short should be used.
On 64-bit platforms, int will similarly be 64 bits, and the same caveat applies. Use of the
name long long is already widely established to indicate a 64-bit integer type, though not
yet standard. Strictly speaking, the C standard allows considerable latitude in dening the
sizes of numeric types (as they will have to reect the nature of the hardware). For example,
C only mandates that a short will not be larger than an int and that a long will not be
smaller than an int. Thus, it is theoretically possible (though very unlikely these days) for a
short, an int, and a long all to be the same size.
1.2.7

The sizeof() Operator

Sometimes it is important to be able to nd the size of a type, such as a numeric type, programmatically. This is especially important where code is intended to run on multiple platforms. C provides the sizeof() operator for this purpose. It has the appearance of a
function call (where the name of the type is the argument), but in fact it is a C keyword. It
returns the size of the type in bytes:
int intsize = sizeof(int);
/* may return 2 or 4, depending on the machine */
We will introduce an important use of the sizeof() operator in section 1.5, in connection
to user-dened data types.2

68

Chapter 1

1.2.8

A Musical Computation

The task here is fairly simple but useful, and just complicated enough to justify a computer
program: to convert a MIDI Note number to the equal-tempered frequency in hertz. We will
develop the task on paper rst, so to speak, and then see how it would appear as a C program.
First we need some background information. MIDI Note data values use an unsigned
seven-bit integer range, i.e. from 0 to 127 inclusive. These numbers have to embrace key
numbers, various controllers (including Pitchbend and Modulation), Key Velocity and Pressure, and so on. So converting MIDI values into other things is going to be a very common
operation.
Middle C on the piano keyboard is dened to be MIDI Note 60. Given the international
tuning standard, in which Concert A 440, and assuming equal temperament (E.T.), middle
C has a frequency of approximately 262.626 Hz. An octave contains 12 semitones, so that C
an octave below middle C would correspond to MIDI Note 48. The full theoretical MIDI
range 0127 (not known to be covered by any keyboard yet made) extends ve octaves
below middle C (to MIDI Note 0) and almost six octaves above it.
An octave jump corresponds to a frequency change by a factor (or ratio) of 2: doubled for
an octave rise, and halved for an octave fall. So an octave below Concert A would have the
frequency 220 Hz. A further octave fall halves the frequency again, to 110 Hz. A single semitone rise corresponds to the (approximate) value 1.0594631, which is (approximately) the
twelfth root of 2 (a semi-tone only in E.T. 12 tuning). That is to say, multiply 1.0594631
by itself twelve times and you should get the value 2.0. This operation, known as raising to a
power, is notated mathematically as 1.059463112 .
To ensure the best possibly accuracy, we can use the computer to calculate the semitone
ratio itself. Recall the rules of indices, according to which a power of 1/2 corresponds to the
square root and a power of 1/12 corresponds to the twelfth root. So we might notate this as
p
(1)
semitone_ratio 12 2.
The two numbers 2.0 and 1.0594631 (or better, its computed value semitone_ratio) are
all we really need to calculate any equal-tempered 12-tone interval, given a starting base
frequency3.
For example, calculate the frequency of the C sharp above Concert A in Equal Temperament. Concert A is 440, and by denition C sharp is four semitones above A (major third),
so the calculation is simply
Csharp 440  (1.05946314 ) Q 554.365.
The one other piece of numerical information we need is the frequency of the lowest note,
MIDI Note 0. This is ve octaves below middle Cwell below the audible range. An octave
drop is a factor of 0.5, so a ve-octave drop requires the value 0.5 to be multiplied by itself
ve times: 0.55 . This gives 0.03125. Multiply this by the frequency of middle C and we get
the frequency of the MIDI Note 0, or C0. As shown above, each rise of a semitone involves a

69

Programming in C

multiplication by 1.0594631. So to nd the interval between MIDI Note 0 and MIDI note 73
(which happens to be the Csharp calculated above), we rst multiply 1.0594631 by itself
73 times: 1.059463273 . We then have to multiply the base frequency C0 by the result of
this calculation to obtain the required result. It is now time to look at listing 1.2, where the
full computation is performed, and the result printed to the console. If you type this in and
compile it, you could call it midi2freq.c, and the program itself as midi2freq.
Listing 1.2
Calculate the frequency of a MIDI note:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

/* listing 1.2. Calculate frequency of a MIDI Note number */


#include <stdio.h>
#include <math.h>
int main()
{
double
double
double
double
int

semitone_ratio;
c0; /* for frequency of MIDI Note 0 */
c5; /* for frequency of Middle C */
frequency; /* . . . which we want to find, */
midinote;
/* . . . given this note.
*/

/* calculate required numbers */


semitone_ratio = pow(2, 1/12.0); /* approx. 1.0594631 */
/* find Middle C, three semitones above low A = 220 */
c5 = 220.0 * pow(semitone_ratio, 3);
/* MIDI Note 0 is C, 5 octaves below Middle C */
c0 = c5 * pow(0.5, 5);
/* calculate a frequency for a given MIDI Note Number */
midinote = 73;
/* C# above A = 440 */
frequency = c0 * pow(semitone_ratio, midinote);
printf("MIDI Note %d has frequency %f\n,"midinote,frequency);
return 0;
}

You can test this program by changing the number assigned to midinote, re-compiling,
and running it. (Sections 1.3 and 1.4 explain how to avoid this tiresome process.)
As expected, there is more code here than in listing 1.1. Nevertheless, the layout of the
program follows the same structure. First, in line 3, a new header le, <math.h>, is included.
This is required to supply the denition of the C library function pow() that is used in the

70

Chapter 1

program. This is the function that raises a number to a power. While you study the details of
the code, notice the comments (inside the /* */ pairs), which describe both the purpose of
the program and what the various lines of code do.
Lines 711 contain declarations of variables, using names that help describe what the code
is doing. Two numeric types are used: the double and the int. These declarations must
appear before any other lines of C code appear.4 The compiler reads the code from the top
down, and needs to know about all these variables, and set aside storage for them, before it
can process code that uses them. You will nd that C++ is more exible, and that it allows
you to declare a variable anywhere, but in C they must all be declared rst, as here.5 Notice
that each declaration is terminated, as usual, with a semicolon.
Because the variables are declared inside the main() function, they are described as local
to that function, or simply as local variables. The alternative is to declare variables outside
main(), after the preprocessor #include statements. Such variables are then global
accessible anywhere in a program. This is generally regarded as a bad idea, as it can be very
difcult to control how they are modied. (For more on this, see section 1.3.)
Each variable is identied by a unique name. There is a lot of exibility allowed in creating
names. Essentially any printable character can be used, as long as it is not otherwise used by
C. This excludes characters such as *, /, -, %, &, and ? (in fact most of the pictorial characters), but it does include the underscore, which is very useful for creating multi-word names,
given that a name cannot incorporate any white space. Also, numeric characters are allowed,
but not as the rst character of a name. So the name 5c would be illegal. It is also illegal to
use a C keyword as a variable name:
int return; /* illegal name! */
You might experiment by deliberately writing illegal names and re-compiling to see what
error messages the compiler gives you. Sometimes error messages are a bit obscure, and it is
very useful to know what they most likely refer to.
Some programmers like to create extremely long names more akin to descriptive commentsfor example,
int an_extremely_long_variable_name_representing_a_MIDI_note;
This can get out of hand very quickly, however, and a good rule of thumb is to try to avoid
names longer than 32 characters or so. It should still be practicable to use a range of expressive names.
Lines 1519 perform the essential intermediate calculation steps described above. They
demonstrate three slightly different ways of using (calling) the pow() function, and in so
doing they reveal a lot about how expressions are handled in C. Inside the parentheses, this
function requires two oating-point numbers: the base number and the number to which
it is to be raised. In the header le <math.h> this function would be declared as follows:
double pow(double base, double power);

71

Programming in C

This shows that the function takes two arguments (dened inside the parentheses, and separated by commas) of type double, and returns a value also of type double. The names
shown above (base and power) are optional (and ignored by the compiler), but are
clearly useful for documenting what the function does. The minimum version of this declaration would therefore be
double pow(double, double);
You can add this line to the list of variable declarations (say into line 11), and then remove
line 3, where <math.h> is included, and the program will compile as before. It is worth looking at the use of this function in listing 1.2 in more detail. Line 15 calls the pow() function
in almost the simplest way possible:
semitone_ratio = pow(2, 1/12.0);
The result or output from the function is assigned to the variable semitone_ratio using
the assignment operator. As we know, everything on the right-hand side of the equals
sign must be an expression that evaluates to a value. That value must be in the form of one
of the legal C types (here, a double). It follows that the type on the left-hand side of the
equals sign must be the same, as it is here. Thus the statement
semitone_ratio = "twelve";
would be illegal, because it tries to make a double equal to a character string, which is obviously impossible. It would be agged as an error by the compiler, probably using the term
type mismatch.
Inside the function call, the rst argument is a simple number, 2. To C this is actually an
integer (int type), but as the required argument is a double, which is another number type,
C is happy to make a conversion automatically. (For more about converting numeric types,
see subsection 1.2.10.) This conversion does take a little time to do, and to eliminate that
you could add a decimal point and a zero to 2:
semitone_ratio = pow(2.0, 1.0/12.0);
C will recognize 2.0 as a oating-point number, and will automatically treat it as a double.
The second argument in this call to pow() is not a simple number but a simple arithmetic
expression, 1/12.0. Because no variables are used, only explicit numbers, the compiler will
usually make this calculation itself, rather than creating code for it. The use of at least one
decimal point ensures that the calculation is performed in oating-point fashion. Writing
1.0/12 would be just as good. However, if you had typed the line as
semitone_ratio = pow(2.0, 1/12);
the numbers 1 and 12 would be treated as integers, and the expression 1/12 would evaluate
to 0 (the result of integer division of 1 by 12), which is clearly not going to work as intended.
However, it is legal code, and the compiler probably will not warn you about it. All the

72

Chapter 1

compiler can assume is that that is what you meant in the rst place. You will not be aware
that something is wrong until you run the program.
In line 17 the rst argument is not an explicit number, but a variable to which we have
previously assigned a value:
c5 = 220.0 * pow(semitone_ratio, 3);
In this case, the call to pow() is itself part of a more lengthy expression, the value of which is
assigned to the variable c5. Line 19 shows pow() called with two numeric arguments (the
second integer again converted by C into a double), and line 23 shows it called with two
variables.
Line 25 is the same printf function introduced in listing 1.1, but here demonstrating one
of its primary uses: to write formatted numeric data to the console. Formatting means
many things; in this context it simply means embedding numbers of various types inside a
text string, the numbers being obtained from variables within the program.
In addition to the message string, two further arguments are given: midinote and frequency. Inside the message string, you will see two occurrences of %. This has the special
meaning for printf of signifying the start of a formatted conversion. For each argument
following the message string, we insert % into the message at the required position, followed
in each case by a letter indicating the type of the argument. A d or an i indicates an integer
(d stands for decimal or denary), and an f indicates a oating-point number. There are
many other possibilities. The rst % conversion uses the rst argument (following the initial
message string), the second conversion uses the second argument, and so on:
printf("%d o'clock %d o'clock %d o'clock rock,\n," 1, 2, 3);
1.2.9

Initializing Variables and Reducing the Line Count

In listing 1.2 each variable is declared on a separate line. For just four variables this does not
really matter, but a larger program may involve dozens or even hundreds of variables, making
the code very long. C allows variables of the same type to be listed together, separated by
commas:
double semitone_ratio,c0,c5,frequency;
You lose the scope for adding comments about each variable doing this; the use of expressive
names can compensate to some extent, but most programmers tend to mix multiple declaration lines like the above with individual lines where comments are needed.
It is also possible to give a variable an initial value when it is declared:
int midinote = 73;
Not surprisingly, this is called initialization. It can be applied to some or all variables in a
multiple declaration statement:

73

Programming in C

double semitone_ratio = 1.0594631, c0 ,c5, frequency = 440.0;


Moreover, in an initialization, the right-hand side can even be an expression:
double semitone_ratio = pow(2, 1/12.0);
Note that this requires that the pow() function has already been declared, as it would be if
the library header le math.h is #included, as shown in line 3. Try rewriting listing 1.2 to
make these changes.
A numeric expression can be used anywhere a simple number can be used. This means,
taken to the extreme, that the whole calculation of listing 1.2 can be compressed into a
single expression, eliminating at a stroke three variables:
frequency = (220.0*pow(pow(2,1.0/12),3)*pow(0.5,5))
*pow(pow(2,1.0/12), midinote);
This has reduced the code to almost the theoretical minimum, but at high costit is far
less understandable much more difcult to modify, it is more likely to lead to errors, and it
is less efcientthe expression pow(2, 1/12.0) is calculated twice. There is no virtue in
trying to make the code as compact as possible if it merely increases the chances of making
mistakes, or if it makes the code harder to understand. The goals of writing code are not
only to be correct and to be efcient, but also to be clearyou need the code to be as selfexplanatory as possible. You may understand it now, having just written it, but not when
you go back to it six months later.
1.2.10

Operator Precedence in Numeric Expressions

As a C expression can combine several calculation steps, C denes strict rules for deciding
in what order to carry them out. These rules are based on the principle of operator precedence. Operations with high precedence are performed before operations with lower precedence. Multiplication and division have higher precedence than addition and subtraction
and thus will be performed before addition and subtraction. The statement
float val = 15 + 440 * 2.5;
will evaluate to 1115.0, as if the expression on the right-hand side were written as
float val = 15 + (440 * 2.5);
Where two operations have equal precedence level, C has to have a rule to decide which to
perform rst. This rule is that operations are performed from left to right. In programming
terminology, the operation associates left to right. In the statement
frequency = 210.0 / 3 * 4.0;
the result will be 280, not 17.5, as the division is performed rst.

74

Chapter 1

In the case of addition, the order of evaluation will not matter: 200 + (10 + 20) is the
same as (200 + 10) + 20. But this is not the case for subtraction:
midinote = 200 - 10 - 20;
Depending on which subtraction is performed rst, midinote will equal 170 or 210.
Again, the operations are performed in order from left to right. The statement above will
accordingly evaluate to 170, as if written as
midinote = (200 - 10) - 20;
If in doubt, use parentheses to make it clear in what order you want the operations carried
out. Parentheses effectively have the highest precedence of all. Many expert programmers
routinely write complex expressions of this kind, without parentheses, as they know what
the evaluation order will be. On the one hand this is entirely reasonable, especially for simple expressions, but on the other hand adding parentheses even where technically they are
not necessary can help to document the code (e.g. to indicate that certain groups of numbers
or expressions relate to each other) and to prevent mistakes.
While all the arithmetic operators associate left to right, the equals (assignment) operator associates right to left. This makes it possible to write statements such as
int
a =
b =
c =
a =

a,b,c;
0;
1;
10;
b = c;

The statement a = b = c; is equivalent to the following two lines:


b = c; /* b now has the value 10 */
a = b; /* a now has the value 10 */
1.2.11

More on Type Conversionthe Cast Operator

In listing 1.2 and in the description that followed it, we have seen two examples in which a
number of one type (e.g. an int) was automatically converted to another (e.g. to a double).
The most explicit example occurs in line 23:
frequency = c0 * pow(semitone_ratio, midinote);
How do we know a type conversion is happening here? First, the function pow() is dened
as having two arguments of type double. However, in the function call above, the second
parameter is in fact midinote, declared as an int. In this case the compiler is automatically
inserting a conversion process known as a cast. It is also possible to specify the cast explicitly
in the code:

75

Programming in C

frequency = c0 * pow(semitone_ratio, (double) midinote);


Thus the cast operator consists of a type specier enclosed in parentheses, placed directly in
front of the number or expression to be converted. It has a higher precedence than the operators *, /, and %. Note that the cast embodies a hidden copy; the type of the object being
cast does not change.
Casting an int to a double is a no-loss change, as this data type can hold much larger
numbers than any of the standard C integer types. This process is often described as promoting the number. Casting a double to an int is a far more drastic change:
int ival;
double dval = 21.5;
ival = (int) dval;
The cast to an int has the effect of removing the fractional part of the double, so that ival
receives the int value 21 in the above example. Also, the 64-bit double can represent numbers outside the range of a 32-bit int. As a result, any conversion from a oating-point value
to an integer, if performed without an explicit cast (and, more generally, any conversion
from any numeric type to a smaller onea demotion), will trigger a warning message
from most compilers. In the context of audio programming, the most common use of the
cast is in converting oating-point samples into integer ones, not least the ubiquitous 16-bit
sample:
double fracsamp;
short samp;
fracsamp = 0.5;
samp = (short) (fracsamp * 32767);
As a revision exercise, you might like to make sure you understand why the following version of the line above will not produce the intended result:
samp = (short) fracsamp * 32767;
1.2.12

Going the Other Way: Converting a Frequency to the Nearest MIDI Note

The code fragment below illustrates the use of the cast operator to obtain a required integer
result; it also introduces another important function from the standard C math library. The
fragment can directly replace lines 2125 of listing 1.2. You also need to add a new variable
fracmidi of type double to the variable list. If you do this, you may want to change to program name, for example to freq2midi. This example uses the C math library function
double log(double arg);
which nds the natural logarithm (to base e) of the argument:

76

Chapter 1

/* find nearest MIDI note to a given frequency in Hz */


/* uses the log rule:
log_a(N) = log_b(N) / log_b(a)
to find the log of a value to base 'semitone_ratio'.
*/
frequency = 400.0;
fracmidi = log(frequency / c0) / log(semitone_ratio);
/* round fracmidi to the nearest whole number */
midinote = (int) (fracmidi + 0.5);
printf("The nearest MIDI note to the frequency %f is %d\n,"
frequency,
midinote);
You may be wondering why midinote is declared as an int rather than a char. In terms
of the calculation, it could be, so you could make the change and the program would run as
expected. However, in doing this you would be relying on the internal promotion of the
char to an int, in the call to printf(), where the format specier %d is used. There is a format specier for chars: %c. But that assumes, reasonably, that the char is an ASCII character
and the value 69, for example, would be written as E. Needless to say, to a musician that
would be doubly confusing.
Note the use in the example above of a simple method for rounding numbers: adding 0.5
to the oating-point value before applying the cast. For audio purposes we often require
more sophisticated rounding methods, so that negative and positive values are rounded symmetrically, but this simple method works very well for simple rounding of positive numbers.
Use of the int type also facilitates the detection of errors caused by a user supplying a
number outside the allowed range. The detection and handling of errors (whether your own
or the users) is an essential aspect of programming, as will be seen in the next section.
1.2.13

Exercises

Exercise 1.2.1
Using the sizeof() operator and printf statements, write a small program to report the
size in bytes of each of the following:
int

char

short

long

float

double

This will be of particular interest and importance if you happen to be using a 64-bit machine.
Exercise 1.2.2
The statement
samp = (short) fracsamp * 32767;

77

Programming in C

is intended to convert a oating-point sample (e1.0) to the 16-bit integer range:


samp = (short) fracsamp * 32767;
However the output is not as expected. Why?
Exercise 1.2.3
Replace the calls to pow() with literal values, in order to simplify the all-in-one statement
shown in subsection 1.2.9. Make sure the computed result is correct. What might you lose
as a programmer by making such a change?
Exercise 1.2.4
As was suggested in the text, modify listing 1.2 to avoid reliance on the compilers automatic
type promotion from int to double. Use a cast where appropriate. For example, change
1/12.0 to 1.0/12.0.
Exercise 1.2.5
(a) The sounding length of a string sounding C4 (middle C) is given as 660 cm. Modify
listing 1.2 to print out the lengths of string required for each semitone up to the next octave
(C5, 330 cm).
(b) What further language facilities would make this task easier?
Exercise 1.2.6
(a) Implement the program freq2midi as suggested in subsection 1.2.12.
(b) We dene pitchbend as a percentage deviation half a semitone above or below a MIDI
note. Maximum deviation is therefore e50 percent. For example, given a frequency of
430 Hz, the nearest notes are 69 (f 440) and 68 (f 415.3). The deviation is therefore
10/24.7 0.398 40 percent (approximately). Extend freq2midi to report the frequency
deviation above or below the calculated MIDI note, as a percentage. (Hint: To print a percentage symbol using printf, precede it with the same symbol.)
int bend = 45;
printf("pitchbend = %d%%\n",bend);
(There is an important follow-up to this exercise in the next section.)
1.3

Introduction to Pointers, Arrays, and the Problem of User Input


It will probably not have escaped your notice, if you have run the programs presented in
the previous sections, that having to re-compile every time you change the number for
midinote or frequency is very tiresome. This section introduces changes to listing 1.1 to
make it function interactivelyit asks the user for some data, which is then processed.

78

Chapter 1

What is involved in receiving input from the user? In the context of a command-line program, the user will be typing something at the keyboard, which the program reads and presumably processes in some way. The challenge here is a basic onewe dont know in
advance, and we have fairly limited means for controlling just what, or how much, the user
is going to type. The user may not even type anything at all. Therefore, some of the code we
write deals not with the core task, but with handling user inputmaking sure we read all of
it, and checking for the unexpected.
Thus an interactive program has to do two things: provide some space (memory), which
we hope is sufcient for anything the user will likely type, and test that data to make sure it
is what we need.
1.3.1

Strings and the C Pointer Type

In the previous programs, we relied on the printf() function to print variables to the console. We now have the opposite taskthe user will (we trust) type numbers as a string of
numeric characters, and the program will have to convert that into a C variable. It is time
to understand a little more about exactly how a string is represented in the computer. Consider the string
"one two three four"
This string is stored in memory one character at a time in successive ascending memory
locations. It is the task of the compiler to decide exactly where it is stored. The above string
will occupy eighteen successive bytes of memory (remember, the space is also a character),
starting at some location N determined by the compiler.
However, an additional null byte (the character \0) is added at the end, so the string
occupies nineteen bytes. Thus, any function that is required to access the string need only
know where the rst character is, and it can completely trust that all following bytes also
belong to this string, as far as that nal null byte. The location in memory of this rst byte
is its address, and that address is accordingly the address of the whole string. The full technical term for this is a null-terminated string; this is the way all strings are implemented in C.6
Thus, in the statement
printf("one two three four");
the printf() function is given the address of the start of the string.
So the address is itself an object, a variable that can be passed around. As it is a variable, it
has to have a typein this case, it is a pointer type, because it points to a location in memory. As it points to a row of characters (which may of course be just one character long), its
full type denition is pointer to char. It declared as
char* message;
which could also incorporate an initialization to a string:

79

Programming in C

char* message = "one two three four";


Here the asterisk is not a multiplier; it is the indicator of a pointer type. In this statement,
message is assigned the address of the start of the given string, in effect the memory location of the rst character. It is now possible to replace the printf() statement above with
printf(message);
The list of format speciers for printf() includes one for strings: %s. This leads to yet
another way of creating messages for printf():
char* two = "two";
char* four = "four";
char* six = "six";
printf("%s %s %s eight," two, four, six);
or even7
char* message = "%s %s %s eight";
printf(message, two, four, six);
1.3.2

The Array

To receive text input from the user, we have to set aside some memory to hold the text. This
requires a character array. An array is simply a row of objects of a single type. This requires
a different notation, one that incorporates information on how much space we want:
char message[256];

/* array of 256 chars */

The brackets (subscript notation) indicate the denition of an array, and the number
inside the brackets indicates how many items of the stated type to provide space for. The
result of the statement above is that a xed block of 256 bytes of memory is set aside (since
a char is one byte in size), and the address of the start of it is stored in the variable message.
The forms
char message[]; /* unspecified size */
and
char message[0];
are illegal. Either of them will trigger an error message from the compiler. Needless to say,
negative sizes will also be rejected. Subscript notation is also used to read and write individual elements of an array:
char Beatle[5] = "John";
char letter;
letter = Beatle[0]; /*
Beatle[2] = 'a';

letter = 'J' */
/* Beatle now = "Joan" */

80

Chapter 1

Here the number in brackets is termed the array index or subscript and, as can be seen,
counts from 0, not (as one might intuitively expect) from 1. In binary arithmetic, a single
bit holds two values, 0 and 1, and so can be used to count two items. 0 therefore has to signify the rst item, and 1 the second.
The power of the subscript notation becomes more apparent when we realize that an integer variable (of any integral type) can be used for the index:
int position;
position = 3;
Beatle[position] = 'l';
position = 0;
Beatle[position] = 'P';
position = 2;
Beatle[position] = 'u';
position = 1;
Beatle[position] = 'a';
/* Beatle now contains "Paul" */
As well as enabling programmatic changing of the index value, this also supports selfdocumenting code, as the index name can become descriptive.
Arrays can be formed using any type. Of course, the string initialization system can be applied only to arrays of char. In declaring the size of an array to contain a string, it is essential
to count the terminating null byte as part of the string. C provides a generic notation using
braces (note the nal semicolon here), which enables arrays of any type to be initialized in a
single statement:
int FullChord[4] = {60,64,67,72};
/* MIDI notes for C Major */
int root = 0, third = 1, rootnote;
FullChord[third] = 63;
/* change chord to C minor */
rootnote = FullChord[root];
/* Middle C */
Figure 1.1 shows the construction of a string array element by element using this notation,
including the all-important terminating null character.

Figure 1.1
A string as a null-terminated array of characters.

81

Programming in C

If there are too many elements in the initialization block, the compiler will ag an error.
However, if there are too few, the remaining elements in the array will be initialized to 0:
int LargeChord[32] = {60}; /* first element = 60, all others set to
zero */
In section 1.5 we will learn how C enables the programmer to step through arrays programmatically, so that possibly very large arrays or other data structures can be initialized
and modied using a small amount of code.
1.3.3

NULL

In addition to the initializations described above, it is possible, and often a very good idea, to
initialize a pointer (of any type) to the special value NULL (dened in stdlib.h). It is always
written in upper case. Exactly how this is dened is discussed in a later section; sufce it to
say here that it is a form of zero for pointers. Many functions that return pointers will use
NULL to indicate errors.
1.3.4

Array Index Errors

It is probable that more bugs in C programs are caused by misuse of pointers and arrays than
by any other single programming error. There are two bad things one can do with an array
index. One is the following:
double buffer[32];
buffer[-2] = 1.0594631; /* ERROR! Index below start of array */
buffer[33] = 2.0; /* ERROR! Index beyond end of array */
The problem here is that the compiler will very likely not warn you about these errors, and if
the index is in fact a variable, it will in any case have no way of knowing. C does very little
hand-holding of programmers, and if you over-run the bounds of an array it will do nothing to stop you. Sometimes the program will seem to run correctly, but it may suddenly
crash on some machines and not on others, and the point of the crash may be in some seemingly unrelated part of the program.
Indexing errors of the kind discussed above are especially damaging when you are modifying data in memory, because in most compilers the memory spaces below and above an
array may belong to the system, holding information on other data used elsewhere. Maybe
you will be affecting other variables in your program, so it will appear that those variables are
causing the crash.
There are nevertheless good reasons why even an explicit negative index is not automatically treated as an error by the compiler. We will see in later sections that a pointer can
be assigned the address of any element, e.g. one in the middle of an array. In this case, a

82

Chapter 1

negative index relative to that new pointer is perfectly valid. The danger arises only if the
bounds of the overall array itself are exceeded.
A subtle problem arises using indexing on strings used to initialize pointers:
char* name = "John";
name[2] = 'a';
/* DANGER: may crash! */
This will compile, but the program may crash with an access violation, because an attempt
is being made to modify a string that is constant and which the compiler has stored in a
protected area of memory. This may be to stop programmers trying to rename Windows
Windoze, which would never do. However, if you declare name as an array, all will be
well:
char name[] = "John";
name[2] = 'a';

/* NB: sizeof(name) = 5 */
/* safe: name now = "Joan" */

This demonstrates probably the easiest way to initialize a char arrayit is not necessary in
such cases to supply a size for the array inside the brackets as that is now available from the
initialization string. Usefully, with this form of initialization, the sizeof operator can also
be used to nd the size of the string programmatically.
As you work through the chapters of this book, you will discover a range of techniques
that programmers use to keep such coding errors to a minimum. Together these form a repertoire of techniques for defensive programming. We have already met some of these techniques: the use of informative variable names, explanatory comments, and the robust testing
of user input. As our programs grow in complexity, we will need to pay ever greater attention
to defensive programming. We hope we will get it right rst time, but the more our programs depend on input from outside, and the larger our programs become, the more we
must be on the lookout for code with unintended side effects.
1.3.5

Converting Number Strings to Variables: The const Keyword

We have used the printf() function to write a numeric variable to the console in text
form. In order to get a number interactively from the user, we need to be able to do the
reverse. The standard C library offers three conversion functions: atof(), atoi(), and
atol(), which convert into double, int, and long, respectively. They are declared in the
header le stdlib.h, which therefore must be #included in any source le that uses them.
They all have the same format, differing only by return type:
double atof(const char*);
int atoi(const char*);
long atol(const char*);
Their use is straightforward:

83

Programming in C

double dval;
int ival;
long lval;
dval = atof("1.0594631"); /* dval = 1.0594631 */
ival = atoi("440"); /* ival = 440 */
lval = atol("65536"); /* lval = 65536 */
The const keyword is often used in such declarations. It indicates that the supplied argument is treated as constant or read only; it will not be modied. That is to say, the
pointer is a pointer to a constant char. When a plain (non-pointer) type is used as a function argument, it is copied into the functions private memory space, so that the variable is
not modied. When the argument is a pointer, the pointer is copied, but the memory it
points to could be modied. Some functions are expressly designed to do this, and would
be declared without the const keyword. An important example is the function gets(),
which obtains text typed at the console:
char* gets( char *buffer );
Here the buffer argument receives the string from the user, and must therefore be a pointer
to a char array of adequate size, while the return value will duplicate the buffer or will be
NULL to indicate an error.
Conversely, the presence of const in a function declaration is a guarantee that the function will not modify your data. It therefore becomes another element in our repertoire of
defensive programming techniques. It is also a signal to the compiler that some sorts of optimizations can be performed.
The use of these functions is easy:
double frequency;
char* message = "1.0594631";
frequency = atof(message);
/* frequency now has the value 1.0594631 */
The only issue to remember is what happens if the message string does not in fact contain
the expected numeric charactersin this case, these functions return 0, in the appropriate
type:
message = "one thousand";
frequency = atof(message);
/* frequency now has the value 0.0 */
However, this will produce a plausible result:
message = "1000pointfive";
frequency = atof(message);

/* frequency = 1000.0 */

as atof() simply stops reading at the rst non-numeric character.

84

Chapter 1

Thus you cannot know with certainty whether the user has actually typed a 0 or whether
it has typed some other incorrect text. Of course, this is an issue only if 0 is a reasonable
number for the user to give. The standard C library offers more elaborate functions, such as
strtod(), that both trap incorrect text, and identify it for you by returning any offending
text via a pointer to a char array, that you provide.
The const keyword can be used to trap the problem mentioned at the end of the previous
section. By declaring the pointer as a pointer to const char:
const char *name = "John";
name[2] = 'a';
The second line will now trigger a warning from the compiler that an attempt is being made
to modify const or read-only memory.
1.3.6

Testing, Comparison, and Decision Making: The if Keyword

Listing 1.2 does very little that cannot be handled by a pocket calculator. In order to write a
reasonably practical interactive program, we need to be able to check user input for correctness, and be able to quit the program if input is incorrect. The ability to make simple
yes-no or true-false decisions (very quickly) lies at the heart of computing. Such a decision is, of course, binarythere are only two possible answers. At the level of binary logic,
yes corresponds to true and is associated with the value 1, while no equates to false
and has the value 0. We will see later that literally every C expression can be evaluated in
terms of true or false. At this stage, all we need is the ability to make a comparison between two expressions (e.g. two numbers) and to take alternative actions based on the result.
A frequent programming requirement is to nd out if two variables are equal. This is one
of many comparison tests for which C provides special operators. These are listed in table
1.3. As they are binary operators (like the arithmetic operators), they are placed between the
two values being compared. For example, the equality operator is used to nd if two variables are equal. The expression
Table 1.3
The C comparison operators.
Operator

Example

Description
a is equal to b

Equal to

= =

a = = b

Not equal to

!=

a ! = b

a is not equal to b

Bigger than

>

a > b

a is bigger than b

Smaller than

<

a < b

a is smaller than b

Bigger than or equal to

>=

a >= b

a is bigger than or equal to b

Smaller than or equal to <=

a <= c

a is smaller than or equal to b

85

Programming in C

(width == height)
(an example of a C conditional expression or conditional test) will evaluate to 1 if the
values are identical, or to 0 otherwise.
The C language includes the keyword if, which is placed in front of the expression to be
tested:
if(width == height)
As it stands, this is incompleteit is not yet either an expression or a statement. To form a
complete if statement, it must be immediately followed by a C statement, to be executed if
the test is successful (i.e. if the expression evaluates to true):
if(width == height)
printf("it's a square!");
By convention, the body of the statement is indented if it appears in the next line, showing
that it depends on the outcome of the preceding if expression. If you need to write more
than one statement depending on the same if expression, the statements must be enclosed
by braces, which serve to indicate a single code block:
if(width==height)
{
found_square = 1;
printf("it's a square!");
}
One of the traps for the unwary with if statements is adding a nal semicolon. This can
easily occur by accident in the process of editing code, and it is often difcult to spot:
if(width == height);
This is in fact legal C, because the semicolon by itself is a legal statement that does nothing
(a null statement). The true meaning is clearer when we use appropriate layout:
if(width == height)
;
/* do nothing! */
C is fairly exible with respect to comparisons between different numeric types. However,
comparisons between plain variables and pointers will be agged as errors by the compiler,
as will be comparisons between pointers to different types (e.g. a float pointer with an int
pointer). The one important exception is NULL, which can be compared with any pointer
type. One of the more idiosyncratic styles I have come across is
if(a == b)
{
do_something();
}

86

Chapter 1

This works perfectly well so long as braces are always used (as is often recommended). However, in
if(a == b)
do_something();
if(c == d)
{
do_something_else();
}
the dependent statements have different degrees of indentation but are in fact at the same
level.
The amount of indentation also attracts different opinions. Indents of two, four, and eight
spaces are common. A subtler issue is whether to use hard spaces or a tab. (Most text editors
can be set to use a requested tab size.) So long as one approach or the other is used consistently throughout, there should be few problems with layout if the text is transferred between
platforms. If in doubt, remember that the use of hard spaces will always be portable.8
1.3.7

An Interactive Version of Listing 1.2

Each time the program is run, it asks the user to type in a MIDI Note number. If it is recognized as a reasonable number, the corresponding frequency is displayed. Otherwise, an error
message is displayed and the program nishes. You can either create a new le (or project)
for this or just edit listing 1.2, keeping the same program name. Apart from the fact that
this version is a little more useable, the latter approach reects more closely how programs
are usually developedincrementally, adding features step by step. The program uses the
function gets(), described above, to read a line of text typed by the user in response to a
prompt.
When gets() is called, it waits for user input from the console (which must include a
nal return keypress), then places whatever was typed in the char array supplied as the
function argument. That array must be large enough to accommodate whatever we suspect
the user might try to type. The return value, also a char pointer, is simply a copy of the
input argument. The function can also signal an error in special circumstances, by returning
NULL. While those circumstances are not likely to apply here, this possibility is nevertheless
tested for in the program. Comparing pointers with NULL is a very common programming
procedure, and is very important, as trying to read from or write to memory pointed to by
NULL can be guaranteed to crash the program. To detect errors, listing 1.3 uses four if statements with three different comparison operators. In each case, the return keyword is used
to exit the program if an error is detected, with 1 as the error value.

87

Programming in C

Listing 1.3
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
int main()
{
double c5,c0,semitone_ratio,frequency;
int midinote;
char message[256];
char* result;
semitone_ratio = pow(2, 1.0/12);
c5 = 220.0 * pow(semitone_ratio, 3);
c0 = c5 * pow(0.5, 5);
printf("Enter MIDI note (0 - 127): ");
result = gets(message);
if(result == NULL){
printf("There was an error reading the input.\n");
return 1;
}
if(message[0] == '\0') {
printf("Have a nice day!\n");
return 1;
}
midinote = atoi(message);
if(midinote < 0){
printf("Sorry - %s is a bad MIDI note number\n",message);
return 1;
}
if(midinote > 127){
printf("Sorry - %s is beyond the MIDI range!\n",message);
return 1;
}
frequency = c0 * pow(semitone_ratio, midinote);
printf("frequency of MIDI note %d = %f\n", midinote, frequency);
return 0;
}
The use of compound expressions was demonstrated in subsection 1.2.8. As any C expression can be evaluated to a value, it follows that one or both sides of a comparison operator
could also be a compound expression; for example, incorporating a function returning a

88

Chapter 1

value. In listing 1.3, the return value from gets() is tested only once, so the code could reasonably be compacted as follows, eliminating a variable:
printf("Enter MIDI note (0 - 127): ");
if(gets(message) == NULL){
printf("There was an error reading the input.\n");
return 1;
}
The compiler will make sure that the function call is executed rst (it has the highest precedence), and the result then passed to the conditional test.
1.3.8

Exercises

Exercise 1.3.1
Listing 1.3 has no comments. How self-documenting is the code? Add some comments
wherever you think they would be useful.
Exercise 1.3.2
(a) Make a new interactive version of freq2midi from the previous section.
(b) Implement rounding of the printed pitch deviation percentage value, so that positive
and negative values are symmetrically rounded. For example, 38.7 should be rounded to 38,
and 38.7 should be rounded to 39.
Exercise 1.3.3
(a) Add a further test on the number supplied from the user in listing 1.3, to detect and
reject oating-point values (since MIDI notes are integers).
(b) Try to detect whatever erroneous input the user might make, and respond with an appropriate error message.
Exercise 1.3.4
What messages does the compiler write if
(a) you omit the nal double quotation marks in a printf statement?
(b) you omit the variable name associated with a format specier in printf?
1.4

The Command LineGetting Data from the User


1.4.1

What Is the Command Line?

As its name suggests, the command line is a means of communicating with a computer using
text commands typed from the keyboard, and using the medium of a command shell or

89

Programming in C

a console programfor example, the Bash shell in UNIX, the DOS console in Windows, or
the Terminal in Macintosh OS X. While it is not the oldest way of sending commands to a
computer, it is by far the longest established, and despite the inexorable progress of WIMPbased systems (Window Icon Menu Pointer), usually referred to simply as a GUI (Graphic
User Interface), it refuses to go away. Signicantly, the Apple Macintosh, having eliminated
the command-line interface as both unnecessary and user-unfriendly, has recently reacquired it with the introduction of OS X, which is based on the UNIX-like BSD kernel.
Thus the three most widely used operating systems all now support a console-based command-line interface, which is ideal for developing and running simple programs such as
those presented in this section. Indeed, for many advanced users (and especially on Linux
and other UNIX-like platforms) it remains the ideal environment in which to work, even
on complex projects.
A typical text command consists of a program name (e.g. copy), followed by one or more
argumentswords separated by spaces. Let us suppose there is a program sfmix that takes
two or more soundles and adds them together (i.e. mixes them) to create a new soundle.
The documentation for the program tells us that the rst argument should be the name of
the new output le, and all following arguments should be the names of input soundles to
add together. The command line might look like this:
sfmix backing.wav drums.wav guitars.wav
This runs the program sfmix, creating the output le backing.wav from the input les
drums.wav and guitars.wav. This works because the operating system supports a standard
method for both running a command-line program and giving it the arguments from the
user. The operating system has no idea what these arguments meanthey are nothing
more than text strings separated by spaces. It does one very useful thing, however: it separates each string into a list, and gives that list to the program to use as it wishes. Thus, rather
than having to ask the user interactively for each lename, the program is given all the
names automatically from the command line.
The command-line environment is most strongly associated with the UNIX operating
system and its variants, including Linux and OS X. However, the principles are much the
same on Windows using the DOS console. Programs can be run just as illustrated above.
The only immediately relevant difference from a classic UNIX-style environment is that the
command to list the contents of a directory is ls on UNIX-style systems, whereas it is dir in
DOS. By using command-line arguments, a program can be designed to handle a range of
inputs from the user. The usual environment for command lines is a shell application or
terminal window such as the Windows DOS Console, Terminal in OS X, or an Xterm in
Linux. Table 1.6 shows some typical shell commands. (N.B: To run a program in the current
directorye.g. in which the program has been builtin a UNIX-like environment (Linux,
OS X Terminal), the program name must be prexed by the current directory shorthand
./ as follows:

90

Chapter 1

> ./cpsmidi 72
Other commands, such as cd to change directory, are the same.
If you have not already done so, you may want to try out some of the pre-compiled command-line programs supplied on the DVD as a way to develop uency in using such tools.
Most of them are projects from later chapters in this book. Although they involve much
more elaborate C programming than we can do at the moment, there is no reason not to
use them as prefabricated tools with which useful creative work can be done. The chances
are high that you will very soon think of some new facility that is not provided by a given
program. There can be no better incentive to develop your programming skills than the prospect of crafting exactly the tools you want for the work you want to do.
1.4.2

The Extended main() Function: Receiving Command-Line Arguments

In the programs demonstrated so far, we have used the main() function with no arguments.
There is an alternative extended form, which is required whenever we want information
from the user at the beginning of the program:
int main(int argc, char* argv[]);
The rst argument, argc, is simple enough. It is an int, and the name means argument
count, the number of space-separated strings supplied in the command line. This number
will always be at least 1, as the rst argument (counting from 0, as is always the case in C) is
the name of the program itself. The second argument is a little more complicated and needs
some detailed explanation. You have already seen declarations of pointers (such as char*
name;) and of arrays (such as char name[32];), but argv combines both the pointer and
array notations. The name argv is shorthand for argument vector, and vector here is a
synonym for array. So we have an array argv[]of pointer to char: char*.
The key aspect to remember here is that a pointer to char can itself signify an array, in the
sense of a character string (i.e. null-terminated), which indeed is the case here. Thus, taking
the example above as our example command line, this array of strings would be laid out as
follows:
argv[0]points
argv[1]points
argv[2]points
argv[3]points

to
to
to
to

"sfmix"
"backing.wav"
"drums.wav"
"guitars.wav"

This is shown graphically in gure 1.2.


An array can also be understood as a container, so instead of points to you could say
contains.9 There is also a nal implicit argv[4] which contains NULL. However, this is
not counted in argc, so for this example argc would have the value 4. This nal NULL element can in principle be used in code that steps through the argv array until the NULL is

91

Programming in C

Figure 1.2
Command-line arguments as pointers to strings.

found. However, as argc is always available (and a more convenient basis for a loop), such
usage is rare. Note that the rst argument (argv[0]) contains the name of the program. This
may seem superuous (after all, we have written the code, so we know what we have called
the program), but it can be surprisingly useful. If nothing else, we will not be caught out if
the user decides to rename the program.
In a program, you might use argv to print a message to the users, perhaps about some
error:
printf("Cannot find the file %s\n," argv[2]);
Since the expression argv[2] evaluates to a char*, it can be used as the associated variable
for the string format specier %s. Similarly, it can be passed as the parameter to any function
expecting a char* :
/* read number of oscillators requested */
int num_oscs = atoi(argv[3]);
if(num_oscs > 100)
printf("too many oscillators!\n");
Just as in the previous program examples, we will have to check each argument for reasonableness. The simplest and most important test, however, is the one applied to argc. We

92

Chapter 1

know how many arguments we need, and we must check for this before trying to read any of
them:
if(argc < 4){
printf("insufficient arguments.\n") ;
printf("usage: sfmix outfile infile1 \
infile2 [infile3 . . ..]\n");
return 1;
/* exit the program */
}
For command-line programs, this usage message is a very common procedure, which users
come to expect and rely on. If they type just the name of the program, they should be given
a full usage message listing the purpose of the program and the name and purpose of each
argument. You will see this incorporated in all the programs that follow.10
1.4.3

The ** Notation for argv

The main function supports an alternative denition of argv:


int main(int argc, char** argv)
where ** means that argv is a pointer to a pointer. In principle, pointer variables can
incorporate any amount of indirection. I have seen the following in programs:
char**** very_indirect_variable;
This might perhaps be a pointer to an array of string arrays, or it might not. The ** notation
is, however, very common in programs that deal with lists of strings. It is arguably less appropriate than the explicit array notation, for main()a variable that is a pointer to a pointer
to charmay, in the end, be pointing only to a single character. Opinions on this differ,
however, and you will nd many main functions dened this wayperhaps merely because
it is marginally quicker to type.
Listing 1.4 shows a revision to listing 1.3 to obtain input from a command-line argument.
This is the rst program in the present chapter that can be considered useful, as the user
can run it with any MIDI note as inputone need not compile the program each time.
Exercise 1.4.1 invites you to incorporate this change into the complementary program outlined in subsection 1.2.11 (converting frequency into MIDI). Listing 1.4 could be made even
more useful by allowing the user to enter any number of MIDI notes.
Listing 1.4
/* cpsmidi.c : convert MIDI note to frequency, using command line
argument */
#include <stdio.h>
#include <stdlib.h>
#include <math.h>

93

Programming in C

int main(int argc, char* argv[])


{
double c5,c0,semitoneratio;
double frequency;
int midinote;

/* could be a char */

semitoneratio = pow(2,1.0/12);
c5 = 220.0 * pow(semitoneratio,3);
c0 = c5 * pow(0.5,5);
/* if the program is not called cpsmidi,
either change the lines below,
or use the argv[0] system, shown commented out below */
if(argc != 2){
printf("cpsmidi : converts MIDI note to frequency.\n"
/*,argv[0]*/);
printf("usage: cpsmidi MIDInote\n" /* ,argv[0]*/);
printf(" range: 0 <= MIDInote <= 127 \n");
return 1;
}
midinote = atoi(argv[1]);
/* use argv[1] to echo a bad argument string to the user */
if(midinote < 0){
printf("Bad MIDI note value: %s\n",argv[1]);
return 1;
}
if(midinote > 127){
printf("%s is beyond the MIDI range!\n",argv[1]);
return 1;
}
frequency = c0 * pow(semitoneratio,midinote);
printf("frequency of MIDI note %d = %f\n",midinote,frequency);
return 0;
}
Finally, if you think that command-line arguments exist only for command-line programs,
think again: a Windows GUI program also reads command-line arguments, as will any UNIX
(or Linux or Apple OS X) program. In Windows, for example, it is possible to invoke the
Notepad application from the DOS console, giving it the name of a le to edit:
notepad freq2midi.c
The procedure on Apple OS X is only slightly different. To run the soundle editor Audacity
from the Terminal, you can type
open -a Audacity.app myfile.aiff

94

Chapter 1

The command-line handling skills you develop here will stand you in good stead when
developing more advanced GUI applications.
1.4.4

Exercises

Exercise 1.4.1
Convert all the programs developed so far to use argc and argv. Dont forget to incorporate
the extensions suggested in previous exercises.
Exercise 1.4.2
Add a usage message to each program.
Exercise 1.4.3 (experimental)
(a) Extend the program with a further command-line argument that denes the root A
tuning. For example, Baroque pitch denes A 415 Hz. In fact, A 440 Hz is itself a relatively recent standardespecially in the United States, where the pitch of A 435 Hz was
recently in widespread use.
(b) (experimental) Add a further argument that denes the division of the octave. For example, a popular alternative temperament uses a 19-note equal-temperament division. A value
of 24 would dene a quarter-tone ET scale. In each case, trap inappropriate user input with
suitable error messages.
1.5

Controlling Repetition: Looping and Counting


So far, with one exception, every one of the example programs has dealt with a single piece
of dataa single MIDI note, or a single frequency value. The exception is the character
string, which takes the form of an array of single characters. It is possible to make arrays of
any type (e.g. a block of oating-point audio samples), and it becomes essential to have the
means for processing the array as a whole, while addressing individual elements within it, in
a structured and systematic manner, regardless of how large the array is. The image of a production line is a very natural one with respect to dealing with arrays. Given that they contain multiple instances of only one type of object (char, or float, for example), we can
imagine a process that more or less automatically repeats itself for each element. We would
like to be able to press a button, so to speak, to start this process, and be able to press another
button to stop it again.
As audio programmers, we will be creating and processing such arrays (and other data
structures) all the time, and the sheer number of elements is far too great for anyone to contemplate writing code that processes each one in a written list. Fortunately, the need to
address an individual element of such arrays directly is rare; most of the time we want to

95

Programming in C

apply the same process to each element just as described abovefor example, to change the
amplitude of an array of audio samples. We can use the power of the C language to make
such tasks easy. With a few lines of code, we will be able to read and process every element
of an array of any size. The tools for this are two cornerstones of all programming: iteration
(counting) and looping (repeating instructions). These are especially important tasks in audio
programming, as we will be working all the time with blocks of samples, such as tables containing waveforms, memory buffers containing delay lines or audio streamed from a disk or
from a soundcard, or tables of frequencies and amplitudes for oscillators. In a typical application we have to step through the block, making a change to each sample as we go. On a
larger scale, when we need to read a soundle, we usually need to read it in blocks (the whole
le may be far too large to t in memory all at once), so we have to use iteration and looping
to read each block and process it. This is, then, a process of mass production. We can use
the power of the language to repeat steps automatically, at very high speed, on blocks of data
of any size. In this section, we move from writing calculations, to writing algorithmsmethods for performing a concretely dened task, usually involving many repeated steps. In a
sense, this is the rst section in which we embark on true computer programming. We will
learn about how to create loops, and also more about the use of Cs auto-increment and autodecrement operators, which are designed to support the highly efcient mass production we
need. Finally, we will apply all this new knowledge to build on the programs we have already
built, and to produce perhaps the rst truly useful program so far.
1.5.1

The while Loop

As its name suggests, a loop is a block of code (it need not be a single instruction) that
repeats over and over. However, a code block that simply repeated foreveran endless
loopwould not be very useful. If you have used a sampler, you will be familiar with the
way a sound is looped (to maintain a sustaining sound) while a key is held down. This is a
form of endless loopit is broken out of only when you release they key. Should it fail to do
that, you have a real problem.
So, the most important aspect of using loops is controlmaking sure that they stop when
we want them to. The simplest way to do this is to count the number of times the loop
repeats, and stop when a limit is reached.
The simplest loop construct in C is the while loop. Listing 1.5.1 shows an example of its
use.
Listing 1.5.1
1
2
3
4

#include <stdio.h>
int main(){
int i;

96

Chapter 1

5
6
7
8
9
10
11

i = 0;
while(i < 10) {
printf("%d ,"i);
i = i + 1;
}
return 0;
}

The output from this program is simply


0 1 2 3 4 5 6 7 8 9
Instead of writing the printf() statement ten times, we have written it just once, and used
the while loop to generate the repeats. In this program we have decided to write ten numbers, so could in principle write each one explicitly. But suppose this was a program called
count and had a command-line argument that set the number to count to. We cannot
know in advance what this number will be, and of course it may be different each time we
run the program. We can only implement this task using a loop, where the input from the
user is used to control the loop.
The while statement is similar to the if statement in that it comprises a conditional test
(inside the parentheses, followed by a dependent statement or code block (in braces). The
main difference is that, whereas an if statement may execute the dependent code once and
move on, the while statement will repeat the dependent code forever, so long as the conditional expression is true. (See gure 1.3.)

Figure 1.3
Comparing if . . . while and do . . . while.

97

Programming in C

There are three essential elements of loop control illustrated in this example:
1. Line 6 is the conditional test, associated with the while keyword itself,: (i < 10). At the
end of each iteration, program control will automatically return to the conditional test, and
evaluate it again. So long as the expression evaluates to true, the code inside the dependent
block { }will execute repeatedly.
2. Line 8 is the update of the control expression, i = i + 1; In the control expression (i < 10),
i is a variable which is declared outside the block, and modied at each repeat (iteration).
The effect is simply that at each repeat i is increased by 1, as shown by the output of
printf().
3. Line 5 is the initializer statement, i = 0 ;This sets the starting value of the controlling variable. This is placed before, and outside, the while statement. In this case it is set to 0, but it
could be anything. For example, in this variation the numbers count downward from 9
to 0 11:
i = 9;
while(i >= 0){
printf("%d," i);
i = i - 1;
}
1.5.2

The ++ and -- Operators

In listing 1.5.1, the update expression increases the variable i by 1:


i = i + 1;
This algebraically impossible expression, commonplace in programming, means let i have
the value it currently has, plus 1. The process of adding 1 to something is termed an increment, and similarly, subtracting 1 is called a decrement. So common is this form of counting
that special automatic increment (++) and decrement (--) operators were added to C, reecting in many cases identical machine-level instructions.
Auto-increment increases a variable by 1:
i++;

/* equivalent to i = i +1; */

Auto-decrement decreases a variable by 1:


i--;

/* equivalent to i = i - 1; */

Since this form can be used as part of a more complex expression, it enables a loop to be
expressed in a very concise form. The example below is exactly equivalent to the code in listing 1.5.1 (note that we now can omit the braces):
i = 0; /* initializer */
while(i < 10) /* conditional expression */

98

Chapter 1

printf("%d ,"i++); /* update in the dependent code */


The comments indicate the three loop elements required.
There is a subtle aspect to these increment and decrement operators. In the printf()
statement above, i is auto-incremented after its value has been used by printf()this is
the post-increment operator. It is also possible to place the ++ characters before the variable name, forming a pre-increment operator:
printf("%d," ++i);
In this case, i is incremented rst, and the new value is passed to printf(). The result will
be that each number printed would be one larger (i.e. 1 to 10 instead of 0 to 9).
The auto-decrement operator functions in the same way:
printf("%d ",i--); /* print the i, then reduce it by one */
printf("%d ",--i); /* reduce i by one, then print the value */
These last examples illustrate the idiomatic way to use the auto-increment and decrement
operators in the context of a larger expression, and especially in the context of loops. They
can be used on their own (sometimes merely to save typing), but they were born for use as
part of larger expressions, in which they are usually the most efcient method of increment.
1.5.3

General Update Operators

While the increment and decrement operators are intended for use with integer variables
(and also with pointers, as well see later), they will also work with oating-point variables:
double var = 1.5;
var++; /* var now = 2.5 */
This is not idiomatic C programming, however, and it is not recommended12 any C programmer reading the expression var++ will assume that var is an integer variable. A more
general update expression that can be used with oating-point types uses one of the arithmetic operators (+, -, *, /) together with the = sign:
float a =
a += 2.5;
a -= 0.5;
a /= 2.0;
a *= 3.0;

1.0;
/* equivalent to a = a + 2.5; a has the value 3.5 */
/* a now = 3.0 */
/* a now = 1.5 */
/* a now = 4.5 */

The % (modulus) operator can also be used in this way, but only for integral types:
int a = 10;
a %= 3;

/* a now = 1 */

99

Programming in C

Note that it is illegal to have a space between the arithmetic operator and the = sign:
a + = 5;

/* syntax error */

In the example below, update notation is used inside a loop to calculate the intervals of
the equal-tempered scale, and write them to an array. Post-increment is also used, applied
to the counter i in the array index expression:
double ratio = 1.0, semitone_ratio = pow(2,1/12.0);
double intervals[12];
int i;
i = 0;
while(i < 12){
intervals[i++] = ratio; /* uses auto-increment*/
ratio *= semitone_ratio; /* uses update operator */
}
The examples in this subsection demonstrate the use of the primary arithmetic conditional testsfor equality (=), less than (<), and greater than (>), together with their
inverses, not equal to (!=), greater or equal (>=), and less or equal (<=). As we shall
see, these already offer the programmer considerable exibility in designing conditional tests
associated with loops. C also supports a group of logical operators that make it practicable
to test multiple conditions together.
1.5.4

The for loop

As was shown above, a complete looping construct involves three control elements: an initializer statement, a conditional expression, and an update statement (e.g. a counter) that
usually updates a parameter in that expression. C offers a compact looping construct based
on the keyword for that places all three elements together; it is by far the most popular
and widely used of all the looping constructs in C. It is best demonstrated by example:
int i;
for(i = 0; i < 12; i++){
intervals[i] = ratio;
ratio *= semitoneratio;
}
This functions identically to the while example in the previous section. The for statement
has the structure
for(initializer_statement; conditional_expression; update_expression)
This must be followed, as in the case of the while construct, by either a single statement or a
block of statements enclosed by braces, as shown above. The three elements are demarcated

100

Chapter 1

Figure 1.4
The for loop.

by the two required semicolonsa semicolon should not be used to terminate the update
expression.
The for loop runs as follows (see gure 1.4), assuming that the conditional test evaluates
to true:
(1) The initializer statement is executed: i = 0;
(2) The conditional expression is evaluated: i < 12;
(3) The result is true, so . . .
(4) The dependent code inside the braces is executed.
(5) Control returns to the top, and the update expression is executed: i++
(6) The loop continues from step (2) until the condition evaluates to false.
(7) When the condition evaluates to false, control passes to the statements after the for
block.13
1.5.5

The do { } while ( ) Loop

There is an argument (among computer scientists, at least) that says that any programming
language requires only one loop construct; anything further is syntactic sugar. In that
case, C is very sweet indeed, as in addition to the while and for loops it also offers a varia-

101

Programming in C

tion on the while loop that places the dependent block at the top and the conditional test
at the bottom:
int i;
i = 0;
do {
printf("%d ,"i++);
}
while(i < 10);
Note that the while expression is terminated by a semicolon, to complete the do - while
statement. The special aspect of this form of loop is that the dependent block will always be
executed at least once, as the conditional test is placed after it. In the example above it makes
no signicant difference, but there are common situations in which this would be the most
appropriate form of loop. One typical situation is reading data from a le (a soundle, for
example); in such a situation, we usually make at least one read attempt before nding out
if we have read everything, or if we have suffered an error. Real code to do this is usually
lengthy, so this example uses some ctitious functions (it is sometimes called pseudocode for this reason)14:
int sampsread;
do {
sampsread = readsamps("tubularbells.wav");
if(sampsread > 0){
process_samps(sampsread);
write_samps("plasticbells.wav," sampsread);
}
}
while (sampsread > 0);
1.5.6

Breaking Out of Loops Early: The break Keyword

The do loop example above has one unsatisfying aspect: the expression
(sampsread > 0)
is used twice: inside the dependent code block (do), and in the conditional test (while). This
is generally regarded by programmers as tautologous and wasteful. We would prefer not to
have to duplicate code in this way if it can be avoided.
Consider what happens if the test inside the do block failsif the function readsamps()
returns 0. This means that no more soundle data is available (we assume we have reached
the end of the le). There is also the ever-present danger of a system error whenever disks are
accesseda fault could arise at any time, and a robust program has to be able to handle that
situation.

102

Chapter 1

In short, we often want to be able to break out of a loop earlyeither when there is no
more data, or if there is a system error. Recalling the comparison with the production line;
if there is a problem, we need to be able to stop the machinery immediately. Happily, C provides the break keyword that does just that. Using break, we can rewrite that example a
little more elegantly, and at the same time more expressively:
int sampsread;
do {
sampsread = readsamps("tubularbells.wav");
if(sampsread == 0)
break; /* end of file, or error */
process_samps(sampsread);
write_samps("plasticbells.wav," sampsread);
}
while (sampsread > 0);
If the break instruction is reached, control jumps right out of the do block and past the
while expression, to whatever the next instruction is.
Given the reasonable assumption that sampsread is in fact guaranteed to become 0 eventually, even if the conditional while part of the statement is really redundant, we can rely
entirely on the sampsread test to exit the loop. This is a surprisingly frequent situation, and
you will often encounter loops constructed this way, with a dummy conditional test that
always evaluates to true, and relying on break instead. So, instead of running an explicit
count to manage the loop, we set the loop to run forever, but watch for a condition or event
that we know will occur eventually.
In the for() initialization expression, all three elements are optional, leaving just the two
semicolons:
for(;;){
sampsread = readsamps("tubularbells.wav");
if(sampsread == 0)
break;
/* end of file */
process_samps(sampsread);
write_samps("plasticbells.wav," sampsread);
}
and any non-zero integer evaluates to true:
while(1){
sampsread = readsamps("tubularbells.wav");
if(sampsread == 0)
break;
/* end of file */
process_samps(sampsread);
write_samps("plasticbells.wav," sampsread);
}

103

Programming in C

These are common constructs in real programs, but they are safe only if break can be guaranteed to be called at some point.
So where are the three elements that all loops are supposed to have? They are now
embedded in the looped code: the assignment to sampsread is both the initializer and the
update, and the conditional test is clearly the if statement.
Assuming that in a real program the function write_samps() will also return a value
(below zero) that can indicate an error, we would have to check that too:
if(write_samps("plasticbells.wav," sampsread) < 0)
break;
In this case it is purely a check for an exceptional situationnormally one hopes that a write
to a le will always succeed.
1.5.7

Writing a Program to Create Unusual Musical Scales

We have learned quite a few C programming skills alreadywe know how to use the
extended form of the main() function to get command-line arguments from the user, we
know how to program simple loops, using auto-increment and update operators, and
we also know something about calculating equal-tempered intervals. The program developed
in this subsection combines all these elements into a useful program that can create scales
with any number of notes to the octave (not just 12 semitones), up to some reasonable limit,
and starting at any MIDI note requested by the user. There is a great interest in the use of
such scales among composers, and one of the much-lauded virtues of experimental computer music is that one need not be limited by conventions such as the classical 12-note
chromatic scale.
Since this is arguably our rst real program, the process of designing and writing it is presented from scratch, with the full program listing presented at the end. Try to resist the
temptation just to look ahead to the listing. Instead, follow the process all the way through,
starting with a new empty project. There is not very much typing involved, and we will reuse
some code from section 1.2. You might assume that the rst thing to do is (somehow) to
start writing code. But it isntthe rst thing to do is to sit down and think carefully about
what the program is going to do, and in particular how the user is going to use it.
The goal of this program is to display the frequencies of equal-tempered intervals for N
notes per octave. If N is 12, we will get the notes of the standard chromatic scale. Another
popular octave division is 19 notes. If N is 24, we will get a quarter-tone scale. If we write
each frequency on a separate line, 24 lines seems a reasonable limit for this program.
We need to set a limit, because we are going to dene an array to hold the interval data,
and we can only dene an array with an explicit constant size. (We will see how to get
around this limitation in section 1.7.) So we need the size to be that of the largest number
N we intend to support.

104

Chapter 1

We know that one command-line argument will be N, the number of notes in the octave.
We also need a MIDI note, so that will be the second argument. Our program (which will be
called nscale) will therefore have the following usage:
nscale notes midinote
where notes is the number of notes per octave and midinote is the base note on which to
build the scale. Therefore, there will be a total of three command-line arguments, including
the programs name.
We can now create a source le (nscale.c) in a new project, and write a skeleton main()
function. We are printing to the console, so we will need <stdio.h>. As we are using mathematical functions such as pow(), we will also need <math.h>. We should also put in a
small comment line, to say what the program is.
Listing 1.5.2 shows the skeleton program so far. This will compile and run; if you run it
with no arguments (or indeed with anything more than two arguments), the usage message
will be written to the screen.
Listing 1.5.2
/* nscale.c. Display E.T frequencies for an N-note octave, from a
given MIDI note */
#include <stdio.h>
#include <math.h>
int main(int argc, char* argv[])
{
/* declare variables here. . .*/
if(argc != 3){
printf("usage: nscale notes midinote\n");
return 1;
}
/* program code goes here */
return 0;
}
We now need to add some functionality to this skeleton, where the comment indicates.
We can start by reading our required arguments into variables and testing them for acceptability. If they are not acceptable, we must inform the user helpfully and quit the program.
Both notes and midinote are integer numbers, so they can both be int. We must declare
all our variables at top level, after the opening brace of main():
int main(int argc, char* argv[])
{
int notes, midinote;

105

Programming in C

First, we read notes from argv[1], and make sure it is between 1 and 24:
notes = atoi(argv[1]);
if(notes < 1){
printf("Error: notes must be positive\n");
return 1;
}
if(notes > 24){
printf("Error: maximum value for notes is 24\n");
return 1;
}
Then we do the same for midinote, from argv[2]. The legal MIDI range is from 0 to 127:
midinote = atoi(argv[2]);
if(midinote < 0){
printf("Error: cannot have negative MIDI notes!\n");
return 1;
}
if(midinote > 127){
printf("Error: maximum MIDInote is 127\n");
return 1;
}
Again, at this point you can compile the program, and test it rst by giving it a correct pair
of arguments (10 and 69, say), and then by various combinations of out-of-range values, to
make sure the error messages appear as intended.
We can now deal with the core functionality of the program. This is to calculate the frequency of the requested MIDI note, and use that to create a table of notes frequencies spanning one octave from that base note. We can reuse the core arithmetic code from listing 1.2,
remembering to add all the variables used in that code, immediately below the variables we
have already declared. If possible, use the copy and paste facilities of your editor to do this:
int notes, midinote;
double frequency,ratio;
double c0,c5;
Note that what was called semitone_ratio is now just called ratiowe are no longer
calculating just semitones, so a more general variable name is appropriate here. Now, we can
copy the calculation code from listing 1.2, complete with comments, into our source le,
after all the argument checking code:
ratio = pow(2.0,1.0 / 12.0);
/* find Middle C, three semitones above low A = 220 */
c5 = 220.0 * pow(ratio,3);

106

Chapter 1

/* MIDI note 0 is C, 5 octaves below Middle C */


c0 = c5 * pow(0.5,5);
frequency = c0 * pow(ratio,midinote);
Note that the hard-coded assignment to midinote in the original program is deleted, as we
are now getting that from a command-line argument.
All we have to do now is to generate the list of frequencies, and print them to the console.
There are two ways of doing this, but either way, we need a loop to generate the numbers, as
they may be from 1 to 24 in number.
The rst way is simply to calculate each note, and print it to the console, all inside the
loop. However, at the back of our minds we are thinking that this data may be useful in
other programs (or maybe even within an extended version of this program), so that it is
worth using an array to contain these notes. We will therefore use two loopsone to create
the frequencies, and the other to print them out.
We decided that the maximum for notes is 24, so we can dene an array that size, of
double. This is the standard type to use for arithmetic calculations, giving maximum precision. Add the following line immediately below the other variable declarations:
double frequencies[24];
To calculate intervals for N notes per octave, we use the same calculation we have already
used for 12 semitones, but replacing 12 with notes:
ratio = pow(2.0, 1.0 / notes);
To generate the complete scale, we just multiply each frequency by this ratio, to get the
next frequency. This is a clear description of an iterative processa loop. All C programmers turn to the for loop as their rst instinct, so we shall be no different:
for(i = 0; i < notes; i++){
intervals[i] = frequency;
frequency *= ratio;
}
We can use the for loop again to print the frequencies to the console:
for(i = 0; i < notes; i++){
printf("%f\n", i, intervals[i]);
}
We are now ready to compile and test the completed program. But it fails to compile. We
have had a compilation error reported. The exact wording will depend on the compiler, but
we will get something like this:
error C2065: 'i' : undeclared identifier

107

Programming in C

The correct response to this error is Oops! We forgot to declare the variable i we have used
in the for loops (in C, all variable names are identiers). This is probably one of the most
common everyday slips for C programmers. Everyone uses i as the counter in a for loop
typing it is almost an automatic processand it is very easy indeed to forget to add it to the
variables list.
So add that variable (an int) to the int list at the top of main(), and this time hope that
the program will compile cleanly. Now we can test it, perhaps with numbers from which we
can predict the results. For example, to reproduce the data in table 1.3, use arguments 12 and
57. The complete program is shown in listing 1.5.3.
Listing 1.5.3
/* nscale.c: Display E.T frequencies for an N-note octave, from a
given MIDI note */
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
int main(int argc, char* argv[])
{
int notes,i,midinote;
double frequency,ratio;
double c0,c5;
double intervals[24];
if(argc != 3){
printf("Usage: nscale notes miditnote\n");
return 1;
}
notes = atoi(argv[1]);
if(notes < 1){
printf("Error: notes must be positive\n");
return 1;
}
if(notes > 24){
printf("Error: maximum value for notes is 24\n");
return 1;
}
midinote = atoi(argv[2]);
if(midinote < 0){
printf("Error: cannot have negative MIDI notes!\n");
return 1;
}

108

Chapter 1

if(midinote > 127){


printf("Error: maximum midinote is 127\n");
return 1;
}
/*** find the frequency of the MIDI note ***/
/* calc standard E.T semitone ratio */
ratio = pow(2.0,1.0 / 12.0);
/* find Middle C, three semitones above low A = 220 */
c5 = 220.0 * pow(ratio,3);
/* MIDI note 0 is C, 5 octaves below Middle C */
c0 = c5 * pow(0.5,5);
frequency = c0 * pow(ratio,midinote);
/* calc ratio from notes, and fill the frequency array */
ratio = pow(2.0,1.0/notes);
for(i = 0; i < notes; i++){
intervals[i] = frequency;
frequency *= ratio;
}
/* finally, read array, write to screen */
for(i = 0; i < notes; i++){
printf("%f\n", intervals[i]);
}
return 0;
}
1.5.8

Exercises

Exercise 1.5.1
Write a short page of user documentation for the program nscale in listing 1.5.3. Assume
the user knows what a chromatic scale is but is not familiar with the mathematical basis of
equal temperament.
Exercise 1.5.2
Rewrite the loops in listing 1.5.3 to use (a) the while loop and (b) the do loop. What are you
relying on in the latter case? Be sure to test both changes by running the program.
Exercise 1.5.3
The output of the program does not exactly match the list in table 1.3the latter lists the
nal octave frequency, giving 13 frequencies in all. Modify listing 1.5.3 to add this feature.

109

Programming in C

The program will then write one more number than the value given for notes. For example,
if notes = 24, the program will print 25 numbers.
Exercise 1.5.4
Modify the nal printf() statement to add a counter to the beginning of each line, that
would have the following form:
1: 220.000000
2: 233.081881
. . .
Exercise 1.5.5
(This requires some mathematical acuity.) You have decided that you want to be able to use
an interval other than the octave as the basis for the frequency calculations. Add another argument to nscale, dening this interval (2.0 one octave, 4.0 two octaves, 1.5 half an
octave, etc.), and modify the program accordingly. Ensure that the new argument is tested
for reasonableness. Update the documentation you wrote in exercise 1.5.1.
Exercise 1.5.6
If you are familiar with Csound, modify the nal printf() statement to write the list of frequencies as a single Csound f1 statement in the score, using GEN 2. It would be worth
giving this program a different name. The rst value p-eld should give the number of frequencies in the list. For example, the 12-note chromatic scale staring at 220 (MIDI note 57)
would be written as
f1 0 16 -2 12 220.000 233.081881 . . . 415.304698
Remember that Csound f-tables must have a size that is a power of two, or power-of-2 plus 1.
(Practical note: Most operating systems support redirection of text output to a le:
nscale 12 57 > ftable.txt. Thus, the text from your program could be copied from the
ftable.txt le into a Csound score.)
Exercise 1.5.7
If you forget to declare a variable (identier) that you are using, the compiler will report
an error. What does your compiler report if you declare a variable but dont use it? What
does it report if you declare it twice?
Exercise 1.5.8
(a) From listing 1.5.3, arbitrarily remove one of the following characters: ( ) { } [ ]. Then
run the compiler. What errors does the compiler report? How accurate and helpful are they?
(b) Do as in (a), but instead replace the removed character with one of the others.

110

Chapter 1

(c) Ask a non-programming friend to make some subtle arbitrary edit. If the compiler reports
an error, use the information to discover and x the change.
1.6

Using Pointer Arithmetic and Adding Options to a Program


1.6.1

Introduction

C is unusual among high-level languages in giving the programmer direct access (under the
supervision of the operating system) to memory addresses, via the pointer type. A machine
address, you will recall, is a number identifying the place in memory where some item of
data is stored. As it is just an integer, it can be used with the basic addition and subtraction
operators. This also includes the use of the ++ and -- operators. Programmers generally refer
to this as pointer arithmetic. All C programmers have to become adept at pointer arithmetic, but it is especially important for audio programmers, who routinely have to step through
arrays of audio samples, or other data, efciently.
In this section you will learn more about how to read from, and write to, a variable via a
pointer to it. This will enable you to read and write to blocks of memory (for example, lines
of text, or wavetables containing synthetic waveforms or sampled sounds, or lines of text)
and, more generally, to manipulate blocks of data in a way that is both efcient and exible.
Much of the time, we will be dealing with lots of somethinglots of MIDI notes, lots of
audio samples, lots of wavetables, lots of envelopes, and so on (in other words, collections
of all the same type of data). In this situation, since the compiler knows which memory
space a single item occupies, we can use pointer arithmetic to move this data around, and to
move it around efciently, even when we may not know in advance how much data there is.
This section demonstrates the use of pointer arithmetic to step through command-line
arguments in a way that enables us to support the use of optional arguments to a program,
and, generally, a variable number of arguments. Thus, we begin to see the potential power of
command-line tools wherever a single highly congurable task is to be performed. As usual,
the program presented here builds further on previously presented code examples. In particular, this program shows just how easy it is to create a le in C, and to write text to it.
1.6.2

Indirection and the contents-of and address-of Operators

As we learned in section 1.3, the declaration below denes ptr as a pointer to a double:
double* ptr;
This is a variable, then, that contains the machine address of another variable (gure 1.5).
We cannot do anything with this, however, until it has something proper to point to (as it
stands, the address it contains is randomwhatever the memory happens to containand

111

Programming in C

Figure 1.5
Pointers and memory addresses.

something we should not use). We have to give it something to point toto initialize the
pointer:
double buffer[1024];
ptr = buffer;
In C, the name of an array is in effect a pointer to the rst element of it (buffer[0]). So
this assignment sets ptr to point to the beginning of buffer. We can now both read and
write to buffer by the means of a pointer. The technical term for this is indirection. Instead
of writing
buffer[0] = 1.0594631;
we can now assign this value indirectly, via the pointer:
*ptr = 1.0594631;
We would typically read this as set the contents of ptr to 1.0594631. So here * is not a
multiplication (which would be meaningless here, of course), but a contents-of operator.
This notation can be used on both the left-hand and the right-hand side of an assignment:
double* ptr; /* pointer; currently uninitialized */
double val;/* a simple number variable */
ptr = buffer; /* now it has something to point to:
the first element of buffer, i.e. buffer[0] */
*ptr = 1.0594631; /* buffer[0] now contains 1.0594631 */
/* read the contents of ptr: */
val = *ptr;
/* val now = 1.0594631 */
This works because the name of the array is equivalent to the address of the rst element
of it. C provides an address-of operator, &, to enable us to write such a thing explicitly:
double* ptr;
double val;
ptr = &val; /* find the address of val, and assign it to ptr */

112

Chapter 1

Any time we declare a variable such as val, the compiler has to nd a place in memory to
store it. It is this address that we assign to the pointer ptr. So in reading the last line of the
above example, we would say set ptr to the address of val. With this done, we can initialize val indirectly, via the pointer:
*ptr = 1.0594631;
printf("%f,"val);
This will print the number 1.0594631. So we can replace the assignment
ptr = buffer;
with the exactly equivalent (if rather more awkward-looking) statement
ptr = &buffer[0];
which would be read as set ptr to the address of element 0 of buffer.
By itself, this notation does not really seem to do very much except make things look confusing. After all, why not just say
val = 1.0594631;
and be done with it?
As will be discussed in the next section (and indeed throughout the book), one of the most
powerful uses of pointers is in conjunction with loops. They enable all sorts of massproduction techniques to be used, at high efciencyexactly what we need for writing
audio applications, which can easily demand more CPU power than we have. Often we can
avoid a great deal of costly copying of data by manipulating pointers and addresses. The key
principle to understand is that addresses can themselves be treated as variables. The compiler is
in fact doing just this, behind the scenesa special aspect of C is that it gives a lot of this
power to the programmer too.
1.6.3

Moving Pointers Around

Because a pointer is an integral type (a memory address), whatever the type of the object it
points to, it is possible to apply the increment operator to them as an alternative way of stepping along an array. It is in tasks such as this that the power and the expressiveness of
pointer arithmetic become most apparent.
The fragment below sets each element of the array buffer to zero:
double buffer[1024];
double* ptr = buffer; /* set ptr to point to the start of table */
int i;
for(i = 0; i < 1024; i++) {
*ptr = 0.0;
ptr++; /* move pointer to next element of the array */
}

113

Programming in C

At the end of the for loop above, the pointer ptr is pointing to the position
table[1024], i.e. one step beyond the end of the array. Audio programmers are especially
fond of this technique, as very often it results in faster code than the array index notation.
Note that you must always make sure that you know what such a pointer is pointing to. If
you follow that loop with another indirect write,
*ptr = 1.0594631;
you could cause a crash, as that memory address is outside the array, and belongs to something else. The same problem arises if the loop test is stated incorrectly:
for(i = 0; i <= 1024; i++) {
*ptr = 0.0;
ptr++;/* move pointer to next element of the array */
}
But few C programmers write this code this way. The most effective use of the ++ operator is
in conjunction with some other operation (again, because such combinations are often available as single machine instructions). Instead, the for loop would be written as follows:
for(i = 0; i < 1024; i++)
*ptr++ = 0.0;
This would be read as set the contents of ptr to 0.0, then increment ptr. This is an
example where it is important to understand C precedence rules. (See subsection 1.2.9.) In this
case, the *ptr combination is applied rst, followed by the increment operation, applied to
the pointer. Note also that, by condensing the loop code to a single statement, this notation
has also enabled us to dispense with braces. You will nd some form of the above two lines
liberally scattered throughout programs in this book.15
More generally, C supports the use of the addition, subtraction, and comparison operations on pointers. This is perhaps the most striking demonstration of how C can be close
to the hardwaremachine code depends a great deal on pointer arithmetic. Pointer arithmetic is both one of the most widely exploited features of C, and also one of the most
criticized. A major selling point of the language Java, for example, was that it did not permit
either pointers or pointer arithmetic.
The code in listing 1.6.1 demonstrates a way of nding the value and position of the maximum positive sample in an array, stepping backward one sample at a time. In this way, it
will nd the maximum sample nearest the start of the buffer. It assumes the same buffer
of 1024 floats, but now lled with audio samples:
Listing 1.6.1
1
2

double* ptr = buffer + 1024; /* point to last element */


double maxval = 0.0;

114

Chapter 1

3
4
5
6
7
8
9
10
11
12

unsigned long pos = 0;


while(--ptr != buffer) {
if(*ptr >= maxval){
maxval = *ptr;
pos = ptr - buffer;
}
}
printf("the maximum sample is %f, at position %d\n",
maxval, pos);

There are several details to observe in this example:


(1) In the conditional test in line 5, the pre-decrement operator is applied to the pointer
ptr. This steps the pointer back one element before the comparison with buffer is made.
Thus ptr has had to be initialized in line 1, using the addition of a constant integer to a
pointer, to point at the element immediately beyond the array (buffer + 1024). As it happens, this is exactly the position it ended up at in the previous code example.
(2) The != operator is used to compare ptr with the address of the start of the array
(buffer). As pointers are integral numbers, such comparisons can be relied upon to be exact,
and as we are stepping back one element at a time, toward the beginning of the array, we
cannot fail to nd it eventually. It would be equally safe to use the > (greater than) operator instead.
(3) To nd the maximum value nearest the start of the buffer, the >= operator is used in line
6. If only the > operator were used, this would fail to nd the nearest maximum sample
where several samples have the same value (e.g. in a square wave).
(4) The position of the maximum sample is found by the calculation in line 8, subtracting
one pointer from another. This exploits the fact that the name of an array (buffer) is equivalently the address of the rst element. The difference between that and the value of ptr
gives the distance from the start of the buffer to the maximum sample.
Automatic pointer increments are commonly used in processing blocks of audio samples.
In such cases, the size of the block is often xed, and can be guaranteed to not be zero. This
is the case, for example, in all the audio processing code in Csound, which uses buffers of
samples that are always ksmps in size. As a result, Csound uses the do loop in an especially
interesting way, in conjunction with pointer increments. Listing 1.6.1 shows a typical example from code that implements a simple lter to block DC in a signal. The variable nsmps is
initialized to ksmps, and the arrays pointed to by samp and ar are both nsmps in size.
Listing 1.6.2
1
2

nsmps = ksmps;
do {

115

Programming in C

Figure 1.6
The compilers view of an array.

3
4
5
6
7

float sample = *samp++;


output = sample - input + (gain * output);
input = sample;
*ar++ = output;
} while (--nsmps);

Note the use of pointer increments (line 3, line 6) to step forward through the blocks of samples, while the loop counter nsmps is pre-decremented inside the while test (line 7). The
loop will terminate when nsmps no longer evaluates to true, which will be the case when
it reaches zero. At that point, the loop has counted down from 30 to 1, as required.
In the expression sample = *samp++, the operator ++ has a higher precedence than the dereference operator *. This means that the pointer is incremented, not the value pointed at.
Because the postx increments after the variable is used, the dereferencing (i.e. the value
pointed at is assigned to sample) occurs before the pointer increment. The counter variable is
not used inside the processing block.
This example demonstrates a very common practice in C programmingincrements tend
to be post, while decrements tend to be pre. Why this back-to-front way of stepping through
a block? It is often the case that auto-decrement is faster than auto-increment, and similarly
that a comparison against zero is faster than comparison with some non-zero value. As this
loop repeats at the audio sample rate, even small gains (maybe just one CPU instruction) can
accumulate to give a big gain.
Figure 1.6 illustrates how array indexing works at the compiler levelthe compiler knows
how large each element is, and automatically calculates the distance from the start of the
array.

116

Chapter 1

Dont worry if pointer arithmetic looks dauntingyou dont have to use it. The code in
listing 1.6.1 can just as well be written stepping forward and using index notation, as in listing 1.6.3.
Listing 1.6.3
1
2
3
4
5
6
7
8
9
10
12

double maxval = 0.0;


unsigned long pos = 0;
int i;
for(i=0; i < 1024; i++){
if(buffer[i] > maxval){
maxval = buffer[i];
pos = i;
}
}
printf("the maximum sample is %f, at position %d\n", maxval,
pos);

Note that as we are now stepping forward, the >= operator is now replaced by the > operator. Many programmers tend to code loops this way to begin with, aiming initially for as
much clarity as possible, and look for ways to optimize it for speed after it has been tested.
It can be argued that a good modern compiler will optimize this code better anyway, and
there is a good case for leaving well alone, unless the speed gain is really signicant. In the
end it is often far more important that you write code you understand, and feel comfortable
withespecially if you are likely to want to change it or add to it later.
1.6.4

Extending the Command Line with Optional Arguments

One way of measuring the power of a computer program is to look at the amount of control it offers the user. A program that does just one thing may be useful, but a program that
offers the user choices may be both useful and powerful. In this subsection the task is to add
to the usefulness of the program presented in the previous section (listing 1.5.3) by adding
some optional controls to change the content of the output, and to add the possibility of
writing the output to a text le. So far, the command line for a program has been completely
xedeither it is correct or it is not. In this section you will learn how to create a command
line that incorporates optional elements. Parsing it will therefore involve not only reading
and testing required arguments, but also detecting and testing optional ones.
Optional arguments are a standard feature of UNIX-style command-line programs, and
some conventions are established which are reected in the approach presented here. To
keep things manageable, all required arguments are kept together, and all optional arguments are placed either before or after these.

117

Programming in C

The program developed in the previous section (listing 1.5.3) printed a list of frequencies
to the screen. The rst renement added in this section is an option to print the plain frequency ratios as well. This will generate the rst two columns of data shown in table 1.2. Secondly, the option is provided to specify the base frequency as a MIDI note value, instead of
a raw hertz value. Finally, an option is provided to write the generated information to a text
le. The rst two of these options are simple switches, to turn a facility on, or to change format to a single alternative; the third requires a lename to be provided by the user.
For the new program iscale, the command line will be dened by the usage message
below (which would be printed to the command line):
iscale [-m] [-i] N startval [outfile.txt]
where:
-m means: interpret startval as a MIDI note
(default: interpret as frequency in hertz)
-i means: print interval ratios as well as frequency values
(default: print just frequency values)
outfile.txt: name of (optional) file to write output to
The purposes of the conventions illustrated here are (1) to indicate optional arguments (in
the usage message) by enclosing them within brackets (not typed by the user) and (2) to
indicate optional arguments placed before required ones by a required prex character: -.
Such arguments are often referred to as switches or ags, especially if, as here, they serve
to turn on a special facility, or to change behavior. This means that the code required to
parse the command line can be very simple. There are two required arguments; there may
be a fourth argument after these; and all optional arguments preceding them use the character - as a prex. To parse this command line, therefore, is a simple three-stage procedure:
(a) Handle any ag arguments, stepping along the argument list. (b) Check that we have at
least two remaining arguments, and process them. (c) If we have a third argument, process
that too.
The rst stage (a) is of course essential. We cannot assume that any of the optional ags are
used, so when we parse the argv[] array of the main() function we cannot rely on N being
argv[1], for example.
We can use pointer arithmetic to solve this problem. Recall from section 1.4 that the various arguments supplied by the user are delivered to the main() function as an array of character strings, together with an argument count (which includes the name of the program):
int main(int argc, char *argv[])
Suppose that the supplied command line is as follows:
iscale -m 19 57
This would be contained in argv as

118

Chapter 1

argv[0]
argv[1]
argv[2]
argv[3]

=
=
=
=

"iscale"
"-m"
"19"
"57"

and argc would be set to 4. We can equivalently say that argv is pointing to the beginning of the array of arguments, and we know there are four arguments.
Each element of argv is simply a char pointer (an address), and the compiler knows how
big this is, just as it does in the case of the buffer[] array in listing 1.6.1. So, as in that
example, we can use the ++ operator to move argv along the array, eliminating each argument as it does so. At the same time, we must decrement argc so that it tells us how many
arguments are left, counting from argvs current position. The following sequence demonstrates how the argument list changes as this procedure is performed.
Step 1
argv++;
argc--; /* argc now = 3 */
This steps over the name of the program, leaving us with the users argument list:
argv[0] = "-m"
argv[1] = "19"
argv[2] = "57"
Step 2
argv++;
argc--; /* argc now = 2 */
Steps 1 and 2 are shown graphically in gure 1.7, from which we can see that argv now
contains the two required arguments:
argv[0] = "19"
argv[1] = "57"
Figure 1.7 illustrates what is clearly an iterative (repeating) process, for which a loop is
appropriate. In this case, the loop test will be for the presence of the prex character -. Since
we have established the convention that the rst argument not using this character is the
rst required argument, the loop must break as soon as this condition is found.
In section 1.3 we learned how to access individual characters of a char array. The same
principle applies to an array of char arrays (strings). Suppose argv[1] contains the address
of the string -mwe can read the individual characters using brackets, but as this is an array
of arrays, we need two pairs (see gure 1.8):
argv[1][0] contains '-'
argv[1][1] contains 'm'

119

Programming in C

Figure 1.7
Stepping through argv[].

Figure 1.8
argv[] as an array of arrays.

If this looks messy to you, you can create a variable to represent an individual argument:
char* arg = argv[1];
arg[0] contains '-'
arg[1] contains 'm'
Programmers sometimes do this, especially if there is heavy use of brackets or if the code is
being repeated many times (the compiler may have to generate more code to translate the
brackets). This technique reduces the level of indirection with respect to each string.
1.6.5

Dealing with Alternatives: if . . . else

In the iscale program we have introduced two possible ag options: -m and -i. Any other
character following the will be an error. In English pseudo-code, we would say
if there is a ag
if the ag is m
set MIDI option
otherwise
if the ag is i
set interval option

120

Chapter 1

otherwise
report an incorrect ag error
In C, the keyword else is used for otherwise; it is an optional extension to the if keyword, and it must be followed by a complete C statement or statement block. It always
belongs to the immediately preceding if. The example below demonstrates a series of
if. . .else tests; it also shows how the individual characters of an argument are accessed
to parse the argument (see section 1.3).
Listing 1.6.4
if(argv[1][0] == '-'){/* is it an optional flag ? */
if(argv[1][1] == 'm')
ismidi = 1;
else if(argv[1][1] == 'i')
write_interval = 1;
else {
printf("error: incorrect flag option %s\n", argv[1]);
return 1;
}
}
The else keyword is used with both ifs to ensure the error message is reported only if the
ag is neither 'm' nor 'i'. Note the code layout in the above example, with else if run
together on a single line. This is the usual convention for this type of code sequence, even
though this is, strictly speaking, a series of nested if. . .else statements. Braces make
the true relationships of the options explicit:
Listing 1.6.5
if(argv[1][0] == '-'){
/* is it an optional flag ? */
if(argv[1][1] == 'm')
ismidi = 1;
else {
if(argv[1][1] == 'i')
write_interval = 1;
else {
printf("error: incorrect flag option %s\n", argv[1]);
return 1;
}
}
}

121

Programming in C

You may want to work through the versions on listings 1.6.4 and 1.6.5 to assure yourself
that the logic is the same in each case for any given ag option. One reason the layout of
listing 1.6.4 is preferable is that it shows that each alternative lies at the same logical level
(and should thus be indented identically).
This also gives a very practical advantage: should you want to add another ag option, all
that is required is the insertion of another if. . .else combination; or rather, an
else. . .if combination. For example, if you had code that could play the calculated scale
through the audio output, you might dene a new command-line ag -p, with an associated variable:
int playit = 0;
This would be detected using a new else. . .if combination inserted into the code above:
Listing 1.6.6
if(argv[1][0] == '-'){
if(argv[1][1] == 'm')
ismidi = 1;
else if(argv[1][1] == 'i')
write_interval = 1;
else if(argv[1][1] == 'p')
playit = 1;
else {
printf("error: incorrect flag option %s\n", argv[1]);
return 1;
}
}
This conveniently minimizes the amount of code indentation, while enabling rapid additions (and removals) without affecting code elsewhere.
1.6.6

Using Local Variablesthe Scope of a Variable

In the above example, the expression argv[1][1] is used three times. It would be good to
simplify this (and maybe even make the code more efcient) by removing this duplication.
This can be done by declaring a new char variable to contain the ag character:
Listing 1.6.7
char flag;
if(argv[1][0] == '-'){
flag = argv[1][1];

122

Chapter 1

if(flag == 'm')
ismidi = 1;
else if(flag == 'i')
write_interval = 1;
else if(flag == 'p')
playit = 1;
else {
printf("error: incorrect flag option %s\n", argv[1]);
return 1;
}
}
C requires that all variables be declared at the top of main(), before any other C statements. However, C also supports the use of variables local to a statement block (enclosed by
braces); these are, naturally enough, termed local variables. As the if test above is followed
by a statement block enclosed by braces, we can place the declaration of flag at the top of
the block.
Listing 1.6.8
if(argv[1][0] == '-'){
char flag;
flag = argv[1][1];
if(flag == 'm')
ismidi = 1;
else if(flag == 'i')
write_interval = 1;
else if(flag == 'p')
playit = 1;
else {
printf("error: incorrect flag option %s\n", argv[1]);
return 1;
}
}
Used this way, the variable char exists only within the block; it cannot be accessed from
outside. The variable is therefore a temporary one, used a few times and then discarded. The
technical term for this behavior is scopethe variable has local scope within the block in
which it is declared. The basic principle of scope is that a variable exists at a given statement
block level, and at any lower level. Thus the variables declared at the top of main have scope
within main, and also within any statement blocks inside main.
Because a local variable exists only within the block in which it is declared, there is no
reason not to use the same name in other blocks at the same level.

123

Programming in C

Listing 1.6.9
float buffer[1024];
float* bufptr = buffer;
int ascending = 0;
/* create ascending or descending ramp */
if(ascending){
int i;
for(i = 0; i < 1024; i++)
*bufptr++ = (float) i;
}
else {
int i;
for(i = 1024; i; i--)
*bufptr++ = (float) i;
}
If the variable i declared in the else block is omitted, the compiler will report an error
because the previous i is not accessible outside its block.
1.6.7

Creating a Text Filethe Special FILE Type

In addition to optional ag arguments preceding our required command-line arguments, we


have also specied an optional extra nal argument giving a lename to which to write the
output of the program (which we will in any case be writing to the screen as usual). Detecting this argument is easyif we have used the procedure of the previous section to step past
any ag arguments, we know that our required arguments are in argv[0] and argv[1],
and that argc must be at least 2. If it is in fact 3, we know that argv[2] contains the lename. Of course, if argc is more than 3, the user has typed something superuous. In this
case, it is purely a matter of courtesy to point this out to the user, if we so choose.
To create and write to a text le, we will need the three functions (declared in stdio.h)
listed in table 1.4. These all use a pointer to a strange object called FILE. As well as the
Table 1.4
Standard C functions to create, write, and close a text le.
Function

Description

FILE *fopen(const char* name, const char* mode);

Open a le for reading or writing.

int fprintf(FILE *fp, const char* format, ...);

Write formatted text to FILE.

int fclose(FILE *fp);

Close an open FILE.

124

Chapter 1

simple types used so far, such as int, double, and char, C allows the programmer to
invent any arbitrary types. The FILE type is also dened in stdio.h, and we need know
nothing about it at all except that it is used by these functions to represent an instance of
a le, that it must always be typed in upper case, and that it is always referred to indirectly
via a pointer.
These functions are far from the full extent of the facilities the C library provides for dealing with les (to say nothing of those specic to a particular operating system), but for
the simple requirements of this program (writing lines of text to a le) they are all we need.
The function fprintf() is perhaps the most interesting, as it is almost identical to the
printf() function we have been using since section 1.2, the only difference being the initial FILE pointer argument. The text formatting options are, however, identical, which
means that we can use the same format string for fprintf() that we use for printf().16
Perhaps unexpectedly, the single function fopen() can be used both for reading and writing a le. It is worth considering briey what sorts of tasks we may want to perform on a le.
Here are some candidates:
n
n
n
n

Create a new le (for writing).


Read an existing le.
Append data (writing) to an existing le.
Overwrite some or all of a le.

The second mode argument to fopen() enables us to specify what we want, in a simple
way. It is a pointer to a const char string (the const qualier meaning, as you will recall,
that the function guarantees not to modify the string in any way). The basic options are: w
for writing, r for reading and a for appending.
In the program as specied, all we need do is create a le and write lines of text to it, so the
w mode is as much as we need. Note that this will overwrite an existing le of the same
name.
1.6.8

Putting It All Together

The full program iscale is shown in listing 1.6.10. It falls into distinct blocks, indicated by
an empty line. The rst stage (lines 140), after the declarations of top-level variables, looks
for any optional ag arguments, setting the ag variables ismidi and write_interval if
required. Note that these are both initialized, in their declarations, to the required default
values. Note also how the ag checking code illustrated in subsection 1.6.6 is embedded in a
loop stepping through the argument list, with pointer arithmetic used to move argv along
the array. Because the code reads argv[1], it is necessary to check that argc is at least 2
(line 24), otherwise the code would be reading outside the argv array. The loop breaks if
the test in line 25 fails, indicating that all optional ags, if any, have been read.

125

Programming in C

The required arguments are processed in the next section (lines 4272). Note that this
code depends on the variable ismidi, in order to interpret the startval argument correctly. The comments in the code show how these arguments are tested for reasonableness.
The error messages are useful not only for the user but also for the person reading the code.
The next stage (lines 7482) checks for the optional le argument by checking the value of
argc. At the very end of the program, we must close this le (line 126), if it has been created.
We will know whether it has been created because the FILE* pointer fp is initialized to
NULL (line 75); if it is still NULL at the end, we know it is unused, and there is nothing
to close. One user-friendly aspect of this code is that if for some reason the le cannot be
created (fopen() returns NULL in this case) the program does not terminate (it can still write
information to the screen) but merely prints a warning message. A C library function
perror() is usedthis prints a message describing the cause of the most recent system
error. A le may fail to be created for many reasons; for example, it may be already open in
another application, so access is denied, the disk may be full, the lename may be incorrectly
formed, or there may be a hardware fault. Similarly, any attempt to write to a le (indeed,
any attempted access at all) may result in an error, so it is equally important to test the return
value of fprintf()this will be the number of bytes written, or a negative value in the case
of an error. In all such situations, perror() can be used to display the cause of the error.
One habitual exception to this rule is fclose(); while this does return a value indicating
success or failure, there is not much that can be done, especially when the program is terminating anyway, so this value is often ignored by programs, as here. In other situations, for
example where multiple les are being used, it may be more important to advise the user if
an error arises.
The next stage (lines 84103) comprises the heart of the program, in which the array of
frequencies is calculated. In line 88 the variables c0 and c5, which have no other use, are
declared local variables. If you tried exercise 3 in the previous section, you may notice that
this program implements one solution to that problem. The array intervals[] is declared
to be one larger than the maximum accepted value for notes, and the loop tests (line 100,
107) use the <= operator, rather than the < operator normally used in such loops.
The nal section of the program (lines 105121) prints the results to the screen, using
printf(), and also to the le if requested, using fprintf with the same format strings.
The program offer the possibility to write out the raw interval values as well as specic frequency values, so the if and else keywords are used several times to deal with each pair
of options, all inside a single loop running through the intervals array. Note that the calls
to fprintf() read the return value into the variable err, and if non-zero break out of the
loop. That variable is then tested (line 123), and perror() used with an optional custom
message (line 124), to report the cause of the error. In line 18 err is initialized to 0. By ensuring that it is not used in any other context, we can simply test this for a negative value (line
123), which is then sufcient to indicate both that the optional le facility was used, and
that there was an error.

126

Chapter 1

Listing 1.6.10
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42

/* iscale.c */
/* generate E.T tables for N-notes to the octave (N <= 24) */
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
/* usage iscale [-m][-i] N startval [outfile.txt]
-m : sets format of startval as MIDI note
-i : prints the calculated interval as well as the abs freq
outfile: optional text filename for output data
*/
int main(int argc, char* argv[])
{
int notes,i;
int ismidi = 0;
int write_interval = 0;
int err = 0;
double startval,basefreq,ratio;
FILE* fp;
double intervals[25];
/* check first arg for flag option: argc at least 2 */
while(argc > 1){
if(argv[1][0]=='-'){
if(argv[1][1]== 'm')
ismidi = 1;
else if(argv[1][1]== 'i')
write_interval = 1;
else {
printf("error: unrecognized option %s\n",argv[1]);
return 1;
}
/* step up to next arg */
argc--;
argv++;
}
else
break;
}
if(argc < 3){

127

Programming in C

43
printf("insufficient arguments\n");
44
printf("Usage: itable [-m][-i] N startval [outfile.txt]\n");
45
return 1;
46
}
47 /* now read and check all arguments */
48 /* we now expect argv[1] to hold N and argv[2] startval */
49
notes = atoi(argv[1]);
50
if(notes < 1 || notes > 24){
51
printf("error: N out of range. Must be between 1 and 24.\n");
52
return 1;
53
}
54
startval = atof(argv[2]);
55
if(ismidi){
56
if(startval > 127.0){
57
printf("error: MIDI startval must be <= 127.\n");
58
return 1;
59
}
60
/* for MIDI, startval = 0 is legal */
61
if(startval < 0.0){
62
printf("error: MIDI startval must be >= 0.\n");
63
return 1;
64
}
65
}
66
else { /* it's freq: must be positive number */
67
/* check low limit */
68
if(startval <= 0.0){
69
printf("error: frequency startval must be positive.\n");
70
return 1;
71
}
72
}
73
74
/* check for optional filename */
75
fp = NULL;
76
if(argc==4){
77
fp = fopen(argv[3],"w");
78
if(fp==NULL){
79
printf("WARNING: unable to create file %s\n",argv[3]);
80
perror("");
81
}
82
}
83
84 /* all params ready - fill array and write to file if created */
85

128

Chapter 1

86
/* find basefreq, if val is MIDI */
87
if(ismidi){
88
double c0,c5;
89
/* find base MIDI note */
90
ratio = pow(2.0,1.0 / 12.0);
91
c5 = 220.0 * pow(ratio,3);
92
c0 = c5 * pow(0.5,5);
93
basefreq = c0 * pow(ratio,startval);
94
}
95
else
96
basefreq = startval;
97
98
/* calc ratio from notes, and fill the array */
99
ratio = pow(2.0,1.0/notes);
100
for(i=0;i <= notes;i++){
101
intervals[i] = basefreq;
102
basefreq *= ratio;
103
}
104
105/* finally, read array, write to screen, and optionally to file */
106
107
for(i=0; i <= notes; i++){
108
if(write_interval)
109
printf("%d:\t%f\t%f\n", i, pow(ratio,i), intervals[i]);
110
else
111
printf("%d:\t%f\n", i, intervals[i]);
112
if(fp){
113
if(write_interval)
114
err = fprintf(fp,"%d:\t%f\t%f\n",
115
i, pow(ratio,i), intervals[i]);
116
else
117
err = fprintf(fp,"%d:\t%f\n", i, intervals[i]);
118
if(err < 0)
119
break;
120
}
121
}
122
123
if(err < 0)
124
perror("There was an error writing the file.\n");
125
if(fp)
126
fclose(fp);
127
return 0;
128 }

129

Programming in C

1.6.9

Testing the Program

This program is clearly more complex than previous ones (shown not least by its length),
and the use of optional command-line arguments gives greatly increased importance to the
process of testing it. Once you have compiled it, be sure to test all combinations of optional
and required arguments, and also make sure that incorrect command lines (e.g. use of a nonexistent ag) are tested. Try your best to make the program crash. What you miss, someone
else (perhaps a paying customer) will be sure to nd, sooner or later. Some specic suggestions for tests are given in the exercises. Before doing this, study the code carefully, while
thinking about all the errors a user might make. Can you anticipate user errors that the
code does not deal with? Dont forget to check the contents of the optional text le, to
make sure it corresponds to the information written to the screen.
1.6.10

Exercises

Exercise 1.6.1
Add the description of all arguments to the usage message in line 44 (see subsection 1.6.4).
Exercise 1.6.2
Without running the program (i.e. by reading the code), decide how the following command
lines will be handled:
(a)
(b)
(c)
(d)

iscale -m -i -m 19 60
iscale -i - 19 60
iscale m 19 60
iscale 19 data.txt

Test your conclusions by running the program with these command lines. If you have found
problems, explain their causes and suggest possible solutions. (Hint: the library documentation for your compiler is your friend.).
Exercise 1.6.3
In lines 107121, the expression pow(ratio,i) is used twice, and the expression
intervals[i] is used four times. Eliminate this repetition by using two variables local
to the for block.
Exercise 1.6.4
Extend iscale with an optional ag that appends data to the output text le, if it already
exists. Thus successive runs of the program will add another set of tables to the le. Using
fprintf(), ensure that these tables are clearly separated, and add a line of text giving a
descriptive name for each set. Remember to update the usage message.

130

Chapter 1

Table 1.5
A simple envelope breakpoint le.
x

0.00

0.00

0.05

0.10

0.75

1.00

0.85
1.50

0.25
0.25

1.95

0.10

2.00

0.00

Exercise 1.6.5
Adding to the above exercise, before each set of data, print to the le the full command line
used.
Exercise 1.6.6
In lines 107121, if fprintf() returns an error, the loop is broken, with the result that output to the screen is also stopped. Is this a serious problem? If so, rewrite the code so as to
separate writing to the screen from writing to the le.
1.7

Creating Types, Functions, and Memory


1.7.1

The Time/Value Breakpoint File

After devoting so much attention to calculating musical scales, it may come as something
of a relief to move onto a different area of musical processing. The object that is the focus of
this section is the standard time/value breakpoint le. If you have explored the compiled
versions of the programs included on the DVD, you will have found several that use such
les, for making time-varying changes to a sound. Some example breakpoint les (with the
le extension .brk) are also provided. You will use the knowledge gained in this chapter
to create some of these programs yourself. You may like to devote a little time to trying out
those programs, before proceeding further with this chapter.
As its name suggests, a breakpoint le is a text le comprising at least two lines containing
pairs of numberstime and valuedening how some parameter (e.g. amplitude) varies
over time. A simple breakpoint le describing an amplitude envelope is shown in table 1.5.
The same data is represented graphically in gure 1.9. If you think that this looks like the
automation tracks often used in audio editors, you would be rightthat is exactly what it
is. Depending on the product, such automation tracks can be more or less sophisticated (e.g.

131

Programming in C

Table 1.6
Some common shell commands.
Action

DOS

UNIX

Directory separator

Change directory

cd

cd

Copy le

copy

cp

List directory
Move le

dir
move

ls
mv

Delete le

del

rm

Create directory

mkdir

mkdir

Remove directory

rmdir

rmdir

Current directory

.\

./

Parent directory

..\

../

Figure 1.9
A simple envelope.

some may allow the denition of curves between points, as well as simple linear ramps), but
the principle is the same.
In order to use a breakpoint le reliably, with multiple applications, we must rely on the
data having certain properties and boundaries. A breakpoint le must comply with a number
of rules, if it is to be portable, and of long-term use. Even from studying the one example in
table 1.5 we can deduce a number of these:
The le lists one breakpoint per line.
A breakpoint comprises two oating-point numbers separated by white-space (spaces and/
or tabs).

n
n

132

Chapter 1

The rst number gives the time for the breakpoint, the second number gives the value at
that time.
n
The times must be increasing.
n
The rst point must have a time of 0.0.
n
There can be any (nite) number of breakpoints.
n

Additional rules might include the following:


Two breakpoints cannot have the same time (i.e. the difference between points must
be 4 0).
n
No empty lines are allowed, except (perhaps) at the end.
n
Only one breakpoint set allowed per le.
n
A breakpoint le must contain at least two points.
n

A breakpoint le has certain properties:


The time difference between adjacent points can vary.
A given breakpoint set will have a duration (time of nal point), and minimum maximum values.

n
n

Some breakpoint les will have properties specic to an audio process:


Breakpoint les with values between 0.0 and 1.0 inclusive can be used as amplitude envelopes.
n
Breakpoint les with values between 1.0 and 1.0 inclusive can be used as (stereo) pan trajectories, where 1 denes full Left, 0 denes Center (mono) and 1 denes full Right.
n

This information is sufcient to enable a programmer, even without seeing an example of a


breakpoint le, to write a program that can read a le, decide if it is legal, and determine its
properties. The same code can also be used as part of a larger application that uses the breakpoint data to perform audio processing. The use of the word object is no accident. In this
section, you will embark on some elementary object-oriented programming. The time/
value breakpoint is the object in question, and we will learn how to represent it as an
object in C. We will also learn how to dene functions that use or create this objectin
Object-Oriented Programming these would be called methods, though we would need to
move into C++ programming to realize that concept fully.
The remaining challenge presented by the breakpoint le arises from the fact that it can be
of any size. As we cannot know in advance how many breakpoints a given le contains, we
cannot dene an array in advance, in which to store them (as an array can only be dened
with a xed size). So we need to be able both to obtain memory of some arbitrary size from
the operating system, but also, if required, we will need to be able to expand the size. In
learning how this is done, we will also, at last, discover exactly how NULL is dened.

133

Programming in C

1.7.2

Dening Custom Objects in C using the struct Keyword

So far, we have learned about the simple variable types supported by C (int, float, char,
etc.), and about the array, which is a collection of a single type. We can also use arbitrary
combinations of these types to dene new ones. We have to be able to do two things
dene a type, and dene a variable of that type. The rst task is fullled by the C keyword
struct (short for structure). The code below uses this to dene a breakpoint as two
doubles:
struct breakpoint {
double time;
double value;
};
Note the use of braces to enclose the elements comprising the structure, and the terminating
semicolon. The name following struct (breakpoint) is formally termed the struct tag.
This structure would be identical to an array of two doubles, except for the crucial difference
that each element has a name. Although not as useful for the program developed in this section, it is also possible, and very common, to dene structs containing different types. Suppose that it was convenient to dene a breakpoint where time is measured not in seconds,
but in discrete ticks (perhaps, samples). The struct could be dened as
struct tickpoint {
unsigned long ticks;
double value;
};
The struct demands more work from the compiler than does the array, as in the former
the type and size of each element has to be recorded, and used whenever that struct is
referenced.
For example, an instance of the breakpoint structure dened above could be declared
simply as
struct breakpoint point;
Note that the keyword struct must be used as well as the struct name; point is the name
of the variable we have dened. We can combine structure denition and variable declaration into one statement:
struct breakpoint {
double time;
double value;
} point;

134

Chapter 1

We can now ll in the elements of the new variable by name. To do this, the element name
is appended to the variable name separated by a stop character:
point.time = 0.0;
point.value = 0.5;
It is also possible to use an initializer list (see subsection 1.3.2):
struct breakpoint point = {0.0,0.5};
An important operation allowed for structs is assignment or copy:
struct breakpoint point1,point2 = {0.0,0.5};
point1 = point2; /* point1 now contains {0.0,0.5} */
No other operators are allowed with structsfor example, you cannot write arithmetic
expressions with them:
struct breakpoint point3;
point3 = point1 + point2;/*error: + not allowed with structs*/
To be able to do such things, we will have to progress to C++, which among many other
things allows such operators to be overloadedthat is, dened for any custom object. In
C, we will have to dene our own functions to do these things17.
1.7.3

Dening Custom Types using the typedef Keyword

It is probably already apparent that typing the struct keyword plus the tag is a somewhat
tiresome amount of typing. We really want to use a single name to specify the object, so that
we can write
breakpoint point;
instead. The magic C keyword that enables us to do exactly that is typedef. The format for
typedef is simple (note the nal semicolon):
typedef <existing type> <typename> ;
A common use for typedef is to dene shorthand names for unsigned integer variables. The
example below is familiar to all Windows programmers:
typedef unsigned short WORD;
typedef unsigned long DWORD;
Though it can seem that typedef simply performs some form of text substitution, it really
does dene a new C type that can, for example, be given to sizeof:
long size = sizeof(DWORD);
As soon as an object has been typedefed in this way, it can be used in struct denitions,
and even as part of a further typedef:

135

Programming in C

/* define an object describing a soundfile */


typedef struct soundfile_info {
DWORD nSamples;
DWORD samplerate;
WORD nChannels;
char* name;
} SFINFO;
/* create an instance of the SFINFO object, and initialize it */
SFINFO info;
info.nSamples = 1234567;
info.samplerate = 96000;
info.name = "main title";
info.nChannels = 6;
You may reasonably ask at this point why we now seem to need two names for this structure
soundle_info and SFINFO, the rst of which is not used. As it happens, when we use
typedef the structure tag (soundfile_info) is optional.18 Thanks to typedef, the declaration can be simplied:
typedef struct {
DWORD nSamples;
DWORD samplerate;
WORD nChannels;
char* name;
} SFINFO;
The two forms are about equally likely in modern C programs.
Returning to the task at hand, we can now use typedef to create the breakpoint type in
the form we wanted:
typedef struct breakpoint {
double time;
double value;
} BREAKPOINT;
By convention, such names are usually written in upper case, as demonstrated above, to
make them distinctive. This is not a hard and fast rule (as you read more code from various
sources you will nd many examples where lower-case type is used), but it is a good convention to adhere to for such type denitions. You can correctly deduce, for example, that FILE
is a type created using typedefas you can easily discover if you study the code in the
stdio.h header le.
We can also declare an array of BREAKPOINTs:
BREAKPOINT points[64];

136

Chapter 1

Should we so choose, we can initialize the rst breakpoint using array index notation
(brackets) and the dot notation for accessing elements of a structure :
point[0].time = 0.0;
point[0].value = 1.0;
1.7.4

Text Substitution: Introducing the #define Preprocessor Directive

While typedef is a C keyword to dene possibly complex types, it is also possible in simple
cases to use text substitution via the C preprocessor directive #define. A simple text substitution would be to replace an explicit constant value with a symbol:
#define BUFFERSIZE 1024
Note that there is no semicolon terminating the linethis is not C code as such, but instructions to the preprocessor. This symbol can then be used in an array declaration:
short sampbuf[BUFFERSIZE];
Similarly,
#define SAMPLE float
This might be used to facilitate experimentation with alternative sample types in a program:
SAMPLE sampbuf[BUFFERSIZE];
This now declares an array of oats. The symbol SAMPLE will probably be used everywhere a
numeric type would be: if late in the design you decide you want to use doubles instead, you
merely have to change the denition and re-compile:
#define SAMPLE double
Thus the preprocessor supports a form of global search and replaceanywhere the same
value is used, with the same meaning, it is better to use a #defined symbol for it, both to
enable to value to be changed globally without tedious editing and to contribute to the documentation of the program, than to use numeric constants directly.
The #define directive, used widely in C programming, can be employed to substitute
complex sequences of frequently used code text with simpler ones.19 The difference between
#define and typedef is that the former is literally text substitutionbefore the compiler
itself is invoked, whereas typedef is a language keyword, understood by the compiler itself,
and thus can create true abstract types in ways that are beyond the scope of the preprocessor.
The main hazard at this stage is getting the order right. With #define, the synonym or alias
name (e.g. SAMPLE) is written rst, followed by the text to be substituted, whereas with
typedef, the order is opposite, and the type description must be a correct C type declaration:
typedef float SAMPLE;

137

Programming in C

1.7.5

Working with Types: User-Dened Functions

Given an array of breakpoints (e.g. read from a le), one of the important pieces of information we may require is the maximum breakpoint value contained in the data. It is easy to see
that any application dealing with breakpoints is likely to want this information. This is a primary reason for wanting to create a set of functions specic to the BREAKPOINT type, which
can be used repeated, in any application.
Creating a function in C is not at all difcult (you do it every time you write a program
involving main()), but there are a few rules to observe, demanded by ANSI C. From what
you have learned already, you know that a function is dened by a name, followed by parentheses that may contain arguments supplied to the function:
float larger(float first, float second);
In this case, the function called larger takes two float arguments and returns a float
result. As its name suggests, it returns the value of the larger of the two arguments. However,
this is not a function buried in some standard C library; we have to implement it in full ourselves. The line above is in fact the declaration of the functionits prototype, to use the
correct terminology. ANSI C requires that every function used in a le must be provided
with a prototype declaration such as this, ahead of the rst use of the function. Normally,
the function prototype is placed at the top of the source le, or is stored in a header le
which is #included in the usual way.
To complete the denition of the function, we have to write the function body:
float larger(float first, float second)
{
if(first < second)
return second;
else
return first;
}
The only difference between this user-dened function and main() is that as well as writing
the function, we have to provide a prototype declaration, which replaces the function body
(enclosed in braces) with a semicolon. It could hardly be simpler, could it? Given the prototype declaration at the top of the le (or supplied in a header le), we can place this function
denition anywhere we like in a source le, other than inside another function. The prototype ensures that wherever the function is used, the compiler knows exactly what to do. It
also protects the programmer from what can be easy mistakes:
result = larger(0.5);
/* error: function requires two arguments */
result = larger(0.5,0.4,0.1);
/* error: too many arguments */
larger("one",2.5);
/* error: incorrect argument type */

138

Chapter 1

One possible error (which as far as C is concerned is not an error at all) that will not be
reported by most compilers is discarding the return value:
larger(0.5,0.2);

/* no warning message! */

Listing 1.7.1 shows the prototype and denition of a function that reads an array of
BREAKPOINTs and returns a BREAKPOINT containing the maximum value, and the time.
Note the pointer argumentas BREAKPOINT is a type, it can be used like any other C type,
including the denition of pointers. Also note that the function returns a BREAKPOINT,
which is not a single value but a struct. This is something that the original version of C
(known as Kernighan and Ritchie C after its authors) did not allow, but was introduced
with ANSI C.
Listing 1.7.1: The User-dened Functionmaxpoint
/* The prototype*/
BREAKPOINT maxpoint(const BREAKPOINT* points, long npoints);
/* input: points = array of BREAKPOINTS,
npoints gives the length of the array */
/* output: copy of the BREAKPOINT containing largest value */
/* the function definition */
BREAKPOINT maxpoint(const BREAKPOINT* points, long npoints)
{
int i;
BREAKPOINT point;
point.time = points[0].time;/* initialize from first point */
point.value = points[0].value;
for(i=0; i < npoints; i++){
if(point.value < points[i].value){
point.value = points[i].value;
point.time = points[i].time;
}
}
return point;
}
Note also from this example that a user-dened function can have its own private (local)
variables, not visible outside the function. In the return statement, a copy of the internal
variable point is made, so that though that variable will not exist after the function is
called, the output is valid.

139

Programming in C

1.7.6

The void and size_t Types: Creating Memory and Dealing with Variable Quantities

Before we can use the new function, we need to obtain some breakpoints. The problem is, as
stated earlier, that when we are reading them from a le, we do not know how many there
are, in advance, so we cannot simply declare an array, and trust that it will be enough.
Instead, we need to request some memory directly from the operating system, and if need
be, request some more later. When we are nished with it, we must release it back to the
system. The technical term for this procedure is dynamic memory allocation. The standard
C library supports four functions to enable us to do this, declared in stdlib.h:
void* malloc(size_t size);
void* calloc(size_t nobj, size_t size);
void* realloc(void *p, size_t size);
void free(void *p);
These functions introduce two new C types:
(1) size_t: This is a symbol #defined by the compiler (in <stdio.h>) according to the
target machine architecture. It will typically be dened as an unsigned int (which is itself
dened according to the platformsee subsection 1.2.6). It is rare for programmers to need
to pay attention to this. Most requests for memory will use an int or a long (which on 32-bit
systems are the same size anyway), and the compiler will take care of any type conversion
without fuss. However, pedantic programmers will ensure that all arithmetic destined for use
with malloc is performed using the type size_t, or at least is cast to that type beforehand.
(2) void*: Of the two types, this is by far the more important. As a C programmer you will
be working with the void keyword a great deal of the time. A memory request to the system
is simply for a block of bytes, and all the system can do is return a generic or type-agnostic
pointer to this block. The name chosen for this is void in the sense of void of meaning.
The one thing the system will guarantee to do is to supply a pointer that is suitable for the
largest single C type (usually a double)e.g. aligned to a memory address that is a multiple
of eight bytes. This will then equally be suitable for any other type. The programmer simply
casts this pointer (see subsection 1.2.10) to the required pointer type. For the same reason,
the amount of memory requested (in bytes) has to be calculated with respect to the type
using the sizeof keyword. Listing 1.7.2 shows an attempt to allocate a block of 4 megabytes
to hold a large sound sample.
Listing 1.7.2
1 /* request enough memory to hold a lot of samples */
2 #define SAMPLELEN 1048576
3
4 float* sampbuf;

140

Chapter 1

5
6
7
8
9
10
11
12
13
14

sampbuf = (float*) malloc(SAMPLELEN * sizeof(float));


if(sampbuf == NULL){
puts("Sorry - not that much memory available!\n");
exit(1);
}
/* do something useful with all this memory. . ...*/
process_sample(sampbuf);
/* and remember to return it at the end */
free(sampbuf);

Note that the return value from malloc will be NULL if the system is unable to meet the
request.20 We can now understand how NULL is dened in <stdio.h>:
#define NULL ((void*) 0)
This denes NULL to be integer 0, cast to a pointer to void. The special aspect of the void*
exploited here is that is can be compared to any pointer type, without need for a cast. There
is no need, for example, for a test such as
if(sampbuf == (float*) NULL)
The code in lines 8 and 9 warrants some comment. If the memory request is rejected, we
cannot assume that there is any memory available at all.21 A problem with the library function printf() is that it may itself request some memory internally, while generating the
string to be output. This may likely also fail in this situation, so that printf() is not a safe
function to use to report memory allocation errors. Instead, the simpler library function
puts() is used.
1.7.7

Expanding Allocated Memory on Demand: Reading Breakpoints from a File

Reading data from a text le presents many challenges, the primary one being, as already
noted, that we do not know in advance how much storage to provide for the data. We also
have to allow for the possibility, since this is a plain text le, very possibly written by hand,
that the contents may be incorrect in some way. Fortunately, the format of a breakpoint le
is very simple, and we can survive with a simple method to read it.
From the list of memory functions above, you will have noted the function realloc().
This offers exactly what we needit will take an existing allocated block and change its
size, returning a pointer to the modied block (or NULL on failure). That pointer may or
may not be the same as the original one. If realloc fails, the original block is still valid,
with all its data, and we have the choice whether to keep that, even though incomplete, or
abandon the task altogether. The solution presented here takes the latter course, condent
that for the data quantities involved, memory failure is not likely.

141

Programming in C

The logical design of this function (for that is what we will write) is worth setting out in
pseudo-code before coding in C. The procedure is, in essence, to start with a small block of
memory obtained using malloc, and commence reading breakpoints from the le, one line
at a time. If we ll that block, we have to expand it by some amount, then continue. When
reading the line, we will do some basic format checking, to ensure we nd two numbers per
line, and we can also check that the rst value (time) is always greater than the previous one,
by keeping track of the current time value. In pseudo-code, this could be written as follows:
blocksize 64
count 0
allocate initial block of blocksize breakpoints
while(we have read a line from the le){
if(line is incorrect) break
if(block is full)
expand block by blocksize; break on realloc error
copy breakpoint to next place in array, increment count
}
return the block and count to user
Since we are designing a function, the information on the input and output specics is
essential, in order to create the function prototype. The output is a list of BREAKPOINTs
this is simply a pointer to a BREAKPOINT, or NULL if there was an error. A second output
is the size of the array, and as a function can only return a single object, we will have to use
a function argument for this, using a pointer to an external variable. The other input is a
pointer to an open FILE. This leads to the following prototype:
BREAKPOINT* get_breakpoints(FILE* fp, long* psize);
We expect this function to be used this way, given an input le envelope.brk (ignoring error
checks for now):
FILE* fp;
long size = 0;
BREAKPOINT* points;
fp = fopen("envelope.brk","r");
points = get_breakpoints(fp,&size);
As a block of allocated memory is created by our function, and returned to the caller, it will
be the callers responsibility to free() the block at the end of the program.
To read and parse a line of text from a le, we will use two functions from the standard C
library. These functions are declared in <stdio.h>:
char *fgets(char *line, int maxline, FILE *fp);
int *sscanf(char *string, const char* format, ...);

142

Chapter 1

The function sscanf() has a clear relationship to the now familiar printf(); indeed the
conversion speciers are almost identical. To extract two oating-point numbers from a
string, sscanf would be used as follows:
char* string = "0.0 0.5\n");
double time;
double value;
if(sscanf(string,"%lf%lf",&time,&value) != 2)
printf("There was an error\n");
Note that the addresses of the target variables are passed to the function (their types must of
course correspond to the type speciers), and that it returns the number of conversions that
were successfully made. sscanf returns the special value EOF (usually 1) if the string is
empty. Note especially that whereas printf() uses the specier %f regardless of whether
the supplied variable is a float or a double, sscanf needs to make the distinction
explicityou must use %f when you supply the address of a float, and %lf (long oat)
if you supply the address of a double.
There is one aspect of sscanf about which we can do nothing without making things
very much more complicated: as soon as the required number of conversions is made, the
function will return. This means that a string such as the following will be scanned successfully, using the above code, with no error reported:
"0.0 0.5 this is the first line of a breakpoint file\n"
Therefore, our parsing of the breakpoint le will not be utterly unforgiving. Any text following the two required numbers will be ignored. It is entirely up to you whether you regard this
as a bug or as a feature.
1.7.8

Skipping Parts of a Loop: The continue Keyword

One facility that sscanf allows us to implement easily is to accept empty lines. Handwritten
text les can easily contain these at the end (from repeated pressing of Enter), and it will
seem silly to report an error in such cases. In this situation there is no other work to do,
except to read the next line. The keyword continue enables us to do just this:
while(fgets(line,size,fp)){
if(sscanf(line,"%lf%lf",&time,&value) < 0)
continue; /* go read the next line */
/* we got data - do other processing from here*/
}
Of course we dont want an endless loopso how is the loop broken? The function
fgets() will return NULL when it has reached the end of the le, and as far as while (or
any conditional test) is concerned, that counts as a zero value, which will terminate the loop.

143

Programming in C

1.7.9

Putting All the Components Together

Listing 1.7.3 shows a complete implementation of get_breakpoints(), incorporating all


the elements discussed. It presumes a command-line environment, and prints some error
and warning messages. The starting size for the breakpoint array is set internally at 64 (line
4), and the size of the string to hold a line is set at 80 (line 7), which should be ample. The
variable lasttime (line 5) is used to track the time of each new breakpoint (lines 2934),
and break if there is an error. Much of the code is devoted to testing the various possible
return values from sscanf, to detect essential errors (lines 1628). Note that sscanf writes
directly into the appropriate element of a private BREAKPOINT. The precedence of the various C operators works in our favor heresimply by prexing the address-of operator to the
array expression
&points[npoints].value
the expression evaluates to the address of the required element of the selected BREAKPOINT.
It is worth analyzing this step by step:
(1) points[npoints] evaluates to a BREAKPOINT object at position npoints in the array.
(2) points[npoints].value evaluates to the value element of that breakpoint.
(3) &points[npoints].value evaluates to the address of that element (type double*).
Finally, the auto-incremented count is tested against size, and if they are equal, realloc is
called to expand the memory (lines 3547). The code allows for the possibility of the
expanded block having a different address, using a temporary pointer private to the if block
(line 36). If realloc fails, it is essential to set the pointer points to NULL, as this will be
checked by the calling function, and if it is not NULL, the caller will expect to free() it after
processing. Freeing a block of memory twice is an error that in the worst case will cause a
program to crash.
Listing 1.7.3: The Function get_breakpoints()
1 BREAKPOINT* get_breakpoints(FILE* fp, long* psize)
2 {
3
int got;
4
long npoints = 0, size = 64;
5
double lasttime = 0.0;
6
BREAKPOINT* points = NULL;
7
char line[80];
8
9
if(fp==NULL)
10
return NULL;
11 points = (BREAKPOINT*) malloc(sizeof(BREAKPOINT) * size);
12 if(points==NULL)

144

Chapter 1

13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52

return NULL;
while(fgets(line,80,fp)){
got = sscanf(line, "%lf%lf",
&points[npoints].time,&points[npoints].value);
if(got <0)
continue;
/* empty line */
if(got==0){
printf("Line %d has non-numeric data\n",npoints+1);
break;
}
if(got==1){
printf("Incomplete breakpoint found at point %d\n",
npoints+1);
break;
}
if(points[npoints].time < lasttime){
printf("data error at point %d: time not increasing\n",
npoints+1);
break;
}
lasttime = points[npoints].time;
if(++npoints == size){
BREAKPOINT* tmp;
size += NPOINTS;
tmp=(BREAKPOINT*)realloc(points,sizeof(BREAKPOINT) *size);
if(tmp == NULL)
{
/* too bad! */
/* have to release the memory, and return NULL to caller */
npoints = 0;
free(points);
points = NULL;
break;
}
points = tmp;
}
}
if(npoints)
*psize = npoints;
return points;
}

Now we can write a full program to read breakpoints and report the maximum value, using
these two functions, get_breakpoints() and maxpoint(). First, the denition of BREAK-

145

Programming in C

POINT must be written, followed by the prototypes of the two functions, followed in turn by
the full denition of those functions, as shown in listings 1.7.1 and 1.7.3. The main program
is responsible for parsing the command line as usual, opening the le, and after processing,
releasing the array of breakpoints obtained from get_breakpoints(). The main() function is shown in listing 1.7.4, and the full program le (breakdur.c) is available on the
DVD. No further comment will be made about this program hereyou should now be able
to understand how it works just by reading the code.
Listing 1.7.4: The breakdur main Function
int main(int argc, char* argv[])
{
long
size;
double
dur;
BREAKPOINT point, *points;
FILE* fp;
printf("breakdur: find duration of breakpoint file\n");
if(argc < 2){
printf("usage: breakdur infile.txt \n");
return 0;
}
fp = fopen(argv[1],"r");
if(fp == NULL){
return 0;
}
size = 0;
points = get_breakpoints(fp, &size);
if(points==NULL){
printf("No breakpoints read.\n");
fclose(fp);
return 1;
}
if(size < 2){
printf("Error: at least two breakpoints required\n");
free(points);
fclose(fp);
return 1;
}
/* we require breakpoints to start from 0 */
if(points[0].time != 0.0){
printf("Error in breakpoint data: first time must be 0.0\n");
free(points);

146

Chapter 1

fclose(fp);
return 1;
}
printf("read %d breakpoints\n", size);
dur = points[size-1].time;
printf("duration: %f seconds\n", dur);
point = maxpoint(points,size);
printf("maximum value: %f at %f secs\n", point.value,point.time);
free(points);
fclose(fp);
return 0;
}
1.7.10

Exercises

Exercise 1.7.1
Listing 1.7.1 can be improved. The local variable point is initialized to the rst point in the
supplied list. The loop then makes a redundant comparison with that same point. Modify the
code to skip this rst test. Include tests to validate the input points argument accordingly.
(Hint: the function should not rely on the caller (main) validating supplied arguments.)
Exercise 1.7.2
Defensive programming: The number of points in a list cannot be negative. Change the
breakpoint code to use unsigned long rather than long. Hint: To write an unsigned
long using printf, use the format specier %lu.
Exercise 1.7.3
Test breakdur by supplying a variety of non-breakpoint lesa C source le, even a soundle.
Exercise 1.7.4
Make some functions that apply simple processing to a BREAKPOINT array. These might
include
(a) stretch/shrink times
(b) normalize values relative to some maximum value
(c) shift values up or down
(d) scale values by some arbitrary factor
(e) truncate or extend to a given duration
(f) insert or delete a point.
(g) something of your own devising.
Which tasks can be performed in place rather than by creating a new array? What does
this exercise demonstrate about the advantages and disadvantages of arrays?

147

1.8

Programming in C

Generating and Displaying Breakpoints


1.8.1

Introduction

If you have worked through the code and exercises in the previous section, you may already
have discovered that reading breakpoints as columns of numbers is less than ideal, the more
so when evaluating possibly subtle changes. Almost by denition an envelope (being what a
breakpoint le typically represents) is a shape. The most intuitive way to inspect such data it
is therefore graphically, as seen in gure 1.9. Fortunately, tools are available that enable such
data to be displayed with very little work from the user. Such displays are of special importance to audio programmers, who deal as a matter of routine both with breakpoints in
general and amplitude envelopes in particular. We may be able to inspect breakpoint les
numerically when they consist of only a few lines, but for anything more extended a graphical display is essential. This is even more the case with regard to audio data itself, which we
will study in subsequent chapters.
This section introduces the widely used free package Gnuplot, available for all platforms.
See the software section of the DVD for instructions on installation for your platform (note
that on OS X you also need to install AquaTerm). It can be used to display any data represented as numeric text les. This may be in the form of a breakpoint le, or of audio data
presented as text. Advanced uses enable multiple data les to be plotted on the same display
for comparison purposes, together with a wide range of formatting and annotation facilities.
In this section, however, we will need only the simplest plotting facilities. It extends the
work done in the previous section by looking at the programmatic creation of simple attack
and decay shapes, where the number of data points generated can easily exceed what can be
meaningfully inspected in text form. The creation of such data may be the goal of a whole
programming project. However, it is at least as important to understand graphical display as
part of the development process for a project; for example to verify that some block of data is
being created or processed correctly. Tools such as Gnuplot thus become part of the testing
and debugging techniques employed by all programmers.
1.8.2

The Basic Gnuplot Plot Command

Gnuplot is an interactive text-based program (a form of interpreter). On launching, it presents


an internal command prompt at which various plotting or other commands are typed. Note
the difference between usage on Windows, compared to OS X and other UNIX-based platforms. On Windows, when launched (the Windows version is called wgnuplot) a selfcontained command window is created, leaving your DOS window free for other work. For
UNIX-like systems such as OS X and Linux, you will want to use two command windows
one for Gnuplot itself, one for running the programs that generate the data to be plotted. In
typical usage, a program may be run multiple times with different parameters to generate the

148

Chapter 1

data, which can then be plotted by reusing the same command within Gnuplot. The description that follows is based on the use of Terminal in OS X.
The rst step is to create a breakpoint le, such as the one shown in table 1.5. Create the
text le exactly as shown, and call it envelope.txt. Open a Terminal session and change to
the directory in which you have stored the le (here assumed to be called datafiles).
Launch Gnuplot (without any arguments) by typing its name. Some announcement text is
printed, followed by the Gnuplot prompt:
G N U P L O T
Version 4.0 patchlevel 0
last modified Thu Apr 15 14:44:22 CEST 2004
System: Darwin 8.11.0
Copyright (C) 1986-1993, 1998, 2004
Thomas Williams, Colin Kelley and many others
This is gnuplot version 4.0. Please refer to the documentation for
command syntax changes. The old syntax will be accepted throughout the
4.0 series, but all save files use the new syntax.
Type 'help' to access the on-line reference manual.
The gnuplot FAQ is available from http://www.gnuplot.info/faq/
Send comments and requests for help to
<[email protected]>
Send bugs, suggestions and mods to
<[email protected]>
Terminal type set to 'aqua'
gnuplot>
Now type the following command exactly and press Enter:
plot "envelope.txt" with lines
Gnuplot will launch an AquaTerm window and display the data, with a simple annotation
giving the name of the le (gure 1.10). Note in particular that Gnuplot automatically scales
and marks the axes of the plot to match the range of the data. Later we will use a small enhancement to this plot command, but for 99 percent of the time the above basic command
is all you will need. You are nevertheless encouraged to study the Gnuplot documentation
(e.g. type help plot at the Gnuplot prompt) to see a description of all the plotting commands and options. Gnuplot is widely used to create gures for technical papers and
booksthe various formatting, titling, and annotation commands are well worth getting to
know, as is the facility for outputting to graphic le formats as an alternative to the screen.
Having displayed envelope.txt for the rst time, experiment with changing the data and
reissuing the plot command. You can do this manually using a text editor, but if you worked

149

Programming in C

Figure 1.10
An example of Gnuplot output.

exercises 1.7.4 and 1.7.5 you should have at least one program that applies a transformation
and outputs the new data. Explore changes to both the time and value columns, to see how
they are reected in the display. Note that Gnuplot uses a command history facility, so that
to repeat a command you do not have to retype it. Use the up-arrow key to select any previous command, and press Enter. Changes can be made and observed very quickly this way.
1.8.3

Generating Breakpoint Data: The Exponential Decay

The principle of the basic time/value breakpoint le is that (as we see in the output of Gnuplot) values at intermediate times lie on a straight line between adjacent points. The values
can mean whatever the user chooses. However, where the data represents amplitude (as in
the case of the amplitude envelope) straight lines do not result in a musically natural sound.
A natural decay (as heard in a plucked string, or in reverberation) follows a curved amplitude trajectory. Thus, instead of the simple linear decays in envelope.txt between 1.0 and 0.25,
or between 0.1 and 0.0; we need a curved linespecically, we need an exponential decay.
Expressed at its simplest, this says that the decay starts quickly, then slowly (almost) levels
off over time, as in this simple series of reducing amplitude values:
1.0, 0.5, 0.25, 0.125, 0.0625, 0.03125, 0.015625, . . . .
We recognize this as a geometric series, with a constant ratio of 0.5each value is half the
preceding one. Clearly, the numbers become unmanageable very quickly, the more so when

150

Chapter 1

we consider how long we may want a natural decay to last, suggesting both a lot of numbers,
and a lot of very small numbers. The answer, of course, is to use the power of the C language
to generate all these numbers for us, and to verify the correctness of the code by displaying
the output via Gnuplot. It is hardly important to a program these days whether it is processing 10 numbers or 10,000, but it is clearly important to us, if the only alternative is to
attempt to write them manually.
One general formula for an exponential decay is
x aek/T ,

(2)

where a and k are constants and T represents the time constantthe rate of decay.22 For our
purposes, a always equals 1, for the starting value of the decay envelope. We can implement
this directly in C using the math library function exp(). The great merit of this calculation
is that we need to use exp only once, to obtain the value for the constant ratio (which we
know will be a value less than 1). To generate the decay, we simply multiply the previous
output by the constant ratio the required number of times. This is illustrated in the program
expdecay (listing 1.8.1), which for brevity omits the argument error checking a full program would have to employ.
Listing 1.8.1
/* expdecay.c */
/* implement formula x[t] = a* exp(-k/T) */
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
int main(int argc, char**argv)
{
int i,nsteps;
double step,x,a,T,k;
double dur;
if(argc != ARG_NARGS){
printf("usage: expdecay dur T steps\n");
return 1;
}
dur = atof(argv[1]);
T = atof(argv[2]);
nsteps = atoi(argv[3]);
k = dur/nsteps; /* the constant time increment */
a = exp(-k/T); /* calc the constant ratio value */
x = 1.0; /* starting value for the decay */

151

Programming in C

step = 0.0;
for(i=0; i < nsteps; i++){
printf("%.4lf\t%.8lf\n",step,x);
x = a * x;
step += k;
}
return 0;
}
One further reason the code is so short is that no attempt is made to write the data to a
le. We can instead use output redirection at the command line to send the output text to
a le, for example using the command
expdecay 1 0.5 200 > expdecay.txt
Then plot the data using the Gnuplot plot command, as shown above.
This example demonstrates a particularly compact way to add data output to a program
temporarily, for the purposes of testing and debugging. Such small programs are also commonly written by C programmers to test something such as a mathematical function or process, prior to incorporation in a more complex application.
Even this small program offers useful scope for exploration. This is especially important for
any readers less familiar with the mathematics. Experiment rst with the value for T (the
time constant). Small values such as 0.5 and lower create a very steep decay, while higher
ones (perhaps above 1.0) create shallow decays. The shallower the decay, the longer it takes
for the values to reach a low level that we might call silent (perhaps down to 0.0001). The
more points that are used, the smoother the displayed curve will be, and the closer the values
will approach 0 without ever reaching that value.
Note that the format specier for the breakpoint value in the printf statement asks
for eight decimal places. One clear disadvantage of text output is that the precision of the
numbers is determined by that of the output text. Even apparently small values of amplitude
matter in audio. The use of eight decimal places ensures that the behavior of the decay calculation can be observed to a sufciently low level to cover all reasonable audio requirements.
1.8.4

The Exponential Attack and the stdout and stderr Output Streams

The fact that the exponential decay never reaches zero presents the audio programmer with
a problem. Just as the most musical shape for the decay stage of an envelope for a musical
note is the exponential, we nd that we really want a similar shape for the attack stage of a
note (e.g. the rst breakpoint pair in envelope.txt). The intuitively obvious solution is simply
to generate the decay envelope in reverse. However, we have to ask ourselves, at what point
can we say the decay envelope has terminated? Naturally for musical purposes it must do so,
but mathematically the decay continues forever. The numbers become ever smaller but, like

152

Chapter 1

Zenos hare, never reach their target.23 Furthermore, we have to remember that any number
multiplied by zero remains zero. If we are to retain the loop that creates the data, some small
but non-zero start value has to be provided. While the program above represents in some
sense a pure implementation using the exp function directly, it does not necessarily give
us the control we need for practical and general envelope generation. We need rst to be able
to create both attack and decay curves, and second to be able to create them over a variety of
numerical ranges.
We encountered a very similar problem in section 1.2, where we used the pow function to
calculate the frequencies of MIDI notes. These ideas lead to the solution shown in listing
1.8.2, which can be used to create exponential attack and decay breakpoint data over an
arbitrary range. Mathematically, the functions exp, pow, and log are members of a family
expressing the same underlying concepts in different ways, and the audio programmer will
seek to use whichever combination is best suited to the task at hand.
Listing 1.8.2: General Exponential Attack and Decay
1 /* expbrk.c generate exponential attack or decay breakpoint data */
2
#include <stdio.h>
3
#include <stdlib.h>
4
#include <math.h>
5
6
7
int main(int argc, char** argv)
8
{
9
int i,npoints;
10
double startval,endval;
11
double dur,step,start,end,thisstep;
12
double fac,valrange,offset;
13
const double verysmall = 1.0e-4;/*~-80dB */
14
if(argc != 5){
15
fprintf(stderr,
16
"Usage: expbrk duration npoints startval endval\n");
17
return 1;
18
}
19
dur = atof(argv[1]);
20
if(dur <=0.0){
21
fprintf(stderr,"Error: duration must be positive.\n");
22
return 1;
23
}
24
npoints = atoi(argv[2]);
25
if(npoints <= 0){
26
fprintf(stderr,"Error: npoints must be positive!\n");

153

Programming in C

27
return 1;
28
}
29
step = dur/npoints;
30
31
startval = atof(argv[3]);
32
endval = atof(argv[4]);
33
valrange = endval - startval;
34
if(valrange == 0.0){
35
fprintf(stderr,
36
"warning: start and end values are the same!\n");}
37
/* initialize normalized exponential as attack or decay */
38
if(startval > endval){
39
start = 1.0;
40
end = verysmall;
41
valrange = -valrange;
42
offset = endval;
43
}
44 else{
45
start = verysmall;
46
end = 1.0;
47
offset = startval;
48
}
49
50
thisstep = 0.0;
51 /* make normalized curve, scale output to input values, range */
52
fac = pow(end/start,1.0/npoints);
53
for(i = 0; i < npoints; i++){
54
fprintf(stdout,"%.4lf\t%.8lf\n",thisstep,
55
offset + (start * valrange));
56
start *= fac;
57
thisstep += step;
58
}
59
/* print final value */
60
fprintf(stdout,"%.4lf\t%.8lf\n",
61
thisstep,offset + (start * valrange));
62
63
fprintf(stderr,"done\n");
64
return 0;
65 }
As a somewhat more complete program than expdecay.c, expbrk.c incorporates basic error
checking of input arguments. These necessarily involve printing messages to the user;
additionally, a simple message is printed at the end to conrm the program has run to

154

Chapter 1

completion. However, this does not interfere with the primary task, inherited from
expdecay.c, of printing the breakpoint output in such a way that it can be redirected to a le
for display by Gnuplot. It achieves this by making use of the fact that C (in conjunction with
the operating system) denes not one but two standard output text streams, stdout and
stderr. The former is automatically used by the plain printf function, but in this case
both are used via the fprintf function to make the difference explicit. When text output
is redirected, only text written to stdout is redirected to a le; anything written to the
stderr stream remains directed to the screen. Thus, a program can generate both primary
output and secondary error, or informative, output, and the two can be clearly separated
when that is required.
The very small starting value for the attack curve is dened internally in line 13; this is
nevertheless something to be experimented with. As the comment indicates, this value
affects the steepness of the attack. As before, the program should be tested and explored
using Gnuplot to display the data. Note that the program arguments now include start and
end valuesit is possible to create exponential decays descending from 2 to 1, from 4 to 3,
and so on.
1.8.5

Not All Curves Are Exponentials: The log10 Gnuplot Test

It has already been noted that the tail of an exponential decay comprises very small numbers. We discover early on that displaying such numbers graphically, in the context of the
data as a whole, is difcult, as eventually they are too small to be drawn even by a one-pixel
line. A solution is to use a logarithmic vertical scale. A standard scale for amplitude values is
the Decibel, dened by the formula
P(dB) 20.0 log10 (x).

(3)

This is discussed further in later chapters. Sufce it to say here that this calculation can be
performed within Gnuplot as an extension of the basic plot command, so that there is no
need to add it to the programs themselves. Using this calculation on an exponential decay
(within the range 1 to 0) is especially illuminating. Example:
plot "longdecay.txt" using (20.0 * log10($2)) with lines
Here the notation $2 picks out values in the second column in the le. An exponential curve
plotted this way appears as a straight line, covering the full range of the data. Other nonexponential curves will typically convert to some form of curve when using the log transformation. The slope of the line correlates directly with the time constant of the exponential.
Audio amplitude values in the range 0 to 1.0 are said to be in the normalized range. If
your data includes any zero values,24 the above command has to be extended further by
adding a small constant, since a value of zero is illegal for logs:
plot "longdecay.txt" using (20.0*log10($2+0.00001)) with lines

155

Programming in C

This example gives but a hint of the very extensive support within Gnuplot for mathematical computationyou can apply a wide range of arithmetical modications to your
data before displaying it.
1.8.6

Exercises

Exercise 1.8.1
The code archive of the musicdsp mailing list (www.musicdsp.org) includes an example
under the Synthesis category of an exponential decay generator (Fast Exponential Envelope
Generator). Make a program to implement and test this code, using Gnuplot to display the
output.
Exercise 1.8.2
The simplest possible envelope generator for sound synthesis comprises a two-segment
attack-decay shape, where the peak value is user-controllable, as is the duration of each segment. Write a program to generate such an AD envelope as standard breakpoints. Use two
duration arguments and a single-level argument, together with an argument to set the overall number of points. Use Gnuplot to verify correctness.
Exercise 1.8.3
(a) The following program generates either an attack or decay curve using the exp function.
How does the attack curve differ from the example of expdecay.c? Where might such an
attack curve be found?
(b) Modify the code that computes thisval within the for loop, to eliminate duplicate calculations. Here is the original program:
/* expad.c generate exponential attack or decay breakpoint data */
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
int main(int argc, char** argv)
{
int i,npoints;
double T,k,a;
double ystart,yend;
double dur,thisval,thisstep;
if(argc != 6){
fprintf(stderr,"insufficient arguments.\n");fprintf(stderr,
"Usage: expad duration npoints startval endval T\n");

156

Chapter 1

return 1;
}
dur = atof(argv[1]);
if(dur <=0.0){
fprintf(stderr,"Error: duration must be positive.\n");
return 1;
}
npoints = atoi(argv[2]);
if(npoints <= 0){
fprintf(stderr,"Error: npoints must be positive!\n");
return 1;
}
ystart = atof(argv[3]);
yend = atof(argv[4]);
if(yend == ystart){
fprintf(stderr, "Warning: start and end values are the
same!\n");
}
T = atof(argv[5]);
k = dur/npoints; /* npoints = "sample rate" */
a = exp(-k/T); /* T = time constant */
thisstep = 0.0;
thisval = ystart;
fprintf(stderr,"a = %.6lf\n",a);
/* make normalized curve, scale output to
input values and range */
for(i=0;i < npoints;i++){
printf("%.4lf\t%.8lf\n",thisstep,thisval);
thisval = a * thisval + (1.0-a) * yend;
thisstep += k;
}
/* print final value */
printf("%.4lf\t%.8lf\n",thisstep,thisval);
fprintf(stderr,"done\n");
return 0;
}
Exercise 1.8.4
(a) Revisit the exercises in the previous section, and develop the transformation functions
into programs that output a breakpoint le to the screen so that it can be displayed by
Gnuplot.

157

Programming in C

(b) Read the documentation for the Gnuplot plot function; then create a single plot that
reads two breakpoint les, to compare breakpoint data before and after transformation.
Exercise 1.8.5
In the example programs, the precision for the times and values is set at 4 and 8 decimal
places respectively (%.4lf, %.8lf). Is this enough? Test with (a) a large number of points
(e.g. 44,100 per second) and (b) very long steep decays. What is the maximum meaningful
precision for writing doubles in text form using printf?
1.9

Toward the Soundle: From Text to Binary


1.9.1

Introduction

In this section we take a signicant step on our journey toward the goal of working directly
with sound. So far, our programs have generated output in the form of text, either to the
screen or to a text le. We have seen how general-purpose tools such as Gnuplot can be
used to present such data graphically. This will continue to be an important activity, but
without some audio data to work with, breakpoint les remain inert, in some sense meaningless until we can hear their effect. Visualization remains an important tool in the programmers armory; this section extends that work to include the generation of audio data
rst in text form, and secondly in the form of a raw binary le. The soundle editor
Audacity (available on the DVD) is introduced here as a freely available tool that can be
used to display and audition raw binary soundles.
The introduction of the raw binary soundle inevitably raises the subject of how such data
is represented in the computer and on disk. Indeed, it immediately exposes programmers not
only to relatively familiar issues such as sample formats, but also to some very low-level
aspects of number representation in the computer, such as byte ordering. As we delve further
into the programming of audio processes, and into the use of soundles and other data formats, we need to become progressively conversant with these subjects. Readers for whom
this aspect is very new may nd it useful to study the appendixes in this book on computer
architecture, number representations, and mathematics. While these topics are referenced as
necessary within individual sections, there is much to be said for a concerted independent or
at least parallel study of them. By their nature these topics lie in the background of the business of designing creative audio software. However, as we progress in our knowledge of C
and of computer audio processing, we will nd that that background becomes increasingly
prominent; indeed it soon becomes indispensable for success as audio and music programmers. With this in mind, the material presented in this section may be taken as a foretaste
of things to come.

158

Chapter 1

Figure 1.11
The sine wave.

1.9.2

The sin Function: Creating a Sine Wave

The sine wave (gure 1.11) is arguably the most important signal both in audio and in audio
processing. We will learn more about its signicance in later sections, where we will use it for
(among other things) additive synthesis. Mathematically it describes the motion of a swinging pendulum (a classic example of simple harmonic motion), and similarly the motion of
vibrating strings. It is a particular form of the more general waveform called a sinusoid, which
denes the shape of the waveform without consideration of the starting value. By itself it is
an extremely simple vibration and musically relatively uninteresting as a soundthe great
majority of sounds in which musicians are interested are much more complex.
There is one familiar musical tool that is conventionally regarded as producing a sound
almost as pure as a sine wave: the tuning fork. In this section we will combine our knowledge of the exponential decay with a sine wave to create a simple software tuning fork. Our
focus is here not so much on the theory or the mathematics of the sine wave but on the creation of a data le or a sound le containing a tuning fork tone that we can both display
graphically and listen to. This in turn will lead us rst to a consideration of the information
we need to associate with such les in order to represent or audition them as intended, and
second to a consideration of the issues we have to bear in mind concerning the representation of numeric data in the computer and on disk. We will also need to consider, even at this
preliminary stage, what we might want in such a program to make it usable, whether musically or for study and experimentation.

159

Programming in C

Listing 1.9.1 presents the minimal code used to create the plot in gure 1.11. It is indeed
more minimal that most of the previous code examples in that it uses no command-line
arguments. It uses the method introduced in section 1.8, writing a list of numbers to stdout
that can be redirected to a text le:
./sinetext >sine.txt
Listing 1.9.1: Generate Single-Cycle Sine Wave
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

/* sinetext.c */
/* write sinewave as text */
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
/* conditional compilation - is M_PI defined? */
#ifndef M_PI
#define M_PI (3.141592654)
#endif
int main(int argc, char** argv)
{
int i,nsamps;
double samp;
double twopi = 2.0 * M_PI;
double angleincr;
/* set number of points to create */
nsamps = 50;
/* make one complete cycle */
angleincr = twopi / nsamps;
for(i=0; i < nsamps; i++){
samp = sin(angleincr *i);
fprintf(stdout,"%.8lf\n",samp);
}
fprintf(stderr,"done\n");
return 0;
}

In section 1.7 we encountered the C preprocessor directive #define. In line 7 of listing


1.9.1 we encounter #ifndef, one of the two complementary directives for conditional
compilation used to detect if some symbol has not been dened. The positive form is #ifdef.
Each of the two forms requires a terminating #endif directive (line 9). Any legal C code can
be placed between ifndef and endifeither a simple denition or extensive blocks of
code. In this case we are looking for M_PI, which is dened in the header le <math.h> in

160

Chapter 1

all standard UNIX C compilers, including gcc in OS X and Linux. As its name indicates, it
supplies the value of the fundamentally important mathematical number (.This is, however,
not a denition mandated by ANSI C. And Microsoft compilers do not provide such a denition, so users of those compilers have to dene it themselves. The term conditional compilation is exactif the test expressed by the directive fails, the associated code is removed before
the le is delivered to the compiler itself. Indeed, the #ifdef/#endif combination is a
handy way of excluding a block of code from compilationor even a whole le of code
if that is necessary. This might be done to select between alternatives, or (as here) to
modify code differences between compilers or platforms. It is also often used (especially in
C++ programs) to prevent the duplication of code in header les that may otherwise be
included more than once.
Lines 15 and 20 of listing 1.9.1 represent the mathematics involved. We will learn more
about the mathematics in future sections. At this stage, the essential aspect to bear in mind
is that a single cycle of a sine wave has a length dened mathematically as 2p radians.
Because the C sin function (line 22) takes an argument in radians, the only number we
need is 2p, which we have dened in line 15 as a variable. The output is a double, with
values ranging between 1.0 and 1.0. We must then sample this cycle over a number of
points to generate the list of numbers to supply to Gnuplot. We met this principle of sampling in section 1.8 when we generated an exponential decay as a breakpoint le. While
handwritten breakpoint data can dene points at arbitrary times, we also dened a special
form of breakpoint data with equally spaced points. This therefore becomes an example of
sampling. The exponential decay drawn with ten points is not very smoothit is in effect a
coarsely sampled approximation of the smoothly curved decay shape. When Gnuplot is
given a single column of numbers, it automatically treats then as equally spaced in this
way. In this example the sine wave is generated using a modest 50 points. The calculation
in line 20 denes the distance (expressed as a small angle increment) between each two
points, here 1/50 of the complete cycle. This angle increment is multiplied by the loop index
to calculate each successive value (line 22).
Although only 50 points were used, the curve in gure 1.11 looks very smooth. We can see
the underlying sampled nature of the data by using a slightly different plot command in
Gnuplot:
plot "sine.txt" with impulses
or
plot "sine.txt" with steps
The latter illustrates the output of a digital-to-analog converter (DAC) when supplied with a
sampled sine waveit requires a special lter to remove the steps and reconstruct the waveform as intended.

161

Programming in C

1.9.3

Toward the Tuning Fork: Frequency Generation and the Keyword enum

To develop sinetext.c into a full tuning-fork simulation, we need rst to elaborate the arithmetic to incorporate a sample rate. As it stands, with our xed 50-point sine wave we can
only generate different pitches by dening a different sample rate. At the standard CD
sample rate of 44,100 Hz, our 50-point sine wave will have a frequency of 882 Hz. To obtain
the international tuning standard frequency for Concert A (440 Hz), we would have to use
a sample rate of 22,000 Hz. Clearly, this is impracticalwe need to scale the number of
points instead.
Listing 1.9.2 expands sinetext.c into a more complete program with arguments. Line 27
employs the revised arithmetic to incorporate both a sample rate and a requested frequency.
This formulation is worth bearing in mind, as it will be used frequently in nearly every program concerned with audio synthesis or processing.
Listing 1.9.2: Expanded Version of sinetext.c with Arguments
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

/* sinetext2.c */
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#ifndef M_PI
#define M_PI (3.141592654)
#endif
/* define our program argument list */
enum {ARG_NAME,ARG_NSAMPS,ARG_FREQ,ARG_SR,ARG_NARGS};
int main(int argc, char** argv)
{
int i,nsamps;
double samp,freq,srate;
double twopi = 2.0 * M_PI;
double angleincr;
if(argc != ARG_NARGS){
fprintf(stderr,"Usage: sinetext2 nsamps freq srate\n");
return 1;
}
nsamps
= atoi(argv[ARG_NSAMPS]);
freq
= atof(argv[ARG_FREQ]);
srate
= atof(argv[ARG_SR]);

162

Chapter 1

27
28
29
30
31
32
33
34
35
36 }

angleincr = twopi*freq/srate;
for(i=0;i < nsamps; i++){
samp = sin(angleincr*i);
fprintf(stdout,"%.8lf\n",samp);
}
fprintf(stderr,"done.\n");
return 0;

This program introduces a new method for managing program arguments. Hitherto we have
simply used numeric indices into the argv array to pick up arguments. Here we use the C
keyword enum to dene a meaningful name for each value. The great value of enum lies in
its automatic assignment of ascending numbers (simple integers) to the symbol list (enclosed
in braces on line 11), starting with zero. (Remember, argv[0] is always the name of the
compiled program.) We can then use those names to select the required argv entry (lines
2426). This technique offers two benets to the programmer. First, it is self-documenting
a name is always more meaningful than a raw number. This is therefore a simple example
of defensive programming, as it helps to reduce the possibilities for errors. Second, this is a program under development, and it we may add, remove, or reorder program arguments as
development proceeds. In the present case we can arbitrarily reorder the arguments, and use
of the enum list ensures that the correct value is always obtained from a given argv entry.
Note that for the sake of brevity (with the exception of the test associated with the usage
message in line 20) input arguments are not checked for correctness. This is reasonable in a
small intermediate test program such as this; should it be found useful as a general-purpose
program among many others, those error checks will have to be added.
1.9.4

The Utility Program text2sf: Converting Text to a Soundle

The program in sinetext.c can be used as before, directing the output to a text le for inspection using Gnuplot. However, since the data clearly represents a sound, we want to hear it,
and we want to conrm that we are indeed generating the frequency we requested. In future
sections we will be writing fully practical programs that output standard soundles. However
the principle introduced in the previous section is still relevantthat in testing and debugging code it is convenient to add simple text output of audio data to programs under development, in just this way, using a bare minimum of code. All we need is a tool that enable us
to audition the output as well as inspect it visually.
The utility program text2sf supplied on the DVD (in both executable and source form)
is provided to perform this task. It is written using the small-scale cross-platform soundle
library portsf which is introduced in the next section, and which is fully amenable to editing
and modication. As supplied, its command line is

163

Programming in C

Figure 1.12
Interleaving samples in a le.

text2sf infile outfile srate chans gain


where infile input text data le, outfile output soundle (.wav or .aiff formats),
srate sample rate of the data, chans number of interleaved audio channels in infile,
and gain amplitude factor applied to input data (1.0 no change)
The outle format is set by the lename extensionuse .wav for a WAVE le and .aiff (or
.aif) for an AIFF format le. One argument that may be conspicuous by its absence is a selector for the output soundle sample type. As supplied, this is set internally to create a plain
16-bit soundle; it is left as an exercise for the reader to modify this in the manner of their
choice to offer some or all of the choices offered by the portsf library.
The chans argument indicates that a data le can contain more than one audio channel.
In a soundle, samples are interleaved, leading to the notion of the multi-channel sample
frame (gure 1.12). In effect, sample for each frame are delivered to the soundcard in order,
followed by the samples for the next frame, and so on.
Once the output soundle has been created, we can play the it using the player or editor of
our choice, and we can verify that the frequency is as specied. (You may want to seek out a
real tuning fork for purposes of comparison.) The equivalent for a text le is very simple:
each line corresponds to a multi-channel frame, so a stereo le would appear as a text le
with two columns of data. We can explore this aspect by making a small modication to
line 23 of listing 1.9.1:
fprintf(stdout,"%.8lf\t%.8lf\n",samp,samp *samp);
This creates a two-channel data le in which the second channel is the square of the rst.25
After re-compiling, run the program to create a le named stereo.txt.
To display the sound data in Gnuplot requires us to know how to interpret multiple columns in a le. When presented with a single column, these columns are treated as data for
the y axis, and the lines are counted internally to provide equally spaced values for the x
axis. However, a two-column data le is interpreted as data for the x and y axes respectively
(in other words, a breakpoint le), which is not what we want here. To overrule this, we have
to ask Gnuplot to plot both columns on the y axis, specifying each column explicitly:
plot "stereo.txt" using($1) with lines, "stereo.txt" using($2) with
lines

164

Chapter 1

The using directive denes an expression inside the brackets to be used to create the data.
This could be an arbitrary arithmetic expression, but here the prex $ is used by itself to
select a particular column from the le (counting from 1). Note that the lename must be
specied for each column. It is equally possible to plot data from two or more different les,
using the same syntax.
1.9.5

Our First Tuning Fork Emulation

From section 1.8 we have two alternative forms of the exponential decay. One might be
called a purist solution, insofar as it is close to the textbook denition of the function.
The second is more pragmatic from the musicians point of view, addressing the very musical
issue of ensuring that the decay always reaches silence whatever the overall length. We need
to explore both solutions. The procedure is, however, broadly the same in the two caseswe
want to apply the exponential decay to our sine-wave generator. We have developed and
tested each component separately, so all that should be required now is to combine them
into one program.
Listing 1.9.3 shows a solution based on expdecay.c. As bets a slightly more user-oriented
program, it incorporates output to a named text le. The all-important code that transforms
the basic sine wave into a decaying tone is simplicity itselfa simple multiplication (line
45). The raw argument T from expdecay.c is changed into the slightly more expressive
name slope (line 22).
Listing 1.9.3: Tuning Fork Program Version 1
1
2
3
4
5
6
7
8
9
10

/* tfork.c virtual tuning fork combining sinetext.c and expdecay.c */


#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#ifndef M_PI
#define M_PI (3.141592654)
#endif
enum {ARG_NAME,ARG_OUTFILE,ARG_DUR,ARG_HZ,
ARG_SR,ARG_SLOPE,ARG_NARGS};

11
12 int main(int argc, char** argv)
13 {
14
int i,sr,nsamps;
15
double samp,dur,freq,srate,k,a,x,slope;
16
double twopi = 2.0 * M_PI;
17
double angleincr;

165

Programming in C

18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51 }

double maxsamp = 0.0;


FILE* fp = NULL;
if(argc != ARG_NARGS){
printf("Usage: tfork outfile.txt dur freq srate
slope\n");
return 1;
}
fp = fopen(argv[ARG_OUTFILE],"w");
if(fp==NULL){
fprintf(stderr,"Error creating output file %s\n",
argv[ARG_OUTFILE]);
return 1;
}
dur = atof(argv[ARG_DUR]);
freq = atof(argv[ARG_HZ]);
srate = atof(argv[ARG_SR]);
slope = atof(argv[ARG_SLOPE]);
nsamps = (int)(dur * srate);
angleincr = twopi * freq / srate;
k = dur/nsamps;
a = exp(-k/slope);
x = 1.0;
for(i=0;i < nsamps; i++){
samp = sin(angleincr*i);
/* apply exp decay */
x *= a;
samp *= x;
fprintf(fp,"%.8lf\n",samp);
}
fclose(fp);
printf("done\n");
return 0;

Apart from that, there is essentially nothing new in this program beyond the fact that it
merges two previous programsexcept perhaps that, because the functional aspects have
already been developed and tested, we can reasonably expect such a merge to work correctly
rst time. We cannot really call this a modular program, but in a sense its development has
been modular. We can expect our programs to increase ever more in complexity (certainly
in length), such that it will become more and more important to test sections of such code

166

Chapter 1

in isolation as much as possible, making full use of tools such as Gnuplot and text2sf and
any other such tools that we may discover or develop along the way.
1.9.6

Tuning Fork Program Version 2

Following the same principles, listing 1.9.4 demonstrates a tuning fork emulator based on
expbrk.c. However, this example does feature a couple of changes to enhance the overall
utility of the program, again in the spirit of looking ahead. The rst of these is the new
command-line argument amp. So far, all the programs have generated exponential decays
starting from the normalized maximum value of 1.0. Any music-oriented program will have
to offer control of amplitude, so this argument is added in case it comes in handy. The exponential decay itself is formally dened always covering the full normalized range, shown
explicitly in lines 3839, while the amplitude parameter is applied to the output of the
sine-wave generator (line 44).
Listing 1.9.4: Tuning fork v2 with maxsamp report
1
2
3
4
5
6
7
8
9
10
11

/* tfork2.c alternate tuning fork generator based on expbrk.c


* - decay is always to ~silence regardless of duration. */
#include <stdio.h>
include <stdlib.h>
#include <math.h>
#ifndef M_PI
#define M_PI (3.141592654)
#endif
enum {ARG_NAME,ARG_OUTFILE,ARG_DUR,
ARG_HZ,ARG_SR,ARG_AMP,ARG_NARGS};

12
13 int main(int argc, char** argv)
14 {
15
int i,sr,nsamps;
16
double samp,dur,freq,srate,amp,maxsamp;
17
double start,end,fac,angleincr;
18
double twopi = 2.0 * M_PI;
19
FILE* fp = NULL;
20
21
if(argc != ARG_NARGS){
22
printf("Usage: tfork2 outfile.txt dur freq srate amp\n");
23
return 1;
24
}

167

Programming in C

25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56 }

fp = fopen(argv[ARG_OUTFILE],"w");
if(fp==NULL){
printf("Error creating output file %s\n",
argv[ARG_OUTFILE]);
return 1;
}
dur = atof(argv[ARG_DUR]);
freq = atof(argv[ARG_HZ]);
srate = atof(argv[ARG_SR]);
amp = atof(argv[ARG_AMP]);
nsamps = (int)(dur * srate);
angleincr = twopi*freq/ nsamps;
start = 1.0;
end = 1.0e-4; /* = -80dB */
maxsamp = 0.0;
fac = pow(end/start,1.0/nsamps);
for(i=0;i < nsamps; i++){
samp = amp * sin(angleincr*i);
samp *= start;
start *= fac;
fprintf(fp,"%.8lf\n",samp);
if(fabs(samp) > maxsamp) {
maxsamp = fabs(samp);
}
}
fclose(fp);
printf("done. Maximum sample value = %.8lf\n",maxsamp);
return 0;

The second addition, following the same philosophy, is the tracking of the maximum sample in the le (lines 40 and 4849). This is such a useful facility in general that it is incorporated as an automatic facility into the portsf library, introduced in chapter 2and indeed
optionally into the header of the soundles themselves. In the context of a simple exponential decay it is almost redundant as the maximum sample would be expected to be the
starting value of the exponential; however, we are generating a sine wave (whose values start
from zero), so that the peak value will not be the rst value in the audio data but at the rst
peak of the waveform. Later in chapter 2 we will be applying envelopes and all manner of

168

Chapter 1

other transformations to soundles, and the automatic reporting of the peak amplitude will
be of considerable value in generally monitoring results and in identifying any gross errors.
1.9.7

The Raw Binary Soundle

An alternative to writing audio streams as text data is to write them directly in binary form.
The difference in code is minimal, but it offers some signicant advantages. The most obvious
of these is the size of the generated le. In the examples presented so far, each sample has
been written as oating-point numbers with some eight decimal digits of precision. With
the additional leading zero and decimal point, this requires ten bytes of storage per sample,
plus further bytes representing white space and end of line. In contrast, a 32-bit oatingpoint sample by denition requires only four bytes of storage, with no further overhead. A
32-bit le may well be one-third the size of the equivalent text le, and a 16-bit le further
reduces that by half while still offering CD-quality precision. It is thus a more tractable format for general storage and distribution, especially where the need is more to display the
data graphically or to audition it than to inspect individual sample values. It is also signicantly more resistant to casual and error-prone editing than a text le.
Starting from listing 1.9.4, the changes required to generate a raw binary le are slight
indeed. The simplest change is to the format specied for fopen (line 25) to indicate binary
mode:
fp = fopen(argv[ARG_OUTFILE],"wb");
This change is not strictly necessary on UNIX-based platforms, but it is required by Visual
C++ and some other Windows compilers.
A slightly more involved change must be made to the code that writes the data to disk.
Instead of using fprintf, which by denition writes formatted text, we must use the function fwrite, which writes arbitrary blocks of memory to disk:
size_t fwrite(const void * ptr,size_t size,size_t count, FILE* fp);
In this declaration, we have the following:
size_t is dened by the compiler as an integer type appropriate to the platform. It can be
assumed to be at least a 32-bit unsigned type, but it may be larger (e.g. 64 bits on a 64-bit
platform).
n
ptr is a pointer to the memory block to be written; being a pointer-to-void the address
can be that of any object, whether a single local variable or a large block of memory.
n
Together, size and count dene the size of the memory block to be written. Typically,
size will refer to the size of any standard or user-dened type such as char or int (i.e.
where its size can be found using the sizeof() operator), while count denes the number
of such elements to be written. This is also the value returned by the function.
n
fp is the pointer-to-FILE to be written, as created by fopen.
n

169

Programming in C

For example, to write a single 32-bit float to the le, line 47 has to be modied to
float fsamp; /* declared at the top of the code block */ . . .
fsamp = (float) samp;
if(fwrite(&fsamp,sizeof(float),1,fp) != 1){
printf("error writing data to disk\n");
return 1;
}
1.9.8

Platform Particulars: The Endianness Issue

Being written in English, the words in this book are read from left to right across the page.
There are other languages, most familiarly Arabic and Hebrew, that are written from right
to left. For reasons too arcane and ancient to discuss here (mainly concerned with very lowlevel implementation of some arithmetic operations), a similar situation exists for the ordering of multi-byte numeric data (such as a 4-byte float) in memory and on disk. Instead of
right and left, we have lower and higher memory addresses. In a big-endian system, the most
signicant byte (MSB) lies at the lower memory address. This corresponds in general terms to
the left-to-right reading order. The alternative is a little-endian architecture, in which the
MSB lies at the higher memory address.
Figure 1.13 shows how a given 32-bit integer is represented in big-endian and little-endian
architectures. The main thing about the little-endian representation is that, while the ordering of the bytes is reversed, the ordering of individual bits within each byte is not. The consequence of this is that when we write a multi-byte numeric value to a le, it is written with
the endianness native to the platform. On Intel-based platforms, data will be written in littleendian form; on machines based on the PowerPC as used in older Macintoshes, it will be
written in big-endian form. Applications that offer the facility to read soundles in raw
binary format have to be told when endianness to use in reading the le. If we want to
read a big-endian le on a little-endian machine, or vice versa, we must reverse the bytes
of each number. As with text les, we must inform the receiving application of the sample

Figure 1.13
Big-endian and little-endian storage.

170

Chapter 1

rate and number of channels. For raw binary les, we also must tell the application what
types and sizes of numbers the le contains, and the endianness.
1.9.9

A Raw Binary Version of tfork2.c

Listing 1.9.5 shows the revised version of tfork2.c that is to write output as a raw binary
soundle. Only minor changes are required. However, the main loop that generates the
data has been expanded to offer a choice between two sample types: 16-bit shorts and 32bit oats (lines 76108). It also uses a small utility function (lines 1725) to report the platform endianness that I came across many years ago when looking at the code of an audio
analysis tool called SNDAN.26 This origin is acknowledged in the code, which uses a simple
pointer-based technique to detect endianness in order to report that information to the user
as a reminder.
Listing 1.9.5: tforkraw.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

/* tforkraw.c gen raw sfile with native endianness */


/* based on tfork2.c */
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#ifndef M_PI
#define M_PI (3.141592654)
#endif
enum
{ARG_NAME,ARG_OUTFILE,ARG_DUR,ARG_HZ,ARG_SR,
ARG_AMP,ARG_TYPE,ARG_NARGS
};
enum samptype {RAWSAMP_SHORT,RAWSAMP_FLOAT};
/* thanks to the SNDAN programmers for this */
/* return 0 for big-endian machine, 1 for little-endian machine*/
/* so we can tell user what order the data is */
int byte_order()
{
int one = 1;
char* endptr = (char *)
return (*endptr);
}

171

Programming in C

27 const char* endianness[2] = {"big_endian","little_endian"};


28
29 int main(int argc, char** argv)
30 {
31
unsigned int i,nsamps;
32
unsigned int maxframe = 0;
33
unsigned int samptype, endian, bitreverse;
34
double samp,dur,freq,srate,amp,step;
35
double start,end,fac,maxsamp;
36
double twopi = 2.0 * M_PI;
37
double angleincr;
38
FILE* fp = NULL;
39
float fsamp;
40
short ssamp;
41
42
if(argc != ARG_NARGS){
43
printf("Usage: tforkraw outsfile.raw dur"
44
"freq srate amp isfloat\n");
45
return 1;
46
}
47
48
dur = atof(argv[ARG_DUR]);
49
freq = atof(argv[ARG_HZ]);
50
srate = atof(argv[ARG_SR]);
51
amp = atof(argv[ARG_AMP]);
52
samptype = (unsigned int) atoi(argv[ARG_TYPE]);
53
if(samptype > 1){
54
printf("error: sampletype can be only 0 or 1\n");
55
return 1;
56
}
57
/* create binary file: not all systems require the 'b' */
58
fp = fopen(argv[ARG_OUTFILE],"wb");
59
if(fp==NULL){
60
fprintf(stderr,"Error creating output file %s\n",
61
argv[ARG_OUTFILE]);
62
return 1;
63
}
64
65
nsamps = (int)(dur * srate);
66
angleincr = twopi * freq / nsamps;
67
step = dur / nsamps;
68
/* normalized range always - just scale by amp */
69
start =1.0;

172

Chapter 1

70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113 }

end = 1.0e-4;
maxsamp = 0.0;
fac = pow(end/start,1.0/nsamps);
endian = byte_order();
printf("Writing %d %s samples\n",nsamps,endianness[endian]);
/* run the loop for this samptype */
if(samptype==RAWSAMP_SHORT){
for(i=0;i < nsamps; i++){
samp = amp * sin(angleincr*i);
samp *= start;
start *= fac;
/* use 32767 to avoid overflow problem */
ssamp = (short) (samp * 32767.0);
if(fwrite(&ssamp,sizeof(short),1,fp) != 1){
printf("Error writing data to file\n");
return 1;
}
if(fabs(samp) > maxsamp) {
maxsamp = fabs(samp);
maxframe = i;
}
}
}
else {
for(i=0;i < nsamps; i++){
samp = amp * sin(angleincr*i);
samp *= start;
start *= fac;
fsamp = (float) samp;
if(fwrite(&fsamp,sizeof(float),1,fp) != 1) {
printf("Error writing data to file\n");
return 1;
}
if(fabs(samp) > maxsamp) {
maxsamp = fabs(samp);
maxframe = i;
}
}
}
fclose(fp);
printf("done. Maximum sample value = %.8lf at frame %d\n",
maxsamp,maxframe);
return 0;

173

Programming in C

Lines 81 and 82 demonstrate a simple if not necessarily purist solution to the problem of
generating 16-bit audio. Audio signals are bipolarin terms of normalized values, samples
range between 1.0 and 1.0. However, all signed integers present a problem: the maximum
16-bit positive value (32,767) does not exactly match the maximum negative value
(32,768). If we scale our oating-point values by 32,768, both 1.0 and 1.0 will translate
to 32,768, which is enough to introduce severe clicking in the output. In tforkraw.c this
danger is avoided by scaling by 32,767 instead. However, by no means do all audio programmers approve of this approach. When a conversion is performed in the opposite direction, receiving 16-bit samples and converting to the normalized range, the natural (and safe)
approach is to divide by 32,768. Only when the same factor is employed in both directions
will the conversion be bit-accuratean important criterion for many professional audio
users. This remains a subject for debate among programmers (especially those implementing
le format support). A simple answer is to avoid generating signals close to digital peak in the
rst placebut Murphys Law dictates that when users can do something, they will do it.
1.9.10

Auditioning Raw Soundles: the Audacity Soundle Editor

Audacity is an open-source cross-platform soundle editor that is in wide use throughout the
audio and music community. (Versions for all major platforms can be found on the DVD.)
Among its many facilities is the loading of raw binary soundles. From the File menu select
Import ( Raw Data. After you select a le, a small dialog window is displayed in which
you can set the required properties of the le (gure 1.14).
A wide range of sample type options is supported in the rst drop-down list. The
Macintosh version has one oddity: by default, the Start offset entry box (the byte position
at which conversion should start) is pre-lled with the value 1. Expect to set this value to
zero for the plain raw soundles considered here. The main purpose of that box is to allow

Figure 1.14
Opening raw soundles in Audacity.

174

Chapter 1

soundles with unrecognized or damaged headers to be opened. This requires knowing how
large that header is, which may require some detective work with a binary le editor; or some
guesswork. Any header data translated into samples will almost invariably appear as rubbish
audio, easy to see and easy to eliminate on a second attempt. Select sample type, endianness,
and channel count according to your knowledge of the le, or make a guess. If in any doubt,
when you have opened a le, set your playback volume to low before playing. Files imported
with the wrong format will almost always be both loud and grossly distorted, though often
audio is recognizable even in this extreme situation.
1.9.11

Exercises

Exercise 1.9.1
Add all necessary error checking to the programs in this section, including testing the return
value from fprintf.
Exercise 1.9.2
A quick revision question: what does const signify for the rst argument to fwrite?
Exercise 1.9.3
Replace the freq argument in the program of your choice with a MIDI note argument.
Exercise 1.9.4
In the waveform generation code of any of the tuning fork programs, change the call to the
sin function with a call to the cos function. View and audition the output in both text and
binary forms.
Exercise 1.9.5
Modify sinetext.c to write both sin and cos outputs in that order (i.e. per line) to a twochannel text le. Use Gnuplot to display the data using its default x/y mode (i.e. do not use
a using directive). Before doing this, predict what the display will show. What does this
demonstrate about the relationship between the sin and cos functions?
Exercise 1.9.6
Look on the DVD for a raw le called mystery.raw. Use Audacity to display and audition
this le, by discovering the properties of the lesample type, number of channels, byte
order, and sample rate.
Exercise 1.9.7
Modify sinetext.c to output sample values as full-scale 16-bit integers (written as a text le).
Conrm the change by displaying the data in Gnuplot. Ensure that the data is represented
correctly.

175

Programming in C

Conclusion
The nine sections of this chapter formed a more or less complete course on the C language
with a particular music and audio direction. If you have followed the examples carefully and
completed some of the exercises, you should be able to start doing some proper audio programming, dealing with digital signals and embarking on an interesting journey through
the eld of musical signal processing. The following chapters will guide you in this exploration, so please proceed to chapter 2, where the focus will start to shift from programming in
general to the specic goal of manipulating sound.
Notes
1. However, this is not quite the whole story. Programmers use the term variable to describe all data
elements dened in a program, whether they are literally variable or not. As we will see in the next section, we can even have a constant variable. To the programmer, a variable is anything that can exist
on the left-hand side of an assignmentsomething that has a symbolic name.
2. Readers will occasionally nd the integer type name size_t employed in programs. This relatively
esoteric name attempts to avoid the problems of guessing what the native integer size is on a particular platform. It is used by some functions in the C standard library and is described in section 1.7.
3. It is illuminating to compare these intervals with those of pure or Just intonation tuning. For
example, the perfect fth in mean tone has the ratio 3/2 1.5, very slightly wider than in E.T. The
major third has the ratio 5/4 1.25; the E.T equivalent is much wider, and really out of tune. These
pure ratios derive directly from the natural harmonic series: the major third is the distance from the
fourth harmonic to the fth harmonic.
4. According to the C89 standard, used in this book and generally the most commonly implemented.
Later standards have changed this.
5. This may be too arcane, but actually you can declare variables in C code at the start of blocks anywhere in your program. The following is legal C code:
main(int argc, char **argv)
{
int i;
for(i = 0; i < 5; i++){
float x; /* check this out! */
x = i * 2.5;
printf("%f\n", x);
}
}
6. However, this is not the only way to implement a string. In the language Pascal, for instance, the
length of a string is stored as the rst element, followed by the characters that make up the string. Pascal
was the original programming language for the Macintosh, and Mac developers still need to know about
Pascal strings.

176

Chapter 1

7. In some cases, white space (spaces and tabs) is important to C; in other cases, it is not. A pointer declaration can be made in any of these styles:
char* message;
char *message;
char * message;
char*message;
They all mean the same thing to the compiler. The rst two forms are about equally popular; the last is
legal but very uncomfortable to the eye, and is not recommended.
8. Regarding the placement of braces in if statements, there are a number of layout styles in common
use. The two most common styles are as follows:
if(a == b)
{
do_something();
}
and
if(a == b) {
do_something();
}
Both of these styles clearly show where the end of the block is in relation to the controlling if expression. The degree of indentation is the same whether or not braces are used. The second style is probably
the most common, not least because is the more economical of space.
9. We will encounter the concept of a container again in DVD chapters 35.
10. This runs somewhat counter to the traditional UNIX command-line tool, which is designed to work
as part of a chain or sequence of programs, the output of one becoming the input to another. The system
as a whole is called a pipe, and the format of the data piped from one process to another is almost always
plain text. For this reason, a usage message can only be invoked by using a special command argument
such as -help; otherwise the program simply starts, and it may run forever waiting for text input.
11. Warning: A variant of the above code that programmers can be tempted to use changes the conditional expression to use the!= (not equal to) operator:
while(i != 0){
printf("%d",i);
i = i - 1;
}
This form of test expression can be simplied further to
while(i){
...
The logic of this is reasonable enough: as the integer variable is reduced by one each time, it must reach
the value 0 eventually, and the test will evaluate to true. However, unlike the rst example using the >=

177

Programming in C

operator, this loop is vulnerable to a bug. If i is initialized to a negative value (which might be caused by
a typing error), the dependent code will make it more negative each time, and the loop will continue to
iterate with the variable becoming increasingly negative foreveror, rather, until the number overows the numeric range of the integer type and wraps around from negative to positive. If this variable is being used to access an array, a crash is inevitable. The safe use of loops depends on the
programmer being absolutely sure that the loop tests can deal with all eventualities. It can also be argued
that the use of the >= operator is more self-documenting, as it expressly indicates that the variable i
cannot be negative.
12. If the destination variable is declared as float, and the expression uses a constant:
float x = 1.0;
x += 2.1;
The compiler may issue a warning about loss of precision, as the constant is treated as a double, forcing
the whole calculation to be performed at double precision, which has to be converted back into a float.
To avoid this warning, the constant can be declared as a oat by using the f sufx:
x += 2.1f;
13. Programming reminder: In any for loop that reads or writes to an array using an increasing
index, as in the example above, the conditional test is always of the form i < arraysize. This is
because C array indices always counts from zero, so that in an array of 12 elements the highest element
is at index 11. The conditional test above ensures that at the nal increment, when i 12, the test
evaluates to false and control passes to the instruction after the for block. Even after many years of experience, programmers can still get caught by indexing errors that point outside the array, so this
warning is likely to appear several times in this book. As a result, techniques to trap such errors are very
important.
14. Style note: Consistent with other C idioms, braces are not required when the dependent code is a
single statement:
do
printf("%d ",i++);
while(i < 10);
Many programmers prefer to use braces anyway, especially while developing a programwhat starts out
as a single statement can very often become several. Others will go in the other direction and reduce the
code to two lines:
do printf("%d ",i++);
while(i < 10);
Both forms make the do loop look rather strange, and the use of braces at all times is strongly recommended.
15. Indirect auto-increment: What would be the meaning of applying ++ to the expression *ptr? This
would be read as increment the variable pointed to by ptr, and it is a common procedure. You must
use parentheses to force the compiler to do it the way you want:

178

Chapter 1

double count = 0;
double* ptr = &count;
(*ptr)++; /* count now = 1.0, ptr still points to it */
Using ++ this way, on a oating-point variable, is legal, and has the expected result. You may occasionally see this in code, but it is rare, and is not recommended, as it is not really idiomatic for oating-point
variables (there are no corresponding machine instructions to exploit). The increment/decrement operators are best used with integral variables, such as counters, as illustrated by all the other examples in this
section.
16. File streams: The use of a computer screen display is taken so much for granted these days that it is
easy to forget that there was a time before such devices were available. Output devices on early computing systems included such things as punched cards and tape, and the classic teletype printer/typewriter. What these devices had in common was that they received (or sent) a stream of data, one
character at a time. Thus the screen, the keyboard, and a disk le are all examples of a stream device.
In C, three standard input/output (i/o) streams are dened:
stdout /* primary output stream */
stderr /* stream to receive error messages */
stdin /* stream from which input is received e.g. teletype keyboard */
A data le on disk is merely a further instance of a stream device. The difference between printf and
fprintf is that printf outputs only to stdout, whereas the more general fprintf can write to any
named stream. It is often used, for example, to write to stderr:
fprintf(stderr,"there was an error\n");
Note that printf and fprintf can be used with the same set of formatting options.
In UNIX-based systems, stdin and stdout have a further stream-related signicance, in that the
stdout stream from one program can be piped to the stdin of another. Many of the arcane programs
found in classic UNIX systems (including Gnu/Linux and OS X) are designed with this piping technique
in mindby default they read from stdin and write to stdout, and only to/from a disk le if expressly
commanded to do so by means of the UNIX redirection command >. This example uses the UNIX command ls to redirect a detailed directory listing to a text le: ls -l >directory.txt. The UNIX pipe
symbol | connects the output of one process to the input of another: ls | grep sine > sinefiles.txt.
This writes the names of all les containing sine to the le sineles.txt.
17. What would it mean to add two breakpoints together? Think about it. It seems obvious enough to
add the value elements, but what about the times? The compiler has no idea what the variables signify. A
struct can contain any dened C type, including pointers. In many, perhaps most cases, even a human
programmer would nd it impossible to dene a meaningful arithmetic operation on a struct, so it is
hardly surprising that the compiler cannot do it.
18. We just need a name somewhere. We can name the struct:
struct
int
int
};

mystruct {
var1;
var2;
/* we can now declare a variable of type struct mysruct */

179

Programming in C

or we can declare a variable using an anonymous struct:


struct{
int var1;
int var2;
} mystructvar;/* mystructvar is a variable: */
mystructvar.var1 = 440;
or we can use typedef as shown in the text.
19. Note the use of an outer pair of brackets in the denition of NULL. This is done to avoid any misinterpretations that might arise through C precedence rules. Here is a simple example of the need for
such brackets:
#define SQUARE(n) n * n
This is a small macro denition, replacing any instance of the text represented by the symbol n (which is
not a variable but a placeholder for whatever text the programmer supplies) with the given substitute
text. The idea is that to square a number of any type, the one macro can be used:
int w, x = 5;
double y,z = 10.0;
w = SQUARE(x);
y = SQUARE(z);
These initializations of x and z will result in the expected values: 25 in w and 100.0 in y. Upper-case
letters for the macro symbol are used here to show, by convention, that this is a macro and not a function call. But suppose the innocent programmer uses an expression instead of a single variable:
w = SQUARE(x+1);
After text substitution (macro expansion) by the preprocessor, the expression will become
w = x + 1 * x + 1;
Since the C precedence rules require that the multiplication be performed rst, the result will be not 36
but 11. By enclosing both the target of the macro in brackets, and the placeholder symbol each time it is
used,
#define SQUARE(n) ((n) * (n))
the problem is avoided. The macro will now expand to
w = ((x+1) * (x+1));
which will give 36, as required. Note that the presence of many levels of brackets is a burden only to the
compilerit will not result in any extra machine code. So if in any doubt, use brackets freely.
20. In line 9, the library function exit() is used, rather than return. As its name suggests, exit()
immediately quits the program, releasing all resources (memory, open les) in the process. The argument
value given to exit is the same as the value that would be used with a call to return. You will see that
exit is very commonly used in command-line programs, especially in response to allocation failures. The
advantage to the programmer is obvious (no cleanup code has to be written), but if this function were to
be incorporated into a GUI-based application (where an internal process may be run and rerun many

180

Chapter 1

times) calls to exit() must be eschewed in favor of a more controlled recovery to a higher level of the
application, with a warning message presented to the user. This does demand much more planning by
the programmer, as it may be that the allocation failure occurs low down in some function, and the error
return has to be rippled back through many parent functions before it can be presented to the user. For
the same reason, calls to functions such as printf or puts may need to be removed or changed, as they do
not generally have a place in a GUI application. In a VST plug-in, for example, if the plug-in initialization function cannot obtain memory, or has any other problem, it has to return an error value, and the
host application will notify the user accordingly.
21. Can malloc() fail these days? The problem is that it is increasingly rare for memory requests to fail,
unless for a truly huge amount. Even here, the machine will try to use virtual memory using space on a
hard disk, and it is more likely that the operating system itself will start to warn that virtual memory is
low, before rejecting a memory request. Thus code written to deal with allocation failures may never get
executed, and therefore may never get tested. Tools do exist (sometimes supplied by compiler vendors)
that deliberately stress memory, to enable such code to be tested. A comparatively simple solution, if
hardly rigorous, is to create a wrapper function for functions such as malloc, in which you can add
code to deliberately reject a request, say if it exceeds some arbitrary limit:
void* stress_malloc(size_t size)
{
size_t limit = 1024 * 1024 * 2; /* limit to 2MB */
if(size < limit)
return malloc(size);
else
return NULL;
}
You can then use #define to make all calls to malloc become calls to your wrapper function:
#define stress_malloc malloc
Note that this memory is all virtual memory. Many operating systems have special function calls that
request memory that must be in hardware RAM. Audio programmers often rely on this to create buffers
used to record or play back audio. These requests are much more likely to fail, and it is especially important to ensure that code to deal with such allocation failures is fully tested.
22. There is also the highly important number e, the base of the natural logarithms, one of several
numbers named and studied by the great 18th-century mathematician Leonhard Eulerwho among a
multitude of mathematical accomplishments wrote a substantial treatise on harmony. The value of e is
approximately 2.718, but like its close companion p it is a transcendental number with no nite numerical representation. One unique property of e is that it is its own derivative. That is, if you measure the
slope of the curve at each point, the result is another exponential curve. The genius of Euler is something
to be celebrated by both musicians and mathematicians.
23. Until of course they fall below the smallest number that the hardware can represent. For doubles this
is a very small number indeed, but even this will be reached eventually, such numbers become denormalized, and this can create problems, especially on Intel-based machines (where it is know as the
denormal problem), which work inconveniently hard to maintain precision, resulting in processing
suddenly slowing to a crawl.

181

Programming in C

24. This can happen if the precision with which values are written to a text le is too low; small values
will be misleadingly written as zero.
25. We are relying on the very useful fact that the square of a number between 0 and 1.0 is a smaller
number within that range; and we remember also that the square of a negative number is a positive
number. Readers are invited to predict what the square of a sine wave will look like, and then to conrm
that prediction with Gnuplot.
26. It is nevertheless a widely documented and employed hacker technique, so it has probably been
reinvented multiple times.

Audio Programming Basics

Audio Programming in C

Richard Dobson

In the previous chapter we learned how to play and display raw binary soundles using
Audacity. In order to play such a soundle, we needed to tell the program, not only the sample rate, sample type and channel count; but also the endianness of the samples themselves.
The purpose of a dedicated soundle format such as WAVE and AIFF is to encapsulate this
information in a header section prexed to the audio data itself, which an application can
read to determine the format of the le. Such a le format can thus be described as selfdescribing. Writing code to read and write such soundles is a complicated job, not least
because of the many alternative formats a soundle can have. There is a clear trade-off for
the audio programmer. It is easy to write a raw soundle, as we have seen, but such les are
only portable with the greatest difculty, and more cumbersome to play. The possibility of
forgetting what format a given le has is very great. Conversely, writing the code to write
(and more particularly, to read) a self-describing le format is a non-trivial task, especially
where that format is very complex with many variations and options.1
Fortunately, several C libraries have been created that deal with all the arcane details of
writing modern soundle formats, so that reading and writing such les need be no more
difcult than reading or writing a text le. The formats dene all the above information,
and often much more, including optional information that while not essential to play a
le, is nevertheless useful.
This chapter demonstrates how to generate simple waveforms, such as a triangle wave and
a sine wave, and write them to a soundle, and also how to modify or combine sounds. In
the process we are introduced to further elements of the C language, and to further techniques of code design. As you may suspect, these programs will be longer, and more complex
than before, as many more tasks are being performed. This complexity includes the use of
multiple source les, and the use of external libraries that need to be linked to our program.
This is, however, such a common task that many tools have been developed over the years to
streamline the process as much as possible. One such tool is the makele, a text le that
denes all the elements and actions needed to build a program, together with any dependencies such as external libraries.

186

Chapter 2

Behind the details of new C keywords, the arithmetic of sine waves, and the denition of
new functions lie deeper and increasingly important issues of robustness, exibility, extensibility, and, embracing all these, comprehensibility. As our programs develop in sophistication, so will our ambitions as programmers rise, and it becomes more and more likely that
in writing any one program we will anticipate many further possibilities.
In particular, just as the programs in this section make use of a pre-written code library,
you will yourself be writing many functions that are likely to be required for many different
projects, and which could therefore also form components of your own library. So in reading
the following sections, consider not only the immediate task at hand, but also how general a
block of code or a function is, and whether you might nd uses for it in other projects.
This chapter therefore assumes and expects that you will now be contemplating, at least,
writing your own programs, above and beyond the examples given here. You will be creating
building blocks of code; and it almost goes without saying that building blocks can be used
to make many very different edices. The challenge is in designing those blocks for maximum re-usability, while ensuring that they are also as easy to use as possible. It remains one
of the most interesting and intriguing exercises for the programmer, to anticipate the future
use of something. Often, new ideas demand that a function be rewritten a posteriori. Yet in
writing a program the programmers rst thought is understandably, to get it working,
and worry about other possibilities later. As a result, it can often happen that a program is
written twice. The rst is a proof of concept version that works; the second version learns
from the rst and implements enhancements, alternative approaches to design, increases
modularity and robustness, and identies blocks of code worth generalizing into a library
for use elsewhere. Once this is done, the program may even be rewritten a third time. This
is normal.
2.1

A Simple Soundle Library: portsf


2.1.1

The Portsf Header FilesPublic and Private Files

The portsf soundle library, developed especially for this book, was designed to be as simple
as possible to use. It is not intended in any way to be an alternative to industrial-strength
libraries such as libsndle. It comprises only two source les, each with an associated header
le:
ieee80.c
ieee80.h
portsf.c
portsf.h
This relatively small size reects the fact that only the two most common soundle formats
are supported, AIFF and WAVE, though this includes 24-bit and 32-bit formats (both integer

187

Audio Programming in C

and oat), and multi-channel les, including the new WAVE_FORMAT_EXTENSIBLE format
introduced by Microsoft to support surround sound and high-precision audio (e.g. 24-bit and
beyond). Libraries such as libsndle (demonstrated elsewhere in this book) support a very
wide range of formats (including compressed formats such as ADPCM), and are highly efcient, but are also very much larger, using a large number of source les, with code that is
very much less accessible to beginning programmers.
The two les ieee80.c and ieee80.h are very small (the header le especially so), and are
made distinct as a courtesy to the author, Bill Gardner of MIT and Wave Arts (you can read
the details in the header le). Both are private les required by portsf.c (specically, to deal
with the sample-rate eld in AIFF headers). There is no technical reason why these les could
not be concatenated into one; you might like to try this as an exercise. However, in the form
shown, they conveniently illustrate an important distinction between private and public
header les.
To use the library, the only header le the user needs to #include is portsf.hthis denes
all the structures and functions in the library. We will be making frequent reference to this
le in the following sections. As this is a public header le, we will use the angle brackets
with #include:
#include <portsf.h>
This header le denes all we need to know about the portsf libraryvarious custom types
and structures, and the specialized functions for handling soundles. The main implementation le portsf.c on the other hand is large and complex, making use of several C language
idioms not so far presented. It will nevertheless repay study. You will for example notice immediately the use of conditional compilation (as introduced in the previous chapter) using
#ifdef and #ifndef. With long headers such as this, any technique that can be used to
avoid duplication of effort is very welcome. In this case, the conditional compilation is used
to detect if portsf.h has already been included by another source le. If so, the whole body of
the le is skipped. As it happens, this also enables the le to be read by C programs, C
being especially disapproving of duplicated type denitions. If at this stage you are alarmed
by portsf.c and feel you do not understand it, the answer is that it doesnt matter (much).
Often, code for a general-purpose library is very complex, not least because it seeks to be
general-purpose; but all we need to know about are the various denitions and declarations
in the header le.
Unlike portsf.h, the header le ieee80.h is, conversely, a private le used internally by the
two C les, and if you inspect these, you will notice that the other form of the #include
directive is used there:
#include "ieee80.h"
This, you will recall from chapter 1, asks the compiler to search rst in the directory that
contains the current source le (i.e. the le containing the #include directive).

188

Chapter 2

When portsf is created as a true library module, the les ieee80.h and ieee80.c, together with
portsf.c, will in effect disappear; the main header le portsf.h will be your only means of access
to the library. Thus the ieee80-related functions are private to the library, not accessible to
the programmer, and in some future implementation may be redened, or maybe even eliminated completely. This is a form of code encapsulation. If the user depended directly on the
existence and exact form of these functions they could not be removed or modied without
breaking user projects. In the following sections you will discover further mechanisms in C
for supporting such encapsulation (you may also encounter the expression data hiding in
this context); the language C develops this much further into a core principle of software
design.
2.1.2

The Makele

For the rst time, we require a special (non-system) header le stored not in our project
directory but elsewhere. As a result we will need to tell the compiler where to nd it, by adding the directory containing required header les (their path) to the list to be searched by the
compiler. The procedure for doing this is compiler-specic, but whichever one you use will
give you an option to set this search path for header les. The recommended approach in
this book is to use the UNIX-style development system based on the use of a makele. This
includes Linux, OS X and the MinGW environment on Windows. On the DVD, a makele is
provided for all example programs in each chapter.
A makele denes all the stages of compiling a programor indeed of several programs; It
is read by a special command-line program called make, standard on all UNIX-based systems
(including OS X). You will nd that a single makele supports building all the programs in a
chapter. Typing make will automatically build all of them (building portsf as well if necessary). For projects involving a number of source les, external libraries, and other special
build options (such as whether to make a debug build, or a release build optimized for
speed), such a makele is essential. The proprietary project les used by compilers such as
Visual Studio or Xcode are themselves in essence makeles, albeit typically somewhat
extended and verbose. One important task handled by make, in conjunction with the makele, is tracking whether source les need to be rebuilt or noti.e. by noting the creation date
of object les and other dependencies. If a project comprises dozens (or hundreds) of source
les, this speeds up development considerably by avoiding unnecessary recompilation.2
The supplied makeles are written specically for the directory structure as found on the
DVDthe paths are relative to the examples directory, for example using the ../ notation
to indicate the parent directory. Later on, you may feel condent enough to create your own
project directory structure, and create or modify makeles accordingly. You will use make in
conjunction with your makele to ensure that common les and libraries exist in only one
place, but can be used in any project by setting their paths in the makele. You will see that
all the makeles have much in common, with le and program names being the main differ-

189

Audio Programming in C

ences. Eventually you will become condent enough to write your own makele, by modifying one of these examples.
Despite the observations above, try to resist the temptation to copy all of the examples
from the DVD together, or to build all the programs at once. A key element of learning to
program lies in physically writing codecreating new les, maybe new directories, even
modifying a makele to support a new program. In some cases, the text presents a preliminary version of a program that is not provided as such on the DVD. It is especially important
to follow the detailed step-by-step descriptions of the process, adding and changing code as
you go. It is only through this direct hands-on experience that you will gain the condence
to work independently on your own projects.
2.1.3

Soundle Formatsenum and typedef

We have already encountered the enum keyword for dening a list of symbols, associating
them in order with ascending integer values. This offers a convenient alternative to a possibly long list of individual denitions using the preprocessor directive #define. It is also
possible to apply the typedef keyword to the enum statement, so that, in principle, each
symbol in the enum list is no longer the default int, but has the new custom type. This
results in a more expressive form of self-documentation than if, for example, a plain int
were to be used, or a list of #defines. The new type can then be employed both for function arguments and return values. For example, portsf.h denes an enum type for a number of
soundle channel properties:
typedef enum { STDWAVE, MC_STD, MC_MONO, MC_STEREO, MC_QUAD,
MC_LCRS, MC_BFMT, MC_DOLBY_5_1, MC_WAVE_EX }
psf_channelformat;
This denes the possible speaker formats supported by portsf. It includes support for the most
common WAVEFORMATEXTENSIBLE speaker position denitions (meaningful only for the
WAVE format). The symbol STD_WAVE serves as a default value, representing any standard soundle with no special speaker positionsany soundle format can be represented
this way. Additionally, the symbol MC_WAVE_EX acts as a category for any other speaker-feed
combinations not otherwise supported explicitly.
Similarly, portsf.h denes enum types for the available le formats and the all-important
sample type:
typedef enum {
PSF_FMT_UNKNOWN = 0,
PSF_STDWAVE,
PSF_WAVE_EX,
PSF_AIFF,
PSF_AIFC
} psf_format;

190

Chapter 2

typedef enum {
PSF_SAMP_UNKNOWN = 0,
PSF_SAMP_8,
/* not yet supported! */
PSF_SAMP_16,
PSF_SAMP_24,
PSF_SAMP_32,
PSF_SAMP_IEEE_FLOAT
} psf_stype;
The signicance of distinguishing PSF_STDWAVE and PSF_WAVE_EX in psf_format is
that les in these formats have the same le extension, .wav, so it is not sufcient to deduce
the le format solely from the le name. Similarly, it is not uncommon for a le with the
extension .aif or .aiff to be in the newer AIFF-C format, despite the fact that Apple advocate
a distinct extension.afc or .aifc.
These custom types are then used in the main structure dened in portsf.h to contain the
essential properties of a soundle, again using typedef:
typedef struct psf_props
{
long
long
psf_stype
psf_format
psf_channelformat
} PSF_PROPS;

srate;
chans;
samptype;
format;
chformat;

This custom type is used variously as argument and return value in a number of functions in
portsf. When a soundle is opened for reading, a PSF_PROPS structure is automatically lled
in; when a le is written, the programmer must likewise ensure that all elds contain valid
values. Taken together, the custom types and the portsf functions dene an application programming interface (API). An API is intended to hide all the low-level implementation details
(e.g. byte-swapping), while providing sufcient exibility and power to support a range of
projects. We will see how all the elements of the portsf API work together in the following
sections, which begin with a demonstration program using the library to convert a soundle
from one format to another. For more details, see appendix C.
There is a limit to the amount of documentation a developer can supply with a library
some degree of prior knowledge has to be assumed. In the case of portsf, it is assumed the
user is familiar with the use of soundles, and with the use of WAVE and AIFF soundles in
particular. The structure PSF_PROPS introduced in the previous section collects this essential
knowledge into one object; the fact that it contains only ve elements demonstrates that
one need not know very much about a soundle in order to be able to use it. This knowledge
can be summarized as follows:

191

Audio Programming in C

Figure 2.1
Interleaved sample frames.

A soundle contains audio data at a particular sample rate (number of samples per second).
Most audio systems support a range of standard sample rates22,050, 44,100, 48,000,
96,000 and even 192,000 hertz for DVD audio. We can store this parameter in

PSF_PROPS structure element: srate


A soundle can contain several channels of audioone for mono, two for stereo, six for
Dolby 5.1, and so on. In a multi-channel le, the sample rate denes the frame rate, where
one frame contains one sample for each channel. As a soundle represents audio that has
either been recorded through, or is intended to be played back through, a soundcard, these
samples are necessarily interleaved, see gure 1.12 in chapter 1. An alternative view of interleaved samples is shown in gure 2.1. This is represented by the following structure element:

PSF_PROPS element: chans


A soundle can contain audio samples in a range of data formats. By far the most common
format is 16-bit, represented by the C short data type. Other formats in widespread use
include 24-bit packed (stored in three bytes), and 32-bit oats, represented by the C float

192

Chapter 2

data type. Less common, but possible, is the 32-bit integer format, represented (at least in a
32-bit platform) by the C long (and, usually, int) data type. Other formats are possible: the
older 8-bit sample type is still in use here and there, but is not currently supported in portsf,
though provision has been made for its support at some future date. A 64-bit oating-point
sample is possible in both the AIFF and WAVE formats, but this is currently not supported by
portsf. A more exotic sample format is supported by the WAVEFORMATEXTENSIBLE le format. This distinguishes between the actual sample size and the container size. A typical
example of this would be a 24-bit unpacked format in which the three bytes of the sample
are contained in a 32-bit word. Such formats (which will be very rare in a soundle) are not
currently supported in portsf, which records only the overall container size. Note the rst
symbol PSF_SAMP_UNKNOWN, which is used to indicate that a soundle has some unrecognized sample type.
PSF_PROPS element: samptype
A soundle can be written in a number of le formats. One reason for this variety is the
different storage formats on different platforms (see note 1). The WAVE and AIFF formats
are the most familiar, but there are many others, only two of which are currently supported
by portsf. The new WAVEFORMATEXTENSIBLE format from Microsoft has already been
mentionedthis is intended to replace WAVE, and includes support for multi-channel audio
with dened speaker positions, and support for high-resolution sample formats including
the concept of a container size, as described above. It also supports user-dened custom formats (new versions of WAVE had to be registered with Microsoft; the new format enables
anyone to dene a new custom format without this requirement). You can nd full technical
documentation on this format at http://www.microsoft.com/hwdev/tech/audio/multichaud
.asp. Additionally, the new AIFF-C format is supported. This was originally designed by
Apple to support a range of compressed sample types, but a recent extension added support
for a 32-bit oating-point format, which the original AIFF format could not support. The
portsf library does not support compressed audio types in any of the le formats. Again, it
is possible that a user may seek to open or create a le in an unsupported format; accordingly, this is provided for in the custom format entry PSF_FMT_UNKNOWN.
n
A soundle can contain audio channels associated with specic speaker positions. This
property is relatively new, but has greatly increased in importance thanks to the popularity
of surround sound. With a stereo signal, it is established largely by convention that channel
1 is front left, and channel 2 is front right. Beyond that, speaker positions were undened
inthe standard WAVE format, and it was primarily for this reason that the WAVEFORMATEXTENSIBLE format was introduced by Microsoft. Apple had, in the initial specication for
the AIFF format, proposed standard speaker positions for several surround formats, but in so
doing highlighted the difculties in doing so. Two four-channel congurations were dened,
for quad surround and LCRS Surround, but there was no means of indicating which of these
was contained in a given soundlea four-channel AIFF le is therefore ambiguous, accordn

193

Audio Programming in C

ing to its own specication. At the time of writing, the only soundle formats unambiguously supporting dened speaker positions are WAVEFORMATEXTENSIBLE, and the new CAF
le format introduced by Apple (not supported by portsf, but supported by libsndle). The
portsf library supports only the primary subset of many possible channel congurations, as
dened for psf_chformat. One symbol in that list, MC_BFMT, requires further explanation.
This refers to a custom version of WAVEFORMATEXTENSIBLE designed to hold an Ambisonic
B-Format signal. This is an advanced form of surround encoding, representing the specially
processed outputs of three coincident gure-eight microphones (oriented to the three spatial
axes), which together can record a complete spherical soundeldthis includes information about the height of a sound, as well as its distance and position in the horizontal plane.
This format therefore contrasts with the other channel formats in that the channels represent not speaker feeds, but a compact encoding of a full three-dimensional soundeld, which
has to be decoded before being passed to loudspeakers. It is possible to decode to speaker layouts containing a large number of speakers; a minimum of four speakers would be required
to decode the signals for horizontal surround sound, and a minimum of eight speakers in a
cube layout, to decode with height.
PSF_PROPS element: chformat
This it is currently only relevant when the le format is PSF_WAVE_EX; it is ignored for all
other formats (STDWAVE is assumed).
Finally, one important property of all the le formats supported by portsf is that the le
header (which stores the properties of the le such as sample rate, sample type and number
of channels) can also contain further optional information contained in special chunks
collectively, the AIFF and WAVE formats store all information in distinct chunks, each of
which has a unique name (both WAVE and AIFF are examples of chunk names). We
will see in the next section how portsf includes support for one very useful optional chunk.
2.1.4

Initializing the portsf Library

Somewhat unusually for a soundle library (and unlike any of the le i/o functions in the C
standard library), portsf includes an initialization function, to be called before any other
functions in the library are called:
int psf_init(void);
Currently this function does very little, but it is expected that future versions of the library
will be more dependent on it. Portsf maintains an internal block of information on all active