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 "MemBuffer.hh"
10#include "MSXException.hh"
11
12#include "aligned.hh"
13#include "narrow.hh"
14#include "ranges.hh"
15#include "one_of.hh"
16#include "vla.hh"
17#include "xrange.hh"
18
19#include <array>
20#include <bit>
21#include <cassert>
22#include <memory>
23
24namespace openmsx {
25
26static MemBuffer<float, SSE_ALIGNMENT> mixBuffer;
27static size_t mixBufferSize = 0;
28
29static void allocateMixBuffer(size_t size)
30{
31 if (mixBufferSize < size) [[unlikely]] {
32 mixBufferSize = size;
33 mixBuffer.resize(mixBufferSize);
34 }
35}
36
37[[nodiscard]] static std::string makeUnique(MSXMixer& mixer, std::string_view name)
38{
39 std::string result(name);
40 if (mixer.findDevice(result)) {
41 unsigned n = 0;
42 do {
43 result = strCat(name, " (", ++n, ')');
44 } while (mixer.findDevice(result));
45 }
46 return result;
47}
48
49void SoundDevice::addFill(float*& buf, float val, unsigned num)
50{
51 // Note: in the past we tried to optimize this by always producing
52 // a multiple of 4 output values. In the general case a SoundDevice is
53 // allowed to do this, but only at the end of the sound buffer. This
54 // method can also be called in the middle of a buffer (so multiple
55 // times per buffer), in such case it does go wrong.
56 assert(num > 0);
57 do {
58 *buf++ += val;
59 } while (--num);
60}
61
62SoundDevice::SoundDevice(MSXMixer& mixer_, std::string_view name_, static_string_view description_,
63 unsigned numChannels_, unsigned inputRate, bool stereo_)
64 : mixer(mixer_)
65 , name(makeUnique(mixer, name_))
66 , description(description_)
67 , numChannels(numChannels_)
68 , stereo(stereo_ ? 2 : 1)
69{
70 assert(numChannels <= MAX_CHANNELS);
71 assert(stereo == one_of(1u, 2u));
72
73 setInputRate(inputRate);
74
75 // initially no channels are muted
76 ranges::fill(channelMuted, false);
77 ranges::fill(channelBalance, 0);
78}
79
81
83{
84 return stereo == 2 || !balanceCenter;
85}
86
88{
89 return 1.0f / 32768.0f;
90}
91
93{
94 const auto& soundConfig = config.getChild("sound");
95 float volume = narrow<float>(soundConfig.getChildDataAsInt("volume", 0)) * (1.0f / 32767.0f);
96 int devBalance = 0;
97 std::string_view mode = soundConfig.getChildData("mode", "mono");
98 if (mode == "mono") {
99 devBalance = 0;
100 } else if (mode == "left") {
101 devBalance = -100;
102 } else if (mode == "right") {
103 devBalance = 100;
104 } else {
105 throw MSXException("balance \"", mode, "\" illegal");
106 }
107
108 for (const auto* b : soundConfig.getChildren("balance")) {
109 auto balance = StringOp::stringTo<int>(b->getData());
110 if (!balance) {
111 throw MSXException("balance ", b->getData(), " illegal");
112 }
113
114 const auto* channel = b->findAttribute("channel");
115 if (!channel) {
116 devBalance = *balance;
117 continue;
118 }
119
120 // TODO Support other balances
121 if (*balance != one_of(0, -100, 100)) {
122 throw MSXException("balance ", *balance, " illegal");
123 }
124 if (*balance != 0) {
125 balanceCenter = false;
126 }
127
128 auto channels = StringOp::parseRange(channel->getValue(), 1, numChannels);
129 channels.foreachSetBit([&](size_t c) {
130 channelBalance[c - 1] = *balance;
131 });
132 }
133
134 mixer.registerSound(*this, volume, devBalance, numChannels);
135}
136
138{
139 mixer.unregisterSound(*this);
140}
141
142void SoundDevice::updateStream(EmuTime::param time)
143{
144 mixer.updateStream(time);
145}
146
147void SoundDevice::setSoftwareVolume(float volume, EmuTime::param time)
148{
149 setSoftwareVolume(volume, volume, time);
150}
151
152void SoundDevice::setSoftwareVolume(float left, float right, EmuTime::param time)
153{
154 updateStream(time);
155 softwareVolumeLeft = left;
156 softwareVolumeRight = right;
157 mixer.updateSoftwareVolume(*this);
158}
159
160void SoundDevice::recordChannel(unsigned channel, const Filename& filename)
161{
162 assert(channel < numChannels);
163 bool wasRecording = writer[channel].has_value();
164 if (!filename.empty()) {
165 writer[channel].emplace(
166 filename, stereo, inputSampleRate);
167 } else {
168 writer[channel].reset();
169 }
170 bool recording = writer[channel].has_value();
171 if (recording != wasRecording) {
172 if (recording) {
173 if (numRecordChannels == 0) {
174 mixer.setSynchronousMode(true);
175 }
176 ++numRecordChannels;
177 assert(numRecordChannels <= numChannels);
178 } else {
179 assert(numRecordChannels > 0);
180 --numRecordChannels;
181 if (numRecordChannels == 0) {
182 mixer.setSynchronousMode(false);
183 }
184 }
185 }
186}
187
188void SoundDevice::muteChannel(unsigned channel, bool muted)
189{
190 assert(channel < numChannels);
191 channelMuted[channel] = muted;
192}
193
194bool SoundDevice::mixChannels(float* dataOut, size_t samples)
195{
196#ifdef __SSE2__
197 assert((uintptr_t(dataOut) & 15) == 0); // must be 16-byte aligned
198#endif
199 if (samples == 0) return true;
200 size_t outputStereo = isStereo() ? 2 : 1;
201
202 std::array<float*, MAX_CHANNELS> bufs_;
203 auto bufs = subspan(bufs_, 0, numChannels);
204
205 unsigned separateChannels = 0;
206 size_t pitch = (samples * stereo + 3) & ~3; // align for SSE access
207 // TODO optimization: All channels with the same balance (according to
208 // channelBalance[]) could use the same buffer when balanceCenter is
209 // false
210 for (auto i : xrange(numChannels)) {
211 if (!channelMuted[i] && !writer[i] && balanceCenter) {
212 // no need to keep this channel separate
213 bufs[i] = dataOut;
214 } else {
215 // muted or recorded channels must go separate
216 // cannot yet fill in bufs[i] here
217 ++separateChannels;
218 }
219 }
220
221 static_assert(sizeof(float) == sizeof(uint32_t));
222 if ((numChannels != 1) || separateChannels) {
223 // The generateChannels() method of SoundDevices with more than
224 // one channel will _add_ the generated channel data in the
225 // provided buffers. Those with only one channel will directly
226 // replace the content of the buffer. For the former we must
227 // start from a buffer containing all zeros.
228 ranges::fill(std::span{dataOut, outputStereo * samples}, 0.0f);
229 }
230
231 if (separateChannels) {
232 allocateMixBuffer(pitch * separateChannels);
233 ranges::fill(std::span{mixBuffer.data(), pitch * separateChannels}, 0.0f);
234 // still need to fill in (some) bufs[i] pointers
235 unsigned count = 0;
236 for (auto i : xrange(numChannels)) {
237 if (channelMuted[i] || writer[i] || !balanceCenter) {
238 bufs[i] = &mixBuffer[pitch * count++];
239 }
240 }
241 assert(count == separateChannels);
242 }
243
244 generateChannels(bufs, narrow<unsigned>(samples));
245
246 if (separateChannels == 0) {
247 return ranges::any_of(xrange(numChannels),
248 [&](auto i) { return bufs[i]; });
249 }
250
251 // record channels
252 for (auto i : xrange(numChannels)) {
253 if (writer[i]) {
254 assert(bufs[i] != dataOut);
255 if (bufs[i]) {
256 auto amp = getAmplificationFactor();
257 if (stereo == 1) {
258 writer[i]->write(
259 std::span{bufs[i], samples},
260 amp.left);
261 } else {
262 writer[i]->write(
263 std::span{std::bit_cast<const StereoFloat*>(bufs[i]), samples},
264 amp.left, amp.right);
265 }
266 } else {
267 writer[i]->writeSilence(narrow<unsigned>(stereo * samples));
268 }
269 }
270 }
271
272 // remove muted channels (explicitly by user or by device itself)
273 bool anyUnmuted = false;
274 unsigned numMix = 0;
275 VLA(int, mixBalance, numChannels);
276 for (auto i : xrange(numChannels)) {
277 if (bufs[i] && !channelMuted[i]) {
278 anyUnmuted = true;
279 if (bufs[i] != dataOut) {
280 bufs[numMix] = bufs[i];
281 mixBalance[numMix] = channelBalance[i];
282 ++numMix;
283 }
284 }
285 }
286
287 if (numMix == 0) {
288 // all extra channels muted
289 return anyUnmuted;
290 }
291
292 // actually mix channels
293 if (!balanceCenter) {
294 size_t i = 0;
295 do {
296 float left0 = 0.0f;
297 float right0 = 0.0f;
298 float left1 = 0.0f;
299 float right1 = 0.0f;
300 unsigned j = 0;
301 do {
302 if (mixBalance[j] <= 0) {
303 left0 += bufs[j][i + 0];
304 left1 += bufs[j][i + 1];
305 }
306 if (mixBalance[j] >= 0) {
307 right0 += bufs[j][i + 0];
308 right1 += bufs[j][i + 1];
309 }
310 j++;
311 } while (j < numMix);
312 dataOut[i * 2 + 0] = left0;
313 dataOut[i * 2 + 1] = right0;
314 dataOut[i * 2 + 2] = left1;
315 dataOut[i * 2 + 3] = right1;
316 i += 2;
317 } while (i < samples);
318
319 return true;
320 }
321
322 // In the past we had ARM and x86-SSE2 optimized assembly routines for
323 // the stuff below. Currently this code is only rarely used anymore
324 // (only when recording or muting individual sound chip channels), so
325 // it's not worth the extra complexity anymore.
326 size_t num = samples * stereo;
327 size_t i = 0;
328 do {
329 auto out0 = dataOut[i + 0];
330 auto out1 = dataOut[i + 1];
331 auto out2 = dataOut[i + 2];
332 auto out3 = dataOut[i + 3];
333 unsigned j = 0;
334 do {
335 out0 += bufs[j][i + 0];
336 out1 += bufs[j][i + 1];
337 out2 += bufs[j][i + 2];
338 out3 += bufs[j][i + 3];
339 ++j;
340 } while (j < numMix);
341 dataOut[i + 0] = out0;
342 dataOut[i + 1] = out1;
343 dataOut[i + 2] = out2;
344 dataOut[i + 3] = out3;
345 i += 4;
346 } while (i < num);
347
348 return true;
349}
350
352{
353 return mixer.getHostSampleClock();
354}
356{
357 return mixer.getEffectiveSpeed();
358}
359
360} // 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
void resize(size_t size)
Grow or shrink the memory block.
Definition MemBuffer.hh:111
const T * data() const
Returns pointer to the start of the memory buffer.
Definition MemBuffer.hh:81
double getEffectiveSpeed() const
void recordChannel(unsigned channel, const Filename &filename)
void updateStream(EmuTime::param time)
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 this a stereo device? This is set in the constructor and cannot be changed anymore.
virtual void generateChannels(std::span< float * > buffers, unsigned num)=0
Abstract method to generate the actual sound data.
void registerSound(const DeviceConfig &config)
Registers this sound device with the Mixer.
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:62
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:9
bool any_of(InputRange &&range, UnaryPredicate pred)
Definition ranges.hh:198
constexpr void fill(ForwardRange &&range, const T &value)
Definition ranges.hh:305
size_t size(std::string_view utf8)
constexpr auto subspan(Range &&range, size_t offset, size_t count=std::dynamic_extent)
Definition ranges.hh:471
std::string strCat()
Definition strCat.hh:703
#define VLA(TYPE, NAME, LENGTH)
Definition vla.hh:12
constexpr auto xrange(T e)
Definition xrange.hh:132