;Scala-Lib, by Thorin Kerr, 17/10/2007. thorin.kerr@gmail.com
;A library for accessing pitch data from scala formatted scale files.
;The scala program and scale archive can be downloaded from the scala download page: http://www.xs4all.nls/~huygensf/scala/downloads.html
;See 'scala:' prefixed procedures and usage examples at the end of this file.
;Internal procedures
;return string representation of character.
(define (tkerr:char->string char)
(if (char? char)
(atom->string char)
#f))
;end of line predicate
(define tkerr:char-eol?
(lambda (char)
(if (char=? char (integer->char 10))
#t
#f)))
;space or tab predicate
(define tkerr:char-space?
(lambda (char)
(if (or (char=? char (integer->char 32)) (char=? char (integer->char 9)))
#t
#f)))
;reads from a file port, returning each line in the file as a string.
;note, this ignores whitespace.
(define tkerr:read-linedata
(lambda (p)
(let loop ((char (read-char p))
(result ""))
(cond ((eof-object? char) char)
((not (char? char)) (loop (read-char p) result))
((tkerr:char-space? char) (loop (read-char p) result))
((tkerr:char-eol? char) result)
(else (loop (read-char p) (string-append result (tkerr:char->string char))))))))
;returns a list of strings for each line of a file. Whitespace removed.
;template borrowed from schematics cookbook http://schemecookbook.org/Cookbook/FileRead
(define (tkerr:read-lines filename)
(call-with-input-file filename
(lambda (p)
(let loop ((line (tkerr:read-linedata p))
(result '()))
(cond ((eof-object? line) (reverse result))
(else (loop (tkerr:read-linedata p) (cons line result))))))))
;from Schematics cookbook http://schemecookbook.org/Cookbook/ListRecipefilterElements
;this procedure removes elements according to a predicate test.
(define (tkerr:filter pred? lst)
(cond
((null? lst) '())
((pred? (car lst)) (cons (car lst) (tkerr:filter pred? (cdr lst))))
(else (tkerr:filter pred? (cdr lst)))))
;---------------------------
;list helpers
;---------------------------
;returns the last value in a list
(define (tkerr:last lst)
(if (null? lst) '()
(list-ref lst (- (length lst) 1))))
;returns head of a list up to ndx. opposite of list-tail.
(define (tkerr:list-head list-of-items ndx)
(cond
((<= (length list-of-items) ndx)'())
(else (cons (car list-of-items) (tkerr:list-head (cdr list-of-items) ndx)))))
;returns a slice of a list between ndx1 and ndx2
(define tkerr:slice
(lambda (list-of-items ndx1 ndx2)
(let ((tail (list-tail list-of-items ndx1)))
(tkerr:list-head tail (- (length tail) (- ndx2 ndx1))))))
;----------------------------
;scala format procedures
;----------------------------
;just beware if the description line is left blank in the scala file. It will confuse the slice.
(define tkerr:remove-scala-comments
(lambda (lines-lst)
(let ((declamation (tkerr:filter (lambda (line) (not (string=? (substring line 0 1) "!"))) lines-lst)))
(tkerr:slice declamation 2 (length declamation)))))
;returns the numerator from a string representation of a ratio
;e.g. "7/9" returns 7. "34/67" returns 34.
(define tkerr:get-numerator
(lambda (str-rat)
(let* ((ndx (cl:position #\/ (string->list str-rat))))
(string->atom (substring str-rat 0 ndx)))))
;returns the denominator from a string representation of a ratio
;e.g. "7/9" returns 9. "34/67" returns 67.
(define tkerr:get-denominator
(lambda (str-rat)
(let* ((ndx (cl:position #\/ (string->list str-rat))))
(string->atom (substring str-rat (+ ndx 1) (string-length str-rat))))))
;converts a numerator and denominator into a cent value.
(define tkerr:ratio->cent
(lambda (numerator denominator)
(* 1200 (/ (log (/ numerator denominator)) (log 2)))))
;convert a string represenation of a ratio into a cent vale
(define (tkerr:str-rat->cent str-rat)
(tkerr:ratio->cent (tkerr:get-numerator str-rat) (tkerr:get-denominator str-rat)))
;predicate, to test if a value is a cent representation.
(define tkerr:str-cent?
(lambda (str)
(let loop ((ndx 0))
(cond
((>= ndx (string-length str)) #f)
((char=? (string-ref str ndx) #\.) #t)
(else (loop (+ ndx 1)))))))
;predicate, to test if a value is a ratio representation.
(define tkerr:str-rat?
(lambda (str)
(let loop ((ndx 0))
(cond
((>= ndx (string-length str)) #f)
((char=? (string-ref str ndx) #\/) #t)
(else (loop (+ ndx 1)))))))
;A procedure to step through a scale-list and convert all to cents
(define tkerr:str-scale->cent-scale
(lambda (str-scale)
(if (null? str-scale)
()
(cons (if (tkerr:str-cent? (car str-scale))
(string->number (car str-scale))
(tkerr:str-rat->cent (car str-scale)))
(tkerr:str-scale->cent-scale (cdr str-scale))))))
;-----------------------------------------------------------------------------------------------------
;USAGE PROCEDURES
;-----------------------------------------------------------------------------------------------------
;read a scala scale file, and return a list of cent equivalent values in a list
(define scala:read-scale
(lambda (path)
(let ((scale (tkerr:remove-scala-comments (tkerr:read-lines path))))
(tkerr:str-scale->cent-scale scale))))
;produces a list of 128 real midi equivalents from a list of cent values
;index number 69 is used as a reference for A4 - 440 hz.
;be aware that some scales may produce negative values
(define (scala:cscale->mscale cscale)
(define cs->ms
(lambda (cs deg oct)
(if (> deg 126) '()
(cons (/ (+ (list-ref cs (fmod deg (length cs))) (* oct (tkerr:last cs))) 100)
(if (< (fmod deg (length cs)) (- (length cs) 1)) (cs->ms cs (+ deg 1) oct)
(cs->ms cs (+ deg 1) (+ oct 1)))))))
(let* ((sc (cons 0 (cs->ms cscale 0 0)))
(a440diff (- 69 (list-ref sc 69))))
(map (lambda (c) (+ c a440diff)) sc)))
;a handy conversion utility
(define scala:midi->hz
(lambda (midi-pitch)
(* 440 (exp (/ (* (log 2.0) (- midi-pitch 69.0)) 12.0)))))
;produces a list of 128 hertz values, with index 69 used as a reference value for A4 - 440 hertz.
(define (scala:cscale->hzscale cscale)
(define cs->ms
(lambda (cs deg oct)
(if (> deg 126) '()
(cons (/ (+ (list-ref cs (fmod deg (length cs))) (* oct (tkerr:last cs))) 100)
(if (< (fmod deg (length cs)) (- (length cs) 1)) (cs->ms cs (+ deg 1) oct)
(cs->ms cs (+ deg 1) (+ oct 1)))))))
(let* ((sc (cons 0 (cs->ms cscale 0 0)))
(a440diff (- 69 (list-ref sc 69))))
(map scala:midi->hz (map (lambda (c) (+ c a440diff)) sc))))
;a handy utility to obtain the index in the scale closest to the value.
(define (scala:get-index lst value)
(define interval-distances
(lambda (lst ref-val)
(if (null? lst) '()
(cons (abs (- ref-val (car lst))) (interval-distances (cdr lst) ref-val)))))
(let ((ilst (interval-distances lst value)))
(cl:position (apply min ilst) ilst)))
;-------------------------------------------------------------------------------------------------------------
;Examples
;-------------------------------------------------------------------------------------------------------------
;return a list of 'real' midi values
;(define myscale (scala:cscale->mscale (scala:read-scale "/Users/... wherever .../meanhalf.scl")))
;return a list of hertz values
;(define hertzscale (scala:cscale->hzscale (scala:read-scale "/Users/... wherever .../meanhalf.scl")))
;check index 69 returns value 69 (A4 - 440hz)
;(list-ref myscale 69)
;check index position of A4 440 hz
;(scala:get-index hertzscale 440)