openMSX
SoundDevice.cc
Go to the documentation of this file.
1#include "SoundDevice.hh"
2
3#include "MSXMixer.hh"
4#include "DeviceConfig.hh"
5#include "Mixer.hh"
6#include "XMLElement.hh"
7#include "Filename.hh"
8#include "StringOp.hh"
9#include "MSXException.hh"
10
11#include "inplace_buffer.hh"
12#include "narrow.hh"
13#include "ranges.hh"
14#include "one_of.hh"
15#include "xrange.hh"
16
17#include <bit>
18#include <cassert>
19#include <cmath>
20
21namespace openmsx {
22
23[[nodiscard]] static std::string makeUnique(const MSXMixer& mixer, std::string_view name)
24{
25 std::string result(name);
26 if (mixer.findDevice(result)) {
27 unsigned n = 0;
28 do {
29 result = strCat(name, " (", ++n, ')');
30 } while (mixer.findDevice(result));
31 }
32 return result;
33}
34
35void SoundDevice::addFill(float*& buf, float val, unsigned num)
36{
37 // Note: in the past we tried to optimize this by always producing
38 // a multiple of 4 output values. In the general case a SoundDevice is
39 // allowed to do this, but only at the end of the sound buffer. This
40 // method can also be called in the middle of a buffer (so multiple
41 // times per buffer), in such case it does go wrong.
42 assert(num > 0);
43 do {
44 *buf++ += val;
45 } while (--num);
46}
47
48SoundDevice::SoundDevice(MSXMixer& mixer_, std::string_view name_, static_string_view description_,
49 unsigned numChannels_, unsigned inputRate, bool stereo_)
50 : mixer(mixer_)
51 , name(makeUnique(mixer, name_))
52 , description(description_)
53 , numChannels(numChannels_)
54 , stereo(stereo_ ? 2 : 1)
55{
56 assert(numChannels <= MAX_CHANNELS);
57 assert(stereo == one_of(1u, 2u));
58
59 setInputRate(inputRate);
60
61 // initially no channels are muted
62 ranges::fill(channelMuted, false);
63 ranges::fill(channelBalance, Balance{1.0f, 1.0f});
64}
65
67
69{
70 return 1.0f / 32768.0f;
71}
72
74{
75 const auto& soundConfig = config.getChild("sound");
76 float volume = narrow<float>(soundConfig.getChildDataAsInt("volume", 0)) * (1.0f / 32767.0f);
77 int devBalance = 0;
78 std::string_view mode = soundConfig.getChildData("mode", "mono");
79 if (mode == "mono") {
80 devBalance = 0;
81 } else if (mode == "left") {
82 devBalance = -100;
83 } else if (mode == "right") {
84 devBalance = 100;
85 } else {
86 throw MSXException("balance \"", mode, "\" illegal");
87 }
88
89 for (const auto* b : soundConfig.getChildren("balance")) {
90 auto balance = StringOp::stringTo<int>(b->getData());
91 if (!balance) {
92 throw MSXException("balance ", b->getData(), " illegal");
93 }
94 if ((*balance < -100) || (balance > 100)) {
95 throw MSXException("balance must be between -100...100: ", *balance);
96 }
97
98 const auto* channel = b->findAttribute("channel");
99 if (!channel) {
100 devBalance = *balance;
101 continue;
102 }
103
104 auto channels = StringOp::parseRange(channel->getValue(), 1, numChannels);
105 channels.foreachSetBit([&](size_t c) {
106 setBalance(unsigned(c - 1), narrow_cast<float>(*balance) * (1.0f / 100.0f));
107 });
108 }
109
110 mixer.registerSound(*this, volume, devBalance, numChannels);
111}
112
113void SoundDevice::setBalance(unsigned channel, float balance)
114{
115 assert(channel < numChannels);
116 assert(-1.0f <= balance);
117 assert(balance <= 1.0f);
118
119 if (balance != 0.0f) balanceCenter = false;
120 auto left = std::sqrt(1.0f - balance);
121 auto right = std::sqrt(1.0f + balance);
122 channelBalance[channel] = Balance{left, right};
123}
124
126{
127 mixer.updateSoftwareVolume(*this);
128}
129
131{
132 mixer.unregisterSound(*this);
133}
134
135void SoundDevice::updateStream(EmuTime::param time)
136{
137 mixer.updateStream(time);
138}
139
140void SoundDevice::setSoftwareVolume(float volume, EmuTime::param time)
141{
142 setSoftwareVolume(volume, volume, time);
143}
144
145void SoundDevice::setSoftwareVolume(float left, float right, EmuTime::param time)
146{
147 updateStream(time);
148 softwareVolumeLeft = left;
149 softwareVolumeRight = right;
150 mixer.updateSoftwareVolume(*this);
151}
152
153void SoundDevice::recordChannel(unsigned channel, const Filename& filename)
154{
155 assert(channel < numChannels);
156 bool wasRecording = writer[channel].has_value();
157 if (!filename.empty()) {
158 writer[channel].emplace(
159 filename, stereo, inputSampleRate);
160 } else {
161 writer[channel].reset();
162 }
163 bool recording = writer[channel].has_value();
164 if (recording != wasRecording) {
165 if (recording) {
166 if (numRecordChannels == 0) {
167 mixer.setSynchronousMode(true);
168 }
169 ++numRecordChannels;
170 assert(numRecordChannels <= numChannels);
171 } else {
172 assert(numRecordChannels > 0);
173 --numRecordChannels;
174 if (numRecordChannels == 0) {
175 mixer.setSynchronousMode(false);
176 }
177 }
178 }
179}
180
181void SoundDevice::muteChannel(unsigned channel, bool muted)
182{
183 assert(channel < numChannels);
184 channelMuted[channel] = muted;
185}
186
187std::span<const float> SoundDevice::getLastBuffer(unsigned channel)
188{
189 assert(channel < numChannels);
190 auto& buf = channelBuffers[channel];
191
192 buf.requestCounter = inputSampleRate; // active for ~1 second
193
194 unsigned requestedSize = getLastBufferSize();
195 if (buf.stopIdx < requestedSize) return {}; // not enough samples (yet)
196 if (buf.silent >= requestedSize) return {}; // silent
197 return {&buf.buffer[buf.stopIdx - requestedSize], requestedSize};
198}
199
200bool SoundDevice::mixChannels(float* dataOut, size_t samples)
201{
202 if (samples == 0) return true;
203 size_t outputStereo = isStereo() ? 2 : 1;
204
206
207 // TODO optimization: All channels with the same balance (according to
208 // channelBalance[]) could use the same buffer when balanceCenter is
209 // false
210 auto needSeparateBuffer = [&](unsigned channel) {
211 return channelBuffers[channel].requestCounter != 0
212 || channelMuted[channel]
213 || writer[channel]
214 || !balanceCenter;
215 };
216 bool anySeparateChannel = false;
217 auto size = narrow<unsigned>(samples * stereo);
218 auto padded = (size + 3) & ~3; // round up to multiple of 4
219 for (auto i : xrange(numChannels)) {
220 auto& cb = channelBuffers[i];
221 if (!needSeparateBuffer(i)) {
222 // no need to keep this channel separate
223 bufs[i] = dataOut;
224 cb.stopIdx = 0; // no valid last data
225 } else {
226 anySeparateChannel = true;
227 cb.requestCounter = (cb.requestCounter < samples) ? 0 : unsigned(cb.requestCounter - samples);
228
229 if (auto remainingSize = narrow<unsigned>(cb.buffer.size() - cb.stopIdx);
230 remainingSize < padded) {
231 // not enough space at the tail of the buffer
232 unsigned lastBufferSize = getLastBufferSize();
233 if (auto allocateSize = 2 * std::max(lastBufferSize, padded);
234 cb.buffer.size() < allocateSize) [[unlikely]] {
235 // increase buffer size
236 cb.buffer.resize(allocateSize);
237 }
238 unsigned reuse = lastBufferSize >= size ? lastBufferSize - size : 0;
239 if (cb.stopIdx > reuse) {
240 // move existing data to front
241 memmove(&cb.buffer[0], &cb.buffer[cb.stopIdx - reuse], reuse * sizeof(float));
242 }
243 cb.stopIdx = reuse;
244 }
245 auto* ptr = &cb.buffer[cb.stopIdx];
246 bufs[i] = ptr;
247 ranges::fill(std::span{ptr, size}, 0.0f);
248 cb.stopIdx += size;
249 }
250 }
251
252 static_assert(sizeof(float) == sizeof(uint32_t));
253 if ((numChannels != 1) || anySeparateChannel) {
254 // The generateChannels() method of SoundDevices with more than
255 // one channel will _add_ the generated channel data in the
256 // provided buffers. Those with only one channel will directly
257 // replace the content of the buffer. For the former we must
258 // start from a buffer containing all zeros.
259 ranges::fill(std::span{dataOut, outputStereo * samples}, 0.0f);
260 }
261
262 generateChannels(bufs, narrow<unsigned>(samples));
263
264 if (!anySeparateChannel) {
265 return ranges::any_of(xrange(numChannels),
266 [&](auto i) { return bufs[i]; });
267 }
268
269 for (auto i : xrange(numChannels)) {
270 // record channels
271 if (writer[i]) {
272 assert(bufs[i] != dataOut);
273 if (bufs[i]) {
274 auto amp = getAmplificationFactor();
275 if (stereo == 1) {
276 writer[i]->write(
277 std::span{bufs[i], samples},
278 amp.left);
279 } else {
280 writer[i]->write(
281 std::span{std::bit_cast<const StereoFloat*>(bufs[i]), samples},
282 amp.left, amp.right);
283 }
284 } else {
285 writer[i]->writeSilence(narrow<unsigned>(stereo * samples));
286 }
287 }
288 // update silent info in channelBuffers
289 auto& cb = channelBuffers[i];
290 if (bufs[i]) {
291 cb.silent = 0;
292 } else {
293 cb.silent += narrow<unsigned>(samples);
294 }
295 }
296
297 // remove muted channels (explicitly by user or by device itself)
298 bool anyUnmuted = false;
299 unsigned numMix = 0;
301 for (auto i : xrange(numChannels)) {
302 if (bufs[i] && !channelMuted[i]) {
303 anyUnmuted = true;
304 if (bufs[i] != dataOut) {
305 bufs[numMix] = bufs[i];
306 mixBalance[numMix] = channelBalance[i];
307 ++numMix;
308 }
309 }
310 }
311
312 if (numMix == 0) {
313 // all extra channels muted
314 return anyUnmuted;
315 }
316
317 // actually mix channels
318 if (!balanceCenter) {
319 size_t i = 0;
320 do {
321 float left0 = 0.0f;
322 float right0 = 0.0f;
323 float left1 = 0.0f;
324 float right1 = 0.0f;
325 unsigned j = 0;
326 do {
327 left0 += bufs[j][i + 0] * mixBalance[j].left;
328 left1 += bufs[j][i + 1] * mixBalance[j].left;
329 right0 += bufs[j][i + 0] * mixBalance[j].right;
330 right1 += bufs[j][i + 1] * mixBalance[j].right;
331 j++;
332 } while (j < numMix);
333 dataOut[i * 2 + 0] = left0;
334 dataOut[i * 2 + 1] = right0;
335 dataOut[i * 2 + 2] = left1;
336 dataOut[i * 2 + 3] = right1;
337 i += 2;
338 } while (i < samples);
339
340 return true;
341 }
342
343 // In the past we had ARM and x86-SSE2 optimized assembly routines for
344 // the stuff below. Currently this code is only rarely used anymore
345 // (only when recording or muting individual sound chip channels), so
346 // it's not worth the extra complexity anymore.
347 size_t num = samples * stereo;
348 size_t i = 0;
349 do {
350 auto out0 = dataOut[i + 0];
351 auto out1 = dataOut[i + 1];
352 auto out2 = dataOut[i + 2];
353 auto out3 = dataOut[i + 3];
354 unsigned j = 0;
355 do {
356 out0 += bufs[j][i + 0];
357 out1 += bufs[j][i + 1];
358 out2 += bufs[j][i + 2];
359 out3 += bufs[j][i + 3];
360 ++j;
361 } while (j < numMix);
362 dataOut[i + 0] = out0;
363 dataOut[i + 1] = out1;
364 dataOut[i + 2] = out2;
365 dataOut[i + 3] = out3;
366 i += 4;
367 } while (i < num);
368
369 return true;
370}
371
373{
374 return mixer.getHostSampleClock();
375}
377{
378 return mixer.getEffectiveSpeed();
379}
380
381} // namespace openmsx
const XMLElement & getChild(std::string_view name) const
Represents a clock with a variable frequency.
This class represents a filename.
Definition Filename.hh:20
bool empty() const
Convenience method to test for empty filename.
Definition Filename.cc:21
double getEffectiveSpeed() const
void recordChannel(unsigned channel, const Filename &filename)
void updateStream(EmuTime::param time)
unsigned getLastBufferSize() const
AmplificationFactors getAmplificationFactor() const
static void addFill(float *&buffer, float value, unsigned num)
Adds a number of samples that all have the same value.
const DynamicClock & getHostSampleClock() const
See MSXMixer::getHostSampleClock().
void setInputRate(unsigned sampleRate)
void setSoftwareVolume(float volume, EmuTime::param time)
Change the 'software volume' of this sound device.
bool mixChannels(float *dataOut, size_t samples)
Calls generateChannels() and combines the output to a single channel.
static constexpr unsigned MAX_CHANNELS
void unregisterSound()
Unregisters this sound device with the Mixer.
SoundDevice(const SoundDevice &)=delete
bool isStereo() const
Is the full output of this device stereo?
virtual void generateChannels(std::span< float * > buffers, unsigned num)=0
Abstract method to generate the actual sound data.
void setBalance(unsigned channel, float balance)
Change the balance of a single channel.
virtual void postSetBalance()
void registerSound(const DeviceConfig &config)
Registers this sound device with the Mixer.
std::span< const float > getLastBuffer(unsigned channel)
Query the last generated audio signal for a specific channel.
void muteChannel(unsigned channel, bool muted)
virtual float getAmplificationFactorImpl() const
Get amplification/attenuation factor for this device.
std::string_view getChildData(std::string_view childName) const
Definition XMLElement.cc:64
static_string_view
IterableBitSet< 64 > parseRange(string_view str, unsigned min, unsigned max)
Definition StringOp.cc:181
This file implemented 3 utility functions:
Definition Autofire.cc:11
constexpr void fill(ForwardRange &&range, const T &value)
Definition ranges.hh:315
constexpr bool any_of(InputRange &&range, UnaryPredicate pred)
Definition ranges.hh:200
std::string strCat()
Definition strCat.hh:703
constexpr auto xrange(T e)
Definition xrange.hh:132