Hi Kassen. I've made a few revisions and bug fixes since the last post,
so I'm attaching my most recent version of the code.
There are some new features, like the option of different types of
interpolation.
I haven't looked at it since December, but I think I already fixed that
integer check bug you found. But the type checks you suggested are a
very good idea.
I'm not quite sure what you mean about setting note 0 to 55 Hz. Right
now it's set up with note 0 being a very low C, no matter what
temperament you choose.
I'm reluctant to move the default note 0 away from C because I think
most musicians like to use C as their basic reference, even though A=55
is much easier math. Of course with (set-diapason ) and (set-base-keynum
) anyone is free to make note 0 any note they'd like.
I also think the default temperament should be equal temperament since
that's what our ears are most accustomed to. That's the way it's set up now.
I'm absolutely fine with you including the code in your fluxa.ss. I'll
put it under GPL2 also to make things easier. The helpmaps and type
checks are a very good idea.
Joel
On 02/19/2012 06:29 PM, Kassen wrote:
On Wed, Dec 21, 2011 at 05:13:02PM -0500, Joel Matthys wrote:
(set-scale ) in my code didn't work because it was reserved for racket. Changed
it to (set-scala ) and now everything seem to work fine. Yuck, nasty bug.
Ok, finally got round to properly playing with this.
I like it a lot, the just intonation looks very good with Racket's
support for exact fractions. We may be slightly behind SC in features,
but we'll have infinite resolution for frequency calculations, that's
worth something too ;-)
I noticed one more bug; (scala-note 9) works, similar for 9.1 but 9.0
gives a error. This was due to a check for a integer, instead of a
exact-integer. The fix was quite trivial as the file only has one such
check.
What I'd like to do is put this in my local copy of fluxa.ss,
replacing the current (note ) with full attributing to Joel if he's ok
with that (that file is GPL2, but clearly I can't licence somebody
else's work). Then I'd like to add helpmap entries and some type-checking
of the exposed functions. Without type-checking (seq ) can generate
rather a barrage of errors at a simple typo and I like to prevent that.
I'd also like to set some defaults like the just intonation that the
docs claim and set the default to note 0 being 55 Hz (a "A"). This is
because the current (note ) implementation quite sensibly sets (note
0) to be in that range, but I only now noticed that the current
implementation starts "counting" at A-sharp, (or b-flat, if you wish),
which I'd say makes mental math a bit harder and starting with "A"
simply makes sense to me. IMHO that would be a nice compromise between
sticking to the sort of thing that we have and this nicer new version,
and where it deviates from the current situation I feel it makes more
sense (unless the a# has some meaning?).
The result I'd like to push into the Redacted branch, from where
interested parties can do as they wish.
Does this make sense? Joel, are you ok with that kind of distribution
and licence?
Yours,
Kas.
; scala.scm, rev. 3
; 21/12/2011
; Copyright 2011-2012 by Joel Matthys
; joel at matthysmusic dot com
;-------------------------------------------------------------------
; This program is free software: you can redistribute it and/or modify
; it under the terms of the GNU General Public License as published by
; the Free Software Foundation, either version 3 of the License, or
; (at your option) any later version.
;
; This program is distributed in the hope that it will be useful,
; but WITHOUT ANY WARRANTY; without even the implied warranty of
; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
; GNU General Public License for more details.
;
; You should have received a copy of the GNU General Public License
; along with this program. If not, see <http://www.gnu.org/licenses/>.
;-------------------------------------------------------------------
; LOAD SCALA FILES INTO FLUXUS
; the basic keynumber->frequency function is
; (scala-note [keynum])
; by default, the function is set up for equal temperament, so
; (scala-note 60)
; > 261.6255653006
;
; This means key number 60 = 261.62... Hz
; * (scala-note also accepts lists of notes)
; There are a number of preset scale options:
;
; equal - equal temperament (12 notes)
; just - just intonation (12 notes)
; mean - meantone temperament (12 notes)
; pythag - diatonic pythagorean scale (7 notes)
; partch - Harry Partch scale (43 notes)
;
; To see the available scales:
; (list-scales)
;
; To change scales, use:
; (set-scala "scale-name")
; You can also change the base frequency (diapason) and keynumber with
; (set-diapason) and (set-base-keynum)
;
; Here, I choose the Pythagorean scale, set the diapason to 200 Hz and set
; the keynum to 0:
; (set-scala "pythag")
; (set-diapason 200)
; (set-base-keynum 0)
; (scala-note 0)
; > 200
; To load a scala file, use
; (load-scala "scalafilename.scl" "scale-nickname")
; * You choose a nickname to keep track of the scale.
; * If you don't choose a nickname, the scale will be
; * nicknamed "scala"
;
; You can find out the description of the file with:
; (scala-description)
;
; Example:
; (load-scala "scala/scl/bohlen-eg.scl" "bohlen")
; (set-scala "bohlen")
; (set-diapason 100)
; (set-base-keynum 0)
; (scala-note '(0 2 3 5 6 8 9 11 12 14 15))
; > (100 109.05... ... 436.203)
;
; By default, microtuning is on, allowing quarter-tones, etc. For instance,
; in equal temperament, (scala-note 60.5) is a quarter step above middle C.
;
; In scala scales, the frequency is linear interpolated by default; that is,
; (scala-note 60.5) returns the frequency halfway between notes 60 and 61.
; If you want a smoother interpolation and can spare the CPU, change to the
; slightly more expensive cosine interpolation with (set-scala-interp 1)
; or, if you're feeling truly extravagant, you can have beautiful cubic
; interpolation with (set-scala-interp 2). Return to linear interp with
; (set-scala-interp 0).
;
; To disable microtuning, use:
; (set-scala-microtonal #f)
; Now all fractional values are rounded to the nearest integer.
;
;--------------------------------------------------------------------------
; CHANGES
;
; 20 Dec 2011 - created file
; fixed error with optional parameters
; 21 Dec 2011 - rev 2 - changed set-diapason-key to set-base-keynum
; changed global var names, eg diapson -> *diapason*
; created table lookup methods to reduce cpu load
; rev 3 - linear interpolation allows microtuning
; bugfix: set-scale already defined in racket, changed to
; set-scala
; 22 Dec 2011 - rev 4 - add methods to eliminate duplicate scale defn
; changed load-scala-file to load-scala
; bug fixes, type checks and optimization
; 30 Dec 2011 - rev 5 - optimized (set-scala )
; added variable interpolation
;--------------------------------------------------------------------------
(define *scala-size* 12)
(define *scala* #f)
(define *scala-description* "12 note equal tempered scale")
(define *diapason* 261.6255653006)
(define *diapason-key* 60)
(define (set-diapason n)
(begin
(set! *diapason* n)
(fill-scala-table (- *diapason-key* 60) (+ *diapason-key* 67))))
(define (set-base-keynum n)
(begin
(set! *diapason-key* n)
(fill-scala-table (- *diapason-key* 60) (+ *diapason-key* 67))))
(define *scala-table* null)
(define *scala-microtonal* #t)
(define (set-scala-microtonal input)
(set! *scala-microtonal* input))
(define *scala-interp* 0)
(define (set-scala-interp n)
(set! *scala-interp* n))
(define *scala-list* '(("equal"
"12 note equal tempered scale"
12
#f)
("just"
"12 note just intoned scale"
12
(1 16/15 9/8 6/5 5/4 4/3 45/32 3/2 8/5 5/3 9/5 15/8 2))
("mean"
"12 note meantone scale"
12
(1 1.069984 1.118034 1.196279 1.25 1.337481 1.397542
1.495349 1.5625 1.671851 1.746928 1.869186 2))
("pythag"
"7 note Pure Pythagorean scale"
7
(1 9/8 81/64 4/3 3/2 27/16 243/128 2))
("partch"
"43 note Harry Partch scale"
43
(1 81/80 33/32 21/20 16/15 12/11
11/10 10/9 9/8 8/7 7/6 32/27 6/5 11/9 5/4 14/11
9/7 21/16 4/3 27/20 11/8 7/5 10/7 16/11 40/27 3/2
32/21 14/9 11/7 8/5 18/11 5/3 27/16 12/7 7/4 16/9
9/5 20/11 11/6 15/8 40/21 64/33 160/81 2))))
(define (scala-description) *scala-description*)
;(define (set-scala name [table *scala-list*])
; (begin
; (cond ((null? table) ;end of list, so it must be equal temp
; (begin (set! *scala* #f)
; (set! *diapason* 261.6255653006)
; (set! *diapason-key* 60)
; (set! *scala-description* "12 note equal tempered scale")))
; ((equal? name (caar table))
; (begin (set! *scala-size* (list-ref (car table) 2))
; (set! *scala* (list-ref (car table) 3))
; (set! *scala-description* (list-ref (car table) 1))))
; (else (set-scala name (cdr table))))
; (fill-scala-table (- *diapason-key* 60) (+ *diapason-key* 67))))
(define (set-scala name)
(set! *scala* #f)
(set! *diapason* 261.6255653006)
(set! *diapason-key* 60)
(set! *scala-description* "12 note equal tempered scale")
(if (equal? name "equal")
(set! *scala-table* *equaltemp-table*)
(begin
(map (lambda (x)
(cond ((equal? name (car x))
(begin
(set! *scala-size* (list-ref x 2))
(set! *scala* (list-ref x 3))
(set! *scala-description* (list-ref x 1))))
(else null)))
*scala-list*)
(fill-scala-table (- *diapason-key* 60) (+ *diapason-key* 67)))))
; helper function: linear interpolation
; (this is probably already in racket somewhere...)
(define (scala-lint val low-source high-source low-target high-target)
(let ((norm-val (/ (- val low-source) (- high-source low-source))))
(+ low-target (* norm-val (- high-target low-target)))))
(define (scala-cosint val low-source high-source low-target high-target)
(let* ((ft (* (/ (- val low-source) (- high-source low-source))
3.141592653589793))
(f (* 0.5 (- 1 (cos ft)))))
(+ (* low-target (- 1 f)) (* high-target f))))
(define (scala-cubint val low-source high-source p0 p1 p2 p3)
(let* ((x (/ (- val low-source) (- high-source low-source)))
(P (- (- p3 p2) (- p0 p1)))
(Q (- (- p0 p1) P))
(R (- p2 p0)))
(+ (* P x x x)
(* Q x x)
(* R x)
p1)))
; for non-equal-temperament scales, fractional keynums are interpolated
; between integer keynums
(define (scala-microtonal input)
(let* ((low-key (inexact->exact (floor input)))
(high-key (inexact->exact (ceiling input)))
(low-freq (parse-scale low-key))
(high-freq (parse-scale high-key)))
(cond ((= *scala-interp* 1)
(scala-cosint input low-key high-key low-freq high-freq))
((= *scala-interp* 2)
(let ((p0 (parse-scale (- low-key 1)))
(p3 (parse-scale (+ high-key 1))))
(scala-cubint input low-key high-key p0 low-freq high-freq p3)))
(else
(scala-lint input low-key high-key low-freq high-freq)))))
; helper function which calculates frequencies that aren't in *scala-table*
(define (parse-scale input)
(let ((keynum (if *scala-microtonal* input
(inexact->exact (round input)))))
(if (not *scala*) ; equal temperament
(* *diapason* (expt 2 (/ (- keynum *diapason-key*) 12)))
; else custom scale
(if (integer? keynum)
(let ((scale-degree (modulo (- keynum *diapason-key*) *scala-size*))
(scale-octave (floor (/ (- keynum *diapason-key*) *scala-size*))))
(* *diapason*
(expt (list-ref *scala* (- (length *scala*) 1))
scale-octave)
(list-ref *scala* scale-degree)))
(scala-microtonal keynum)))))
; the main keynum->freq function. Checks if keynum is in table; if not,
; calls on (parse-scale ) to calculate it.
(define (scala-note input)
(cond ((number? input)
(let ((test (find-in-table input *scala-table*)))
(cond (test test)
(else (parse-scale input)))))
((list? input) (map scala-note input))
(else "[scala] error: invalid type in (scala-note )")))
; loads scala file into list
(define (parse-scala-file filename)
(begin
(let ((output null))
(call-with-input-file filename
(lambda (input-port)
(let loop ((x (read-line input-port)))
(if (not (eof-object? x))
(begin
(set! output (append output
(list x)))
(loop (read-line input-port))) #t))))
output)))
; removes all lines that start with !
(define (remove-comment-lines input [result null])
(cond ((null? input) result)
((equal? #\! (string-ref (car input) 0))
(remove-comment-lines (cdr input) result))
(else (remove-comment-lines (cdr input)
(append result
(list (car input)))))))
; I think this is right. I'm curious if a cent is the same size with
; non-octave scales. I couldn't find any documentation on it.
(define (cents->ratio c)
(expt 2 (/ c 1200)))
(define (scala-intervals input-file [result '(1)])
(if (null? input-file)
result
(let ((test (scala-numberize (string->list (car input-file)))))
(if (number? test)
(scala-intervals (cdr input-file)
(append result
(list test)))
(scala-intervals (cdr input-file) result)))))
; ok, this is big and ugly, but basically (scala-numberize)
; converts a list of characters into a number
(define (scala-numberize input [found-number #f] [result null])
(cond ((null? input) ; end of list
(let ((test-number (string->number (list->string result))))
(cond ((not (number? test-number)) #f) ; no number found
((not (exact? test-number))
(cents->ratio test-number)) ; if not exact, must be cents
(else test-number)))) ; ratio
(else
(let* ((current-char (car input)) ; convert char to value
(char-val (- (char->integer current-char) 48)))
(cond ((and (not found-number) ; -16 = space
(= char-val -16)) ; leading spaces get skipped
(scala-numberize (cdr input) #f result))
((and (not found-number)
(or (< char-val -3) (> char-val 9))) #f)
; if we find non-numbers at the
; beginning of a line, we assume it's a comment
(else
(scala-numberize (cdr input)
(or found-number ; have we found a number?
(and (>= char-val 0) (<= char-val 9)))
(if (and (>= char-val -3) ; is it a number?
(<= char-val 9)) ; then add it to list
(append result (list current-char))
; otherwise, don't add it
result))))))))
(define (load-scala filename [new-name "scala"])
(let ((scale-definition null)
(scala-file null))
(begin
(set! scala-file (remove-comment-lines (parse-scala-file filename)))
(set! scale-definition (list new-name ; scale name
(car scala-file) ; scale description
(scala-numberize
(string->list (list-ref scala-file 1)))
; scale size
(scala-intervals (cddr scala-file))))
; to do: check if scale exists in *scala-list*
; if so, replace it
(set! *scala-list* (append (list scale-definition)
(remove-duplicates new-name)))
(set-scala new-name)
(print new-name))))
(define (remove-duplicates name [table *scala-list*] [count (length *scala-list*)])
(cond ((<= count 0) table)
((equal? name (caar table))
(remove-duplicates name
(cdr table)
(- count 1)))
(else (remove-duplicates name
(append (cdr table) (list (car table)))
(- count 1)))))
(define (list-scales)
(map (lambda (x) (car x)) *scala-list*))
(define (fill-scala-table low high [current #f] [result null])
(cond ((not current) (fill-scala-table low high low null))
((> current high)
(set! *scala-table* result))
(else (fill-scala-table low high (+ current 1)
(append result
(list (list current
(parse-scale current))))))))
(define (find-in-table num table)
(if (null? table) #f
(let ((result (member num (car table))))
(if (and (list? result)
(= 2 (length result)))
(list-ref result 1)
(find-in-table num (cdr table))))))
; default keynum->frequency table (equal temperament)
(define *equaltemp-table* '((0 8.17579891564375) (1 8.661957218027297)
(2 9.177023997419036) (3 9.722718241315079) (4 10.300861153527237)
(5 10.91338223228143) (6 11.562325709738635) (7 12.249857374429727)
(8 12.978271799373355) (9 13.75) (10 14.567617547440383)
(11 15.43385316425396) (12 16.3515978312875) (13 17.323914436054597)
(14 18.354047994838066)
(15 19.445436482630157)
(16 20.601722307054477)
(17 21.826764464562853)
(18 23.12465141947727)
(19 24.49971474885946)
(20 25.956543598746702)
(21 27.5)
(22 29.135235094880773)
(23 30.867706328507914)
(24 32.703195662575)
(25 34.647828872109194)
(26 36.70809598967613)
(27 38.890872965260314)
(28 41.20344461410895)
(29 43.653528929125706)
(30 46.24930283895454)
(31 48.99942949771892)
(32 51.913087197493404)
(33 55)
(34 58.27047018976155)
(35 61.73541265701583)
(36 65.40639132515)
(37 69.29565774421839)
(38 73.41619197935228)
(39 77.78174593052063)
(40 82.4068892282179)
(41 87.30705785825143)
(42 92.49860567790908)
(43 97.99885899543783)
(44 103.82617439498682)
(45 110)
(46 116.54094037952308)
(47 123.47082531403167)
(48 130.8127826503)
(49 138.59131548843678)
(50 146.83238395870455)
(51 155.56349186104126)
(52 164.8137784564358)
(53 174.61411571650285)
(54 184.99721135581817)
(55 195.99771799087566)
(56 207.65234878997364)
(57 220)
(58 233.08188075904616)
(59 246.94165062806334)
(60 261.6255653006)
(61 277.18263097687355)
(62 293.6647679174091)
(63 311.1269837220825)
(64 329.6275569128716)
(65 349.2282314330057)
(66 369.99442271163633)
(67 391.9954359817513)
(68 415.30469757994723)
(69 440)
(70 466.1637615180923)
(71 493.88330125612663)
(72 523.2511306012)
(73 554.3652619537471)
(74 587.3295358348182)
(75 622.253967444165)
(76 659.2551138257433)
(77 698.4564628660114)
(78 739.9888454232727)
(79 783.9908719635026)
(80 830.6093951598946)
(81 880)
(82 932.3275230361846)
(83 987.7666025122534)
(84 1046.5022612024)
(85 1108.7305239074942)
(86 1174.6590716696362)
(87 1244.50793488833)
(88 1318.5102276514865)
(89 1396.9129257320226)
(90 1479.9776908465453)
(91 1567.9817439270055)
(92 1661.218790319789)
(93 1760)
(94 1864.6550460723695)
(95 1975.5332050245065)
(96 2093.0045224048)
(97 2217.4610478149884)
(98 2349.3181433392724)
(99 2489.01586977666)
(100 2637.020455302973)
(101 2793.825851464045)
(102 2959.9553816930907)
(103 3135.963487854011)
(104 3322.437580639578)
(105 3520)
(106 3729.310092144739)
(107 3951.066410049013)
(108 4186.0090448096)
(109 4434.922095629976)
(110 4698.636286678547)
(111 4978.03173955332)
(112 5274.040910605945)
(113 5587.651702928092)
(114 5919.910763386181)
(115 6271.92697570802)
(116 6644.8751612791575)
(117 7040)
(118 7458.620184289476)
(119 7902.132820098028)
(120 8372.0180896192)
(121 8869.844191259952)
(122 9397.272573357093)
(123 9956.06347910664)
(124 10548.08182121189)
(125 11175.303405856184)
(126 11839.821526772363)
(127 12543.85395141604)))
(define *scala-table* *equaltemp-table*)