;; xmkit - a toolbox for parsing eXtended Modules
;; (c) 2019 Michael Neidel
;;
;; Permission is hereby granted, free of charge, to any person obtaining a copy
;; of this software and associated documentation files (the "Software"), to deal
;; in the Software without restriction, including without limitation the rights
;; to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
;; copies of the Software, and to permit persons to whom the Software is
;; furnished to do so, subject to the following conditions:
;;
;; The above copyright notice and this permission notice shall be included in
;; all copies or substantial portions of the Software.
;;
;; THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
;; IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
;; FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
;; AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
;; LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
;; OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
;; SOFTWARE.
;;; [[tags: egg]]
;;;
;;; == xmkit
;;;
;;; [[toc:]]
;;;
;;; === Description
;;;
;;; The xmkit egg provides a toolbox for extracting information from
;;; [[https://en.wikipedia.org/wiki/XM_(file_format)|eXtended Module (XM)]]
;;; files.
;;;
;;; === Requirements
;;;
;;; Those wishing to build the documentation files locally will need
;;; [[https://github.com/utz82/scm2wiki|scm2wiki]], and optionally
;;; [[manual-labor]].
;;;
;;; === Documentation
;;;
;;; ==== General Notes
;;;
;;; xmkit preserves some quirks of XM terminology when it comes to indices.
;;; Specifically, indices for pattern tracks and instruments start at 1, whereas
;;; everything else uses 0-based indexing.
;;;
;;; The order list, pattern list, or song structure is called {{sequence}} in
;;; xmkit.
;;;
(module xmkit
(xm:module?
xm:pattern?
xm:instrument?
xm:sample?
xm:u8vector->module
xm:file->module
xm:is-valid-xm-file?
xm:module-name
xm:tracker-name
xm:song-length
xm:restart-position
xm:number-of-tracks
xm:number-of-patterns
xm:number-of-instruments
xm:use-linear-frequency-table?
xm:default-tempo
xm:default-bpm
xm:sequence
xm:sequence-ref
xm:pattern-used?
xm:patterns
xm:pattern-ref
xm:pattern-length
xm:pattern-rows
xm:pattern-row-ref
xm:pattern-notes
xm:pattern-instruments
xm:pattern-volumes
xm:pattern-volumes-normalized
xm:pattern-volume-fx
xm:pattern-fx
xm:pattern-fx-cmds
xm:pattern-fx-params
xm:pattern-tracks
xm:pattern-track-ref
xm:pattern-track-notes
xm:pattern-track-instruments
xm:pattern-track-volumes
xm:pattern-track-volumes-normalized
xm:pattern-track-volume-fx
xm:pattern-track-fx
xm:pattern-track-fx-cmds
xm:pattern-track-fx-params
xm:instrument-name
xm:instrument-number-of-samples
xm:instruments
xm:instrument-ref
xm:instrument-sample-ref
xm:instrument-name
xm:instrument-number-of-samples
xm:instrument-has-samples?
xm:instrument-sample-map
xm:instrument-volume-type
xm:instrument-volume-envelope
xm:instrument-volume-env-length
xm:instrument-volume-env-on?
xm:instrument-volume-env-sustain?
xm:instrument-volume-env-loop?
xm:instrument-volume-sustain-point
xm:instrument-volume-loop-start
xm:instrument-volume-loop-end
xm:instrument-volume-fadeout
xm:instrument-panning-type
xm:instrument-panning-envelope
xm:instrument-panning-env-length
xm:instrument-panning-env-on?
xm:instrument-panning-env-sustain?
xm:instrument-panning-env-loop?
xm:instrument-panning-sustain-point
xm:instrument-panning-loop-start
xm:instrument-panning-loop-end
xm:instrument-vibrato-waveform
xm:instrument-vibrato-sine?
xm:instrument-vibrato-square?
xm:instrument-vibrato-saw?
xm:instrument-vibrato-inverse-saw?
xm:instrument-vibrato-depth
xm:instrument-vibrato-rate
xm:instrument-vibrato-sweep
xm:samples
xm:sample-ref
xm:sample-length
xm:sample-name
xm:sample-loop-start
xm:sample-loop-length
xm:sample-loop-type
xm:sample-loop-enabled?
xm:sample-loop-forward?
xm:sample-loop-ping-pong?
xm:sample-16bit-data?
xm:sample-volume
xm:sample-finetune
xm:sample-panning
xm:sample-relative-note
xm:sample->dpcm
xm:sample->pcm
xm:export-sample)
(import chicken scheme)
(use srfi-1 srfi-4 srfi-13 extras data-structures)
(include "definitions.scm")
;; All xm objects are internally treated as a single record type that is just
;; a thin wrapper around the binary content of a module/pattern/instrument
;; and a flag to distinguish the data type.
;; The constructor is inaccessible outside the xm Scheme module, to enforce
;; additional checks on construction.
(define-record-type xm:data
(xm:make-data type aux bytes)
xm:data?
(type xm:data-type)
(aux xm:data-aux)
(bytes xm:data-bytes))
(define-record-printer (xm:data data out)
(fprintf out "#<~s>" (xm:data-type data)))
;;;
;;; ==== Type Predicates
;;;
;; could short circuit this with an AND, but that would mean relying on
;; evaluation order which is not specified by Scheme standard
;;;
(define (xm:module? x)
(if (xm:data? x)
(eq? (xm:data-type x) 'xm:module)
#f))
;;;
(define (xm:pattern? x)
(if (xm:data? x)
(eq? (xm:data-type x) 'xm:pattern)
#f))
;;;
(define (xm:instrument? x)
(if (xm:data? x)
(eq? (xm:data-type x) 'xm:instrument)
#f))
;;;
(define (xm:sample? x)
(if (xm:data? x)
(eq? (xm:data-type x) 'xm:sample)
#f))
;; aliases for constructing xm:data records of various types from a u8vector
(define (xm:make-module u8v)
(xm:make-data 'xm:module #f u8v))
(define (xm:make-pattern tracks u8v)
(xm:make-data 'xm:pattern tracks u8v))
(define (xm:make-instrument u8v)
(xm:make-data 'xm:instrument #f u8v))
(define (xm:make-sample header u8v)
(xm:make-data 'xm:sample header u8v))
(define (xm:data-size xmdata)
(u8vector-length (xm:data-bytes xmdata)))
(define (xm:track-count pattern)
(xm:data-aux pattern))
(define (xm:sample-header sample)
(xm:data-aux sample))
;; check if all flags given in mask are set
(define (xm:flags-set? mask val)
(= mask (bitwise-and mask val)))
;; convert a flag to a 1 if set, else to a 0
(define (xm:flag->1 mask val)
(if (xm:flags-set? mask val)
1 0))
;; convert unsigned int represented in an arbitrary number of bits to its
;; signed counterpart, where posint-max is the largest positive number that
;; can be represented with the given bit width, eg. #x7f for u8, #x7fff for
;; u16
(define (xm:signed val posint-max)
(if (> val posint-max)
(- (+ 1 (bitwise-and posint-max (bitwise-not val))))
val))
;; read a signed byte from a u8vector
(define (xm:read-s8-raw u8v offset)
(xm:signed (u8vector-ref u8v offset) #x7f))
;; read a signed byte from an xm:data record
(define (xm:read-s8 xmdata offset)
(xm:signed (xm:read-u8 xmdata offset) #x7f))
;; read a signed word from an xm:data record
(define (xm:read-s16 xmdata offset)
(xm:signed (xm:read-u16 xmdata offset) #x7fff))
;; read an unsigned byte from an xm:data record
(define (xm:read-u8 xmdata offset)
(u8vector-ref (xm:data-bytes xmdata) offset))
;; read an unsigned word from a u8vector
(define (xm:read-u16-raw u8v offset)
(+ (u8vector-ref u8v offset)
(* 256 (u8vector-ref u8v (+ 1 offset)))))
;; read an unsigned word from an xm:data record
(define (xm:read-u16 xmdata offset)
(xm:read-u16-raw (xm:data-bytes xmdata) offset))
;; read an unsigned dword from a u8vector
(define (xm:read-u32-raw u8v offset)
(+ (xm:read-u16-raw u8v offset)
(* #x10000 (xm:read-u16-raw u8v (+ 2 offset)))))
;; read an unsigned dword from an xm:data record
(define (xm:read-u32 xmdata offset)
(xm:read-u32-raw (xm:data-bytes xmdata) offset))
;; drop the leading n-1 bytes from the given u8vector
(define (xm:drop-bytes u8v n)
(subu8vector u8v n (u8vector-length u8v)))
;; extract a string from a u8vector
(define (xm:read-string-raw u8v offset len)
(blob->string
(u8vector->blob
(list->u8vector
(filter (lambda (x)
(not (equal? x #x0)))
(u8vector->list (subu8vector u8v offset (+ offset len))))))))
;; read a string from an xm:data record
(define (xm:read-string xmdata offset len)
(xm:read-string-raw (xm:data-bytes xmdata)
offset len))
;;;
;;; ==== Module Related Procedures
;; Checks if the given {{xm}} record contains a well-formed eXtended Module.
(define (xm:is-valid-xm? xm)
(let ((check (lambda (thunk)
(condition-case (thunk)
[(exn) #f]
[() #t]))))
(and (= (xm:read-u8 xm 37) #x1a)
(= (xm:read-u16 xm xm:offset-version-number) xm:legal-version)
(string=? (xm:read-string xm 0 17) xm:magicbytes)
(check (lambda () (xm:patterns xm)))
(check (lambda () (xm:instruments xm))))))
;;; Determines whether the given file is a well-formed eXtended Module, by
;;; validating the header and performing integrity checks.
(define (xm:is-valid-xm-file? filename)
(xm:is-valid-xm? (xm:make-module (with-input-from-file filename
read-u8vector))))
;;; Constructs an xm:module record from a u8vector.
(define (xm:u8vector->module u8v)
(let ((mod (xm:make-module u8v)))
(if (xm:is-valid-xm? mod)
mod
(error "Not a valid eXtended Module"))))
;; return the header size, including the first 60 bytes
(define (xm:header-size xm)
(+ (xm:read-u32 xm xm:offset-header-size)
xm:offset-header-size))
;;; Construct an xm:module record from an .xm file
(define (xm:file->module filename)
(xm:u8vector->module (with-input-from-file filename read-u8vector)))
;;; Returns the module name.
(define (xm:module-name xm)
(xm:read-string xm xm:offset-module-name xm:module-name-length))
;;; Returns the tracker identifier string.
(define (xm:tracker-name xm)
(xm:read-string xm xm:offset-tracker-name xm:tracker-name-length))
;;; Returns the song (sequence) length.
(define (xm:song-length xm)
(xm:read-u8 xm xm:offset-song-length))
;;; Returns the restart position.
(define (xm:restart-position xm)
(xm:read-u8 xm xm:offset-restart-position))
;;; Returns the number of tracks (channels).
(define (xm:number-of-tracks xm)
(xm:read-u8 xm xm:offset-number-of-tracks))
;;; Returns the number of patterns.
(define (xm:number-of-patterns xm)
(xm:read-u8 xm xm:offset-number-of-patterns))
;;; Returns the number of instruments.
(define (xm:number-of-instruments xm)
(xm:read-u8 xm xm:offset-number-of-instruments))
;;; Check whether the module uses linear or Amiga frequencies. Returns true
;;; if using a linear table.
(define (xm:use-linear-frequency-table? xm)
(equal? 1 (xm:read-u8 xm xm:offset-frequency-table-flags)))
;;; Returns the default tempo.
(define (xm:default-tempo xm)
(xm:read-u8 xm xm:offset-default-tempo))
;;; Returns the default BPM.
(define (xm:default-bpm xm)
(xm:read-u8 xm xm:offset-default-bpm))
;;; Returns the sequence (order list) as a list.
(define (xm:sequence xm)
(u8vector->list
(subu8vector (xm:data-bytes xm)
xm:offset-sequence
(+ xm:offset-sequence (xm:song-length xm)))))
;;; Returns the position {{pos}} in the sequence of {{xm}}.
(define (xm:sequence-ref xm pos)
(if (< pos (xm:song-length xm))
(list-ref (xm:sequence xm) pos)
(error "Invalid sequence position")))
;;; Check if the given {{pattern}} is used in {{xm}}. A pattern is considered
;;; used if it is linked in the sequence at least once.
(define (xm:pattern-used? xm pattern)
(member pattern (xm:sequence xm)))
;;;
;;; ==== Pattern Related Procedures
;;;
;;; Extract the module's patterns. Returns a list of xm:pattern records.
(define (xm:patterns xm)
(letrec* ((pattern-size
(lambda (init-offset)
(+ (xm:read-u32 xm init-offset)
(xm:read-u16 xm (+ init-offset
xm:pattern-offset-packed-size)))))
(extract-patterns
(lambda (init-offset remaining)
(if (= 0 remaining)
'()
(let ((pattern-end (+ init-offset
(pattern-size init-offset))))
(cons (xm:make-pattern
(xm:number-of-tracks xm)
(subu8vector (xm:data-bytes xm)
init-offset pattern-end))
(extract-patterns pattern-end
(- remaining 1))))))))
(extract-patterns (xm:header-size xm)
(xm:number-of-patterns xm))))
(define (xm:pattern-ref xm i)
(if (< i (xm:number-of-patterns xm))
(list-ref (xm:patterns xm) i)
(error: "Pattern does not exist")))
;;; Returns the number of rows in the given pattern.
;;; Accepts either a raw pattern (xm:data) or unpacked pattern as input.
(define (xm:pattern-length pattern)
(if (xm:pattern? pattern)
(xm:read-u16 pattern xm:pattern-offset-rows)
(length pattern)))
;; return the size of the first packed track segment of the first packed
;; pattern row in the given pattern data block
(define (xm:packed-track-row-size data)
(let ((flag->1 (lambda (mask)
(xm:flag->1 mask (u8vector-ref data 0)))))
(if (= 0 (flag->1 xm:flag-packed-row))
xm:unpacked-row-size
(+ 1 (flag->1 xm:flag-note) (flag->1 xm:flag-instrument)
(flag->1 xm:flag-volume) (flag->1 xm:flag-fx-cmd)
(flag->1 xm:flag-fx-param)))))
;; return the size of the first packed pattern row in the given pattern data
;; block
(define (xm:packed-row-size data tracks)
(if (= 0 tracks)
0
(let ((next-ch-size (xm:packed-track-row-size data)))
(+ next-ch-size
(xm:packed-row-size (xm:drop-bytes data next-ch-size)
(- tracks 1))))))
;; determine offset of the given slot (note, instrument, volume, etc)
;; counting from the flag byte
(define (xm:packed-offset flag-byte slot)
(apply + (map (lambda (pos)
(xm:flag->1 pos flag-byte))
(take xm:flags-pattern-data (+ 1 slot)))))
;; auxiliary function for unpacking compressed pattern data
;; unpacks one pattern row from the given block of data that starts with a
;; note or flag byte
;; unset values are returned as #f, except for unset fx parameters after set
;; fx commands, which are normalized to 0.
(define (xm:unpack-track-row data)
(let ((flag-byte (u8vector-ref data 0)))
(if (xm:flags-set? xm:flag-packed-row flag-byte)
(let ((unpacked-row
(map (lambda (flag slot)
(if (xm:flags-set? flag flag-byte)
(u8vector-ref data
(xm:packed-offset flag-byte slot))
#f))
xm:flags-pattern-data xm:slots-pattern-data)))
(if (and (cadddr unpacked-row)
(not (list-ref unpacked-row 4)))
(append (take unpacked-row 4) '(0))
unpacked-row))
;; if row is unpacked, just copy bytes to resulting list
(u8vector->list (subu8vector data 0 xm:unpacked-row-size)))))
;; unpack a row of raw pattern data
(define (xm:unpack-row data tracks)
(if (= 0 tracks)
'()
(cons (xm:unpack-track-row data)
(xm:unpack-row
(xm:drop-bytes data (xm:packed-track-row-size data))
(- tracks 1)))))
;;; unpack the given {{pattern}} into a list of rows, where each row is a list
;;; containing values for note, instrument, volume, fx command, and fx param
(define (xm:pattern-rows pattern)
(letrec*
((tracks (xm:track-count pattern))
(unpack-rows
(lambda (init-offset)
(if (>= init-offset (sub1 (xm:data-size pattern)))
'()
(let ((data-block (xm:drop-bytes (xm:data-bytes pattern)
init-offset)))
(cons (xm:unpack-row data-block tracks)
(unpack-rows (+ init-offset
(xm:packed-row-size data-block
tracks)))))))))
;; dword at offset 0 specifies header size
(unpack-rows (xm:read-u32 pattern 0))))
;;; unpack the given {{pattern}} into a list of tracks, where each track is
;;; a list of rows containing note, instrument, volume, fx command, and fx
;;; parameter values
(define (xm:pattern-tracks pattern)
(let ((rows (xm:pattern-rows pattern)))
(map (lambda (track-ref)
(map (lambda (row)
(list-ref row track-ref))
rows))
(iota (xm:track-count pattern)))))
;;; Return the note values of the given {{pattern}}, sorted in rows.
(define (xm:pattern-notes pattern)
(map (lambda (row)
(map car row))
(xm:pattern-rows pattern)))
;;; Return the instrument values of the given {{pattern}}, sorted in rows.
(define (xm:pattern-instruments pattern)
(map (lambda (row)
(map cadr row))
(xm:pattern-rows pattern)))
;;; Return the volume values of the given {{pattern}}, sorted in rows.
(define (xm:pattern-volumes pattern)
(map (lambda (row)
(map caddr row))
(xm:pattern-rows pattern)))
;; return normalized vol if vol represents an actual volume change command,
;; else return #f
(define (xm:normalize-vol vol)
(if vol
(if (and (>= vol #x10)
(<= vol #x50))
(- vol #x10)
#f)
#f))
;; construct a filter predicate function from the given list of volume fx
;; commands
(define (xm:make-vfx-filter-pred fx-lst)
(if (null? fx-lst)
(lambda (v)
(if v
(if (>= v #x60) v #f)
#f))
(let ((filter-list
(map (lambda (fx)
(car (alist-ref fx xm:volume-fx)))
fx-lst)))
(lambda (v)
(if v
(if (member (bitwise-and v #xf0)
filter-list)
v #f)
#f)))))
;;; Return the volume values of the given {{pattern}}, sorted in rows. Volumes
;;; are normalized to the 0..#x40 range, and volume effects are discarded.
(define (xm:pattern-volumes-normalized pattern)
(map (lambda (row)
(map xm:normalize-vol row))
(xm:pattern-volumes pattern)))
;;; Return the volume effects in the given {{pattern}}, sorted in rows.
;;; Optionally, the output can be filtered to include only the given
;;; {{effects}}. {{effects}} can be any combination of '+x, '-x',
;;; Dx', Lx', 'Mx, 'Px, 'Rx, 'Sx, 'Ux, and 'Vx.
(define (xm:pattern-volume-fx pattern . effects)
(map (lambda (row)
(map (xm:make-vfx-filter-pred effects)
row))
(xm:pattern-volumes pattern)))
;; construct a filter predicate function from the given list of fx commands
(define (xm:make-fx-filter-pred fx-lst)
(if (null? fx-lst)
values
(let* ((make-filter-list
(lambda (fx-alist)
(map (lambda (f) (car (alist-ref f fx-alist)))
(filter (lambda (f) (member f (map car fx-alist)))
fx-lst))))
(regular-fx (make-filter-list xm:fx))
(ext-fx (make-filter-list xm:extended-fx))
(port-fx (make-filter-list xm:fine-port-fx)))
(lambda (cmd/param)
(if cmd/param
(if (or (member (car cmd/param) regular-fx)
(and (equal? #x0e (car cmd/param))
(member (bitwise-and #xf0 (cadr cmd/param))
ext-fx))
(and (equal? (car (alist-ref 'Xxx xm:fx))
(car cmd/param))
(member (bitwise-and #x30 (cadr cmd/param))
port-fx)))
cmd/param
'(#f #f))
#f)))))
;;; Return the effect command/parameter value pairs in the given {{pattern}}.
;;; The output can optionally be filtered to return only the given
;;; {{effects}}. For example,
;;;
;;; (xm:pattern-track-fx my-xm 0 1 '1xx '2xx '3xx)
;;; will only return portamento effects. All common effects (0xx, 1xx, ..
;;; Fxx) are supported, as well as the extended effects (E0x, E1x, .. EFx),
;;; and the fine portamento effects (X1x, X2x).
(define (xm:pattern-fx pattern . effects)
(let ((filter-pred (xm:make-fx-filter-pred effects)))
(map (lambda (row)
(map (lambda (track)
(filter-pred (cdddr track)))
row))
(xm:pattern-rows pattern))))
;;; Return the effect command values of the given {{pattern}}, sorted in rows.
;;; Optionally, the output can be filtered to only include the given
;;; {{effects}}. See xm:pattern-fx for details.
(define (xm:pattern-fx-cmds pattern . effects)
(map (lambda (row)
(map car row))
(apply xm:pattern-fx (cons pattern effects))))
;;; Return the effect parameters of the given {{pattern}}, sorted in rows.
;;; Optionally, the output can be filtered to only include the
;;; parameters of the given {{effects}}. For extended and fine porta effects,
;;; the effect subcommand is included in the output.
;;; See xm:pattern-fx for details.
(define (xm:pattern-fx-params pattern . effects)
(map (lambda (row)
(map cadr row))
(apply xm:pattern-fx (cons pattern effects))))
;;; Get row {{i}} of the given {{pattern}} as a nested list of track values.
(define (xm:pattern-row-ref pattern i)
(if (>= i (xm:pattern-length pattern))
(error "Row does not exist")
(list-ref (xm:pattern-rows pattern)
i)))
;;; Return track {{i}} of the {{pattern}} as a nested list of values.
;;; Note that track indices are 1-based, in line with XM terminology.
(define (xm:pattern-track-ref pattern i)
(if (or (> i (xm:track-count pattern))
(= i 0))
(error "Track does not exist")
(map (lambda (row)
(list-ref row (- i 1)))
(xm:pattern-rows pattern))))
;;; Extract the note column of track {{i}} in the given {{pattern}}.
(define (xm:pattern-track-notes pattern i)
(map car (xm:pattern-track-ref pattern i)))
;;; Extract the instrument column of track {{i}} in the given {{pattern}}.
(define (xm:pattern-track-instruments pattern i)
(map cadr (xm:pattern-track-ref pattern i)))
;;; Extract the volume column of track {{i}} in the given {{pattern}}.
(define (xm:pattern-track-volumes pattern i)
(map caddr (xm:pattern-track-ref pattern i)))
;;; Extract the volume column of track {{i}} in the given {{pattern}},
;;; and normalize volumes to the 0..#x40 range, omitting volume column fx.
(define (xm:pattern-track-volumes-normalized pattern i)
(map xm:normalize-vol (xm:pattern-track-volumes pattern i)))
;;; Extract the volume effects of track {{i}} in the given
;;; {{pattern}}. The output can optionally be filtered to return only the
;;; given {{effects}}. See xm:pattern-volume-fx for details.
(define (xm:pattern-track-volume-fx pattern i . effects)
(map (xm:make-vfx-filter-pred effects)
(xm:pattern-track-volumes pattern i)))
;;; Extract the effect command/parameter columns of track {{i}} in
;;; the given {{pattern}}. The output can optionally be filtered to return
;;; only the given {{effects}}. See xm:pattern-fx for details.
(define (xm:pattern-track-fx pattern i . effects)
(map (xm:make-fx-filter-pred effects)
(map cdddr (xm:pattern-track-ref pattern i))))
;;; Extract the effect command column of the given track {{i}} in the given
;;; {{pattern}}. The output can optionally be filtered to return on the
;;; {{effects}}. See xm:pattern-fx for details.
(define (xm:pattern-track-fx-cmds pattern i . effects)
(map car (apply xm:pattern-track-fx (append (list pattern i)
effects))))
;;; Extract the effect paramter column of track {{i}} in the given
;;; {{pattern}}. The output can optionally be filtered to return only the
;;; parameters of the given {{effects}}. For extended/fine porta effects, the
;;; effect subcommand is inlcuded in the output.
;;; See xm:pattern-fx for details.
(define (xm:pattern-track-fx-params pattern i . effects)
(map cadr (apply xm:pattern-track-fx (append (list pattern i)
effects))))
;;;
;;; ==== Instrument Related Procedures
;;;
;;; These procedures will generally return 0 or null if the given instrument
;;; has no samples.
;;;
(define (xm:instrument-header-size instr)
(xm:read-u32 instr xm:instr-offset-header-size))
;;; Returns the instrument name.
(define (xm:instrument-name instr)
(xm:read-string instr xm:instr-offset-name xm:instr-name-length))
;;; Returns the number of samples in the given instrument.
(define (xm:instrument-number-of-samples instr)
(xm:read-u16 instr xm:instr-offset-number-of-samples))
;;; Returns {{#t}} if the given instrument contains samples, else {{#f}}.
(define (xm:instrument-has-samples? instr)
(> (xm:instrument-number-of-samples instr) 0))
;; return the combined size of all sample headers.
(define (xm:instrument-sample-header-size instr)
(if (xm:instrument-has-samples? instr)
(* (xm:instrument-number-of-samples instr)
(xm:read-u32 instr xm:instr-offset-sample-headers-size))
0))
;;; Sample to note mapping for all notes. Returns a list.
(define (xm:instrument-sample-map instr)
(if (xm:instrument-has-samples? instr)
(u8vector->list
(subu8vector (xm:data-bytes instr) xm:instr-offset-sample-map
(+ xm:instr-offset-sample-map
xm:instr-sample-map-length)))
'()))
;;; Returns the length of the volume envelope.
(define (xm:instrument-volume-env-length instr)
(if (xm:instrument-has-samples? instr)
(xm:read-u8 instr xm:instr-offset-volume-env-length)
0))
;; convenience proc to convert volume/panning envelopes to offset/value pairs
(define (xm:raw-env->pt/val raw-env)
(if (= 0 (u8vector-length raw-env))
'()
(cons (list (xm:read-u16-raw raw-env 0)
(xm:read-u16-raw raw-env 2))
(xm:raw-env->pt/val (xm:drop-bytes raw-env 4)))))
;; call to xm:instrument-has-samples? may seem redundant, but is necessary
;; to prevent a potential out-of-range error from subu8vector
;;; Returns the volume envelope as a list of offset/value pairs.
(define (xm:instrument-volume-envelope instr)
(if (xm:instrument-has-samples? instr)
(xm:raw-env->pt/val
(subu8vector (xm:data-bytes instr) xm:instr-offset-volume-env
(+ xm:instr-offset-volume-env
(* 4 (xm:instrument-volume-env-length instr)))))
'()))
;;; Returns the volume envelope sustain point.
(define (xm:instrument-volume-sustain-point instr)
(if (xm:instrument-has-samples? instr)
(xm:read-u8 instr xm:instr-offset-volume-sustain-point)
0))
;;; Returns the volume envelope loop start point.
(define (xm:instrument-volume-loop-start instr)
(if (xm:instrument-has-samples? instr)
(xm:read-u8 instr xm:instr-offset-volume-loop-start-point)
0))
;;; Returns the volume envelope loop end point.
(define (xm:instrument-volume-loop-end instr)
(if (xm:instrument-has-samples? instr)
(xm:read-u8 instr xm:instr-offset-volume-loop-end-point)
0))
;;; Returns the volume envelope configuration byte.
(define (xm:instrument-volume-type instr)
(if (xm:instrument-has-samples? instr)
(xm:read-u8 instr xm:instr-offset-volume-type)
0))
;;; Returns {{#t}} if the volume envelope is enabled.
(define (xm:instrument-volume-env-on? instr)
(xm:flags-set? xm:flag-env-on (xm:instrument-volume-type instr)))
;;; Returns {{#t}} if volume envelope sustain is enabled.
(define (xm:instrument-volume-env-sustain? instr)
(xm:flags-set? xm:flag-env-sustain (xm:instrument-volume-type instr)))
;;; Returns {{#t}} if volume envelope looping is enabled.
(define (xm:instrument-volume-env-loop? instr)
(xm:flags-set? xm:flag-env-loop (xm:instrument-volume-type instr)))
;;; Returns the instrument volume fadeout setting.
(define (xm:instrument-volume-fadeout instr)
(if (xm:instrument-has-samples? instr)
(xm:read-u16 instr xm:instr-offset-volume-fadeout)
0))
;;; Returns the length of the panning envelope.
(define (xm:instrument-panning-env-length instr)
(if (xm:instrument-has-samples? instr)
(xm:read-u8 instr xm:instr-offset-panning-env-length)
0))
;;; Returns the panning envelope as a list of offset/value pairs.
(define (xm:instrument-panning-envelope instr)
(if (xm:instrument-has-samples? instr)
(xm:raw-env->pt/val
(subu8vector (xm:data-bytes instr) xm:instr-offset-panning-env
(+ xm:instr-offset-panning-env
(* 4 (xm:instrument-panning-env-length instr)))))
'()))
;;; Returns the panning envelope sustain point.
(define (xm:instrument-panning-sustain-point instr)
(if (xm:instrument-has-samples? instr)
(xm:read-u8 instr xm:instr-offset-panning-sustain-point)
0))
;;; Returns the panning envelope loop start point.
(define (xm:instrument-panning-loop-start instr)
(if (xm:instrument-has-samples? instr)
(xm:read-u8 instr xm:instr-offset-panning-loop-start-point)
0))
;;; Returns the panning envelope loop end point.
(define (xm:instrument-panning-loop-end instr)
(if (xm:instrument-has-samples? instr)
(xm:read-u8 instr xm:instr-offset-panning-loop-end-point)
0))
;;; Returns the panning envelope configuration byte.
(define (xm:instrument-panning-type instr)
(if (xm:instrument-has-samples? instr)
(xm:read-u8 instr xm:instr-offset-panning-type)
0))
;;; Returns {{#t}} if the panning envelope is enabled.
(define (xm:instrument-panning-env-on? instr)
(xm:flags-set? xm:flag-env-on (xm:instrument-panning-type instr)))
;;; Returns {{#t}} if panning envelope sustain is enabled.
(define (xm:instrument-panning-env-sustain? instr)
(xm:flags-set? xm:flag-env-sustain (xm:instrument-panning-type instr)))
;;; Returns {{#t}} if panning envelope looping is enabled.
(define (xm:instrument-panning-env-loop? instr)
(xm:flags-set? xm:flag-env-loop (xm:instrument-panning-type instr)))
;; Returns the instrument vibrato waveform byte
(define (xm:instrument-vibrato-waveform instr)
(if (xm:instrument-has-samples? instr)
(xm:read-u8 instr xm:instr-offset-vibrato-type)
0))
;;; Returns {{#t}} if using sine waveform vibrato
(define (xm:instrument-vibrato-sine? instr)
(= xm:instr-vibrato-sine (xm:instrument-vibrato-waveform instr)))
;;; Returns {{#t}} if using square waveform vibrato
(define (xm:instrument-vibrato-square? instr)
(= xm:instr-vibrato-square (xm:instrument-vibrato-waveform instr)))
;;; Returns {{#t}} if using saw waveform vibrato
(define (xm:instrument-vibrato-saw? instr)
(= xm:instr-vibrato-saw (xm:instrument-vibrato-waveform instr)))
;;; Returns {{#t}} if using inverse saw waveform vibrato
(define (xm:instrument-vibrato-inverse-saw? instr)
(= xm:instr-vibrato-inverse-saw (xm:instrument-vibrato-waveform instr)))
;;; Returns the instrument vibrato depth.
(define (xm:instrument-vibrato-depth instr)
(if (xm:instrument-has-samples? instr)
(xm:read-u8 instr xm:instr-offset-vibrato-depth)
0))
;;; Returns the instrument vibrato rate.
(define (xm:instrument-vibrato-rate instr)
(if (xm:instrument-has-samples? instr)
(xm:read-u8 instr xm:instr-offset-vibrato-rate)
0))
;;; Returns the instrument vibrato sweep setting.
(define (xm:instrument-vibrato-sweep instr)
(if (xm:instrument-has-samples? instr)
(xm:read-u8 instr xm:instr-offset-vibrato-sweep)
0))
;; returns all of the instrument's sample headers as a list of u8vectors
;; will fail if instrument does not contain any sample headers
(define (xm:instrument-sample-headers instr)
(letrec* ((sample-header-size
(xm:read-u32 instr xm:instr-offset-sample-headers-size))
(extract-sample-headers
(lambda (init-offset remaining)
(if (= 0 remaining)
'()
(cons (subu8vector (xm:data-bytes instr) init-offset
(+ init-offset sample-header-size))
(extract-sample-headers
(+ init-offset sample-header-size)
(- remaining 1)))))))
(extract-sample-headers (xm:instrument-header-size instr)
(xm:instrument-number-of-samples instr))))
;; not a lazy check of xm:data-size, so it works for proto-instruments
(define (xm:raw-instrument-size instr)
(if (xm:instrument-has-samples? instr)
(+ (xm:instrument-header-size instr)
(xm:instrument-sample-header-size instr)
(apply + (map (lambda (header)
(xm:sample-data-size header))
(xm:instrument-sample-headers instr))))
(xm:instrument-header-size instr)))
;; determine the start of the instrument data
(define (xm:instrument-block-offset xm)
(+ (xm:header-size xm)
(apply + (map xm:data-size (xm:patterns xm)))))
;;; Returns a list of raw instrument data blocks
(define (xm:instruments xm)
(letrec
((extract-instruments
(lambda (init-offset remaining)
(if (= 0 remaining)
'()
(let* ((proto-instr
(xm:make-instrument (xm:drop-bytes (xm:data-bytes xm)
init-offset)))
(instr
(xm:make-instrument
(subu8vector (xm:data-bytes proto-instr)
0 (xm:raw-instrument-size proto-instr)))))
(cons instr
(extract-instruments
(+ init-offset (xm:data-size instr))
(- remaining 1))))))))
(extract-instruments (xm:instrument-block-offset xm)
(xm:number-of-instruments xm))))
;;; Returns the instrument at the given index {{i}}. This uses 1-based
;;; indexing, in line with the way indexing is done in XM.
(define (xm:instrument-ref xm i)
(let ((instruments (xm:instruments xm)))
(if (> i (length instruments))
#f
(list-ref instruments (- i 1)))))
;;;
;;; ==== Sample Related Procedures
;;;
;;; Returns the sample at index {{smp}} of the instrument at index {{instr}}.
;;; {{instr}} uses 1-based indexing, in line with the way indexing is done in
;;; XM.
(define (xm:instrument-sample-ref xm instr smp)
(let ((instrument (xm:instrument-ref xm instr)))
(if instrument
(xm:sample-ref instrument smp)
#f)))
;;; Returns the number of sample points, not the number of bytes.
(define (xm:sample-length sample)
(let ((raw-length (xm:read-u32-raw (xm:sample-header sample)
xm:sample-offset-length)))
(if (xm:sample-16bit-data? sample)
(/ raw-length 2)
raw-length)))
;;; Returns the sample name.
(define (xm:sample-name sample)
(xm:read-string-raw (xm:sample-header sample)
xm:sample-offset-name xm:sample-name-length))
;;; Returns the sample loop start position.
(define (xm:sample-loop-start sample)
(xm:read-u32-raw (xm:sample-header sample)
xm:sample-offset-loop-start))
;;; Returns the sample loop length.
(define (xm:sample-loop-length sample)
(xm:read-u32-raw (xm:sample-header sample)
xm:sample-offset-loop-length))
;;; Returns the sample loop type byte.
(define (xm:sample-loop-type sample)
(u8vector-ref (xm:sample-header sample)
xm:sample-offset-loop-type))
;;; Returns {{#t}} if sample looping is enabled.
(define (xm:sample-loop-enabled? sample)
(let ((loop-type (xm:sample-loop-type sample)))
(or (xm:flags-set? xm:flag-sample-loop-forward loop-type)
(xm:flags-set? xm:flag-sample-loop-ping-pong loop-type))))
;;; Returns {{#t}} if using forward type looping. Correctly handles "invalid"
;;; flag settings produced by MPT 1.09.
(define (xm:sample-loop-forward? sample)
(let ((loop-type (xm:sample-loop-type sample)))
(if (xm:flags-set? (bitwise-ior xm:flag-sample-loop-forward
xm:flag-sample-loop-ping-pong)
loop-type)
#f
(xm:flags-set? xm:flag-sample-loop-forward loop-type))))
;;; Returns {{#t}} if using ping-pong type looping.
(define (xm:sample-loop-ping-pong? sample)
(xm:flags-set? xm:flag-sample-loop-ping-pong (xm:sample-loop-type sample)))
;;; Returns {{#t}} if the sample data uses 16-bit values.
(define (xm:sample-16bit-data? sample)
(xm:flags-set? xm:flag-16bit-sample (xm:sample-loop-type sample)))
;;; Returns the volume setting.
(define (xm:sample-volume sample)
(u8vector-ref (xm:sample-header sample)
xm:sample-offset-volume))
;;; Returns the finetune setting.
(define (xm:sample-finetune sample)
(xm:read-s8-raw (xm:sample-header sample)
xm:sample-offset-finetune))
;;; Returns the panning position.
(define (xm:sample-panning sample)
(u8vector-ref (xm:sample-header sample)
xm:sample-offset-panning))
;;; Returns the relative note setting.
(define (xm:sample-relative-note sample)
(xm:read-s8-raw (xm:sample-header sample)
xm:sample-offset-relative-note))
;; Determine the size of the raw sample data. This function expects a
;; sample header (plain u8vector) as input.
(define (xm:sample-data-size header)
(xm:read-u32-raw header xm:sample-offset-length))
;;; Returns a list of the given instrument's samples, preserving the original
;;; order.
(define (xm:samples instr)
(letrec
((extract-samples
(lambda (init-offset headers)
(if (null? headers)
'()
(let ((next-offset
(+ init-offset (xm:sample-data-size (car headers)))))
(cons (xm:make-sample (car headers)
(subu8vector (xm:data-bytes instr)
init-offset next-offset))
(extract-samples next-offset (cdr headers))))))))
(extract-samples (+ (xm:instrument-header-size instr)
(xm:instrument-sample-header-size instr))
(xm:instrument-sample-headers instr))))
;;; Returns the sample at index {{i}}. This uses 1-based indexing, in line
;;; with the way indexing is done in XM.
(define (xm:sample-ref instr i)
(let ((samples (xm:samples instr)))
(if (>= i (length samples))
#f
(list-ref samples i))))
;;; Retrieves the raw XM sample data in internal DPCM format.
(define (xm:sample->dpcm sample)
(letrec* ((is-16bit? (xm:sample-16bit-data? sample))
(extract-sample-data
(lambda (init-offset)
(if (>= init-offset (xm:data-size sample))
'()
(if is-16bit?
(cons (xm:read-s16 sample init-offset)
(extract-sample-data (+ 2 init-offset)))
(cons (xm:read-s8 sample init-offset)
(extract-sample-data (+ 1 init-offset))))))))
(extract-sample-data 0)))
;;; Retrieves the internal sample data and convert it to standard RAW PCM.
(define (xm:sample->pcm sample)
(letrec* ((dpcm->pcm
(lambda (dpcm prev-val)
(if (null? dpcm)
'()
(let ((next-val
(xm:signed (+ prev-val (car dpcm))
(if (xm:sample-16bit-data? sample)
#x7fff #x7f))))
(cons next-val (dpcm->pcm (cdr dpcm) next-val)))))))
(dpcm->pcm (xm:sample->dpcm sample) 0)))
;;; Extract the sample data of given {{sample}} and export as a
;;; little-endian, signed, mono PCM RAW file with 8-bit or 16-bit data
;;; depending on input sample data type.
(define (xm:export-sample sample filename)
(let* ((pcm (xm:sample->pcm sample))
(bytes (if (xm:sample-16bit-data? sample)
(concatenate
(map (lambda (word)
(list (bitwise-and #xff word)
(/ (bitwise-and #xff00 word) #x100)))
pcm))
pcm)))
(call-with-output-file filename
(lambda (port)
(for-each (lambda (byte)
(write-byte byte port))
bytes)))))
) ;; end module
;;;
;;; ==== Examples
;;;
;;; Extract the notes of pattern 1 to a list of rows
;;;
;;; (xm:pattern-notes (xm:pattern-ref (xm:file->module "myxm.xm") 1))
;;;
;;;
;;; Export sample 0 in instrument 2 to a RAW PCM file
;;;
;;; (xm:export-sample (xm:instrument-sample-ref (xm:file->module "myxm.xm")
;;; 2 0)
;;; "my-sample.raw")
;;;
;;; ==== Author
;;;
;;; (c) 2019 Michael Neidel
;;;
;;; ==== License
;;;
;;; MIT
;;;
;;; ==== Version History
;;;
;;; * 0.1.0 Initial Release
;;;