jspsych 7.3.4 → 8.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/README.md +1 -1
  2. package/css/jspsych.css +18 -8
  3. package/dist/index.browser.js +3080 -4286
  4. package/dist/index.browser.js.map +1 -1
  5. package/dist/index.browser.min.js +6 -2
  6. package/dist/index.browser.min.js.map +1 -1
  7. package/dist/index.cjs +2314 -4066
  8. package/dist/index.cjs.map +1 -1
  9. package/dist/index.d.ts +984 -6
  10. package/dist/index.js +2313 -4066
  11. package/dist/index.js.map +1 -1
  12. package/package.json +6 -5
  13. package/src/ExtensionManager.spec.ts +123 -0
  14. package/src/ExtensionManager.ts +81 -0
  15. package/src/JsPsych.ts +195 -690
  16. package/src/ProgressBar.spec.ts +60 -0
  17. package/src/ProgressBar.ts +60 -0
  18. package/src/index.scss +29 -8
  19. package/src/index.ts +4 -9
  20. package/src/modules/data/DataCollection.ts +1 -1
  21. package/src/modules/data/DataColumn.ts +12 -1
  22. package/src/modules/data/index.ts +92 -103
  23. package/src/modules/extensions.ts +4 -0
  24. package/src/modules/plugin-api/AudioPlayer.ts +101 -0
  25. package/src/modules/plugin-api/KeyboardListenerAPI.ts +1 -1
  26. package/src/modules/plugin-api/MediaAPI.ts +48 -106
  27. package/src/modules/plugin-api/__mocks__/AudioPlayer.ts +38 -0
  28. package/src/modules/plugin-api/index.ts +11 -14
  29. package/src/modules/plugins.ts +26 -27
  30. package/src/modules/randomization.ts +1 -1
  31. package/src/timeline/Timeline.spec.ts +921 -0
  32. package/src/timeline/Timeline.ts +342 -0
  33. package/src/timeline/TimelineNode.ts +174 -0
  34. package/src/timeline/Trial.spec.ts +897 -0
  35. package/src/timeline/Trial.ts +419 -0
  36. package/src/timeline/index.ts +232 -0
  37. package/src/timeline/util.spec.ts +124 -0
  38. package/src/timeline/util.ts +146 -0
  39. package/dist/JsPsych.d.ts +0 -112
  40. package/dist/TimelineNode.d.ts +0 -34
  41. package/dist/migration.d.ts +0 -3
  42. package/dist/modules/data/DataCollection.d.ts +0 -46
  43. package/dist/modules/data/DataColumn.d.ts +0 -15
  44. package/dist/modules/data/index.d.ts +0 -25
  45. package/dist/modules/data/utils.d.ts +0 -3
  46. package/dist/modules/extensions.d.ts +0 -22
  47. package/dist/modules/plugin-api/HardwareAPI.d.ts +0 -15
  48. package/dist/modules/plugin-api/KeyboardListenerAPI.d.ts +0 -34
  49. package/dist/modules/plugin-api/MediaAPI.d.ts +0 -32
  50. package/dist/modules/plugin-api/SimulationAPI.d.ts +0 -44
  51. package/dist/modules/plugin-api/TimeoutAPI.d.ts +0 -17
  52. package/dist/modules/plugin-api/index.d.ts +0 -8
  53. package/dist/modules/plugins.d.ts +0 -136
  54. package/dist/modules/randomization.d.ts +0 -42
  55. package/dist/modules/turk.d.ts +0 -40
  56. package/dist/modules/utils.d.ts +0 -13
  57. package/src/TimelineNode.ts +0 -544
  58. package/src/modules/plugin-api/HardwareAPI.ts +0 -32
@@ -0,0 +1,60 @@
1
+ import { ProgressBar } from "./ProgressBar";
2
+
3
+ describe("ProgressBar", () => {
4
+ let containerElement: HTMLDivElement;
5
+ let progressBar: ProgressBar;
6
+
7
+ beforeEach(() => {
8
+ containerElement = document.createElement("div");
9
+ progressBar = new ProgressBar(containerElement, "My message");
10
+ });
11
+
12
+ it("sets up proper HTML markup when created", () => {
13
+ expect(containerElement.innerHTML).toMatchInlineSnapshot(
14
+ '"<span>My message</span><div id="jspsych-progressbar-outer"><div id="jspsych-progressbar-inner" style="width: 0%;"></div></div>"'
15
+ );
16
+ });
17
+
18
+ describe("progress", () => {
19
+ it("updates the bar width accordingly", () => {
20
+ expect(progressBar.progress).toEqual(0);
21
+ expect(containerElement.innerHTML).toContain('style="width: 0%;"');
22
+ progressBar.progress = 0.5;
23
+ expect(progressBar.progress).toEqual(0.5);
24
+ expect(containerElement.innerHTML).toContain('style="width: 50%;"');
25
+
26
+ progressBar.progress = 1;
27
+ expect(progressBar.progress).toEqual(1);
28
+ expect(containerElement.innerHTML).toContain('style="width: 100%;"');
29
+ });
30
+
31
+ it("errors if an invalid progress value is provided", () => {
32
+ expect(() => {
33
+ // @ts-expect-error
34
+ progressBar.progress = "0";
35
+ }).toThrowErrorMatchingInlineSnapshot(
36
+ '"jsPsych.progressBar.progress must be a number between 0 and 1"'
37
+ );
38
+ expect(() => {
39
+ progressBar.progress = -0.1;
40
+ }).toThrowErrorMatchingInlineSnapshot(
41
+ '"jsPsych.progressBar.progress must be a number between 0 and 1"'
42
+ );
43
+ expect(() => (progressBar.progress = 1.1)).toThrowErrorMatchingInlineSnapshot(
44
+ '"jsPsych.progressBar.progress must be a number between 0 and 1"'
45
+ );
46
+ });
47
+
48
+ it("should work when message is a function", () => {
49
+ // Override default container element and progress bar
50
+ containerElement = document.createElement("div");
51
+ progressBar = new ProgressBar(containerElement, (progress: number) => String(progress));
52
+ let messageSpan: HTMLSpanElement = containerElement.querySelector("span");
53
+
54
+ expect(messageSpan.innerHTML).toEqual("0");
55
+
56
+ progressBar.progress = 0.5;
57
+ expect(messageSpan.innerHTML).toEqual("0.5");
58
+ });
59
+ });
60
+ });
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Maintains a visual progress bar using HTML and CSS
3
+ */
4
+ export class ProgressBar {
5
+ constructor(
6
+ private readonly containerElement: HTMLDivElement,
7
+ private readonly message: string | ((progress: number) => string)
8
+ ) {
9
+ this.setupElements();
10
+ }
11
+
12
+ private _progress = 0;
13
+
14
+ private innerDiv: HTMLDivElement;
15
+ private messageSpan: HTMLSpanElement;
16
+
17
+ /** Adds the progress bar HTML code into `this.containerElement` */
18
+ private setupElements() {
19
+ this.messageSpan = document.createElement("span");
20
+
21
+ this.innerDiv = document.createElement("div");
22
+ this.innerDiv.id = "jspsych-progressbar-inner";
23
+ this.update();
24
+
25
+ const outerDiv = document.createElement("div");
26
+ outerDiv.id = "jspsych-progressbar-outer";
27
+ outerDiv.appendChild(this.innerDiv);
28
+
29
+ this.containerElement.appendChild(this.messageSpan);
30
+ this.containerElement.appendChild(outerDiv);
31
+ }
32
+
33
+ /** Updates the progress bar according to `this.progress` */
34
+ private update() {
35
+ this.innerDiv.style.width = this._progress * 100 + "%";
36
+
37
+ if (typeof this.message === "function") {
38
+ this.messageSpan.innerHTML = this.message(this._progress);
39
+ } else {
40
+ this.messageSpan.innerHTML = this.message;
41
+ }
42
+ }
43
+
44
+ /**
45
+ * The bar's current position as a number in the closed interval [0, 1]. Set this to update the
46
+ * progress bar accordingly.
47
+ */
48
+ set progress(progress: number) {
49
+ if (typeof progress !== "number" || progress < 0 || progress > 1) {
50
+ throw new Error("jsPsych.progressBar.progress must be a number between 0 and 1");
51
+ }
52
+
53
+ this._progress = progress;
54
+ this.update();
55
+ }
56
+
57
+ get progress() {
58
+ return this._progress;
59
+ }
60
+ }
package/src/index.scss CHANGED
@@ -30,7 +30,6 @@
30
30
  }
31
31
 
32
32
  .jspsych-content {
33
- max-width: 95%; /* this is mainly an IE 10-11 fix */
34
33
  text-align: center;
35
34
  margin: auto; /* this is for overflowing content */
36
35
  }
@@ -58,11 +57,25 @@
58
57
  font-size: 14px;
59
58
  }
60
59
 
61
- /* borrowing Bootstrap style for btn elements, but combining styles a bit */
60
+ /* Buttons and Button Groups */
61
+
62
+ .jspsych-btn-group-flex {
63
+ display: flex;
64
+ justify-content: center;
65
+ }
66
+
67
+ .jspsych-btn-group-grid {
68
+ display: grid;
69
+ grid-auto-columns: max-content;
70
+ max-width: fit-content;
71
+ margin-right: auto;
72
+ margin-left: auto;
73
+ }
74
+
62
75
  .jspsych-btn {
63
76
  display: inline-block;
64
- padding: 6px 12px;
65
- margin: 0px;
77
+ padding: 8px 12px;
78
+ margin: 0.75em;
66
79
  font-size: 14px;
67
80
  font-weight: 400;
68
81
  font-family: "Open Sans", "Arial", sans-serif;
@@ -108,10 +121,11 @@
108
121
  width: 100%;
109
122
  background: transparent;
110
123
  }
124
+
111
125
  .jspsych-slider:focus {
112
126
  outline: none;
113
127
  }
114
- /* track */
128
+
115
129
  .jspsych-slider::-webkit-slider-runnable-track {
116
130
  appearance: none;
117
131
  -webkit-appearance: none;
@@ -123,6 +137,7 @@
123
137
  border-radius: 2px;
124
138
  border: 1px solid #aaa;
125
139
  }
140
+
126
141
  .jspsych-slider::-moz-range-track {
127
142
  appearance: none;
128
143
  width: 100%;
@@ -133,6 +148,7 @@
133
148
  border-radius: 2px;
134
149
  border: 1px solid #aaa;
135
150
  }
151
+
136
152
  .jspsych-slider::-ms-track {
137
153
  appearance: none;
138
154
  width: 99%;
@@ -143,7 +159,7 @@
143
159
  border-radius: 2px;
144
160
  border: 1px solid #aaa;
145
161
  }
146
- /* thumb */
162
+
147
163
  .jspsych-slider::-webkit-slider-thumb {
148
164
  border: 1px solid #666;
149
165
  height: 24px;
@@ -154,6 +170,7 @@
154
170
  -webkit-appearance: none;
155
171
  margin-top: -9px;
156
172
  }
173
+
157
174
  .jspsych-slider::-moz-range-thumb {
158
175
  border: 1px solid #666;
159
176
  height: 24px;
@@ -162,6 +179,7 @@
162
179
  background: #ffffff;
163
180
  cursor: pointer;
164
181
  }
182
+
165
183
  .jspsych-slider::-ms-thumb {
166
184
  border: 1px solid #666;
167
185
  height: 20px;
@@ -172,7 +190,7 @@
172
190
  margin-top: -2px;
173
191
  }
174
192
 
175
- /* jsPsych progress bar */
193
+ /* progress bar */
176
194
 
177
195
  #jspsych-progressbar-container {
178
196
  color: #555;
@@ -184,10 +202,12 @@
184
202
  width: 100%;
185
203
  line-height: 1em;
186
204
  }
205
+
187
206
  #jspsych-progressbar-container span {
188
207
  font-size: 14px;
189
208
  padding-right: 14px;
190
209
  }
210
+
191
211
  #jspsych-progressbar-outer {
192
212
  background-color: #eee;
193
213
  width: 50%;
@@ -197,13 +217,14 @@
197
217
  vertical-align: middle;
198
218
  box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1);
199
219
  }
220
+
200
221
  #jspsych-progressbar-inner {
201
222
  background-color: #aaa;
202
223
  width: 0%;
203
224
  height: 100%;
204
225
  }
205
226
 
206
- /* Control appearance of jsPsych.data.displayData() */
227
+ /* Appearance of jsPsych.data.displayData() */
207
228
  #jspsych-data-display {
208
229
  text-align: left;
209
230
  }
package/src/index.ts CHANGED
@@ -62,12 +62,7 @@ export function initJsPsych(options?) {
62
62
  }
63
63
 
64
64
  export { JsPsych } from "./JsPsych";
65
- export {
66
- JsPsychPlugin,
67
- PluginInfo,
68
- TrialType,
69
- ParameterType,
70
- universalPluginParameters,
71
- UniversalPluginParameters,
72
- } from "./modules/plugins";
73
- export { JsPsychExtension, JsPsychExtensionInfo } from "./modules/extensions";
65
+ export type { JsPsychPlugin, PluginInfo, TrialType } from "./modules/plugins";
66
+ export { ParameterType } from "./modules/plugins";
67
+ export type { JsPsychExtension, JsPsychExtensionInfo } from "./modules/extensions";
68
+ export { DataCollection } from "./modules/data/DataCollection";
@@ -89,7 +89,7 @@ export class DataCollection {
89
89
  }
90
90
 
91
91
  addToLast(properties) {
92
- if (this.trials.length != 0) {
92
+ if (this.trials.length > 0) {
93
93
  Object.assign(this.trials[this.trials.length - 1], properties);
94
94
  }
95
95
  return this;
@@ -10,7 +10,18 @@ export class DataColumn {
10
10
  }
11
11
 
12
12
  mean() {
13
- return this.sum() / this.count();
13
+ let sum = 0;
14
+ let count = 0;
15
+ for (const value of this.values) {
16
+ if (typeof value !== "undefined" && value !== null) {
17
+ sum += value;
18
+ count++;
19
+ }
20
+ }
21
+ if (count === 0) {
22
+ return undefined;
23
+ }
24
+ return sum / count;
14
25
  }
15
26
 
16
27
  median() {
@@ -1,106 +1,104 @@
1
- import { JsPsych } from "../../JsPsych";
1
+ import { TrialResult } from "../../timeline";
2
+ import { Trial } from "../../timeline/Trial";
2
3
  import { DataCollection } from "./DataCollection";
3
4
  import { getQueryString } from "./utils";
4
5
 
6
+ export type InteractionEvent = "blur" | "focus" | "fullscreenenter" | "fullscreenexit";
7
+
8
+ export interface InteractionRecord {
9
+ event: InteractionEvent;
10
+ trial: number;
11
+ time: number;
12
+ }
13
+
14
+ /**
15
+ * Functions and options needed by the `JsPsychData` module
16
+ */
17
+ export interface JsPsychDataDependencies {
18
+ /**
19
+ * Returns progress information for interaction records.
20
+ */
21
+ getProgress: () => { trial: number; time: number };
22
+
23
+ onInteractionRecordAdded: (record: InteractionRecord) => void;
24
+
25
+ getDisplayElement: () => HTMLElement;
26
+ }
27
+
5
28
  export class JsPsychData {
6
- // data storage object
7
- private allData: DataCollection;
29
+ private results: DataCollection;
30
+ private resultToTrialMap: WeakMap<TrialResult, Trial>;
8
31
 
9
- // browser interaction event data
10
- private interactionData: DataCollection;
32
+ /** Browser interaction event data */
33
+ private interactionRecords: DataCollection;
11
34
 
12
- // data properties for all trials
35
+ /** Data properties for all trials */
13
36
  private dataProperties = {};
14
37
 
15
38
  // cache the query_string
16
39
  private query_string;
17
40
 
18
- constructor(private jsPsych: JsPsych) {
41
+ constructor(private dependencies: JsPsychDataDependencies) {
19
42
  this.reset();
20
43
  }
21
44
 
22
45
  reset() {
23
- this.allData = new DataCollection();
24
- this.interactionData = new DataCollection();
46
+ this.results = new DataCollection();
47
+ this.resultToTrialMap = new WeakMap<TrialResult, Trial>();
48
+ this.interactionRecords = new DataCollection();
25
49
  }
26
50
 
27
51
  get() {
28
- return this.allData;
52
+ return this.results;
29
53
  }
30
54
 
31
55
  getInteractionData() {
32
- return this.interactionData;
56
+ return this.interactionRecords;
33
57
  }
34
58
 
35
- write(data_object) {
36
- const progress = this.jsPsych.getProgress();
37
- const trial = this.jsPsych.getCurrentTrial();
38
-
39
- //var trial_opt_data = typeof trial.data == 'function' ? trial.data() : trial.data;
40
-
41
- const default_data = {
42
- trial_type: trial.type.info.name,
43
- trial_index: progress.current_trial_global,
44
- time_elapsed: this.jsPsych.getTotalTime(),
45
- internal_node_id: this.jsPsych.getCurrentTimelineNodeID(),
46
- };
47
-
48
- this.allData.push({
49
- ...data_object,
50
- ...trial.data,
51
- ...default_data,
52
- ...this.dataProperties,
53
- });
59
+ write(trial: Trial) {
60
+ const result = trial.getResult();
61
+ Object.assign(result, this.dataProperties);
62
+ this.results.push(result);
63
+ this.resultToTrialMap.set(result, trial);
54
64
  }
55
65
 
56
66
  addProperties(properties) {
57
67
  // first, add the properties to all data that's already stored
58
- this.allData.addToAll(properties);
68
+ this.results.addToAll(properties);
59
69
 
60
70
  // now add to list so that it gets appended to all future data
61
71
  this.dataProperties = Object.assign({}, this.dataProperties, properties);
62
72
  }
63
73
 
64
74
  addDataToLastTrial(data) {
65
- this.allData.addToLast(data);
66
- }
67
-
68
- getDataByTimelineNode(node_id) {
69
- return this.allData.filterCustom(
70
- (x) => x.internal_node_id.slice(0, node_id.length) === node_id
71
- );
75
+ this.results.addToLast(data);
72
76
  }
73
77
 
74
78
  getLastTrialData() {
75
- return this.allData.top();
79
+ return this.results.top();
76
80
  }
77
81
 
78
82
  getLastTimelineData() {
79
- const lasttrial = this.getLastTrialData();
80
- const node_id = lasttrial.select("internal_node_id").values[0];
81
- if (typeof node_id === "undefined") {
82
- return new DataCollection();
83
- } else {
84
- const parent_node_id = node_id.substr(0, node_id.lastIndexOf("-"));
85
- const lastnodedata = this.getDataByTimelineNode(parent_node_id);
86
- return lastnodedata;
87
- }
83
+ const lastResult = this.getLastTrialData().values()[0];
84
+
85
+ return new DataCollection(
86
+ lastResult ? this.resultToTrialMap.get(lastResult).parent.getResults() : []
87
+ );
88
88
  }
89
89
 
90
90
  displayData(format = "json") {
91
91
  format = format.toLowerCase();
92
- if (format != "json" && format != "csv") {
92
+ if (format !== "json" && format !== "csv") {
93
93
  console.log("Invalid format declared for displayData function. Using json as default.");
94
94
  format = "json";
95
95
  }
96
96
 
97
- const data_string = format === "json" ? this.allData.json(true) : this.allData.csv();
97
+ const dataContainer = document.createElement("pre");
98
+ dataContainer.id = "jspsych-data-display";
99
+ dataContainer.textContent = format === "json" ? this.results.json(true) : this.results.csv();
98
100
 
99
- const display_element = this.jsPsych.getDisplayElement();
100
-
101
- display_element.innerHTML = '<pre id="jspsych-data-display"></pre>';
102
-
103
- document.getElementById("jspsych-data-display").textContent = data_string;
101
+ this.dependencies.getDisplayElement().replaceChildren(dataContainer);
104
102
  }
105
103
 
106
104
  urlVariables() {
@@ -114,61 +112,52 @@ export class JsPsychData {
114
112
  return this.urlVariables()[whichvar];
115
113
  }
116
114
 
117
- createInteractionListeners() {
118
- // blur event capture
119
- window.addEventListener("blur", () => {
120
- const data = {
121
- event: "blur",
122
- trial: this.jsPsych.getProgress().current_trial_global,
123
- time: this.jsPsych.getTotalTime(),
124
- };
125
- this.interactionData.push(data);
126
- this.jsPsych.getInitSettings().on_interaction_data_update(data);
127
- });
128
-
129
- // focus event capture
130
- window.addEventListener("focus", () => {
131
- const data = {
132
- event: "focus",
133
- trial: this.jsPsych.getProgress().current_trial_global,
134
- time: this.jsPsych.getTotalTime(),
135
- };
136
- this.interactionData.push(data);
137
- this.jsPsych.getInitSettings().on_interaction_data_update(data);
138
- });
139
-
140
- // fullscreen change capture
141
- const fullscreenchange = () => {
142
- const data = {
143
- event:
144
- // @ts-expect-error
145
- document.isFullScreen ||
115
+ private addInteractionRecord(event: InteractionEvent) {
116
+ const record: InteractionRecord = { event, ...this.dependencies.getProgress() };
117
+ this.interactionRecords.push(record);
118
+ this.dependencies.onInteractionRecordAdded(record);
119
+ }
120
+
121
+ private interactionListeners = {
122
+ blur: () => {
123
+ this.addInteractionRecord("blur");
124
+ },
125
+ focus: () => {
126
+ this.addInteractionRecord("focus");
127
+ },
128
+ fullscreenchange: () => {
129
+ this.addInteractionRecord(
130
+ // @ts-expect-error
131
+ document.isFullScreen ||
146
132
  // @ts-expect-error
147
133
  document.webkitIsFullScreen ||
148
134
  // @ts-expect-error
149
135
  document.mozIsFullScreen ||
150
136
  document.fullscreenElement
151
- ? "fullscreenenter"
152
- : "fullscreenexit",
153
- trial: this.jsPsych.getProgress().current_trial_global,
154
- time: this.jsPsych.getTotalTime(),
155
- };
156
- this.interactionData.push(data);
157
- this.jsPsych.getInitSettings().on_interaction_data_update(data);
158
- };
159
-
160
- document.addEventListener("fullscreenchange", fullscreenchange);
161
- document.addEventListener("mozfullscreenchange", fullscreenchange);
162
- document.addEventListener("webkitfullscreenchange", fullscreenchange);
163
- }
137
+ ? "fullscreenenter"
138
+ : "fullscreenexit"
139
+ );
140
+ },
141
+ };
142
+
143
+ createInteractionListeners() {
144
+ window.addEventListener("blur", this.interactionListeners.blur);
145
+ window.addEventListener("focus", this.interactionListeners.focus);
164
146
 
165
- // public methods for testing purposes. not recommended for use.
166
- _customInsert(data) {
167
- this.allData = new DataCollection(data);
147
+ document.addEventListener("fullscreenchange", this.interactionListeners.fullscreenchange);
148
+ document.addEventListener("mozfullscreenchange", this.interactionListeners.fullscreenchange);
149
+ document.addEventListener("webkitfullscreenchange", this.interactionListeners.fullscreenchange);
168
150
  }
169
151
 
170
- _fullreset() {
171
- this.reset();
172
- this.dataProperties = {};
152
+ removeInteractionListeners() {
153
+ window.removeEventListener("blur", this.interactionListeners.blur);
154
+ window.removeEventListener("focus", this.interactionListeners.focus);
155
+
156
+ document.removeEventListener("fullscreenchange", this.interactionListeners.fullscreenchange);
157
+ document.removeEventListener("mozfullscreenchange", this.interactionListeners.fullscreenchange);
158
+ document.removeEventListener(
159
+ "webkitfullscreenchange",
160
+ this.interactionListeners.fullscreenchange
161
+ );
173
162
  }
174
163
  }
@@ -1,5 +1,9 @@
1
+ import { ParameterInfos } from "./plugins";
2
+
1
3
  export interface JsPsychExtensionInfo {
2
4
  name: string;
5
+ version?: string;
6
+ data?: ParameterInfos;
3
7
  }
4
8
 
5
9
  export interface JsPsychExtension {
@@ -0,0 +1,101 @@
1
+ export interface AudioPlayerOptions {
2
+ useWebAudio: boolean;
3
+ audioContext?: AudioContext;
4
+ }
5
+
6
+ export interface AudioPlayerInterface {
7
+ load(): Promise<void>;
8
+ play(): void;
9
+ stop(): void;
10
+ addEventListener(eventName: string, callback: EventListenerOrEventListenerObject): void;
11
+ removeEventListener(eventName: string, callback: EventListenerOrEventListenerObject): void;
12
+ }
13
+
14
+ export class AudioPlayer implements AudioPlayerInterface {
15
+ private audio: HTMLAudioElement | AudioBufferSourceNode;
16
+ private webAudioBuffer: AudioBuffer;
17
+ private audioContext: AudioContext | null;
18
+ private useWebAudio: boolean;
19
+ private src: string;
20
+
21
+ constructor(src: string, options: AudioPlayerOptions = { useWebAudio: false }) {
22
+ this.src = src;
23
+ this.useWebAudio = options.useWebAudio;
24
+ this.audioContext = options.audioContext || null;
25
+ }
26
+
27
+ async load() {
28
+ if (this.useWebAudio) {
29
+ this.webAudioBuffer = await this.preloadWebAudio(this.src);
30
+ } else {
31
+ this.audio = await this.preloadHTMLAudio(this.src);
32
+ }
33
+ }
34
+
35
+ play() {
36
+ if (this.audio instanceof HTMLAudioElement) {
37
+ this.audio.play();
38
+ } else {
39
+ // If audio is not HTMLAudioElement, it must be a WebAudio API object, so create a source node.
40
+ if (!this.audio) this.audio = this.getAudioSourceNode(this.webAudioBuffer);
41
+ this.audio.start();
42
+ }
43
+ }
44
+
45
+ stop() {
46
+ if (this.audio instanceof HTMLAudioElement) {
47
+ this.audio.pause();
48
+ this.audio.currentTime = 0;
49
+ } else {
50
+ this.audio!.stop();
51
+ // Regenerate source node for audio since the previous one is stopped and unusable.
52
+ this.audio = this.getAudioSourceNode(this.webAudioBuffer);
53
+ }
54
+ }
55
+
56
+ addEventListener(eventName: string, callback: EventListenerOrEventListenerObject) {
57
+ // If WebAudio buffer exists but source node doesn't, create it.
58
+ if (!this.audio && this.webAudioBuffer)
59
+ this.audio = this.getAudioSourceNode(this.webAudioBuffer);
60
+ this.audio.addEventListener(eventName, callback);
61
+ }
62
+
63
+ removeEventListener(eventName: string, callback: EventListenerOrEventListenerObject) {
64
+ // If WebAudio buffer exists but source node doesn't, create it.
65
+ if (!this.audio && this.webAudioBuffer)
66
+ this.audio = this.getAudioSourceNode(this.webAudioBuffer);
67
+ this.audio.removeEventListener(eventName, callback);
68
+ }
69
+
70
+ private getAudioSourceNode(audioBuffer: AudioBuffer): AudioBufferSourceNode {
71
+ const source = this.audioContext!.createBufferSource();
72
+ source.buffer = audioBuffer;
73
+ source.connect(this.audioContext!.destination);
74
+ return source;
75
+ }
76
+
77
+ private async preloadWebAudio(src: string): Promise<AudioBuffer> {
78
+ const buffer = await fetch(src);
79
+ const arrayBuffer = await buffer.arrayBuffer();
80
+ const audioBuffer = await this.audioContext!.decodeAudioData(arrayBuffer);
81
+ const source = this.audioContext!.createBufferSource();
82
+ source.buffer = audioBuffer;
83
+ source.connect(this.audioContext!.destination);
84
+ return audioBuffer;
85
+ }
86
+
87
+ private async preloadHTMLAudio(src: string): Promise<HTMLAudioElement> {
88
+ return new Promise<HTMLAudioElement>((resolve, reject) => {
89
+ const audio = new Audio(src);
90
+ audio.addEventListener("canplaythrough", () => {
91
+ resolve(audio);
92
+ });
93
+ audio.addEventListener("error", (err) => {
94
+ reject(err);
95
+ });
96
+ audio.addEventListener("abort", (err) => {
97
+ reject(err);
98
+ });
99
+ });
100
+ }
101
+ }
@@ -125,7 +125,7 @@ export class KeyboardListenerAPI {
125
125
  this.cancelKeyboardResponse(listener);
126
126
  }
127
127
 
128
- callback_function({ key, rt });
128
+ callback_function({ key: e.key, rt });
129
129
  }
130
130
  };
131
131