openMSX
VLM5030.cc
Go to the documentation of this file.
1/*
2 vlm5030.c
3
4 VLM5030 emulator
5
6 Written by Tatsuyuki Satoh
7 Based on TMS5220 simulator (tms5220.c)
8
9 note:
10 memory read cycle(==sampling rate) = 122.9u(440clock)
11 interpolator (LC8109 = 2.5ms) = 20 * samples(125us)
12 frame time (20ms) = 4 * interpolator
13 9bit DAC is composed of 5bit Physical and 3bitPWM.
14
15 todo:
16 Noise Generator circuit without 'rand()' function.
17
18----------- command format (Analytical result) ----------
19
201)end of speech (8bit)
21:00000011:
22
232)silent some frame (8bit)
24:????SS01:
25
26SS : number of silent frames
27 00 = 2 frame
28 01 = 4 frame
29 10 = 6 frame
30 11 = 8 frame
31
323)-speech frame (48bit)
33function: 6th : 5th : 4th : 3rd : 2nd : 1st :
34end : --- : --- : --- : --- : --- :00000011:
35silent : --- : --- : --- : --- : --- :0000SS01:
36speech :11111122:22233334:44455566:67778889:99AAAEEE:EEPPPPP0:
37
38EEEEE : energy : volume 0=off,0x1f=max
39PPPPP : pitch : 0=noise , 1=fast,0x1f=slow
40111111 : K1 : 48=off
4122222 : K2 : 0=off,1=+min,0x0f=+max,0x10=off,0x11=+max,0x1f=-min
42 : 16 == special function??
433333 : K3 : 0=off,1=+min,0x07=+max,0x08=-max,0x0f=-min
444444 : K4 :
45555 : K5 : 0=off,1=+min,0x03=+max,0x04=-max,0x07=-min
46666 : K6 :
47777 : K7 :
48888 : K8 :
49999 : K9 :
50AAA : K10 :
51
52 ---------- chirp table information ----------
53
54DAC PWM cycle == 88system clock , (11clock x 8 pattern) = 40.6KHz
55one chirp == 5 x PWM cycle == 440system clock(8,136Hz)
56
57chirp 0 : volume 10- 8 : with filter
58chirp 1 : volume 8- 6 : with filter
59chirp 2 : volume 6- 4 : with filter
60chirp 3 : volume 4 : no filter ??
61chirp 4- 5: volume 4- 2 : with filter
62chirp 6-11: volume 2- 0 : with filter
63chirp 12-..: volume 0 : silent
64
65 ---------- digital output information ----------
66 when ME pin = high , some status output to A0..15 pins
67
68 A0..8 : DAC output value (abs)
69 A9 : DAC sign flag , L=minus,H=Plus
70 A10 : energy reload flag (pitch pulse)
71 A11..15 : unknown
72
73 [DAC output value(signed 6bit)] = A9 ? A0..8 : -(A0..8)
74
75*/
76
77#include "VLM5030.hh"
78
79#include "DeviceConfig.hh"
80#include "FileOperations.hh"
81#include "XMLElement.hh"
82#include "serialize.hh"
83
84#include "cstd.hh"
85#include "narrow.hh"
86#include "one_of.hh"
87#include "random.hh"
88#include "ranges.hh"
89#include "xrange.hh"
90
91#include <algorithm>
92#include <array>
93#include <cmath>
94
95namespace openmsx {
96
97
98// interpolator per frame
99static constexpr int FR_SIZE = 4;
100// samples per interpolator
101static constexpr uint8_t IP_SIZE_SLOWER = 240 / FR_SIZE;
102static constexpr uint8_t IP_SIZE_SLOW = 200 / FR_SIZE;
103static constexpr uint8_t IP_SIZE_NORMAL = 160 / FR_SIZE;
104static constexpr uint8_t IP_SIZE_FAST = 120 / FR_SIZE;
105static constexpr uint8_t IP_SIZE_FASTER = 80 / FR_SIZE;
106
107// speed parameter
108// SPC SPB SPA
109// 1 0 1 more slow (05h) : 42ms (150%) : 60sample
110// 1 1 x slow (06h,07h) : 34ms (125%) : 50sample
111// x 0 0 normal (00h,04h) : 25.6ms (100%) : 40sample
112// 0 0 1 fast (01h) : 20.2ms (75%) : 30sample
113// 0 1 x more fast (02h,03h) : 12.2ms (50%) : 20sample
114static constexpr std::array<uint8_t, 8> VLM5030_speed_table =
115{
116 IP_SIZE_NORMAL,
117 IP_SIZE_FAST,
118 IP_SIZE_FASTER,
119 IP_SIZE_FASTER,
120 IP_SIZE_NORMAL,
121 IP_SIZE_SLOWER,
122 IP_SIZE_SLOW,
123 IP_SIZE_SLOW
124};
125
126// ROM Tables
127
128// This is the energy lookup table
129
130// sampled from real chip
131static constexpr std::array<uint16_t, 0x20> energyTable =
132{
133 0, 2, 4, 6, 10, 12, 14, 18, // 0-7
134 22, 26, 30, 34, 38, 44, 48, 54, // 8-15
135 62, 68, 76, 84, 94,102,114,124, // 16-23
136 136,150,164,178,196,214,232,254 // 24-31
137};
138
139// This is the pitch lookup table
140static constexpr std::array<uint8_t, 0x20> pitchTable =
141{
142 1, // 0 : random mode
143 22, // 1 : start=22
144 23, 24, 25, 26, 27, 28, 29, 30, // 2- 9 : 1step
145 32, 34, 36, 38, 40, 42, 44, 46, // 10-17 : 2step
146 50, 54, 58, 62, 66, 70, 74, 78, // 18-25 : 4step
147 86, 94, 102,110,118,126 // 26-31 : 8step
148};
149
150static constexpr std::array<int16_t, 64> K1_table = {
151 -24898, -25672, -26446, -27091, -27736, -28252, -28768, -29155,
152 -29542, -29929, -30316, -30574, -30832, -30961, -31219, -31348,
153 -31606, -31735, -31864, -31864, -31993, -32122, -32122, -32251,
154 -32251, -32380, -32380, -32380, -32509, -32509, -32509, -32509,
155 24898, 23995, 22963, 21931, 20770, 19480, 18061, 16642,
156 15093, 13416, 11610, 9804, 7998, 6063, 3999, 1935,
157 0, -1935, -3999, -6063, -7998, -9804, -11610, -13416,
158 -15093, -16642, -18061, -19480, -20770, -21931, -22963, -23995
159};
160static constexpr std::array<int16_t, 32> K2_table = {
161 0, -3096, -6321, -9417, -12513, -15351, -18061, -20770,
162 -23092, -25285, -27220, -28897, -30187, -31348, -32122, -32638,
163 0, 32638, 32122, 31348, 30187, 28897, 27220, 25285,
164 23092, 20770, 18061, 15351, 12513, 9417, 6321, 3096
165};
166static constexpr std::array<int16_t, 16> K3_table = {
167 0, -3999, -8127, -12255, -16384, -20383, -24511, -28639,
168 32638, 28639, 24511, 20383, 16254, 12255, 8127, 3999
169};
170static constexpr std::array<int16_t, 8> K5_table = {
171 0, -8127, -16384, -24511, 32638, 24511, 16254, 8127
172};
173
174unsigned VLM5030::getBits(unsigned sBit, unsigned bits) const
175{
176 unsigned offset = address + (sBit / 8);
177 unsigned data = rom[(offset + 0) & address_mask] +
178 rom[(offset + 1) & address_mask] * 256;
179 data >>= (sBit & 7);
180 data &= (0xFF >> (8 - bits));
181 return data;
182}
183
184// get next frame
185int VLM5030::parseFrame()
186{
187 // remember previous frame
188 old_energy = new_energy;
189 old_pitch = new_pitch;
190 old_k = new_k;
191 // command byte check
192 if (uint8_t cmd = rom[address & address_mask];
193 cmd & 0x01) {
194 // extend frame
195 new_energy = new_pitch = 0;
196 ranges::fill(new_k, 0);
197 ++address;
198 if (cmd & 0x02) {
199 // end of speech
200 return 0;
201 } else {
202 // silent frame
203 int nums = ((cmd >> 2) + 1) * 2;
204 return nums * FR_SIZE;
205 }
206 }
207 // pitch
208 new_pitch = narrow_cast<uint8_t>((pitchTable[getBits(1, 5)] + pitch_offset) & 0xff);
209 // energy
210 new_energy = energyTable[getBits(6, 5)];
211
212 // 10 K's
213 new_k[9] = K5_table[getBits(11, 3)];
214 new_k[8] = K5_table[getBits(14, 3)];
215 new_k[7] = K5_table[getBits(17, 3)];
216 new_k[6] = K5_table[getBits(20, 3)];
217 new_k[5] = K5_table[getBits(23, 3)];
218 new_k[4] = K5_table[getBits(26, 3)];
219 new_k[3] = K3_table[getBits(29, 4)];
220 new_k[2] = K3_table[getBits(33, 4)];
221 new_k[1] = K2_table[getBits(37, 5)];
222 new_k[0] = K1_table[getBits(42, 6)];
223
224 address += 6;
225 return FR_SIZE;
226}
227
228// decode and buffering data
229void VLM5030::generateChannels(std::span<float*> bufs, unsigned num)
230{
231 // Single channel device: replace content of bufs[0] (not add to it).
232 using enum Phase;
233 if (phase == IDLE) {
234 bufs[0] = nullptr;
235 return;
236 }
237
238 int buf_count = 0;
239
240 // running
241 if (phase == one_of(RUN, STOP)) {
242 // playing speech
243 while (num > 0) {
244 // check new interpolator or new frame
245 if (sample_count == 0) {
246 if (phase == STOP) {
247 phase = END;
248 sample_count = 1;
249 goto phase_stop; // continue to end phase
250 }
251 sample_count = frame_size;
252 // interpolator changes
253 if (interp_count == 0) {
254 // change to new frame
255 interp_count = narrow_cast<uint8_t>(parseFrame()); // with change phase
256 if (interp_count == 0) {
257 // end mark found
258 interp_count = FR_SIZE;
259 sample_count = frame_size; // end -> stop time
260 phase = STOP;
261 }
262 // Set old target as new start of frame
263 current_energy = old_energy;
264 current_pitch = old_pitch;
265 ranges::copy(old_k, current_k); // no assignment because arrays have different type (intentional?)
266 // is this a zero energy frame?
267 if (current_energy == 0) {
268 target_energy = 0;
269 target_pitch = narrow_cast<uint8_t>(current_pitch);
270 ranges::copy(current_k, target_k); // no assignment because arrays have different type (intentional?)
271 } else {
272 // normal frame
273 target_energy = new_energy;
274 target_pitch = new_pitch;
275 target_k = new_k;
276 }
277 }
278 // next interpolator
279 // Update values based on step values 25%, 50%, 75%, 100%
280 interp_count -= interp_step;
281 // 3,2,1,0 -> 1,2,3,4
282 int interp_effect = FR_SIZE - (interp_count % FR_SIZE);
283 current_energy = old_energy + (target_energy - old_energy) * interp_effect / FR_SIZE;
284 if (old_pitch > 1) {
285 current_pitch = old_pitch + (target_pitch - old_pitch) * interp_effect / FR_SIZE;
286 }
287 for (auto i : xrange(10))
288 current_k[i] = old_k[i] + (target_k[i] - old_k[i]) * interp_effect / FR_SIZE;
289 }
290 // calculate digital filter
291 int current_val = [&] {
292 if (old_energy == 0) {
293 // generate silent samples here
294 return 0;
295 } else if (old_pitch <= 1) {
296 // generate unvoiced samples here
297 return random_bool() ? int(current_energy)
298 : -int(current_energy);
299 } else {
300 // generate voiced samples here
301 return (pitch_count == 0) ? int(current_energy) : 0;
302 }
303 }();
304
305 // Lattice filter here
306 std::array<int, 11> u;
307 u[10] = current_val;
308 for (int i = 9; i >= 0; --i) {
309 u[i] = u[i + 1] - ((current_k[i] * x[i]) / 32768);
310 }
311 for (int i = 9; i >= 1; --i) {
312 x[i] = x[i - 1] + ((current_k[i - 1] * u[i - 1]) / 32768);
313 }
314 x[0] = u[0];
315
316 // clipping, buffering
317 bufs[0][buf_count] = narrow<float>(std::clamp(u[0], -511, 511));
318 ++buf_count;
319 --sample_count;
320 ++pitch_count;
321 if (pitch_count >= current_pitch) {
322 pitch_count = 0;
323 }
324 --num;
325 }
326 // return;
327 }
328phase_stop:
329 switch (phase) {
330 using enum Phase;
331 case SETUP:
332 if (sample_count <= num) {
333 sample_count = 0;
334 // pin_BSY = true;
335 phase = WAIT;
336 } else {
337 sample_count -= narrow<uint8_t>(num);
338 }
339 break;
340 case END:
341 if (sample_count <= num) {
342 sample_count = 0;
343 pin_BSY = false;
344 phase = IDLE;
345 } else {
346 sample_count -= narrow<uint8_t>(num);
347 }
348 break;
349 default:
350 break; // nothing
351 }
352 // silent buffering
353 while (num > 0) {
354 bufs[0][buf_count++] = 0;
355 --num;
356 }
357}
358
359float VLM5030::getAmplificationFactorImpl() const
360{
361 return 1.0f / (1 << 9);
362}
363
364// setup parameter option when RST=H
365void VLM5030::setupParameter(uint8_t param)
366{
367 // latch parameter value
368 parameter = param;
369
370 // bit 0,1 : 4800bps / 9600bps , interpolator step
371 if (param & 2) { // bit 1 = 1 , 9600bps
372 interp_step = 4; // 9600bps : no interpolator
373 } else if (param & 1) { // bit1 = 0 & bit0 = 1 , 4800bps
374 interp_step = 2; // 4800bps : 2 interpolator
375 } else { // bit1 = bit0 = 0 : 2400bps
376 interp_step = 1; // 2400bps : 4 interpolator
377 }
378
379 // bit 3,4,5 : speed (frame size)
380 frame_size = VLM5030_speed_table[(param >> 3) & 7];
381
382 // bit 6,7 : low / high pitch
383 if (param & 0x80) { // bit7=1 , high pitch
384 pitch_offset = -8;
385 } else if (param & 0x40) { // bit6=1 , low pitch
386 pitch_offset = 8;
387 } else {
388 pitch_offset = 0;
389 }
390}
391
393{
394 phase = Phase::RESET;
395 address = 0;
396 vcu_addr_h = 0;
397 pin_BSY = false;
398
399 old_energy = old_pitch = 0;
400 new_energy = new_pitch = 0;
401 current_energy = current_pitch = 0;
402 target_energy = target_pitch = 0;
403 ranges::fill(old_k, 0);
404 ranges::fill(new_k, 0);
405 ranges::fill(current_k, 0);
406 ranges::fill(target_k, 0);
407 interp_count = sample_count = pitch_count = 0;
408 ranges::fill(x, 0);
409 // reset parameters
410 setupParameter(0x00);
411}
412
413// get BSY pin level
414bool VLM5030::getBSY(EmuTime::param time) const
415{
416 const_cast<VLM5030*>(this)->updateStream(time);
417 return pin_BSY;
418}
419
420// latch control data
421void VLM5030::writeData(uint8_t data)
422{
423 latch_data = data;
424}
425
426void VLM5030::writeControl(uint8_t data, EmuTime::param time)
427{
428 updateStream(time);
429 setRST((data & 0x01) != 0);
430 setVCU((data & 0x04) != 0);
431 setST ((data & 0x02) != 0);
432}
433
434// set RST pin level : reset / set table address A8-A15
435void VLM5030::setRST(bool pin)
436{
437 if (pin_RST) {
438 if (!pin) { // H -> L : latch parameters
439 pin_RST = false;
440 setupParameter(latch_data);
441 }
442 } else {
443 if (pin) { // L -> H : reset chip
444 pin_RST = true;
445 if (pin_BSY) {
446 reset();
447 }
448 }
449 }
450}
451
452// set VCU pin level : ?? unknown
453void VLM5030::setVCU(bool pin)
454{
455 // direct mode / indirect mode
456 pin_VCU = pin;
457}
458
459// set ST pin level : set table address A0-A7 / start speech
460void VLM5030::setST(bool pin)
461{
462 if (pin_ST == pin) {
463 // pin level unchanged
464 return;
465 }
466 if (!pin) {
467 // H -> L
468 pin_ST = false;
469 if (pin_VCU) {
470 // direct access mode & address High
471 vcu_addr_h = narrow<uint16_t>((latch_data << 8) + 0x01);
472 } else {
473 // check access mode
474 if (vcu_addr_h) {
475 // direct access mode
476 address = (vcu_addr_h & 0xff00) + latch_data;
477 vcu_addr_h = 0;
478 } else {
479 // indirect access mode
480 int table = (latch_data & 0xfe) + ((int(latch_data) & 1) << 8);
481 address = uint16_t((rom[(table + 0) & address_mask] << 8) |
482 (rom[(table + 1) & address_mask] << 0));
483 }
484 // reset process status
485 sample_count = frame_size;
486 interp_count = FR_SIZE;
487 // clear filter
488 // start after 3 sampling cycle
489 phase = Phase::RUN;
490 }
491 } else {
492 // L -> H
493 pin_ST = true;
494 // setup speech, BSY on after 30ms?
495 phase = Phase::SETUP;
496 sample_count = 1; // wait time for busy on
497 pin_BSY = true;
498 }
499}
500
501
502static XMLElement* getRomConfig(
503 DeviceConfig& config, const std::string& name, std::string_view romFilename)
504{
505 auto& doc = config.getXMLDocument();
506 auto* voiceROMconfig = doc.allocateElement(doc.allocateString(name));
507 voiceROMconfig->setFirstAttribute(doc.allocateAttribute("id", "name"));
508 auto* romElement = voiceROMconfig->setFirstChild(doc.allocateElement("rom"));
509 romElement->setFirstChild(doc.allocateElement( // load by sha1sum
510 "sha1", "4f36d139ee4baa7d5980f765de9895570ee05f40"))
511 ->setNextSibling(doc.allocateElement( // load by predefined filename in software rom's dir
512 "filename",
513 doc.allocateString(tmpStrCat(FileOperations::stripExtension(romFilename), "_voice.rom"))))
514 ->setNextSibling(doc.allocateElement( // or hardcoded filename in ditto dir
515 "filename", "keyboardmaster/voice.rom"));
516 return voiceROMconfig;
517}
518
519static constexpr auto INPUT_RATE = unsigned(cstd::round(3579545 / 440.0));
520
521VLM5030::VLM5030(const std::string& name_, static_string_view desc,
522 std::string_view romFilename, const DeviceConfig& config)
523 : ResampledSoundDevice(config.getMotherBoard(), name_, desc, 1, INPUT_RATE, false)
524 , rom(name_ + " ROM", "rom", DeviceConfig(config, *getRomConfig(const_cast<DeviceConfig&>(config), name_, romFilename)))
525{
526 reset();
527 phase = Phase::IDLE;
528
529 assert(rom.size() != 0);
530 address_mask = narrow<unsigned>(rom.size() - 1);
531
532 registerSound(config);
533}
534
539
540static constexpr std::initializer_list<enum_string<VLM5030::Phase>> phaseInfo = {
541 { "RESET", VLM5030::Phase::RESET },
542 { "IDLE", VLM5030::Phase::IDLE },
543 { "SETUP", VLM5030::Phase::SETUP },
544 { "WAIT", VLM5030::Phase::WAIT },
545 { "RUN", VLM5030::Phase::RUN },
546 { "STOP", VLM5030::Phase::STOP },
547 { "END", VLM5030::Phase::END },
548};
550
551// version 1: initial version
552// version 2: serialize 'phase' as an enum instead of uint8_t
553template<typename Archive>
554void VLM5030::serialize(Archive& ar, unsigned version)
555{
556 ar.serialize("address_mask", address_mask,
557 "frame_size", frame_size,
558 "pitch_offset", pitch_offset,
559 "current_energy", current_energy,
560 "current_pitch", current_pitch,
561 "current_k", current_k,
562 "x", x,
563 "address", address,
564 "vcu_addr_h", vcu_addr_h,
565 "old_k", old_k,
566 "new_k", new_k,
567 "target_k", target_k,
568 "old_energy", old_energy,
569 "new_energy", new_energy,
570 "target_energy", target_energy,
571 "old_pitch", old_pitch,
572 "new_pitch", new_pitch,
573 "target_pitch", target_pitch,
574 "interp_step", interp_step,
575 "interp_count", interp_count,
576 "sample_count", sample_count,
577 "pitch_count", pitch_count,
578 "latch_data", latch_data,
579 "parameter", parameter,
580 "pin_BSY", pin_BSY,
581 "pin_ST", pin_ST,
582 "pin_VCU", pin_VCU,
583 "pin_RST", pin_RST);
584 if (ar.versionAtLeast(version, 2)) {
585 ar.serialize("phase", phase);
586 } else {
587 assert(Archive::IS_LOADER);
588 int tmp = 0; // dummy init to avoid warning
589 ar.serialize("phase", tmp);
590 switch (tmp) {
591 using enum Phase;
592 case 0: phase = RESET; break;
593 case 1: phase = IDLE; break;
594 case 2: phase = SETUP; break;
595 case 3: phase = WAIT; break;
596 case 4: phase = RUN; break;
597 case 5: phase = STOP; break;
598 default: phase = END; break;
599 }
600 }
601}
602
604
605} // namespace openmsx
auto size() const
Definition Rom.hh:36
void updateStream(EmuTime::param time)
void unregisterSound()
Unregisters this sound device with the Mixer.
void registerSound(const DeviceConfig &config)
Registers this sound device with the Mixer.
void writeData(uint8_t data)
latch control data
Definition VLM5030.cc:421
bool getBSY(EmuTime::param time) const
get BSY pin level
Definition VLM5030.cc:414
VLM5030(const std::string &name, static_string_view desc, std::string_view romFilename, const DeviceConfig &config)
Definition VLM5030.cc:521
void writeControl(uint8_t data, EmuTime::param time)
set RST / VCU / ST pins
Definition VLM5030.cc:426
void serialize(Archive &ar, unsigned version)
Definition VLM5030.cc:554
XMLAttribute * setFirstAttribute(XMLAttribute *attribute)
static_string_view
constexpr double round(double x)
Definition cstd.hh:247
string_view stripExtension(string_view path)
Returns the path without extension.
This file implemented 3 utility functions:
Definition Autofire.cc:11
constexpr void fill(ForwardRange &&range, const T &value)
Definition ranges.hh:315
constexpr auto copy(InputRange &&range, OutputIter out)
Definition ranges.hh:252
bool random_bool()
Return a random boolean value.
Definition random.hh:24
#define INSTANTIATE_SERIALIZE_METHODS(CLASS)
#define SERIALIZE_ENUM(TYPE, INFO)
TemporaryString tmpStrCat(Ts &&... ts)
Definition strCat.hh:742
constexpr auto xrange(T e)
Definition xrange.hh:132