;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)