|
| 1 | +OSD audio support |
| 2 | +================= |
| 3 | + |
| 4 | +Introduction |
| 5 | +------------ |
| 6 | + |
| 7 | +The audio support in Mame tries to allow the user to freely map |
| 8 | +between the emulated system audio outputs (called speakers) and the |
| 9 | +host system audio. A part of it is the OSD support, where a |
| 10 | +host-specific module ensures the interface between Mame and the host. |
| 11 | +This is the documentation for that module. |
| 12 | + |
| 13 | +Note: this is currenty output-only, but input should follow. |
| 14 | + |
| 15 | + |
| 16 | +Capabitilies |
| 17 | +------------ |
| 18 | + |
| 19 | +The OSD interface is designed to allow three levels of support, |
| 20 | +depending on what the API allows and the amount of effort to expend. |
| 21 | +Those are: |
| 22 | + |
| 23 | +* Level 1: One or more audio targets, only one stream allowed per target (aka exclusive mode) |
| 24 | +* Level 2: One or more audio targets, multiple streams per target |
| 25 | +* Level 3: One or more audio targets, multiple streams per target, user-visible per-stream-channel volume control |
| 26 | + |
| 27 | +In any case we support having the user use an external interface to |
| 28 | +change the target of a stream and, in level 3, change the volumes. By |
| 29 | +support we mean storing the information in the per-game configuration |
| 30 | +and keeping in the internal UI in sync. |
| 31 | + |
| 32 | + |
| 33 | +Terminology |
| 34 | +----------- |
| 35 | + |
| 36 | +For this module, we use the terms: |
| 37 | + |
| 38 | +* node: some object we can send audio to. Can be physical, like speakers, or virtual, like an effect system. It should have a unique, user-presentable name for the UI. |
| 39 | +* port: a channel of a node, has a name (non-unique, like "front left") and a 3D position |
| 40 | +* stream: a connection to a node with allows to send audio to it |
| 41 | + |
| 42 | + |
| 43 | +Reference documentation |
| 44 | +----------------------- |
| 45 | + |
| 46 | +Adding a module |
| 47 | +~~~~~~~~~~~~~~~ |
| 48 | + |
| 49 | +Adding a module is done by adding a cpp file to src/osd/modules/sound |
| 50 | +which follows this structure, |
| 51 | + |
| 52 | +.. code-block:: C++ |
| 53 | + |
| 54 | + // License/copyright |
| 55 | + #include "sound_module.h" |
| 56 | + #include "modules/osdmodules.h" |
| 57 | + |
| 58 | + #ifdef MODULE_SUPPORT_KEY |
| 59 | + |
| 60 | + #include "modules/lib/osdobj_common.h" |
| 61 | + |
| 62 | + // [...] |
| 63 | + namespace osd { |
| 64 | + namespace { |
| 65 | + |
| 66 | + class sound_module_class : public osd_module, public sound_module |
| 67 | + { |
| 68 | + sound_module_class() : osd_module(OSD_SOUND_PROVIDER, "module_name"), |
| 69 | + sound_module() |
| 70 | + // ... |
| 71 | + }; |
| 72 | + |
| 73 | + } |
| 74 | + } |
| 75 | + #else |
| 76 | + namespace osd { namespace { |
| 77 | + MODULE_NOT_SUPPORTED(sound_module_class, OSD_SOUND_PROVIDER, "module_name") |
| 78 | + }} |
| 79 | + #endif |
| 80 | + |
| 81 | + MODULE_DEFINITION(SOUND_MODULE_KEY, osd::sound_module_class) |
| 82 | + |
| 83 | +In that code, four names must be chosen: |
| 84 | + |
| 85 | +* MODULE_SUPPORT_KEY some #define coming from the genie scripts to tell that this particular module can be compiled (like NO_USE_PIPEWIRE or SDLMAME_MACOSX) |
| 86 | +* sound_module_class is the name of the class which makes up the module (like sound_coreaudio) |
| 87 | +* module_name is the name to be used in -sound <xxx> to select that particular module (like coreaudio) |
| 88 | +* SOUND_MODULE_KEY is a symbol that represents the module internally (like SOUND_COREAUDIO) |
| 89 | + |
| 90 | +The file path needs to be added to scripts/src/osd/modules.lua in |
| 91 | +osdmodulesbuild() and the module reference to |
| 92 | +src/osd/modules/lib/osdobj_common.cpp in |
| 93 | +osd_common_t::register_options with the line: |
| 94 | + |
| 95 | +.. code-block:: C++ |
| 96 | + |
| 97 | + REGISTER_MODULE(m_mod_man, SOUND_MODULE_KEY); |
| 98 | + |
| 99 | +This should ensure that the module is reachable through -sound <xxx> |
| 100 | +on the appropriate hosts. |
| 101 | + |
| 102 | + |
| 103 | +Interface |
| 104 | +~~~~~~~~~ |
| 105 | + |
| 106 | +The full interface is: |
| 107 | + |
| 108 | +.. code-block:: C++ |
| 109 | + |
| 110 | + virtual bool split_streams_per_source() const override; |
| 111 | + virtual bool external_per_channel_volume() const override; |
| 112 | + |
| 113 | + virtual int init(osd_interface &osd, osd_options const &options) override; |
| 114 | + virtual void exit() override; |
| 115 | + |
| 116 | + virtual uint32_t get_generation() override; |
| 117 | + virtual osd::audio_info get_information() override; |
| 118 | + virtual uint32_t stream_sink_open(uint32_t node, std::string name, uint32_t rate) override; |
| 119 | + virtual void stream_set_volumes(uint32_t id, const std::vector<float> &db) override; |
| 120 | + virtual void stream_close(uint32_t id) override; |
| 121 | + virtual void stream_update(uint32_t id, const int16_t *buffer, int samples_this_frame) override; |
| 122 | +
|
| 123 | + |
| 124 | +The class sound_module provides default for minimum capabilities: one |
| 125 | +stereo target and stream at default sample rate. To support that, |
| 126 | +only *init*, *exit* and *stream_update* need to be implemented. |
| 127 | +*init* is called at startup and *exit* when quitting and can do |
| 128 | +whatever they need to do. *stream_update* will be called on a regular |
| 129 | +basis with a buffer of sample_this_frame*2*int16_t with the audio |
| 130 | +to play. From this point in the documentation we'll assume more than |
| 131 | +a single stereo channel is wanted. |
| 132 | + |
| 133 | + |
| 134 | +Capabilities |
| 135 | +~~~~~~~~~~~~ |
| 136 | + |
| 137 | +Two methods are used by the module to indicate the level of capability |
| 138 | +of the module: |
| 139 | + |
| 140 | +* split_streams_per_source() should return true when having multiple streams for one target is expected (e.g. Level 2 or 3) |
| 141 | +* external_per_channel_volume() should return true when the streams have per-channel volume control that can be externally controlled (e.g. Level 3) |
| 142 | + |
| 143 | + |
| 144 | +Hardware information and generations |
| 145 | +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
| 146 | + |
| 147 | +The core runs on the assumption that the host hardware capabilities |
| 148 | +can change at any time (bluetooth devices coming and going, usb |
| 149 | +hot-plugging...) and that the module has some way to keep tabs on what |
| 150 | +is happening, possibly using multi-threading. To keep it |
| 151 | +lightweight-ish, we use the concept of a *generation* which is a |
| 152 | +32-bits number that is incremented by the module every time something |
| 153 | +changes. The core checks the current generation value at least once |
| 154 | +every update (once per frame, usually) and if it changed asks for the |
| 155 | +new state and detects and handles the differences. *generation* |
| 156 | +should be "eventually stable", e.g. it eventually stops changing when |
| 157 | +the user stops changing things all the time. A systematic increment |
| 158 | +every frame would be a bad idea. |
| 159 | + |
| 160 | +.. code-block:: C++ |
| 161 | + |
| 162 | + virtual uint32_t get_generation() override; |
| 163 | + |
| 164 | +That method returns the current generation number. It's called at a |
| 165 | +minimum once per update, which usually means per frame. It whould be |
| 166 | +reasonably lightweight when nothing special happens. |
| 167 | + |
| 168 | +.. code-block: C++ |
| 169 | +
|
| 170 | + virtual osd::audio_info get_information() override; |
| 171 | +
|
| 172 | + struct audio_rate_range { |
| 173 | + uint32_t m_default_rate; |
| 174 | + uint32_t m_min_rate; |
| 175 | + uint32_t m_max_rate; |
| 176 | + }; |
| 177 | +
|
| 178 | + struct audio_info { |
| 179 | + struct port_info { |
| 180 | + std::string m_name; |
| 181 | + std::array<double, 3> m_position; |
| 182 | + }; |
| 183 | +
|
| 184 | + struct node_info { |
| 185 | + std::string m_name; |
| 186 | + uint32_t m_id; |
| 187 | + audio_rate_range m_rate; |
| 188 | + std::vector<port_info> m_sinks; |
| 189 | + std::vector<port_info> m_sources; |
| 190 | + }; |
| 191 | +
|
| 192 | + struct stream_info { |
| 193 | + uint32_t m_id; |
| 194 | + uint32_t m_node; |
| 195 | + std::vector<float> m_volumes; |
| 196 | + }; |
| 197 | +
|
| 198 | + uint32_t m_generation; |
| 199 | + uint32_t m_default_sink; |
| 200 | + uint32_t m_default_source; |
| 201 | + std::vector<node_info> m_nodes; |
| 202 | + std::vector<stream_info> m_streams; |
| 203 | + }; |
| 204 | +
|
| 205 | +This method must provide all the information about the current state |
| 206 | +of the host and the module. This state is: |
| 207 | + |
| 208 | +* m_generation: The current generation number |
| 209 | +* m_nodes: The vector available nodes (*node_info*) |
| 210 | + |
| 211 | + * m_name: The name of the node |
| 212 | + * m_id: The numeric ID of the node |
| 213 | + * m_rate: The minimum, maximum and preferred sample rate for the node |
| 214 | + * m_sinks: The vector of sink (output) ports of the node (*port_info*) |
| 215 | + |
| 216 | + * m_name: The name of the port |
| 217 | + * m_position: The 3D position of the port. Refer to src/emu/speaker.h for the "standard" positions |
| 218 | + |
| 219 | + * m_sources: The vector of source (input) ports of the node. Currently unused |
| 220 | + |
| 221 | +* m_default_sink: ID of the node that is the current "system default" for audio output, 0 if there's no such concept |
| 222 | +* m_default_source: same for audio input (currently unused) |
| 223 | +* m_streams: The vector of active streams (*stream_info*) |
| 224 | + |
| 225 | + * m_id: The numeric ID of the stream |
| 226 | + * m_node: The target node of the stream |
| 227 | + * m_volumes: empty if *external_per_channel_volume* is false, current volume value per-channel otherwise |
| 228 | + |
| 229 | +IDs, for nodes and streams, are (independant) 32-bit unsigned non-zero |
| 230 | +values associated to respectively nodes and streams. IDs should not |
| 231 | +be reused. A node that goes away then comes back should get a new ID. |
| 232 | +A stream that is closed should not enable reuse of its ID. |
| 233 | + |
| 234 | +When external control exists, a module should change the value of |
| 235 | +*stream_info::m_node* when the user changes it, and same for |
| 236 | +*stream_info::m_volumes*. Generation number should be incremented |
| 237 | +when this happens, so that the core knows to look for changes. |
| 238 | + |
| 239 | +Volumes are floats in dB, where 0 means 100% and -96 means no sound. |
| 240 | +audio.h provides osd::db_to_linear and osd::linear_to_db if such a |
| 241 | +conversion is needed. |
| 242 | + |
| 243 | +There is an inherent race condition with this system, because things |
| 244 | +can change at any point after returning for the method. The idea is |
| 245 | +that the information returned must be internally consistent (a stream |
| 246 | +should not point to a node ID that does not exist in the structure, |
| 247 | +same for default sink) and that any external change from that state |
| 248 | +should increment the generation number, but that's it. Through the |
| 249 | +generation system the core will eventually be in sync with the |
| 250 | +reality. |
| 251 | + |
| 252 | + |
| 253 | +Output streams |
| 254 | +~~~~~~~~~~~~~~ |
| 255 | + |
| 256 | +.. code-block: C++ |
| 257 | +
|
| 258 | + virtual uint32_t stream_sink_open(uint32_t node, std::string name, uint32_t rate) override; |
| 259 | + virtual void stream_set_volumes(uint32_t id, const std::vector<float> &db) override; |
| 260 | + virtual void stream_close(uint32_t id) override; |
| 261 | + virtual void stream_update(uint32_t id, const int16_t *buffer, int samples_this_frame) override; |
| 262 | +
|
| 263 | +Streams are the concept used to send audio to the host audio system. |
| 264 | +A stream is first opened through *stream_sink_open* and targets a |
| 265 | +specific node at a specific sample rate. It is given a name for use |
| 266 | +by the host sound services for user UI purposes (currently the game |
| 267 | +name if split_streams_per_source is false, the speaker_device tag if |
| 268 | +true). The returned ID must be a non-zero, never-used-before for |
| 269 | +streams value in case of success. Failures, like when the node went |
| 270 | +away between the get_information call and the open one, should be |
| 271 | +silent and return zero. |
| 272 | + |
| 273 | +*stream_set_volumes* is used only then *external_per_channel_volume* |
| 274 | +is true and is used by the core to set the per-channel volume. The |
| 275 | +call should just be ignored if the stream ID does not exist (or is |
| 276 | +zero). Do not try to apply volumes in the module if the host API |
| 277 | +doesn't provide for it, let the core handle it. |
| 278 | + |
| 279 | +*stream_close* closes a stream, The call should just be ignored if the |
| 280 | +stream ID does not exist (or is zero). |
| 281 | + |
| 282 | +Opening a stream, closing a stream or changing the volume does not |
| 283 | +need to touch the generation number. |
| 284 | + |
| 285 | +*stream_update* is the method used to send data to the node through a |
| 286 | +given stream. It provides a buffer of *samples_this_frame* * *node |
| 287 | +channel count* channel-interleaved int16_t values. The lifetime of |
| 288 | +the data in the buffer or the buffer pointer itself is undefined after |
| 289 | +return from the method call. The call should just be ignored if the |
| 290 | +stream ID does not exist (or is zero). |
| 291 | + |
| 292 | +When a stream goes away because the target node is lost it should just |
| 293 | +be removed from the information, and the core will pick up the node |
| 294 | +departure and close the stream. |
| 295 | + |
| 296 | +Given the assumed raceness of the interface, all the methods should be |
| 297 | +tolerant of obsolete or zero IDs being used by the core, and that is |
| 298 | +why ID reuse must be avoided. |
| 299 | + |
0 commit comments