-
Notifications
You must be signed in to change notification settings - Fork 0
/
Csound.lhs
1603 lines (1361 loc) · 63.4 KB
/
Csound.lhs
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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
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
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
\section{Haskore Support for CSound}
\label{csound-sect}
\begin{verbatim}
> module CSound (module Performance, module Basics, module CSound)
> where
>
> import Performance
> import IO
> import List (find, nub)
\end{verbatim}
[Note: if this module is loaded into Hugs98, the following error
message may result:
\begin{verbatim}
Reading file "CSound.lhs":
ERROR "CSound.lhs" (line 707):
*** Cannot derive Eq OrcExp after 40 iterations.
*** This may indicate that the problem is undecidable. However,
*** you may still try to increase the cutoff limit using the -c
*** option and then try again. (The current setting is -c40)
\end{verbatim}
This is apparently due to the size of the {\tt OrcExp} data type. For
correct operation, start Hugs with a larger cutoff limit, such as {\tt
-c1000}.]
CSound is a software synthesizer that allows its user to create a
virtually unlimited number of sounds and instruments. It is extremely
portable because it is written entirely in C. Its strength lies
mainly in the fact that all computations are performed in software, so
it is not reliant on sophisticated musical hardware. The output of a
CSound computation is a file representing the signal which can be
played by an independent application, so there is no hard upper limit
on computation time. This is important because many sophisticated
signals take much longer to compute than to play. The purpose of this
module is to create an interface between Haskore and CSound in order
to give the Haskore user access to all the powerful features of a
software sound synthesizer.
CSound takes as input two plain text files: a {\em score} (.sco) file
and an {\em orchestra} (.orc) file. The score file is similar to a
Midi file, and the orchestra file defines one or more {\em
instruments} that are referenced from the score file (the orchestra
file can thus be thought of as the software equivalent of Midi
hardware). The CSound program takes these two files as input, and
produces a {\em sound file} as output, usually in {\tt .wav} format.
Sound files are generally much larger than Midi files, since they
describe the actual sound to be generated, represented as a sequence
of values (typically 44,100 of them for each second of music), which
are converted directly into voltages that drive the audio speakers.
Sound files can be played by any standard media player found on
conventional PC's.
Each of these files is described in detail in the following sections.
\subsection{The Score File}
\label{score-file-sect}
We will represent a score file as a sequence of {\em score
statements}:
\begin{verbatim}
> type Score = [ScoreStmt]
\end{verbatim}
The {\tt ScoreStmt} data type is designed to simulate CSound's three
kinds of score statements:
\begin{enumerate}
\item A {\em tempo} statement, which sets the tempo. In the absence
of a tempo statement, the tempo defaults to 60 beats per minute.
\item A {\em note event}, which defines the start time, pitch,
duration (in beats), volume (in decibels), and instrument to play a
note (and is thus more like a Haskore {\tt Event} than a Midi event,
thus making the conversion to CSound easier than to Midi, as we shall
see later). Each note event also contains a number of optional
arguments called {\em p-fields}, which determine other properties of
the note, and whose interpretation depends on the instrument that
plays the note. This will be discussed further in a later section.
\item {\em Function table} definitions. A function table is used by
instruments to produce audio signals. For example, sequencing through
a table containing a perfect sine wave will produce a very pure tone,
while a table containing an elaborate polynomial will produce a
complex sound with many overtones. The tables can also be used to
produce control signals that modify other signals. Perhaps the
simplest example of this is a tremolo or vibrato effect, but more
complex sound effects, and FM (frequency modulation) synthesis in
general, is possible.
\end{enumerate}
\begin{verbatim}
> data ScoreStmt = CSTempo Bpm
> | CSNote Inst StartTime Duration Pch Vlm [Pfield]
> | CSTable Tbl CreatTime TblSize Normalize GenRoutine
> deriving Show
>
> type Bpm = Int
> type Inst = Int
> type StartTime = Float
> type Duration = Float
> type Pch = Float
> type Vlm = Float
> type Pfield = Float
> type Tbl = Int
> type CreatTime = Float
> type TblSize = Int
> type Normalize = Bool
\end{verbatim}
This is all rather straightforward, except for function table
generation, which requires further explanation.
\subsubsection{Function Tables}
\label{function-table-sect}
Each function table must have a unique integer ID ({\tt Tbl}),
creation time (usually 0), size (which must be a power of 2), and a
{\tt Normalize} flag. Most tables in CSound are normalized, i.e.\
rescaled to a maximum absolute value of 1. The normalization process
can be skipped by setting the {\tt Normalize} flag to {\tt False}.
Such a table may be desirable to generate a control or modifying
signal, but is not very useful for audio signal generation.
Tables are simply arrays of floating point values. The values stored
in the table are calculated by one of CSound's predefined {\em generating
routines}, represented by the type {\tt GenRoutine}:
\begin{verbatim}
> data GenRoutine = GenRoutine GenNum [GenArg]
> | SoundFile SFName SkipTime ChanNum
> deriving Show
>
> type SFName = String
> type SkipTime = Float
> type ChanNum = Float
> type GenNum = Int
> type GenArg = Float
\end{verbatim}
{\tt GenRoutine n args} refers to CSound's generating routine $n$ (an
integer), called with floating point arguments {\tt args}. There is
only one generating routine (called GEN01) in CSound that takes an
argument type other than floating point, and thus we represent this
using the special constructor {\tt SoundFile}, whose functionality
will be described shortly.
Knowing which of CSound's generating routines to use and with what
arguments can be a daunting task. The newest version of CSound
(version 4.01) provides 23 different generating routines, and each one
of them assigns special meanings to its arguments. To avoid having to
reference routines using integer ids, the following functions are
defined for the most often-used generating routines. A brief
discussion of each routine is also included. For a full description
of these and other routines, refer to the CSound manual or consult the
following webpage: {\tt
http://www.leeds.ac.uk/music/Man/Csound/Function/GENS.html}. The user
familiar with CSound is free to write helper functions like the ones
below to capture other generating routines.
\paragraph*{GEN01.} Transfers data from a soundfile into a function
table. Recall that the size of the function table in CSound must be a
power of two. If the soundfile is larger than the table size, reading
stops when the table is full; if it is smaller, then the table is
padded with zeros. One exception is allowed: if the file is of type
AIFF and the table size is set to zero, the size of the function table
is allocated dynamically as the number of points in the soundfile.
The table is then unusable by normal oscillators, but can be used by a
special {\tt SampOsc} constructor (discussed in Section
\ref{orchestra-file-sect}). The first argument passed to the GEN01
subroutine is a string containing the name of the source file. The
second argument is skip time, which is the number of seconds into the
file that the reading begins. Finally there is an argument for the
channel number, with 0 meaning read all channels. GEN01 is
represented in Haskore as {\tt SoundFile SFName SkipTime ChanNum}, as
discussed earlier. To make the use of {\tt SoundFile} consistent with
the use of other functions to be described shortly, we define a simple
equivalent:
\begin{verbatim}
> soundFile :: SFName -> SkipTime -> ChanNum -> GenRoutine
> soundFile = SoundFile
\end{verbatim}
\paragraph*{GEN02.} Transfers data from its argument fields directly
into the function table. We represent its functionality as follows:
\begin{verbatim}
> tableValues :: [GenArg] -> GenRoutine
> tableValues gas = GenRoutine 2 gas
\end{verbatim}
\paragraph*{GEN03.} Fills the table by evaluating a polynomial over a
specified interval and with given coefficients. For example, calling
GEN03 with an interval of $(-1,1)$ and coefficients 5, 4, 3, 2, 0, 1
will generate values of the function $5+4x+3x^2+2x^3+x^5$ over the
interval $-1$ to $1$. The number of values generated is equal to the
size of the table. Let's express this by the following function:
\begin{verbatim}
> polynomial :: Interval -> Coefficients -> GenRoutine
> polynomial (x1,x2) cfs = GenRoutine 3 (x1:x2:cfs)
>
> type Interval = (Float, Float)
> type Coefficients = [Float]
\end{verbatim}
\paragraph*{GEN05.} Constructs a table from segments of exponential
curves. The first argument is the starting point. The meaning of the
subsequent arguments alternates between the length of a segment in
samples, and the endpoint of the segment. The endpoint of one segment
is the starting point of the next. The sum of all the segment lengths
normally equals the size of the table: if it is less the table is
padded with zeros, if it is more, only the first {\tt TblSize}
locations will be stored in the table.
\begin{verbatim}
> exponential1 :: StartPt -> [(SegLength, EndPt)] -> GenRoutine
> exponential1 sp xs = GenRoutine 5 (sp : flattenTuples2 xs)
>
> type StartPt = Float
> type SegLength = Float
> type EndPt = Float
\end{verbatim}
{\tt flattenTuples2} flattens a list of pairs into a list. Similarly,
{\tt flattenTuples3} flattens a list of 3-tuples into a list, and so
on.
\begin{verbatim}
> flattenTuples2 :: [(a,a)] -> [a]
> flattenTuples3 :: [(a,a,a)] -> [a]
> flattenTuples4 :: [(a,a,a,a)] -> [a]
>
> flattenTuples2 [] = []
> flattenTuples2 ((x,y) : xs) = x : y : flattenTuples2 xs
>
> flattenTuples3 [] = []
> flattenTuples3 ((x,y,z) : xs) = x : y : z : flattenTuples3 xs
>
> flattenTuples4 [] = []
> flattenTuples4 ((x, y, z, w) : xs) = x : y : z : w : flattenTuples4 xs
\end{verbatim}
\paragraph*{GEN25.} Similar to GEN05 in that it produces segments of
exponential curves, but instead of representing the lengths of
segments and their endpoints, its arguments represent $(x,y)$
coordinates in the table, and the subroutine produces curves between
successive locations. The $x$-coordinates must be in increasing
order.
\begin{verbatim}
> exponential2 :: [Point] -> GenRoutine
> exponential2 pts = GenRoutine 25 (flattenTuples2 pts)
>
> type Point = (Float,Float)
\end{verbatim}
\paragraph*{GEN06.} Generates a table from segments of cubic
polynomial functions, spanning three points at a time. We define a
function {\tt cubic} with two arguments: a starting position and a
list of segment length (in number of samples) and segment endpoint
pairs. The endpoint of one segment is the starting point of the next.
The meaning of the segment endpoint alternates between a local
minimum/maximum and point of inflexion. Whether a point is a maximum
or a minimum is determined by its relation to the next point of
inflexion. Also note that for two successive minima or maxima, the
inflexion points will be jagged, whereas for alternating maxima and
minima, they will be smooth. The slope of the two segments is
independent at the point of inflection and will likely vary. The
starting point is a local minimum or maximum (if the following point
is greater than the starting point, then the starting point is a
minimum, otherwise it is a maximum). The first pair of numbers will
in essence indicate the position of the first inflexion point in
$(x,y)$ coordinates. The folowing pair will determine the next local
minimum/maximum, followed by the second point of inflexion, etc.
\begin{verbatim}
> cubic :: StartPt -> [(SegLength, EndPt)] -> GenRoutine
> cubic sp pts = GenRoutine 6 (sp : flattenTuples2 pts)
\end{verbatim}
\paragraph*{GEN07.} Similar to GEN05, except that it generates
straight lines instead of exponential curve segments. All other
issues discussed about GEN05 also apply to GEN07. We represent it as:
\begin{verbatim}
> lineSeg1 :: StartPt -> [(SegLength, EndPt)] -> GenRoutine
> lineSeg1 sp pts = GenRoutine 7 (sp : flattenTuples2 pts)
\end{verbatim}
\paragraph*{GEN27.} As with GEN05 and GEN25, produces straight line
segments between points whose locations are given as $(x,y)$
coordinates, rather than a list of segment length, endpoint pairs.
\begin{verbatim}
> lineSeg2 :: [Point] -> GenRoutine
> lineSeg2 pts = GenRoutine 27 (flattenTuples2 pts)
\end{verbatim}
\paragraph*{GEN08.} Produces a smooth piecewise cubic spline curve
through the specified points. Neighboring segments have the same
slope at the common points, and it is that of a parabola through that
point and its two neighbors. The slope is zero at the ends.
\begin{verbatim}
> cubicSpline :: StartPt -> [(SegLength, EndPt)] -> GenRoutine
> cubicSpline sp pts = GenRoutine 8 (sp : flattenTuples2 pts)
\end{verbatim}
\paragraph*{GEN10.} Produces a composite sinusoid. It takes a list of
relative strengths of harmonic partials 1, 2, 3, etc. Partials not
required should be given strength of zero.
\begin{verbatim}
> compSine1 :: [PStrength] -> GenRoutine
> compSine1 pss = GenRoutine 10 pss
>
> type PStrength = Float
\end{verbatim}
\paragraph*{GEN09.} Also produces a composite sinusoid, but requires
three arguments to specify each contributing partial. The arguments
specify the partial number, which doesn't have to be an integer (i.e.\
inharmonic partials are allowed), the relative partial strength, and
the initial phase offset of each partial, expressed in degrees.
\begin{verbatim}
> compSine2 :: [(PNum, PStrength, PhaseOffset)] -> GenRoutine
> compSine2 args = GenRoutine 9 (flattenTuples3 args)
>
> type PNum = Float
> type PhaseOffset = Float
\end{verbatim}
\paragraph*{GEN19.} Provides all of the functionality of GEN09, but in
addition a DC offset must be specified for each partial. The DC
offset is a vertical displacement, so that a value of 2 will lift a
2-strength partial from range $[-2,2]$ to range $[0,4]$ before further
scaling.
\begin{verbatim}
> compSine3 :: [(PNum, PStrength, PhaseOffset, DCOffset)] -> GenRoutine
> compSine3 args = GenRoutine 19 (flattenTuples4 args)
>
> type DCOffset = Float
\end{verbatim}
\paragraph*{GEN11.} Produces an additive set of harmonic cosine
partials, similar to GEN10. We will represent it by a function that
takes three arguments: the number of harmonics present, the lowest
harmonic present, and a multiplier in an exponential series of
harmonics amplitudes (if the $x$'th harmonic has strength coefficient
of $A$, then the $(x+n)$'th harmonic will have a strength of
$A*(r^n)$, where $r$ is the multiplier).
\begin{verbatim}
> cosineHarms :: NHarms -> LowestHarm -> Mult -> GenRoutine
> cosineHarms n l m = GenRoutine 11 [float n, float l, m]
>
> type NHarms = Int
> type LowestHarm = Int
> type Mult = Float
\end{verbatim}
\paragraph*{GEN21.} Produces tables having selected random distributions.
\begin{verbatim}
> randomTable :: RandDist -> GenRoutine
> randomTable rd = GenRoutine 21 [float rd]
>
> type RandDist = Int
>
> uniform, linear, triangular, expon,
> biexpon, gaussian, cauchy, posCauchy :: Int
> uniform = 1
> linear = 2
> triangular = 3
> expon = 4
> biexpon = 5
> gaussian = 6
> cauchy = 7
> posCauchy = 8
\end{verbatim}
\paragraph*{Common Tables}
For convenience, here are some common function tables, which take as
argument the identifier integer:
\begin{verbatim}
> simpleSine, square, sawtooth, triangle, whiteNoise :: Tbl -> ScoreStmt
>
> simpleSine n = CSTable n 0 8192 True
> (compSine1 [1])
> square n = CSTable n 0 1024 True
> (lineSeg1 1 [(256, 1), (0, -1), (512, -1), (0, 1), (256, 1)])
> sawtooth n = CSTable n 0 1024 True
> (lineSeg1 0 [(512, 1), (0, -1), (512, 0)])
> triangle n = CSTable n 0 1024 True
> (lineSeg1 0 [(256, 1), (512, -1), (256, 0)])
> whiteNoise n = CSTable n 0 1024 True
> (randomTable uniform)
\end{verbatim}
The following function for a composite sine has an extra argument, a
list of harmonic partial strengths:
\begin{verbatim}
> compSine :: Tbl -> [PStrength] -> ScoreStmt
> compSine n s = CSTable 6 0 8192 True (compSine1 s)
\end{verbatim}
\subsubsection{Naming Instruments and Tables}
In CSound, each table and instrument has a unique identifying integer
associated with it. Haskore, on the other hand, uses strings to name
instruments. What we need is a way to convert Haskore instrument
names to identifier integers that CSound can use. Similar to
Haskore's player maps, we define a notion of a {\em CSound name map}
for this purpose.
\begin{verbatim}
> type NameMap = [(Name, Int)]
> type Name = String
\end{verbatim}
A name map can be provided directly in the form
{\tt [("name1", int1), ("name2", int2), ...]}, or the programmer can
define auxiliary functions to make map construction easier.
For example:
\begin{verbatim}
> makeNameMap :: [Name] -> NameMap
> makeNameMap nms = zip nms [1..]
\end{verbatim}
The following function will add a name to an existing name map. If
the name is already in the map, an error results.
\begin{verbatim}
> addToMap :: NameMap -> Name -> Int -> NameMap
> addToMap nmap nm i =
> case (getId nmap nm) of
> Nothing -> (nm,i) : nmap
> Just _ -> error (" addToMap: the name " ++ nm ++
> " is already in the name map")
>
> getId :: NameMap -> Name -> Maybe Int
> getId nmap nm =
> case (find (\(n,_) -> n==nm) nmap) of
> Nothing -> Nothing
> Just (n,i) -> Just i
\end{verbatim}
Note the use of the function {\tt find} imported from the {\tt List}
library.
\subsubsection{Converting Haskore Music to a CSound Score File}
To convert a {\tt Music} value into a CSound score file, we need to:
\begin{enumerate}
\item Convert the {\tt Music} value to a {\tt Performance}.
\item Convert the {\tt Performance} value to a {\tt Score}.
\item Write the {\tt Score} value to a CSound score file.
\end{enumerate}
We already know how to do the first step. Steps two and three will be
achieved by the following two functions:
\begin{verbatim}
perfToScore :: NameMap -> Performance -> Score
writeScore :: Score -> IO ()
\end{verbatim}
The three steps can be put together in whatever way the user wishes,
but the most general way would be this:
\begin{verbatim}
> musicToCSound :: NameMap -> PMap -> Tables -> Context -> Music -> IO ()
> musicToCSound nmap pmap tables cont m =
> writeScore (tables ++ perfToScore nmap (perform pmap cont m))
>
> type Tables = Score
\end{verbatim}
The {\tt Tables} argument is a user-defined set of function tables,
represented as a sequence of {\tt ScoreStmt}s (specifically, {\tt
CSTable} constructors). (See Section \ref{function-tables-sect}.)
\paragraph*{From Performance to Score}
The translation between performance {\tt Events} and score {\tt
CSNotes} is straightforward, the only tricky parts being:
\begin{itemize}
\item The unit of time in a {\tt Performance} is the second, whereas
in a {\tt Score} it is the beat. However, the default CSound tempo is
60 beats per minute, or one beat per second, as was already mentioned,
and we use this default for our {\em Score} files. Thus the two are
equivalent, and no translation is necessary.
\item In a {\tt Performance}, pitch is represented as an {\tt
AbsPitch} value, an integer denoting the absolute position of the
pitch in semitones (MIDI uses the same representation). CSound,
however, uses different pitch representations, one of them being {\em
octave point pitch class}, or ``pch.'' Using pch, a pitch is
represented by a decimal number whose integer part represents the
octave, and decimal part represents the semitone within the octave
(thus 0.00 is the lowest C, 0.11 the lowest B, 8.00 is middle C,
etc.). The function {\tt absToPch} defined below performs the
required conversion for us.
\end{itemize}
\begin{verbatim}
> perfToScore :: NameMap -> Performance -> Score
> perfToScore _ [] = []
> perfToScore nmap (Event t i p d v pfs : evs) =
> case (getId nmap i) of
> Nothing -> error ("perfToScore: instrument " ++ i ++ " is unknown")
> Just num -> CSNote num t d (absToPch p) v pfs : perfToScore nmap evs
>
> absToPch :: AbsPitch -> Pch
> absToPch ap = float (ap `quot` 12) + (float (ap `mod` 12) / 100.0)
\end{verbatim}
\paragraph*{From Score to Score File}
Now that we have a value of type {\tt Score}, we must write it into a
plain text ASCII file with an extension {\tt .sco} in a way that
CSound will recognize. This is done by the following function:
\begin{verbatim}
> writeScore :: Score -> IO ()
> writeScore s = do putStr "\nName your score file "
> putStr "(.sco extension will be added): "
> name <- getLine
> h <- openFile (name ++ ".sco") WriteMode
> printScore h s
> hClose h
\end{verbatim}
This function asks the user for the name of the score file, opens that
file for writing, writes the score into the file using the function
{\tt printScore}, and then closes the file.
The score file is a plain text file containing one statement per line.
Each statement consists of an opcode, which is a single letter that
determines the action to be taken, and a number of arguments. The
opcodes we will use are ``e'' for end of score, ``t'' to set tempo,
``f'' to create a function table, and ``i'' for note events.
\begin{verbatim}
> printScore :: Handle -> Score -> IO ()
> printScore h [] = hPutStr h "e\n" -- end of score
> printScore h (s : ss) = do printStatement h s
> printScore h ss
\end{verbatim}
In the following we will come across several instances where
we will need to write a list of floating point numbers into the file,
one number at a time, separated by spaces. To do this, we will need
to convert the list to a string. This is done by the following
function:
\begin{verbatim}
> listToString :: [Float] -> String
> listToString [] = ""
> listToString (n : ns) = " " ++ show n ++ listToString ns
\end{verbatim}
Finally, the {\tt printStatement} function:
\begin{verbatim}
> printStatement :: Handle -> ScoreStmt -> IO ()
> printStatement h (CSTempo t) =
> hPutStr h ("t 0 " ++ show t ++ "\n")
> printStatement h (CSNote i st d p v pfs) =
> hPutStr h
> ("i " ++ show i ++ " " ++ show st ++ " " ++ show d ++ " " ++
> show p ++ " " ++ show v ++ listToString pfs ++ "\n")
> printStatement h (CSTable t ct s n gr) =
> hPutStr h
> ("f " ++ show t ++ " " ++ show ct ++ " " ++ show s ++
> (if n then " " else " -") ++ showGenRoutine gr ++ "\n")
> where showGenRoutine (SoundFile nm st cn) =
> "1 " ++ nm ++ " " ++ show st ++ " 0 " ++ show cn
> showGenRoutine (GenRoutine gn gas) =
> show gn ++ listToString gas
\end{verbatim}
\subsection{The Orchestra File}
\label{orchestra-file-sect}
The orchestra file consists of two parts: a {\em header}, and one or
more {\em instrument blocks}. The header sets global parameters
controlling sampling rate, control rate, and number of output
channels. The instrument blocks define instruments, each identified
by a unique integer ID, and containing statements modifying or
generating various audio signals. Each note statement in a score file
passes all its arguments---including the p-fields---to its
corresponding instrument in the orchestra file. While some properties
vary from note to note, and should therefore be designed as p-fields,
many can be defined within the instrument; the choice is up to the
user.
The orchestra file is represented as:
\begin{verbatim}
> type Orchestra = (Header, [InstBlock])
\end{verbatim}
The orchestra header sets the audio rate, control rate, and number of
output channels:
\begin{verbatim}
> type Header = (AudRate, CtrlRate, Chnls)
>
> type AudRate = Int -- samples per second
> type CtrlRate = Int -- samples per second
> type Chnls = Int -- mono=1, stereo=2, surround=4
\end{verbatim}
Digital computers represent continuous analog audio waveforms as a
sequence of discrete samples. The audio rate ({\tt AudRate}) is the
number of these samples calculated each second. Theoretically, the
maximum frequency that can be represented is equal to one-half the
audio rate. Audio CDs contain 44,100 samples per second of music,
giving them a maximum sound frequency of 22,050 Hz, which is as high
as most human ears are able to hear.
Computing 44,100 values each second can be a demanding task for a CPU,
even by today's standards. However, some signals used as inputs to
other signal generating routines don't require such a high resolution,
and can thus be generated at a lower rate. A good example of this is
an amplitude envelope, which changes relatively slowly, and thus can
be generated at a rate much lower than the audio rate. This rate is
called the {\em control rate} ({\tt CtrlRate}), and is set in the
orchestra file header. The audio rate is usually a multiple of the
control rate, but this is not a requirement.
Each instrument block contains a unique identifying integer, as well
as an {\em orchestra expression} that defines the audio signal
characterizing that instrument:
\begin{verbatim}
> type InstBlock = (Inst, OrcExp)
\end{verbatim}
Recall that {\tt Inst} is a type synonym for an {\tt Int}. This value
may be obtained from a string name and a name map using the function
{\tt getId :: NameMap -> Name -> Maybe Int} discussed earlier.
\subsubsection{Orchestra Expressions}
The data type {\tt OrcExp} is the largest deviation that we will make
from the actual CSound design. In CSound, instruments are defined
using a sequence of statements that, in a piecemeal manner, define the
various oscillators, summers, constants, etc.\ that make up an
instrument. These pieces can be given names, and these names can be
referenced from other statements. But despite this rather imperative,
statement-oriented approach, it is acually completely functional. In
other words, every CSound instrument can be rewritten as a single
expression. It is this ``expression language'' that we capture in
{\tt OrcExp}. A pleasant attribute of the result is that CSound's ad
hoc naming mechanism is replaced with Haskell's conventional way of
naming things.
The entire {\tt OrcExp} data type declaration is shown in Figure
\ref{OrcExp-fig}. In what follows, we describe each of the various
constructors in turn.
\begin{figure}
{\scriptsize\vspace{-.7in}
\begin{verbatim}
> data OrcExp = Const Float
> | Pfield Int
>
> | Plus OrcExp OrcExp
> | Minus OrcExp OrcExp
> | Times OrcExp OrcExp
> | Divide OrcExp OrcExp
> | Power OrcExp OrcExp
> | Modulo OrcExp OrcExp
>
> | Int OrcExp
> | Frac OrcExp
> | Neg OrcExp
> | Abs OrcExp
> | Sqrt OrcExp
> | Sin OrcExp
> | Cos OrcExp
> | Exp OrcExp
> | Log OrcExp
>
> | AmpToDb OrcExp
> | DbToAmp OrcExp
> | PchToHz OrcExp
> | HzToPch OrcExp
>
> | GreaterThan OrcExp OrcExp OrcExp OrcExp
> | LessThan OrcExp OrcExp OrcExp OrcExp
> | GreaterOrEqTo OrcExp OrcExp OrcExp OrcExp
> | LessOrEqTo OrcExp OrcExp OrcExp OrcExp
> | Equals OrcExp OrcExp OrcExp OrcExp
> | NotEquals OrcExp OrcExp OrcExp OrcExp
>
> | MonoOut OrcExp
> | LeftOut OrcExp
> | RightOut OrcExp
> | StereoOut OrcExp OrcExp
> | FrontLeftOut OrcExp
> | FrontRightOut OrcExp
> | RearRightOut OrcExp
> | RearLeftOut OrcExp
> | QuadOut OrcExp OrcExp OrcExp OrcExp
>
> | Line EvalRate Start Durn Finish
> | Expon EvalRate Start Durn Finish
> | LineSeg EvalRate Start Durn Finish [(Durn, Finish)]
> | ExponSeg EvalRate Start Durn Finish [(Durn, Finish)]
> | Env EvalRate Sig RTime Durn DTime RShape SAttn DAttn Steep
> | Phasor EvalRate Freq InitPhase
> | TblLookup EvalRate Index Table IndexMode
> | TblLookupI EvalRate Index Table IndexMode
> | Osc EvalRate Amp Freq Table
> | OscI EvalRate Amp Freq Table
> | FMOsc Amp Freq CarFreq ModFreq ModIndex Table
> | FMOscI Amp Freq CarFreq ModFreq ModIndex Table
> | SampOsc Amp Freq Table
> | Random EvalRate Amp
> | RandomHold EvalRate Amp HoldHz
> | RandomI EvalRate Amp HoldHz
> | GenBuzz Amp Freq NumHarms LoHarm Multiplier Table
> | Buzz Amp Freq NumHarms Table
> | Pluck Amp Freq Table DecayMethod DecArg1 DecArg2
> | Delay MaxDel AudioSig
> | DelTap TapTime DelLine
> | DelTapI TapTime DelLine
> | DelayW AudioSig
> | Comb AudioSig RevTime LoopTime
> | AlPass AudioSig RevTime LoopTime
> | Reverb AudioSig RevTime
> deriving (Show, Eq)
\end{verbatim}}
\caption{The OrcExp Data Type}
\label{OrcExp-fig}
\end{figure}
\paragraph*{Constants}
{\tt Const x} represents the floating-point constant {\tt x}.
\paragraph*{P-field Arguments}
{\tt Pfield n} refers to the $n$th p-field argument. Recall that all
note characteristics, including pitch, volume, and duration, are
passed into the orchestra file as p-fields. For example, to access
the pitch, one would write {\tt Pfield 4}. To make the access of
these most common p-fields easier, we define the following constants:
\begin{verbatim}
> noteDur, notePit, noteVol :: OrcExp
> noteDur = Pfield 3
> notePit = Pfield 4
> noteVol = Pfield 5
\end{verbatim}
It is also useful to define the following standard names, which are
identical to those used in CSound:
\begin{verbatim}
> p1,p2,p3,p4,p5,p6,p7,p8,p9 :: OrcExp
> p1 = Pfield 6
> p2 = Pfield 7
> p3 = Pfield 8
> p4 = Pfield 9
> p5 = Pfield 10
> p6 = Pfield 11
> p7 = Pfield 12
> p8 = Pfield 13
> p9 = Pfield 14
\end{verbatim}
\paragraph*{Arithmetic and Transcendental Functions}
Arithmetic expressions are represented by the constructors {\tt Plus},
{\tt Minus}, {\tt Times}, {\tt Divide}, {\tt Power}, and {\tt Modulo},
each taking two {\tt OrcExps} as arguments. In addition, there are a
number of unary arithmatic functions: {\tt Int x} and {\tt Frac x}
represent the integer and fractional parts, respectively, of {\tt x}.
{\tt Abs x}, {\tt Neg x}, {\tt Sqrt x}, {\tt Sin x}, and {\tt Cos x}
represent the absolute value, negation, square root, sine and cosine
(in radians) of {\tt x}, respectively. {\tt Exp x} represents $e$
raised to the power {\tt x}, and {\tt Log x} is the natural logarithm
of {\tt x}.
To facilitate the use of these arithmetic functions, we can make {\tt
OrcExp} an instance of certain numeric type classes, thus providing
more conventional names for the various operations.
\begin{verbatim}
> instance Num OrcExp where
> (+) = Plus
> (-) = Minus
> (*) = Times
> negate = Neg
> abs = Abs
> fromInteger i = Const (fromInteger i)
> -- omitted: signum
> instance Fractional OrcExp where
> (/) = Divide
> fromRational x = Const (fromRational x)
> instance Floating OrcExp where
> exp = Exp
> log = Log
> sqrt = Sqrt
> (**) = Power
> sin = Sin
> cos = Cos
> pi = Const pi
> tan x = Sin x / Cos x
> -- omitted: asin, acos, atan :: a -> a
> -- sinh, cosh, tanh :: a -> a
> -- asinh, acosh, atanh :: a -> a
\end{verbatim}
For example, {\tt Plus (Pfield 3) (Power (Sin (Pfield 6)) (Const 2))}
can now be written simply as {\tt noteDur + sin p6 ** 2}.
\paragraph*{Pitch and Volume Coercions}
The next set of constructors represent functions that convert between
different CSound pitch and volume representations. Recall that the
{\tt CSNote} constructor uses decibels for volume and ``pch'' notation
for pitch. If these values are to be used as inputs into a signal
generating or modifying routine, they must first be converted into
units of raw amplitude and hertz, respectively. This is accomplished
by the functions represented by {\tt DbToAmp} and {\tt PchToHz},
and their inverses are {\tt AmpToDb} and {\tt HzToPch}. Thus note
that {\tt PchToHz (notePit + 0.01)} raises the pitch by one semitone,
whereas {\tt PchToHz notePit + 0.01} raises the pitch by 0.01 Hz.
\paragraph*{Comparison Operators}
{\tt OrcExp} also includes comparison constructors {\tt GreaterThan},
{\tt LessThan}, {\tt GreaterOrEqTo}, {\tt LessOrEqTo}, {\tt Equals},
and {\tt NotEquals}. Each takes four {\tt OrcExp} arguments: the
values of the first two are compared, and if the result is true, the
expression evaluates to the third argument; otherwise, it takes on the
value of the fourth.\footnote{This design emulates that of CSound. A
more conventional design would have comparison operators that return a
Boolean value, and a conditional expression to choose between two
values based on a Boolean. One could then add Boolean operators such
as ``and'', ``or'', etc. It seems possible to do this in Haskore, but
its translation into CSound would be more difficult, and thus we take
the more conservative approach for now.}
\paragraph*{Output Operators}
The next group of constructors represent CSound's {\em output
statements}. The constructors are {\tt MonoOut}, {\tt LeftOut}, {\tt
RightOut}, {\tt StereoOut}, {\tt FrontLeftOut}, {\tt FrontRightOut},
{\tt RearRightOut}, {\tt RearLeftOut}, and {\tt QuadOut}. {\tt
StereoOut} takes two {\tt OrcExp} arguments, {\tt QuadOut} takes four,
and the rest take one.
The top-level of an instrument's {\tt OrcExp} (i.e., the one in the
{\tt InstBlock} value) will normally be an application of one of
these. Furthermore, the constructor used must be in agreement with
the number of output channels specified in the orchestra header---for
example, using {\tt LeftOut} when the header declares the resulting
sound file to be mono will result in an error.
\paragraph*{Signal Generation and Modification}
The most sophisticated {\tt OrcExp} constructors are those that
emulate CSound's signal generation and modification functions. There
are quite a few of them, and they are all described here, although the
reader is encouraged to read the CSound manual for further details.
Before defining each constructor, however, there are two general
issues to discuss:
First, signals in CSound can be generated at three rates: the note
rate (i.e., with every note event), the control rate, and the audio
rate (we discussed the latter two earlier). Many of the signal
generating routines can produce signals at more than one rate, so the
rate must be specified as an argument. The following simple data
structure serves this purpose:
\begin{verbatim}
> data EvalRate = NR -- note rate
> | CR -- control rate
> | AR -- audio rate
> deriving (Show, Eq)
\end{verbatim}
Second, note in Figure \ref{OrcExp-fig} that this collection of
constructors uses quite a few other type names. In all cases,
however, these are simply type synonyms for {\tt OrcExp}, and are used
only for clarity. These type synonyms are listed here in one fell
swoop:
\begin{verbatim}
> type Start = OrcExp
> type Durn = OrcExp
> type Finish = OrcExp
> type Sig = OrcExp
> type RTime = OrcExp
> type DTime = OrcExp
> type RShape = OrcExp
> type SAttn = OrcExp
> type DAttn = OrcExp
> type Steep = OrcExp
> type Freq = OrcExp
> type InitPhase = OrcExp
> type Index = OrcExp
> type Table = OrcExp
> type IndexMode = OrcExp
> type Amp = OrcExp
> type CarFreq = OrcExp
> type ModFreq = OrcExp
> type ModIndex = OrcExp
> type HoldHz = OrcExp
> type NumHarms = OrcExp
> type LoHarm = OrcExp
> type Multiplier = OrcExp
> type DecayMethod = OrcExp
> type DecArg1 = OrcExp
> type DecArg2 = OrcExp
> type MaxDel = OrcExp
> type AudioSig = OrcExp
> type TapTime = OrcExp
> type DelLine = OrcExp
> type RevTime = OrcExp
> type LoopTime = OrcExp
\end{verbatim}
We can now discuss each constructor in turn:
\begin{enumerate}
\item {\tt Line evalrate start durn finish} produces values along a
straight line from {\tt start} to {\tt finish}. The values can be
generated either at control or audio rate, and the line covers a
period of time equal to {\tt durn} seconds.
\item {\tt Expon} is similar to {\tt Line}, but
{\tt Expon evalrate start durn finish} produces an exponential curve
instead of a straight line.
\item If a more elaborate signal is required, one can use the
constructors {\tt LineSeg} or {\tt ExponSeg}, which take arguments of
type {\tt EvalRate}, {\tt Start}, {\tt Durn}, and {\tt Finish}, as
above, and also {\tt [(Durn, Finish)]}. The first four arguments work
as before, but only for the first of a number of segments. The
subsequent segment lengths and endpoints are given in the fifth
argument. A signal containing both straight line and exponential
segments can be obtained by adding a {\tt LineSeg} signal and {\tt
ExponSeg} signal together in an appropriate way.
\item {\tt Env evalrate sig rtime durn dtime rshape sattn dattn steep}
modifies the signal {\tt sig} by applying an envelope to
it.\footnote{Although this function is widely-used in CSound, the same
effect can be accomplished by creating a signal that is a combination
of straight line and exponential curve segments, and multiplying it by
the signal to be modified.} {\tt rtime} and {\tt dtime} are the rise