| 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
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249 |
7×
7×
1×
6×
7×
4×
2×
2×
2×
8×
8×
8×
15×
1×
14×
1×
13×
1×
12×
10×
3×
7×
1×
1×
1×
8×
8×
8×
8×
8×
8×
8×
8×
8×
8×
8×
13×
13×
13×
13×
11×
11×
11×
1×
11×
2×
2×
2×
1×
2×
13×
| /** @module */
/* exported AudioChart */
/* global getAudioContext FrequencyPitchMapper Sounder Player KeyboardHandler GoogleDataWrapper googleVisualCallbackMaker JSONDataWrapper HTMLTableDataWrapper htmlTableVisualCallbackMaker C3DataWrapper c3VisualCallbackMaker */
/**
* Array index number (starts at zero).
* Used to specify series and row in visual callbacks.
* @typedef {integer} index
*/
/**
* A function that highlights the current datum, in all series, visually.
* Different callbacks must be created for different types of chart.
* @callback VisualCallback
* @param {index} row - The row of the cell to highlight
*/
/**
* @typedef {Object} AudioChartOptions
* @todo move the documentation here? (Downside is that the branching/groups
* of different options required for different chart types would be less
* clear.)
*/
/**
* @typedef {Object} WrapperAndCallbackResults
* @property {Class} WrapperClass - the data wrapper class
* @property {Object|HTMLTableElement} dataSource - the data source to wrap
* @property {VisualCallback} visualCallback - created if requested by the user
*/
/** Main object for API consumers */
class AudioChart {
/**
* Create an AudioChart object.
* This first checks to see if the Web Audio API is available, and throws
* an {Error} if not. Then check the options given by the user.
* @param {AudioChartOptions} options - AudioChart options
*/
constructor(options) {
const context = getAudioContext()
if (context === null) {
throw Error(
"Sorry, your browser doesn't support the Web Audio API.")
}
this._setUp(context, options)
}
/**
* Passes through play/pause commands to the Player
*/
playPause() {
this.player.playPause()
}
/**
* Returns the current set of options (passed in at object creation, or
* computed when options were updated).
*/
get options() {
return this._options
}
/**
* Updates an AudioChart object to reflect new options. Can accept a subset
* of the standard options, so if only, for example, duration changes, then
* you need only specify the new duration and not the type and other
* paramaters.
* @param {AudioChartOptions} newOptions - Partial/full AudioChart options
*/
updateOptions(newOptions) {
if (newOptions === undefined || Object.keys(newOptions).length === 0) {
throw Error('No new options given')
}
const patchedOptions = Object.assign({}, this._options, newOptions)
this._setUp(getAudioContext(), patchedOptions)
}
/**
* Checks options (either when a new AudioChart object is created, or when
* the user has asked for them to be updated) and then set everything up.
* @param {AudioContext} context - the Web Audio context
* @param {AudioChartOptions} options - AudioChart options
* @private
*/
_setUp(context, options) {
// The testing for this next bit is a bit of a fudge as curerntly I've
// not come up with a beter way than having the testing done on static
// functions and checking that they've been called with appropraite
// values, return appropriate values, or if they throw an exception.
//
// The thing blocking this is that I don't know how to stub out global
// ES6 classes/functions *or* how to run each test via Karma in an
// isolated environment where I can mock those global classes.
//
// TODO, as per https://github.com/matatk/audiochart/issues/37
// Testing this separately allows us to check the options-checking code
// without having to pass in functioning source data objects.
AudioChart._checkOptions(options)
// This is not currently tested; to mitigate, it doesn't make decisions.
// Actually, it does call _assignWrapperCallback() but that /is/ tested.
this._wireUpStuff(context, options)
// Re _assignWrapperCallback(): that is also tested separately to avoid
// the need for very detailed mocks for the data sources.
this._options = Object.freeze(options)
}
/**
* Checks the passed-in options opbject for validity. This does not perform
* detailed checks that are covered in the various components' constructors;
* they run such checks themselves.
* @param {AudioChartOptions} options - AudioChart options
* @private
*/
static _checkOptions(options) {
if (!options.hasOwnProperty('duration')) {
throw Error('No duration given')
}
if (!options.hasOwnProperty('frequencyLow')) {
throw Error('No minimum frequency given')
}
if (!options.hasOwnProperty('frequencyHigh')) {
throw Error('No maximum frequency given')
}
switch (options.type) {
case 'google':
case 'c3':
case 'json':
if (!options.hasOwnProperty('data')) {
throw Error("Options must include a 'data' key")
}
break
case 'htmlTable':
Eif (!options.hasOwnProperty('table')) {
throw Error("Options must include a 'table' key")
}
break
default:
throw Error(`Invalid data type '${options.type}' given.`)
}
}
/**
* Make a data wrapper of the appropriate class, instantiated with the
* appropriate data paramater (from options.data) and, optionally, make
* a visual callback (which may use options.chart, or other options if
* it's an HTML table visual callback).
* FIXME: Test somehow
* @private
* @param {AudioContext} context - the Web Audio context
* @param {AudioChartOptions} options - given by the user
*/
_wireUpStuff(context, options) {
const assigned = AudioChart._assignWrapperCallback(options)
const dataWrapper = new assigned.WrapperClass(assigned.dataSource)
const callback = assigned.visualCallback
const numberOfSeries = dataWrapper.numSeries()
const seriesInfo = []
for (let i = 0; i < numberOfSeries; i++ ) {
seriesInfo.push({
minimumDatum: dataWrapper.seriesMin(i),
maximumDatum: dataWrapper.seriesMax(i),
minimumFrequency: options.frequencyLow,
maximumFrequency: options.frequencyHigh
})
}
const frequencyPitchMapper = new FrequencyPitchMapper(seriesInfo)
const sounder = new Sounder(context, numberOfSeries)
this.player = new Player(
options.duration,
dataWrapper,
frequencyPitchMapper,
sounder,
callback)
Iif (options.chartContainer) {
new KeyboardHandler(
options.chartContainer,
this.player)
}
}
/**
* Works out which data source wrapper and visual callback (if requested)
* should be used with this chart.
* @param {AudioChartOptions} options - given by the user
* @returns {WrapperAndCallbackResults}
* - data wrapper, data source and callback (if applicable) for this chart
* @private
*/
static _assignWrapperCallback(options) {
const result = {
'WrapperClass': null,
'dataSource': null,
'visualCallback': null
}
const chartTypeToWrapperClass = {
google: GoogleDataWrapper,
json: JSONDataWrapper,
htmlTable: HTMLTableDataWrapper,
c3: C3DataWrapper
}
const chartTypeToVisualCallbackMaker = {
google: googleVisualCallbackMaker,
c3: c3VisualCallbackMaker
}
switch (options.type) {
case 'google':
case 'json':
case 'c3':
result.WrapperClass = chartTypeToWrapperClass[options.type]
result.dataSource = options.data
if (options.hasOwnProperty('chart')) {
result.visualCallback =
chartTypeToVisualCallbackMaker[options.type](
options.chart)
}
break
case 'htmlTable':
result.WrapperClass = HTMLTableDataWrapper
result.dataSource = options.table
if (options.hasOwnProperty('highlightClass')) {
result.visualCallback = htmlTableVisualCallbackMaker(
options.table,
options.highlightClass)
}
break
}
return result
}
}
|