aboutsummaryrefslogtreecommitdiff
path: root/wav.bqn
blob: 0355af2f429493d76289d750cd3f1654b92486e5 (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
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
# Functions to read to and write from wave files.
# Does not support many kinds of wave files, such as compressed data.

Read, Write, Read_set, Read_coerce

"wav.bqn takes a single option namespace, or no arguments" ! 1≥≠•args
o  •Import"options.bqn",  •args

# The output from Read, or input to Write, is either:
# - A list of:
#     The sample rate (in Hz)
#     The PCM format (see below)
#     PCM data, which has shape n‿l for n channels with l samples.
# - The PCM data only
#   (options.freq and options.fmt are used for rate and format)
# 
# Read returns the plain PCM data if the settings matched the
# options while Read_set and Read_coerce always return the
# plain data.

# A PCM format consists of the type of audio and the bit depth.
# The type is one of:
#   1  unsigned integer
#   3  floating point
# Other audio formats may be supported in the future.

# Wave file header format
wh  {
  # Field properties are:
  #   len:  Length of field in bytes
  #   typ:  Whether to leave as chars (c) or convert to an integer (i)
  #   err:  Behavior on invalid value: fail (e), warn (w), or ignore (.)
  #         (Fields with ? depend on context)
  #   name: Field name
  #   def:  How to compute value
  lentyperrnamedef  <˘⍉>
    4'c''e'"chunkID"      ‿⟨"RIFF"
    4'i''w'"chunkSize"    ‿⟨20++´,"subchunk1Size","subchunk2Size"
    4'c''e'"format"       ‿⟨"WAVE"
    4'c''e'"subchunk1ID"  ‿⟨"fmt "
    4'i''?'"subchunk1Size"‿⟨16
    2'i''.'"audioFormat"  ‿⟨⟩
    2'i''.'"numChannels"  ‿⟨⟩
    4'i''.'"sampleRate"   ‿⟨⟩
    4'i''w'"byteRate"     ‿⟨×´÷8˙,"sampleRate","numChannels","bitsPerSample"
    2'i''w'"blockAlign"   ‿⟨×´÷8˙,"numChannels","bitsPerSample"
    2'i''.'"bitsPerSample"‿⟨⟩
    4'c''?'"subchunk2ID"  ‿⟨"data"
    4'i''.'"subchunk2Size"‿⟨⟩
  
  def  name(1)(1<≠)¨ def

  # Topological order for field definitions
  order  {{𝕊(l>≠⊢)(𝕩·/𝕨<)𝕨∨∧´𝕨¨l}/0¨l𝕩} 1¨def

  # Then fill blank definitions with a self-reference
  def  ({𝕨(0=≠)𝕩}¨) def

  # Turn list of values into namespace
  makeNS  •BQN "{"(1↓∾"‿"¨name)"←𝕩}"
}

# Return an undoable (⁼) function to convert bytes to PCM data.
_audioConvert  {
  audioFormatbitsPerSample  𝕗
  "Bits per sample must be a multiple of 8" ! 0=8|bitsPerSample
  "Bits per sample cannot exceed 64" ! bitsPerSample  64
  l  bitsPerSample÷8
  _withInv_  {F _𝕣_ G: {𝕊:F𝕩 ; 𝕊:G𝕩}}
  # Convert 𝕗-byte sequences to ints
  _int  {
    b  256
    (+(b×)˝˜(-(b÷2)¯1)·⍉⌊𝕗⥊⊢) _withInv_ (>b|⌊÷b(𝕗))
  }
  # Convert int to float
  _float  {emb𝕗  # exponent and mantissa length in bits; bias
    maxval(1-2⋆-m+1)×2(2e)-b+1
    {
      𝕩 × 2⋆-m
      ps  (2e) (| < ÷˜) 𝕩
      p + ¬n0<p
      (¯1s)×(2p-b) × n+1|𝕩
    }_withInv_{
      s𝕩<0  𝕩maxval⌊|𝕩
      p0b+⌊2𝕩  hp+s×2e
      p + ¬n0<p
      0.5 + (2m) × h + n-˜𝕩×2b-p
    }
  }
  # Look up the appropriate function
  {
  1:
    l _int -@ ;
  3:
    "Float formats other than 32-bit are not supported" ! 4=l
    823127 _float ·4 _int -@ ;
  𝕩:
    0 !˜ "Unsupported audio format: "  •Repr audioFormat
  }audioFormat
}

# 𝕨 is audioFormat‿bitsPerSample.
# Force 𝕩 to fit in format 𝕨, emitting approprate warnings.
ForceFormat  { fb 𝕊 pcm:
  Dither  {
    •Outo.warn_dither "Signal is non-integral; dithering..."
    o.Dither 𝕩
  }
  Clip  {
    •Outo.warn_clip "Signal out of bounds; clipping..."
    max  min  𝕩
  }
  minmax  (-≍-1) 2b-1
  (Clip(min> (´) >max) Dither)(f=1) pcm
}

Decode  {
  If  {𝕏𝕎@}´
  While  {𝕨{𝕊𝔾𝔽𝕩}𝕩@}´
  # Integer from little-endian unsigned bytes
  ToInt  256×+˜´ -@

  hdrdat  wh.len +´( < ) 𝕩

  # Assign field values to field names.
  hdr  ('i'=wh.typ) ToInt¨ wh.len / hdr
  subchunk1Sizesubchunk2Sizesubchunk2IDaudioFormatbitsPerSamplesampleRatenumChannels  wh.MakeNS hdr
  # Handle extensible format
  "subchunk1Size is invalid" ! 0224 ˜ sesubchunk1Size-16
  If (se>0){𝕤
    ! se = 2 + ToInt 2subchunk2ID
    ext@  extdat  se ( < ) dat
    If (se>2){𝕤
      If (audioFormat = 65534){𝕤 audioFormat  ToInt 4ext }
    }
  }
  # Ignore remaining subchunks
  s  subchunk2Size
  While {𝕤"data"subchunk2ID}{𝕤
    subchunk2IDsdat  (4 < ToInt((4+↕4)) < 8) s  dat
    subchunk2Size + s+8
  }
  # Check that fields match their definitions
  e  hdr ({𝕎𝕩hdr}1)¨ wh.def
  Msg  "Values for fields "  (∾∾" "¨)  "are incorrect"˙
  _alert  {(𝔽Msg /(wh.name))(´) e  wh.err}
  !0 _alert "e"(se<0)/"?"
  (•Out "Warning: "∾⊢) _alert "w"

  fmt  audioFormatbitsPerSample
  Cvt  fmt _audioConvert
  sampleRate, fmt,  numChannels  Cvt(Cvt subchunk2Size) dat
}

Encode  { ratefmtpcm:
  ! 2  =pcm
  pcm 0˜ 2
  dat  fmt _audioConvert⁼ fmt ForceFormat ⥊⍉pcm
  inameival  <˘2
    "sampleRate"   , rate
    "numChannels"  , pcm
    "subchunk2Size", dat
    "audioFormat"  , 0fmt
    "bitsPerSample", 1fmt
  
  val  def  (<¨ival)((wh.nameiname)) wh.def

  { val  ({𝕎𝕩val}1)(𝕩) val }¨ wh.order

  hdr   (wh.len×wh.typ='i') 256{@+𝕗|⌊÷𝕗(𝕨)𝕩}(>0)¨ val
  hdr  dat
}

ReadFull  Decode{ o.FBytes 𝕩 }
Read   { ¯1(o.freq,o.fmt  2) ReadFull 𝕩 }
Write  { 𝕩 o.FBytes Encode(o.freq,o.fmt∾<)(1≥≡) 𝕨 }

# Read, setting o.freq and o.fmt as a side effect.
Read_set  { tReadFull 𝕩  o.Set ¯1t  ¯1t }

# Read, resampling to fit current frequency, using options.Resample
Read_coerce  { ((o.freq˜0) o.Resample ¯1) ReadFull 𝕩 }