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