1 /*
2 * Copyright (C) 2021-2021 The DOSBox Staging Team
3 * Copyright (C) 2002-2021 The DOSBox Team
4 *
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License along
16 * with this program; if not, write to the Free Software Foundation, Inc.,
17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 */
19
20 #include "innovation.h"
21
22 #include "control.h"
23 #include "pic.h"
24
25 // Innovation Settings
26 // -------------------
27 constexpr uint16_t SAMPLES_PER_BUFFER = 2048;
28
Open(const std::string & model_choice,const std::string & clock_choice,const int filter_strength_6581,const int filter_strength_8580,const int port_choice)29 void Innovation::Open(const std::string &model_choice,
30 const std::string &clock_choice,
31 const int filter_strength_6581,
32 const int filter_strength_8580,
33 const int port_choice)
34 {
35 Close();
36
37 // Sentinel
38 if (model_choice == "none")
39 return;
40
41 std::string model_name;
42 int filter_strength = 0;
43 auto sid_service = std::make_unique<reSIDfp::SID>();
44
45 // Setup the model and filter
46 if (model_choice == "8580") {
47 model_name = "8580";
48 sid_service->setChipModel(reSIDfp::MOS8580);
49 filter_strength = filter_strength_8580;
50 if (filter_strength > 0) {
51 sid_service->enableFilter(true);
52 sid_service->setFilter8580Curve(filter_strength / 100.0);
53 }
54 } else {
55 model_name = "6581";
56 sid_service->setChipModel(reSIDfp::MOS6581);
57 filter_strength = filter_strength_6581;
58 if (filter_strength > 0) {
59 sid_service->enableFilter(true);
60 sid_service->setFilter6581Curve(filter_strength / 100.0);
61 }
62 }
63
64 // Determine chip clock frequency
65 if (clock_choice == "default")
66 chip_clock = 894886.25;
67 else if (clock_choice == "c64ntsc")
68 chip_clock = 1022727.14;
69 else if (clock_choice == "c64pal")
70 chip_clock = 985250;
71 else if (clock_choice == "hardsid")
72 chip_clock = 1000000;
73 assert(chip_clock);
74
75 // Setup the mixer and get it's sampling rate
76 using namespace std::placeholders;
77 const auto mixer_callback = std::bind(&Innovation::MixerCallBack, this, _1);
78 const auto mixer_channel = MIXER_AddChannel(mixer_callback, 0, "INNOVATION");
79 sid_sample_rate = mixer_channel->GetSampleRate();
80
81 // Determine the passband frequency, which is capped at 90% of Nyquist.
82 const double passband = 0.9 * sid_sample_rate / 2;
83
84 // Assign the sampling parameters
85 sid_service->setSamplingParameters(chip_clock, reSIDfp::RESAMPLE,
86 sid_sample_rate, passband);
87
88 // Setup and assign the port address
89 const auto read_from = std::bind(&Innovation::ReadFromPort, this, _1, _2);
90 const auto write_to = std::bind(&Innovation::WriteToPort, this, _1, _2, _3);
91 base_port = check_cast<io_port_t>(port_choice);
92 read_handler.Install(base_port, read_from, io_width_t::byte, 0x20);
93 write_handler.Install(base_port, write_to, io_width_t::byte, 0x20);
94
95 // Move the locals into members
96 service = std::move(sid_service);
97 channel = std::move(mixer_channel);
98
99 // Ready state-values for rendering
100 last_used = 0;
101 play_buffer_pos = 0;
102 keep_rendering = true;
103
104 // Start rendering
105 renderer = std::thread(std::bind(&Innovation::Render, this));
106 set_thread_name(renderer, "dosbox:innovatn"); // < 16-character cap
107 play_buffer = playable.Dequeue(); // populate the first play buffer
108
109 if (filter_strength == 0)
110 LOG_MSG("INNOVATION: Running on port %xh with a SID %s at %0.3f MHz",
111 base_port, model_name.c_str(), chip_clock / 1000000.0);
112 else
113 LOG_MSG("INNOVATION: Running on port %xh with a SID %s at %0.3f MHz filtering at %d%%",
114 base_port, model_name.c_str(), chip_clock / 1000000.0,
115 filter_strength);
116
117 is_open = true;
118 }
119
Close()120 void Innovation::Close()
121 {
122 if (!is_open)
123 return;
124
125 DEBUG_LOG_MSG("INNOVATION: Shutting down the SSI-2001 on port %xh", base_port);
126
127 // Stop playback
128 if (channel)
129 channel->Enable(false);
130
131 // Stop rendering and drain the queues
132 keep_rendering = false;
133 if (!backstock.Size())
134 backstock.Enqueue(std::move(play_buffer));
135 while (playable.Size())
136 play_buffer = playable.Dequeue();
137
138 // Wait for the rendering thread to finish
139 if (renderer.joinable())
140 renderer.join();
141
142 // Remove the IO handlers before removing the SID device
143 read_handler.Uninstall();
144 write_handler.Uninstall();
145
146 // Reset the members
147 channel.reset();
148 service.reset();
149 is_open = false;
150 }
151
ReadFromPort(io_port_t port,io_width_t)152 uint8_t Innovation::ReadFromPort(io_port_t port, io_width_t)
153 {
154 const auto sid_port = static_cast<io_port_t>(port - base_port);
155 const std::lock_guard<std::mutex> lock(service_mutex);
156 return service->read(sid_port);
157 }
158
WriteToPort(io_port_t port,io_val_t value,io_width_t)159 void Innovation::WriteToPort(io_port_t port, io_val_t value, io_width_t)
160 {
161 const auto data = check_cast<uint8_t>(value);
162 const auto sid_port = static_cast<io_port_t>(port - base_port);
163 { // service-lock
164 const std::lock_guard<std::mutex> lock(service_mutex);
165 service->write(sid_port, data);
166 }
167 // Turn on the channel after the data's written
168 if (!last_used) {
169 channel->Enable(true);
170 }
171 last_used = PIC_Ticks;
172 }
173
Render()174 void Innovation::Render()
175 {
176 const auto cycles_per_sample = static_cast<uint16_t>(chip_clock /
177 sid_sample_rate);
178
179 // Allocate one buffer and reuse it for the duration.
180 std::vector<int16_t> buffer(SAMPLES_PER_BUFFER);
181
182 // Populate the backstock queue using copies of the current buffer.
183 while (backstock.Size() < backstock.MaxCapacity() - 1)
184 backstock.Enqueue(buffer); // copied
185 backstock.Enqueue(std::move(buffer)); // moved; buffer is hollow
186 assert(backstock.Size() == backstock.MaxCapacity());
187
188 while (keep_rendering.load()) {
189 // Variables populated during rendering.
190 uint16_t n = 0;
191 buffer = backstock.Dequeue();
192 std::unique_lock<std::mutex> lock(service_mutex);
193
194 while (n < SAMPLES_PER_BUFFER) {
195 const auto buffer_pos = buffer.data() + n;
196 const auto n_remaining = SAMPLES_PER_BUFFER - n;
197 const auto cycles = static_cast<unsigned int>(cycles_per_sample * n_remaining);
198 n += service->clock(cycles, buffer_pos);
199 }
200 assert(n == SAMPLES_PER_BUFFER);
201 lock.unlock();
202
203 // The buffer is now populated so move it into the playable queue.
204 playable.Enqueue(std::move(buffer));
205 }
206 }
207
MixerCallBack(uint16_t requested_samples)208 void Innovation::MixerCallBack(uint16_t requested_samples)
209 {
210 while (requested_samples) {
211 const auto n = std::min(GetRemainingSamples(), requested_samples);
212 const auto buffer_pos = play_buffer.data() + play_buffer_pos;
213 channel->AddSamples_m16(n, buffer_pos);
214 requested_samples -= n;
215 play_buffer_pos += n;
216 }
217 // Stop the channel after 5 seconds of idle-time.
218 if (last_used + 5000 < PIC_Ticks) {
219 last_used = 0;
220 channel->Enable(false);
221 }
222 }
223
224 // Return the number of samples left to play in the current buffer.
GetRemainingSamples()225 uint16_t Innovation::GetRemainingSamples()
226 {
227 // If the current buffer has some samples left, then return those ...
228 if (play_buffer_pos < SAMPLES_PER_BUFFER)
229 return SAMPLES_PER_BUFFER - play_buffer_pos;
230
231 // Otherwise put the spent buffer in backstock and get the next buffer.
232 backstock.Enqueue(std::move(play_buffer));
233 play_buffer = playable.Dequeue();
234 play_buffer_pos = 0; // reset the sample counter to the beginning.
235
236 return SAMPLES_PER_BUFFER;
237 }
238
239 Innovation innovation;
innovation_destroy(Section * sec)240 static void innovation_destroy([[maybe_unused]] Section *sec)
241 {
242 innovation.Close();
243 }
244
innovation_init(Section * sec)245 static void innovation_init(Section *sec)
246 {
247 assert(sec);
248 Section_prop *conf = static_cast<Section_prop *>(sec);
249
250 const auto model_choice = conf->Get_string("sidmodel");
251 const auto clock_choice = conf->Get_string("sidclock");
252 const auto port_choice = conf->Get_hex("sidport");
253 const auto filter_strength_6581 = conf->Get_int("6581filter");
254 const auto filter_strength_8580 = conf->Get_int("8580filter");
255
256 innovation.Open(model_choice, clock_choice, filter_strength_6581,
257 filter_strength_8580, port_choice);
258
259 sec->AddDestroyFunction(&innovation_destroy, true);
260 }
261
init_innovation_dosbox_settings(Section_prop & sec_prop)262 static void init_innovation_dosbox_settings(Section_prop &sec_prop)
263 {
264 constexpr auto when_idle = Property::Changeable::WhenIdle;
265
266 // Chip type
267 auto *str_prop = sec_prop.Add_string("sidmodel", when_idle, "none");
268 const char *sid_models[] = {"auto", "6581", "8580", "none", 0};
269 str_prop->Set_values(sid_models);
270 str_prop->Set_help(
271 "Model of chip to emulate in the Innovation SSI-2001 card:\n"
272 " - auto: Selects the 6581 chip.\n"
273 " - 6581: The original chip, known for its bassy and rich character.\n"
274 " - 8580: A later revision that more closely matched the SID specification.\n"
275 " It fixed the 6581's DC bias and is less prone to distortion.\n"
276 " The 8580 is an option on reproduction cards, like the DuoSID.\n"
277 " - none: Disables the card.");
278
279 // Chip clock frequency
280 str_prop = sec_prop.Add_string("sidclock", when_idle, "default");
281 const char *sid_clocks[] = {"default", "c64ntsc", "c64pal", "hardsid", 0};
282 str_prop->Set_values(sid_clocks);
283 str_prop->Set_help(
284 "The SID chip's clock frequency, which is jumperable on reproduction cards.\n"
285 " - default: uses 0.895 MHz, per the original SSI-2001 card.\n"
286 " - c64ntsc: uses 1.023 MHz, per NTSC Commodore PCs and the DuoSID.\n"
287 " - c64pal: uses 0.985 MHz, per PAL Commodore PCs and the DuoSID.\n"
288 " - hardsid: uses 1.000 MHz, available on the DuoSID.");
289
290 // IO Address
291 auto *hex_prop = sec_prop.Add_hex("sidport", when_idle, 0x280);
292 const char *sid_ports[] = {"240", "260", "280", "2a0", "2c0", 0};
293 hex_prop->Set_values(sid_ports);
294 hex_prop->Set_help("The IO port address of the Innovation SSI-2001.");
295
296 // Filter strengths
297 auto *int_prop = sec_prop.Add_int("6581filter", when_idle, 50);
298 int_prop->SetMinMax(0, 100);
299 int_prop->Set_help(
300 "The SID's analog filtering meant that each chip was physically unique.\n"
301 "Adjusts the 6581's filtering strength as a percent from 0 to 100.");
302
303 int_prop = sec_prop.Add_int("8580filter", when_idle, 50);
304 int_prop->SetMinMax(0, 100);
305 int_prop->Set_help(
306 "Adjusts the 8580's filtering strength as a percent from 0 to 100.");
307 }
308
INNOVATION_AddConfigSection(Config * conf)309 void INNOVATION_AddConfigSection(Config *conf)
310 {
311 assert(conf);
312 Section_prop *sec = conf->AddSection_prop("innovation",
313 &innovation_init, true);
314 assert(sec);
315 init_innovation_dosbox_settings(*sec);
316 }
317