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