summaryrefslogtreecommitdiff
path: root/chickadee/audio/wav.scm
blob: d5db1cbd84a9eb208da86a0bfb95459c87a1d5e8 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
;;; Chickadee Game Toolkit
;;; Copyright © 2019 David Thompson <dthompson2@worcester.edu>
;;;
;;; Licensed under the Apache License, Version 2.0 (the "License");
;;; you may not use this file except in compliance with the License.
;;; You may obtain a copy of the License at
;;;
;;;    http://www.apache.org/licenses/LICENSE-2.0
;;;
;;; Unless required by applicable law or agreed to in writing, software
;;; distributed under the License is distributed on an "AS IS" BASIS,
;;; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
;;; See the License for the specific language governing permissions and
;;; limitations under the License.

;;; Commentary:
;;
;; WAV decoder.
;;
;;; Code:

(define-module (chickadee audio wav)
  #:use-module (ice-9 binary-ports)
  #:use-module (ice-9 format)
  #:use-module (rnrs bytevectors)
  #:use-module (srfi srfi-9)
  #:use-module (srfi srfi-9 gnu)
  #:export (open-wav
            close-wav
            wav?
            wav-num-channels
            wav-sample-rate
            wav-byte-rate
            wav-bits-per-sample
            wav-length
            wav-read
            wav-time-seek))

(define-record-type <wav>
  (make-wav num-channels sample-rate byte-rate
            block-align bits-per-sample length port)
  wav?
  (num-channels wav-num-channels)
  (sample-rate wav-sample-rate)
  (byte-rate wav-byte-rate)
  (block-align wav-block-align)
  (bits-per-sample wav-bits-per-sample)
  (length wav-length)
  (port wav-port))

(define (display-wav wav port)
  (format port "#<wav num-channels: ~d sample-rate: ~d byte-rate: ~d block-align: ~d bits-per-sample: ~d length: ~d>"
          (wav-num-channels wav)
          (wav-sample-rate wav)
          (wav-byte-rate wav)
          (wav-block-align wav)
          (wav-bits-per-sample wav)
          (wav-length wav)))

(set-record-type-printer! <wav> display-wav)

(define (open-wav file-name)
  (let* ((port (open-file file-name "r"))
         (bv (get-bytevector-n port 44))
         (data-length (bytevector-uint-ref bv 40 'little 4)))
    ;; Validate the magic values that must be present in the header.
    (unless (and (= (bytevector-uint-ref bv 0 'big 4) #x52494646) ; RIFF
                 (= (bytevector-uint-ref bv 8 'big 4) #x57415645) ; WAVE
                 (= (bytevector-uint-ref bv 12 'big 4) #x666d7420) ; fmt
                 (= (bytevector-uint-ref bv 36 'big 4) #x64617461)) ; data
      (error "invalid WAV header in file:" file-name))
    ;; Only PCM data is supported.
    (unless (= (bytevector-uint-ref bv 20 'little 2) 1)
      (error "WAV data not in PCM format:" file-name))
    (make-wav (bytevector-uint-ref bv 22 'little 2) ; num channels
              (bytevector-uint-ref bv 24 'little 4) ; sample rate
              (bytevector-uint-ref bv 28 'little 4) ; byte rate
              (bytevector-uint-ref bv 32 'little 2) ; block align
              (bytevector-uint-ref bv 34 'little 2) ; bits per sample
              data-length
              port)))

(define (close-wav wav)
  "Close the open file port for WAV."
  (close-port (wav-port wav)))

(define (wav-read wav bv)
  "Fill BV with audio samples from WAV and return the number of bytes
read."
  (let ((bytes-read (get-bytevector-n! (wav-port wav) bv 0 (bytevector-length bv))))
    (if (eof-object? bytes-read) 0 bytes-read)))

(define (wav-time-seek wav time)
  "Change the play head to TIME in WAV."
  (seek (wav-port wav)
        (+ 44 (inexact->exact
               (ceiling
                (* time
                   (wav-num-channels wav)
                   (wav-byte-rate wav)
                   (wav-sample-rate wav)))))
        SEEK_SET))