-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathkc85-player.js
168 lines (150 loc) · 4.98 KB
/
kc85-player.js
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
/*
* Robotron KC85 player using the web audio API (https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API)
* Origin: https://github.com/chhu/kc85-tape-player
*/
const KC85Config = {
default : {
zero : 2000, // Frequencies
zero_amp : 1,
one : 1100,
one_amp : 1,
stop : 550,
stop_amp : 1,
first : 8000, // N complete "one" waves for first block
silence : 4400, // silence between blocks in samples (0.1s if sampling rate is 48k)
block : 160, // N complete "one" waves for each block
},
turbo: { // just need to specify overrides
silence : 0,
block: 0
}
}
class KC85Player {
constructor(raw_data, config_key) { // RAW data should be ByteArray or normal array, config key optional
this.raw_data = raw_data
this.config = Object.assign({}, KC85Config.default);
this.config = Object.assign(this.config, KC85Config[config_key]);
this.ac = new (window.AudioContext || window.webkitAudioContext)()
this.sample_rate = this.ac.sampleRate
this.audio = this.ac.createBuffer(1, this.sample_rate * ((raw_data.length / 128) * 1.2 + 10), this.sample_rate)
this.duration = this.audio.duration
this.one = KC85Player.WaveGen(this.sample_rate, this.config.one, this.config.one_amp)
this.zero = KC85Player.WaveGen(this.sample_rate, this.config.zero, this.config.zero_amp)
this.stop = KC85Player.WaveGen(this.sample_rate, this.config.stop, this.config.stop_amp)
// Generate audio samples
this.audio_pos = 0 // within audio
this.data_pos = 0 // within raw_data
this.block = 1
// Prepare first block
for (let i = 0; i < this.config.first; i++)
this.add_one()
while (this.data_pos < raw_data.length) {
// Prepare complete block
for (let i = 0; i < this.config.block; i++)
this.add_one()
this.add_stop()
let sum = 0
this.add_byte((raw_data.length - this.data_pos) <= 128 ? 0xFF : this.block)
for (let i = 0; i < 128; i++) {
let data = Number(raw_data[this.data_pos]) // we may read beyond end, should yield zero
if (isNaN(data))
data = 0
sum += data
this.add_byte(data)
this.data_pos++
}
this.add_byte(sum)
this.audio_pos += this.config.silence
this.block++
if (this.block >= 0xFF)
this.block = 1
}
let signal = this.audio.getChannelData(0).slice(0, this.audio_pos)
this.audio = this.ac.createBuffer(1, this.audio_pos, this.sample_rate)
this.audio.copyToChannel(signal, 0, 0)
}
static WaveGen(sample_rate, frequency, amplitude) { // Generates a single wave for given frequency and sr
let result = new Float32Array(Math.round(sample_rate / frequency))
let h = result.length / 2; // half
result[0] = result[h] = 0;
result[1] = result[h-1] = amplitude * 0.5;
result[2] = result[h-2] = amplitude * 1;
result[3] = result[h-3] = amplitude * 1.2
for (let i = 4; i < h-4; i++)
result[i] = amplitude
// mirror
for (let i = 0; i < h; i++)
result[h+i] = -result[h-i]
// result[i] = -Math.sin((i / result.length) * 2 * Math.PI) * amplitude
return result
}
// Converts uint8 buffer, tries to remove .TAP header(s) and / or split .853 files (returns array of uint8 buffer)
// Will not be called by class.
static FixFormat(buffer) {
var kctap_header = [0xC3, 0x4B, 0x43, 0x2D, 0x54, 0x41, 0x50, 0x45, 0x20, 0x62, 0x79, 0x20, 0x41, 0x46, 0x2E, 0x20]
var tap_find = function(element, index, array) {
for (let i = 0; i < kctap_header.length; i++)
if (array[index+i] !== kctap_header[i])
return false
return true
}
let pos = buffer.findIndex(tap_find)
if (pos < 0)
return [buffer]
var result = []
while (pos >= 0) {
buffer = buffer.slice(pos + 16)
pos = buffer.findIndex(tap_find)
let part = buffer.slice(0, pos < 0 ? undefined : pos)
let part_a = []
// Filter block numbers out
for (let i = 0; i < part.length; i++) {
if (i % 129 == 0)
console.log(part[i] + "> ")
else
part_a.push(part[i])
}
result.push(part_a)
}
return result
}
add_one() {
this.audio.copyToChannel(this.one, 0, this.audio_pos);
this.audio_pos += this.one.length;
}
add_zero() {
this.audio.copyToChannel(this.zero, 0, this.audio_pos);
this.audio_pos += this.zero.length;
}
add_stop() {
this.audio.copyToChannel(this.stop, 0, this.audio_pos);
this.audio_pos += this.stop.length;
}
add_byte(b) {
for (let bit = 0; bit < 8; bit++)
if (b & (1 << bit))
this.add_one()
else
this.add_zero()
this.add_stop()
}
stop_play() {
if (window.asource) {
window.asource.stop()
window.asource.disconnect()
}
}
play() {
this.stop_play()
// Get an AudioBufferSourceNode.
// This is the AudioNode to use when we want to play an AudioBuffer
window.asource = this.ac.createBufferSource();
// set the buffer in the AudioBufferSourceNode
window.asource.buffer = this.audio;
// connect the AudioBufferSourceNode to the
// destination so we can hear the sound
window.asource.connect(this.ac.destination);
// start the source playing
window.asource.start();
}
}