From f660238ab5e3d0abdbcb2804a346baf22c300504 Mon Sep 17 00:00:00 2001 From: Coen Rampen Date: Sat, 15 May 2021 22:03:28 +0200 Subject: [PATCH] AUDIO: Move MIDI multisource to base class This moves the multisource functionality from the MT-32/GM MIDI driver to a new base class so it can be reused by future multisource drivers. --- audio/mididrv_ms.cpp | 254 ++++++++++++++++++++++++++++++ audio/mididrv_ms.h | 365 +++++++++++++++++++++++++++++++++++++++++++ audio/miles.h | 9 +- audio/miles_midi.cpp | 17 +- audio/module.mk | 1 + audio/mt32gm.cpp | 239 +++++----------------------- audio/mt32gm.h | 202 +++--------------------- 7 files changed, 689 insertions(+), 398 deletions(-) create mode 100644 audio/mididrv_ms.cpp create mode 100644 audio/mididrv_ms.h diff --git a/audio/mididrv_ms.cpp b/audio/mididrv_ms.cpp new file mode 100644 index 00000000000..aac6d41264f --- /dev/null +++ b/audio/mididrv_ms.cpp @@ -0,0 +1,254 @@ +/* ScummVM - Graphic Adventure Engine + * + * ScummVM is the legal property of its developers, whose names + * are too numerous to list here. Please refer to the COPYRIGHT + * file distributed with this source distribution. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#include "common/config-manager.h" + +#include "audio/mididrv_ms.h" + +const uint8 MidiDriver_Multisource::MAXIMUM_SOURCES; +const uint16 MidiDriver_Multisource::DEFAULT_SOURCE_NEUTRAL_VOLUME; +const uint16 MidiDriver_Multisource::FADING_DELAY; + +MidiDriver_Multisource::MidiSource::MidiSource() : + type(SOURCE_TYPE_UNDEFINED), + volume(DEFAULT_SOURCE_NEUTRAL_VOLUME), + neutralVolume(DEFAULT_SOURCE_NEUTRAL_VOLUME), + fadeStartVolume(0), + fadeEndVolume(0), + fadePassedTime(0), + fadeDuration(0) { } + +MidiDriver_Multisource::MidiDriver_Multisource() : + _userVolumeScaling(false), + _userMusicVolume(192), + _userSfxVolume(192), + _userMute(false), + _timerRate(0), + _fadeDelay(0), + _timer_param(0), + _timer_proc(0) { + for (int i = 0; i < MAXIMUM_SOURCES; ++i) { + // Default source type: 0 = music, 1+ = SFX + _sources[i].type = (i == 0 ? SOURCE_TYPE_MUSIC : SOURCE_TYPE_SFX); + } +} + +void MidiDriver_Multisource::send(uint32 b) { + send(-1, b); +} + +uint32 MidiDriver_Multisource::property(int prop, uint32 param) { + switch (prop) { + case PROP_USER_VOLUME_SCALING: + if (param == 0xFFFF) + return _userVolumeScaling ? 1 : 0; + _userVolumeScaling = param > 0; + break; + default: + return MidiDriver::property(prop, param); + } + return 0; +} + +void MidiDriver_Multisource::startFade(uint16 duration, uint16 targetVolume) { + for (int i = 0; i < MAXIMUM_SOURCES; ++i) { + startFade(i, duration, targetVolume); + } +} + +void MidiDriver_Multisource::startFade(uint8 source, uint16 duration, uint16 targetVolume) { + Common::StackLock lock(_fadingMutex); + + assert(source < MAXIMUM_SOURCES); + + // Reset the number of microseconds which have passed since the start of + // the fade. + _sources[source].fadePassedTime = 0; + // Set start volume to current volume. + _sources[source].fadeStartVolume = _sources[source].volume; + _sources[source].fadeEndVolume = targetVolume; + // Convert to microseconds and set the duration. A duration > 0 will cause + // the fade to be processed by updateFading. + _sources[source].fadeDuration = duration * 1000; +} + +void MidiDriver_Multisource::abortFade(FadeAbortType abortType) { + for (int i = 0; i < MAXIMUM_SOURCES; ++i) { + abortFade(i, abortType); + } +} + +void MidiDriver_Multisource::abortFade(uint8 source, FadeAbortType abortType) { + Common::StackLock lock(_fadingMutex); + + assert(source < MAXIMUM_SOURCES); + + if (!isFading(source)) { + // Nothing to abort. + return; + } + + // Set the fade duration to 0. This will stop the fade from being processed + // by updateFading. + _sources[source].fadeDuration = 0; + + // Now set the intended end volume. + uint16 newSourceVolume; + switch (abortType) { + case FADE_ABORT_TYPE_END_VOLUME: + newSourceVolume = _sources[source].fadeEndVolume; + break; + case FADE_ABORT_TYPE_START_VOLUME: + newSourceVolume = _sources[source].fadeStartVolume; + break; + case FADE_ABORT_TYPE_CURRENT_VOLUME: + default: + return; + } + setSourceVolume(source, newSourceVolume); +} + +bool MidiDriver_Multisource::isFading() { + for (int i = 0; i < MAXIMUM_SOURCES; ++i) { + if (isFading(i)) + return true; + } + return false; +} + +bool MidiDriver_Multisource::isFading(uint8 source) { + assert(source < MAXIMUM_SOURCES); + + return _sources[source].fadeDuration > 0; +} + +void MidiDriver_Multisource::updateFading() { + Common::StackLock lock(_fadingMutex); + + // Decrease the fade delay by the time that has passed since the last + // fading update. + _fadeDelay -= (_fadeDelay < _timerRate ? _fadeDelay : _timerRate); + + bool updatedVolume = false; + for (int i = 0; i < MAXIMUM_SOURCES; ++i) { + + if (_sources[i].fadeDuration > 0) { + // This source has an active fade. + + // Update the time that has passed since the start of the fade. + _sources[i].fadePassedTime += _timerRate; + + if (_sources[i].fadePassedTime >= _sources[i].fadeDuration) { + // The fade has finished. + + // Set the end volume. + setSourceVolume(i, _sources[i].fadeEndVolume); + updatedVolume = true; + + // Stop further processing of this fade. + _sources[i].fadeDuration = 0; + } else if (_fadeDelay == 0) { + // The fade has not yet finished and the fade delay has run + // down. Waiting for the fade delay prevents sending out volume + // updates on every updateFading call, which can overflow + // slower MIDI hardware. + + // Set the new volume value. + setSourceVolume(i, ((_sources[i].fadePassedTime * (_sources[i].fadeEndVolume - _sources[i].fadeStartVolume)) / + _sources[i].fadeDuration) + _sources[i].fadeStartVolume); + updatedVolume = true; + } + } + } + + if (updatedVolume) + // Set the fade delay to delay the next volume update. + _fadeDelay = FADING_DELAY; +} + +void MidiDriver_Multisource::deinitSource(uint8 source) { + abortFade(source, FADE_ABORT_TYPE_END_VOLUME); + + // Stop all active notes for this source. + stopAllNotes(source, 0xFF); +} + +void MidiDriver_Multisource::setSourceType(SourceType type) { + for (int i = 0; i < MAXIMUM_SOURCES; ++i) { + setSourceType(i, type); + } +} + +void MidiDriver_Multisource::setSourceType(uint8 source, SourceType type) { + assert(source < MAXIMUM_SOURCES); + + _sources[source].type = type; + + // A changed source type can mean a different user volume level should be + // used for this source. Calling applySourceVolume will apply the user + // volume. + applySourceVolume(source); +} + +void MidiDriver_Multisource::setSourceVolume(uint16 volume) { + for (int i = 0; i < MAXIMUM_SOURCES; ++i) { + setSourceVolume(i, volume); + } +} + +void MidiDriver_Multisource::setSourceVolume(uint8 source, uint16 volume) { + assert(source < MAXIMUM_SOURCES); + + _sources[source].volume = volume; + + // Set the volume for active notes and/or MIDI channels for this source. + applySourceVolume(source); +} + +void MidiDriver_Multisource::setSourceNeutralVolume(uint16 volume) { + for (int i = 0; i < MAXIMUM_SOURCES; ++i) { + setSourceNeutralVolume(i, volume); + } +} + +void MidiDriver_Multisource::setSourceNeutralVolume(uint8 source, uint16 volume) { + assert(source < MAXIMUM_SOURCES); + + _sources[source].neutralVolume = volume; +} + +void MidiDriver_Multisource::syncSoundSettings() { + // Get user volume settings. + _userMusicVolume = MIN(256, ConfMan.getInt("music_volume")); + _userSfxVolume = MIN(256, ConfMan.getInt("sfx_volume")); + _userMute = ConfMan.getBool("mute"); + + // Calling applySourceVolume will apply the user volume. + applySourceVolume(0xFF); +} + +void MidiDriver_Multisource::onTimer() { + updateFading(); + + if (_timer_proc && _timer_param) + _timer_proc(_timer_param); +} diff --git a/audio/mididrv_ms.h b/audio/mididrv_ms.h new file mode 100644 index 00000000000..f014a840265 --- /dev/null +++ b/audio/mididrv_ms.h @@ -0,0 +1,365 @@ +/* ScummVM - Graphic Adventure Engine + * + * ScummVM is the legal property of its developers, whose names + * are too numerous to list here. Please refer to the COPYRIGHT + * file distributed with this source distribution. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#ifndef AUDIO_MIDIDRV_MS_H +#define AUDIO_MIDIDRV_MS_H + +#include "common/mutex.h" + +#include "audio/mididrv.h" + +/** + * Abstract base class for MIDI drivers supporting multiple simultaneous + * sources of MIDI data. + * + * These drivers support the following features: + * + * - Multiple MIDI sources + * If the game plays multiple streams of MIDI data at the same time, each + * stream can be marked with a source number. When a source has finished + * playing, it must be deinitialized to release any resources allocated to + * it. This is done automatically when an End Of Track MIDI meta event is + * received, or manually by calling deinitSource. + * Using source numbers enables the following features: + * - Music/SFX volume + * Using setSourceType a MIDI source can be designated as music or sound + * effects. The driver will then apply the appropriate user volume setting + * to the MIDI channel volume. This setting sticks after deinitializing a + * source, so if you use the same source numbers for the same types of MIDI + * data, you don't need to set the source type repeatedly. The default setup + * is music for source 0 and SFX for sources 1 and higher. + * - Source volume + * If the game changes the volume of the MIDI playback, you can use + * setSourceVolume to set the volume level for a source. The driver will + * then adjust the current MIDI channel volume and any received MIDI volume + * controller messages. Use setSourceNeutralVolume to set the neutral volume + * for a source (MIDI volume is not changed when source volume is at this + * level; if it is lower or higher, MIDI volume is reduced or increased). + * - Volume fading + * If the game needs to gradually change the volume of the MIDI playback + * (typically for a fade-out), you can use the startFade function. You can + * check the status of the fade using isFading, and abort a fade using + * abortFade. An active fade is automatically aborted when the fading source + * is deinitialized. + * The fading functionality uses the source volume, so you should not set + * this while a fade is active. After the fade the source volume will remain + * at the target level, so if you perform f.e. a fade-out, the source volume + * will remain at 0. If you want to start playback again using this source, + * use setSourceVolume to set the correct playback volume. + * Note that when you stop MIDI playback, notes will not be immediately + * silent but will gradually die out ("release"). So if you fade out a + * source, stop playback, and immediately reset the source volume, the + * note release will be audible. It is recommended to wait about 0.5s + * before resetting the source volume. + * + * - User volume settings + * The driver can scale the MIDI channel volume using the user specified + * volume settings. Just call syncSoundSettings when the user has changed the + * volume settings. Set the USER_VOLUME_SCALING property to true to enable + * this functionality. + * + * A driver extending this class must implement the following functions: + * - send(source, data): process a MIDI event for a specific source. + * - stopAllNotes(source, channel): stop all active notes for a source and/or + * MIDI channel (called when a source is deinitialized). + * - applySourceVolume(source): set the current source volume on active notes + * and/or MIDI channels. + */ +class MidiDriver_Multisource : public MidiDriver { +public: + /** + * The maximum number of sources supported. This can be increased if + * necessary, but this will consume more memory and processing time. + */ + static const uint8 MAXIMUM_SOURCES = 10; + /** + * The default neutral volume level for a source. If the source volume is + * set to this level, the volume levels in the MIDI data are used directly; + * if source volume is lower or higher, output volume is decreased or + * increased, respectively. Use @see setSourceNeutralVolume to change the + * default neutral volume. + */ + static const uint16 DEFAULT_SOURCE_NEUTRAL_VOLUME = 255; + +protected: + // Timeout between updates of the channel volume for fades (25ms) + static const uint16 FADING_DELAY = 25 * 1000; + +public: + /** + * The type of audio produced by a MIDI source (music or sound effects). + */ + enum SourceType { + /** + * Source type not specified (generally treated as music). + */ + SOURCE_TYPE_UNDEFINED, + /** + * Source produces music. + */ + SOURCE_TYPE_MUSIC, + /** + * Source produces sound effects. + */ + SOURCE_TYPE_SFX + }; + + /** + * Specifies what happens to the volume when a fade is aborted. + */ + enum FadeAbortType { + /** + * The volume is set to the fade's end volume level. + */ + FADE_ABORT_TYPE_END_VOLUME, + /** + * The volume remains at the current level. + */ + FADE_ABORT_TYPE_CURRENT_VOLUME, + /** + * The volume is reset to the fade's start volume level. + */ + FADE_ABORT_TYPE_START_VOLUME + }; + +protected: + // This stores data about a specific source of MIDI data. + struct MidiSource { + // Whether this source sends music or SFX MIDI data. + SourceType type; + // The source volume (relative volume for this source as defined by the + // game). Default is the default neutral value (255). + uint16 volume; + // The source volume level at which no scaling is performed (volume as + // defined in the MIDI data is used directly). Volume values below this + // decrease volume, values above increase volume (up to the maximum MIDI + // channel volume). Set this to match the volume values used by the game + // engine to avoid having to convert them. Default value is 255; minimum + // value is 1. + uint16 neutralVolume; + // The volume level at which the fade started. + uint16 fadeStartVolume; + // The target volume level for the fade. + uint16 fadeEndVolume; + // How much time (microseconds) has passed since the start of the fade. + int32 fadePassedTime; + // The total duration of the fade (microseconds). + int32 fadeDuration; + + MidiSource(); + }; + +public: + MidiDriver_Multisource(); + + // MidiDriver functions + using MidiDriver_BASE::send; + void send(uint32 b) override; + void send(int8 source, uint32 b) override = 0; + + uint32 property(int prop, uint32 param) override; + + /** + * Deinitializes a source. This will abort active fades and stop any active + * notes. + * + * @param source The source to deinitialize. + */ + virtual void deinitSource(uint8 source); + /** + * Sets the type for all sources (music or SFX). + * + * @param type The new type for all sources. + */ + void setSourceType(SourceType type); + /** + * Sets the type for a specific sources (music or SFX). + * + * @param source The source for which the type should be set. + * @param type The new type for the specified source. + */ + void setSourceType(uint8 source, SourceType type); + /** + * Sets the source volume for all sources. + * + * @param volume The new source volume for all sources. + */ + void setSourceVolume(uint16 volume); + /** + * Sets the volume for this source. The volume values in the MIDI data sent + * by this source will be scaled by the source volume. + * + * @param source The source for which the source volume should be set. + * @param volume The new source volume for the specified source. + */ + void setSourceVolume(uint8 source, uint16 volume); + /** + * Sets the neutral volume for all sources. See the source-specific + * setSourceNeutralVolume function for details. + * + * @param volume The new neutral volume for all sources. + */ + void setSourceNeutralVolume(uint16 volume); + /** + * Sets the neutral volume for this source. If the source volume is at this + * level, the volume values in the MIDI data sent by this source will not + * be changed. At source volumes below or above this value, the MIDI volume + * values will be decreased or increased accordingly. + * + * @param source The source for which the neutral volume should be set. + * @param volume The new neutral volume for the specified source. + */ + void setSourceNeutralVolume(uint8 source, uint16 volume); + + /** + * Starts a fade for all sources. + * See the source-specific startFade function for more information. + * + * @param duration The fade duration in milliseconds + * @param targetVolume The volume at the end of the fade + */ + void startFade(uint16 duration, uint16 targetVolume); + /** + * Starts a fade for a source. This will linearly increase or decrease the + * volume of the MIDI channels used by the source to the specified target + * value over the specified length of time. + * + * @param source The source to fade + * @param duration The fade duration in milliseconds + * @param targetVolume The volume at the end of the fade + */ + void startFade(uint8 source, uint16 duration, uint16 targetVolume); + /** + * Aborts any active fades for all sources. + * See the source-specific abortFade function for more information. + * + * @param abortType How to set the volume when aborting the fade (default: + * set to the target fade volume). + */ + void abortFade(FadeAbortType abortType = FADE_ABORT_TYPE_END_VOLUME); + /** + * Aborts an active fade for a source. Depending on the abort type, the + * volume will remain at the current value or be set to the start or end + * volume. If there is no active fade for the specified source, this + * function does nothing. + * + * @param source The source that should have its fade aborted + * @param abortType How to set the volume when aborting the fade (default: + * set to the target fade volume). + */ + void abortFade(uint8 source, FadeAbortType abortType = FADE_ABORT_TYPE_END_VOLUME); + /** + * Check if any source has an active fade. + * + * @return True if any source has an active fade. + */ + bool isFading(); + /** + * Check if the specified source has an active fade. + * + * @return True if the specified source has an active fade. + */ + bool isFading(uint8 source); + + /** + * Applies the user volume settings to the MIDI driver. MIDI channel + * volumes will be scaled using the user volume. + * This function must be called by the engine when the user has changed the + * volume settings. + */ + void syncSoundSettings(); + + using MidiDriver::stopAllNotes; + /** + * Stops all active notes (including sustained notes) for the specified + * source and MIDI channel. For both source and channel the value 0xFF can + * be specified, in which case active notes will be stopped for all sources + * and/or MIDI channels. + * + * @param source The source for which all notes should be stopped, or all + * sources if 0xFF is specified. + * @param channel The MIDI channel on which all notes should be stopped, or + * all channels if 0xFF is specified. + */ + virtual void stopAllNotes(uint8 source, uint8 channel) = 0; + + /** + * Sets a callback which will be called whenever the driver's timer + * callback is called by the underlying emulator or hardware driver. The + * callback will only be called when the driver is open. Use + * @see getBaseTempo to get the delay between each callback invocation. + * + * @param timer_param A parameter that will be passed to the callback + * function. Optional. + * @param timer_proc The function that should be called. + */ + void setTimerCallback(void *timer_param, Common::TimerManager::TimerProc timer_proc) override { + _timer_param = timer_param; + _timer_proc = timer_proc; + } + +protected: + /** + * Applies the current source volume to the active notes and/or MIDI + * channels of the specified source. 0xFF can be specified to apply the + * source volume for all sources. + * + * @param source The source for which the source volume should be applied, + * or all sources if 0xFF is specified. + */ + virtual void applySourceVolume(uint8 source) = 0; + /** + * Processes active fades and sets new volume values if necessary. + */ + void updateFading(); + /** + * Runs the MIDI driver's timer related functionality. Will update volume + * fades and calls the timer callback if necessary. + */ + virtual void onTimer(); + + // MIDI source data + MidiSource _sources[MAXIMUM_SOURCES]; + + // True if the driver should scale MIDI channel volume to the user + // specified volume settings. + bool _userVolumeScaling; + + // User volume settings + uint16 _userMusicVolume; + uint16 _userSfxVolume; + bool _userMute; + + Common::Mutex _fadingMutex; // For operations on fades + + // The number of microseconds to wait before the next fading step. + uint16 _fadeDelay; + + // The number of microseconds between timer callback invocations. + uint32 _timerRate; + + // External timer callback + void *_timer_param; + Common::TimerManager::TimerProc _timer_proc; +}; + +#endif diff --git a/audio/miles.h b/audio/miles.h index 73b3ba81a4d..e3ebeb49174 100644 --- a/audio/miles.h +++ b/audio/miles.h @@ -132,14 +132,6 @@ public: * Automatically executed when an End Of Track meta event is received. */ void deinitSource(uint8 source) override; - /** - * Set the volume for this source. This will be used to scale the volume values in the MIDI - * data from this source. Expected volume values are 0 - 256. - * Note that source volume remains set for the source number even after deinitializing the - * source. If the same source numbers are consistently used for music and SFX sources, the - * source volume will only need to be set once. - */ - void setSourceVolume(uint8 source, uint16 volume) override; void stopAllNotes(bool stopSustainedNotes = false) override; @@ -148,6 +140,7 @@ public: protected: void initControlData() override; void initMidiDevice() override; + void applySourceVolume(uint8 source) override; private: void writeRhythmSetup(byte note, byte customTimbreId); diff --git a/audio/miles_midi.cpp b/audio/miles_midi.cpp index 23740b6768d..85a44e57dae 100644 --- a/audio/miles_midi.cpp +++ b/audio/miles_midi.cpp @@ -55,11 +55,6 @@ MidiDriver_Miles_Midi::MidiDriver_Miles_Midi(MusicType midiType, MilesMT32Instru _instrumentTablePtr = instrumentTablePtr; _instrumentTableCount = instrumentTableCount; - // Disable user volume scaling by default. Most (all?) - // engines using Miles implement this themselves. Can - // be turned on using the property function. - _userVolumeScaling = false; - setSourceNeutralVolume(MILES_DEFAULT_SOURCE_NEUTRAL_VOLUME); } @@ -141,7 +136,7 @@ void MidiDriver_Miles_Midi::send(int8 source, uint32 b) { byte command = b & 0xf0; byte dataChannel = b & 0xf; - byte outputChannel = source < 0 ? dataChannel : _sources[source].channelMap[dataChannel]; + byte outputChannel = source < 0 ? dataChannel : _channelMap[source][dataChannel]; MidiChannelEntry &outputChannelEntry = _midiChannels[outputChannel]; // Only send the message to the MIDI device if the channel is not locked or @@ -382,7 +377,7 @@ void MidiDriver_Miles_Midi::lockChannel(uint8 source, uint8 dataChannel) { _midiChannels[lockChannel].locked = true; _midiChannels[lockChannel].lockDataChannel = dataChannel; - _sources[source].channelMap[dataChannel] = lockChannel; + _channelMap[source][dataChannel] = lockChannel; // Copy current controller values so they can be restored when unlocking the channel *_midiChannels[lockChannel].unlockData = *_midiChannels[lockChannel].currentData; _midiChannels[lockChannel].currentData->source = source; @@ -424,7 +419,7 @@ void MidiDriver_Miles_Midi::unlockChannel(uint8 outputChannel) { // Unlock the channel channel.locked = false; - _sources[channel.currentData->source].channelMap[channel.lockDataChannel] = channel.lockDataChannel; + _channelMap[channel.currentData->source][channel.lockDataChannel] = channel.lockDataChannel; channel.lockDataChannel = -1; channel.currentData->source = channel.unlockData->source; @@ -874,11 +869,7 @@ void MidiDriver_Miles_Midi::deinitSource(uint8 source) { MidiDriver_MT32GM::deinitSource(source); } -void MidiDriver_Miles_Midi::setSourceVolume(uint8 source, uint16 volume) { - assert(source < MAXIMUM_SOURCES); - - _sources[source].volume = volume; - +void MidiDriver_Miles_Midi::applySourceVolume(uint8 source) { for (int i = 0; i < MIDI_CHANNEL_COUNT; ++i) { if (!isOutputChannelUsed(i)) continue; diff --git a/audio/module.mk b/audio/module.mk index e4ef93fa2f0..24212552d31 100644 --- a/audio/module.mk +++ b/audio/module.mk @@ -6,6 +6,7 @@ MODULE_OBJS := \ audiostream.o \ fmopl.o \ mididrv.o \ + mididrv_ms.o \ midiparser_qt.o \ midiparser_smf.o \ midiparser_xmidi.o \ diff --git a/audio/mt32gm.cpp b/audio/mt32gm.cpp index 53eb55c7694..c5df819ba11 100644 --- a/audio/mt32gm.cpp +++ b/audio/mt32gm.cpp @@ -30,13 +30,10 @@ // The initialization of the static const integral data members is done in the class definition, // but we still need to provide a definition if they are odr-used. -const uint8 MidiDriver_MT32GM::MAXIMUM_SOURCES; -const uint16 MidiDriver_MT32GM::DEFAULT_SOURCE_NEUTRAL_VOLUME; const uint8 MidiDriver_MT32GM::MT32_DEFAULT_CHANNEL_VOLUME; const uint8 MidiDriver_MT32GM::GM_DEFAULT_CHANNEL_VOLUME; const uint8 MidiDriver_MT32GM::MAXIMUM_MT32_ACTIVE_NOTES; const uint8 MidiDriver_MT32GM::MAXIMUM_GM_ACTIVE_NOTES; -const uint16 MidiDriver_MT32GM::FADING_DELAY; // These are the power-on default instruments of the Roland MT-32 family. const byte MidiDriver_MT32GM::MT32_DEFAULT_INSTRUMENTS[8] = { @@ -78,6 +75,14 @@ const uint8 MidiDriver_MT32GM::GS_DRUMKIT_FALLBACK_MAP[128] = { 0, 0, 0, 0, 0, 0, 0, 127 // No drumkit defined; CM-64/32L (127) }; +// Callback hooked up to the driver wrapped by the MIDI driver +// object. Executes onTimer and the external callback set by +// the setTimerCallback function. +void MidiDriver_MT32GM::timerCallback(void *data) { + MidiDriver_MT32GM *driver = (MidiDriver_MT32GM *)data; + driver->onTimer(); +} + MidiDriver_MT32GM::MidiDriver_MT32GM(MusicType midiType) : _driver(0), _nativeMT32(false), @@ -85,18 +90,10 @@ MidiDriver_MT32GM::MidiDriver_MT32GM(MusicType midiType) : _midiDataReversePanning(false), _midiDeviceReversePanning(false), _scaleGSPercussionVolumeToMT32(false), - _userVolumeScaling(true), - _userMusicVolume(192), - _userSfxVolume(192), - _userMute(false), _isOpen(false), _outputChannelMask(65535), // Channels 1-16 _baseFreq(250), - _timerRate(0), - _fadeDelay(0), - _sysExDelay(0), - _timer_param(0), - _timer_proc(0) { + _sysExDelay(0) { memset(_controlData, 0, sizeof(_controlData)); switch (midiType) { @@ -113,11 +110,10 @@ MidiDriver_MT32GM::MidiDriver_MT32GM(MusicType midiType) : } for (int i = 0; i < MAXIMUM_SOURCES; ++i) { - // Default source type: 0 = music, 1+ = SFX - _sources[i].type = i == 0 ? SOURCE_TYPE_MUSIC : SOURCE_TYPE_SFX; + _availableChannels[i] = 0; // Default MIDI channel mapping: data channel == output channel for (int j = 0; j < MIDI_CHANNEL_COUNT; ++j) { - _sources[i].channelMap[j] = j; + _channelMap[i][j] = j; } } @@ -382,19 +378,13 @@ void MidiDriver_MT32GM::close() { uint32 MidiDriver_MT32GM::property(int prop, uint32 param) { switch (prop) { - case PROP_USER_VOLUME_SCALING: - if (param == 0xFFFF) - return _userVolumeScaling ? 1 : 0; - _userVolumeScaling = param > 0; - break; case PROP_MIDI_DATA_REVERSE_PANNING: if (param == 0xFFFF) return _midiDataReversePanning ? 1 : 0; _midiDataReversePanning = param > 0; break; default: - MidiDriver::property(prop, param); - break; + return MidiDriver_Multisource::property(prop, param); } return 0; } @@ -871,100 +861,6 @@ void MidiDriver_MT32GM::stopAllNotes(bool stopSustainedNotes) { _activeNotesMutex.unlock(); } -void MidiDriver_MT32GM::startFade(uint16 duration, uint16 targetVolume) { - for (int i = 0; i < MAXIMUM_SOURCES; ++i) { - startFade(i, duration, targetVolume); - } -} - -void MidiDriver_MT32GM::startFade(uint8 source, uint16 duration, uint16 targetVolume) { - assert(source < MAXIMUM_SOURCES); - - _fadingMutex.lock(); - - _sources[source].fadePassedTime = 0; - _sources[source].fadeStartVolume = _sources[source].volume; - _sources[source].fadeEndVolume = targetVolume; - _sources[source].fadeDuration = duration * 1000; - - _fadingMutex.unlock(); -} - -void MidiDriver_MT32GM::abortFade(FadeAbortType abortType) { - for (int i = 0; i < MAXIMUM_SOURCES; ++i) { - abortFade(i, abortType); - } -} - -void MidiDriver_MT32GM::abortFade(uint8 source, FadeAbortType abortType) { - assert(source < MAXIMUM_SOURCES); - - if (!isFading(source)) { - return; - } - - _fadingMutex.lock(); - - _sources[source].fadeDuration = 0; - uint16 newSourceVolume; - switch (abortType) { - case FADE_ABORT_TYPE_END_VOLUME: - newSourceVolume = _sources[source].fadeEndVolume; - break; - case FADE_ABORT_TYPE_START_VOLUME: - newSourceVolume = _sources[source].fadeStartVolume; - break; - case FADE_ABORT_TYPE_CURRENT_VOLUME: - default: - _fadingMutex.unlock(); - return; - } - setSourceVolume(source, newSourceVolume); - - _fadingMutex.unlock(); -} - -bool MidiDriver_MT32GM::isFading() { - for (int i = 0; i < MAXIMUM_SOURCES; ++i) { - if (isFading(i)) - return true; - } - return false; -} - -bool MidiDriver_MT32GM::isFading(uint8 source) { - assert(source < MAXIMUM_SOURCES); - - return _sources[source].fadeDuration > 0; -} - -void MidiDriver_MT32GM::updateFading() { - Common::StackLock lock(_fadingMutex); - - _fadeDelay -= _fadeDelay < _timerRate ? _fadeDelay : _timerRate; - - bool updatedVolume = false; - for (int i = 0; i < MAXIMUM_SOURCES; ++i) { - - if (_sources[i].fadeDuration > 0) { - _sources[i].fadePassedTime += _timerRate; - - if (_sources[i].fadePassedTime >= _sources[i].fadeDuration) { - // Fade has finished - setSourceVolume(i, _sources[i].fadeEndVolume); - updatedVolume = true; - _sources[i].fadeDuration = 0; - } else if (_fadeDelay == 0) { - setSourceVolume(i, ((_sources[i].fadePassedTime * (_sources[i].fadeEndVolume - _sources[i].fadeStartVolume)) / - _sources[i].fadeDuration) + _sources[i].fadeStartVolume); - updatedVolume = true; - } - } - } - - if (updatedVolume) - _fadeDelay = FADING_DELAY; -} void MidiDriver_MT32GM::clearSysExQueue() { Common::StackLock lock(_sysExQueueMutex); @@ -1031,23 +927,23 @@ bool MidiDriver_MT32GM::allocateSourceChannels(uint8 source, uint8 numChannels) } // Clear the source channel mapping. if (i != MIDI_RHYTHM_CHANNEL) - _sources[source].channelMap[i] = -1; + _channelMap[source][i] = -1; } _allocationMutex.unlock(); - _sources[source].availableChannels = claimedChannels; + _availableChannels[source] = claimedChannels; return true; } int8 MidiDriver_MT32GM::mapSourceChannel(uint8 source, uint8 dataChannel) { - int8 outputChannel = _sources[source].channelMap[dataChannel]; + int8 outputChannel = _channelMap[source][dataChannel]; if (outputChannel == -1) { for (int i = 0; i < MIDI_CHANNEL_COUNT; ++i) { - if ((_sources[source].availableChannels >> i) & 1) { - _sources[source].availableChannels &= ~(1 << i); - _sources[source].channelMap[dataChannel] = i; + if ((_availableChannels[source] >> i) & 1) { + _availableChannels[source] &= ~(1 << i); + _channelMap[source][dataChannel] = i; outputChannel = i; break; } @@ -1062,7 +958,7 @@ int8 MidiDriver_MT32GM::mapSourceChannel(uint8 source, uint8 dataChannel) { void MidiDriver_MT32GM::deinitSource(uint8 source) { assert(source < MAXIMUM_SOURCES); - abortFade(source, FADE_ABORT_TYPE_END_VOLUME); + MidiDriver_Multisource::deinitSource(source); // Free channels which were used by this source. for (int i = 0; i < MIDI_CHANNEL_COUNT; ++i) { @@ -1072,102 +968,47 @@ void MidiDriver_MT32GM::deinitSource(uint8 source) { if (_controlData[i]->source == source) _controlData[i]->source = -1; } - _sources[source].availableChannels = 0xFFFF; + _availableChannels[source] = 0xFFFF; // Reset the data to output channel mapping for (int i = 0; i < MIDI_CHANNEL_COUNT; ++i) { - _sources[source].channelMap[i] = i; + _channelMap[source][i] = i; } - _activeNotesMutex.lock(); - - // Stop any active notes. - for (int i = 0; i < _maximumActiveNotes; ++i) { - if (_activeNotes[i].source == source) { - if (_activeNotes[i].sustain) { - // Turn off sustain - controlChange(_activeNotes[i].channel, MIDI_CONTROLLER_SUSTAIN, 0x00, source, *_controlData[i]); - } else { - // Send note off - noteOnOff(_activeNotes[i].channel, MIDI_COMMAND_NOTE_OFF, _activeNotes[i].note, 0x00, source, *_controlData[i]); - } - } - } - - _activeNotesMutex.unlock(); - // TODO Optionally reset some controllers to their // default values? Pitch wheel, volume, sustain... } -void MidiDriver_MT32GM::setSourceType(SourceType type) { - for (int i = 0; i < MAXIMUM_SOURCES; ++i) { - setSourceType(i, type); - } -} - -void MidiDriver_MT32GM::setSourceType(uint8 source, SourceType type) { - assert(source < MAXIMUM_SOURCES); - - _sources[source].type = type; - - // Make sure music/sfx volume gets applied +void MidiDriver_MT32GM::applySourceVolume(uint8 source) { for (int i = 0; i < MIDI_CHANNEL_COUNT; ++i) { if (!isOutputChannelUsed(i)) continue; - if (_controlData[i]->source == source) - controlChange(i, MIDI_CONTROLLER_VOLUME, _controlData[i]->volume, source, *_controlData[i]); + if (source == 0xFF || _controlData[i]->source == source) + controlChange(i, MIDI_CONTROLLER_VOLUME, _controlData[i]->volume, _controlData[i]->source, *_controlData[i]); } } -void MidiDriver_MT32GM::setSourceVolume(uint16 volume) { - for (int i = 0; i < MAXIMUM_SOURCES; ++i) { - setSourceVolume(i, volume); +void MidiDriver_MT32GM::stopAllNotes(uint8 source, uint8 channel) { + _activeNotesMutex.lock(); + + for (int i = 0; i < _maximumActiveNotes; ++i) { + if ((source == 0xFF || _activeNotes[i].source == source) && + (channel == 0xFF || _activeNotes[i].channel == channel)) { + if (_activeNotes[i].sustain) { + // Turn off sustain + controlChange(_activeNotes[i].channel, MIDI_CONTROLLER_SUSTAIN, 0x00, _activeNotes[i].source, *_controlData[i]); + } else { + // Send note off + noteOnOff(_activeNotes[i].channel, MIDI_COMMAND_NOTE_OFF, _activeNotes[i].note, 0x00, _activeNotes[i].source, *_controlData[i]); + } + } } -} -void MidiDriver_MT32GM::setSourceVolume(uint8 source, uint16 volume) { - assert(source < MAXIMUM_SOURCES); - - _sources[source].volume = volume; - - for (int i = 0; i < MIDI_CHANNEL_COUNT; ++i) { - if (!isOutputChannelUsed(i)) - continue; - - if (_controlData[i]->source == source) - controlChange(i, MIDI_CONTROLLER_VOLUME, _controlData[i]->volume, source, *_controlData[i]); - } -} - -void MidiDriver_MT32GM::setSourceNeutralVolume(uint16 volume) { - for (int i = 0; i < MAXIMUM_SOURCES; ++i) { - setSourceNeutralVolume(i, volume); - } -} - -void MidiDriver_MT32GM::setSourceNeutralVolume(uint8 source, uint16 volume) { - assert(source < MAXIMUM_SOURCES); - - _sources[source].neutralVolume = volume; -} - -void MidiDriver_MT32GM::syncSoundSettings() { - _userMusicVolume = MIN(256, ConfMan.getInt("music_volume")); - _userSfxVolume = MIN(256, ConfMan.getInt("sfx_volume")); - _userMute = ConfMan.getBool("mute"); - - // Make sure music/sfx volume gets applied - for (int i = 0; i < MIDI_CHANNEL_COUNT; ++i) { - if (!isOutputChannelUsed(i)) - continue; - - controlChange(i, MIDI_CONTROLLER_VOLUME, _controlData[i]->volume, _controlData[i]->source, *_controlData[i]); - } + _activeNotesMutex.unlock(); } void MidiDriver_MT32GM::onTimer() { - updateFading(); + MidiDriver_Multisource::onTimer(); _sysExQueueMutex.lock(); diff --git a/audio/mt32gm.h b/audio/mt32gm.h index a3bf74490ef..0bb471de640 100644 --- a/audio/mt32gm.h +++ b/audio/mt32gm.h @@ -24,6 +24,7 @@ #define AUDIO_MT32GM_H #include "audio/mididrv.h" +#include "audio/mididrv_ms.h" #include "common/mutex.h" #include "common/queue.h" @@ -60,18 +61,12 @@ * or override the mapMT32InstrumentToGM and mapGMInstrumentToMT32 functions * for more advanced mapping algorithms. * - * - User volume settings - * The driver will scale the MIDI channel volume using the user specified - * volume settings. Just call syncSoundSettings when the user has changed the - * volume settings. Set the USER_VOLUME_SCALING property to false to disable - * this functionality. - * * - Reverse stereo * If the game has MIDI data with reversed stereo compared to the targeted * output device, set the MIDI_DATA_REVERSE_PANNING property to reverse * stereo. The driver wil automatically reverse stereo when MT-32 data is * sent to a GM/GS device or the other way around. - * + * * - Correct Roland GS bank and drumkit selects * Some games' MIDI data relies on a feature of the Roland SC-55 MIDI module * which automatically corrects invalid bank selects and drumkit program @@ -88,19 +83,18 @@ * necessary amount of time for the MIDI device to process the message. * Use clearSysExQueue to remove all messages from the queue, in case device * initialization has to be aborted. -* + * * - Multiple MIDI sources * If the game plays multiple streams of MIDI data at the same time, each * stream can be marked with a source number. This enables the following - * features: + * feature: * - Channel mapping * If multiple sources use the same MIDI channels, the driver can map the * data channels to different output channels to avoid conflicts. Use * allocateSourceChannels to allocate output channels to a source. The * data channels are automatically mapped to the allocated output channels * during playback. The allocated channels are freed when the source is - * deinitialized; this is done automatically when an End Of Track MIDI event - * is received, or manually by calling deinitSource. + * deinitialized. * If you only have one source of MIDI data or the sources do not use * conflicting channels, you do not need to allocate channels - the channels * in the MIDI data will be used directly. If you do use this feature, you @@ -114,42 +108,9 @@ * using the allocateChannel function and MidiChannel objects. These two * methods are not coordinated in any way, so don't use both at the same * time. - * - Music/SFX volume - * Using setSourceType a MIDI source can be designated as music or sound - * effects. The driver will then apply the appropriate user volume setting - * to the MIDI channel volume. This setting sticks after deinitializing a - * source, so if you use the same source numbers for the same types of MIDI - * data, you don't need to set the source type repeatedly. The default setup - * is music for source 0 and SFX for sources 1 and higher. - * - Source volume - * If the game changes the volume of the MIDI playback, you can use - * setSourceVolume to set the volume level for a source. The driver will - * then adjust the current MIDI channel volume and any received MIDI volume - * controller messages. Use setSourceNeutralVolume to set the neutral volume - * for a source (MIDI volume is not changed when source volume is at this - * level; if it is lower or higher, MIDI volume is reduced or increased). - * - Volume fading - * If the game needs to gradually change the volume of the MIDI playback - * (typically for a fade-out), you can use the startFade function. You can - * check the status of the fade using isFading, and abort a fade using - * abortFade. An active fade is automatically aborted when the fading source - * is deinitialized. - * The fading functionality uses the source volume, so you should not set - * this while a fade is active. After the fade the source volume will remain - * at the target level, so if you perform f.e. a fade-out, the source volume - * will remain at 0. If you want to start playback again using this source, - * use setSourceVolume to set the correct playback volume. - * Note that when you stop MIDI playback, notes will not be immediately - * silent but will gradually die out ("release"). So if you fade out a - * source, stop playback, and immediately reset the source volume, the - * note release will be audible. It is recommended to wait about 0.5s - * before resetting the source volume. */ -class MidiDriver_MT32GM : public MidiDriver { +class MidiDriver_MT32GM : public MidiDriver_Multisource { public: - static const uint8 MAXIMUM_SOURCES = 10; - static const uint16 DEFAULT_SOURCE_NEUTRAL_VOLUME = 255; - static const byte MT32_DEFAULT_INSTRUMENTS[8]; static const byte MT32_DEFAULT_PANNING[8]; static const uint8 MT32_DEFAULT_CHANNEL_VOLUME = 98; @@ -161,21 +122,6 @@ protected: static const uint8 MAXIMUM_MT32_ACTIVE_NOTES = 48; static const uint8 MAXIMUM_GM_ACTIVE_NOTES = 96; - // Timeout between updates of the channel volume for fades (25ms) - static const uint16 FADING_DELAY = 25 * 1000; - -public: - enum SourceType { - SOURCE_TYPE_UNDEFINED, - SOURCE_TYPE_MUSIC, - SOURCE_TYPE_SFX - }; - - enum FadeAbortType { - FADE_ABORT_TYPE_END_VOLUME, - FADE_ABORT_TYPE_CURRENT_VOLUME, - FADE_ABORT_TYPE_START_VOLUME - }; protected: /** * This stores the values of the MIDI controllers for @@ -289,6 +235,11 @@ protected: }; public: + // Callback hooked up to the driver wrapped by the MIDI driver + // object. Executes onTimer and the external callback set by + // the setTimerCallback function. + static void timerCallback(void *data); + MidiDriver_MT32GM(MusicType midiType); ~MidiDriver_MT32GM(); @@ -340,45 +291,6 @@ public: void metaEvent(int8 source, byte type, byte *data, uint16 length) override; void stopAllNotes(bool stopSustainedNotes = false) override; - /** - * Starts a fade for all sources. - * See the source-specific startFade function for more information. - */ - void startFade(uint16 duration, uint16 targetVolume); - /** - * Starts a fade for a source. This will linearly increase or decrease the - * volume of the MIDI channels used by the source to the specified target - * value over the specified length of time. - * - * @param source The source to fade - * @param duration The fade duration in ms - * @param targetVolume The volume at the end of the fade - */ - void startFade(uint8 source, uint16 duration, uint16 targetVolume); - /** - * Aborts any active fades for all sources. - * See the source-specific abortFade function for more information. - */ - void abortFade(FadeAbortType abortType = FADE_ABORT_TYPE_END_VOLUME); - /** - * Aborts an active fade for a source. Depending on the abort type, the - * volume will remain at the current value or be set to the start or end - * volume. If there is no active fade for the specified source, this - * function does nothing. - * - * @param source The source that should have its fade aborted - * @param abortType How to set the volume when aborting the fade (default: - * set to the target fade volume). - */ - void abortFade(uint8 source, FadeAbortType abortType = FADE_ABORT_TYPE_END_VOLUME); - /** - * Returns true if any source has an active fade. - */ - bool isFading(); - /** - * Returns true if the specified source has an active fade. - */ - bool isFading(uint8 source); /** * Removes all SysEx messages in the SysEx queue. */ @@ -409,49 +321,7 @@ public: * Deinitializes a source. This will abort active fades, free any output * channels allocated to the source and stop active notes. */ - virtual void deinitSource(uint8 source); - /** - * Sets the type for all sources (music or SFX). - */ - void setSourceType(SourceType type); - /** - * Sets the type for a specific sources (music or SFX). - */ - void setSourceType(uint8 source, SourceType type); - /** - * Sets the volume for all sources. - */ - void setSourceVolume(uint16 volume); - /** - * Sets the volume for this source. The volume values in the MIDI data sent - * by this source will be scaled by the source volume. - */ - virtual void setSourceVolume(uint8 source, uint16 volume); - void setSourceNeutralVolume(uint16 volume); - /** - * Sets the neutral volume for this source. If the source volume is at this - * level, the volume values in the MIDI data sent by this source will not - * be changed. At source volumes below or above this value, the MIDI volume - * values will be decreased or increased accordingly. - */ - void setSourceNeutralVolume(uint8 source, uint16 volume); - /** - * Applies the user volume settings to the MIDI driver. MIDI channel volumes - * will be scaled using the user volume. - * This function must be called by the engine when the user has changed the - * volume settings. - */ - void syncSoundSettings(); - - void setTimerCallback(void *timer_param, Common::TimerManager::TimerProc timer_proc) override { - _timer_param = timer_param; - _timer_proc = timer_proc; - } - /** - * Runs the MIDI driver's timer related functionality. Will update volume - * fades and sends messages from the SysEx queue if necessary. - */ - virtual void onTimer(); + void deinitSource(uint8 source) override; protected: /** @@ -576,11 +446,6 @@ protected: */ byte correctInstrumentBank(byte outputChannel, byte patchId); - /** - * Processes active fades and sets new volume values if necessary. - */ - void updateFading(); - /** * Returns the MIDI output channel mapped to the specified data channel. * If the data channel has not been mapped yet, a new mapping to one of the @@ -592,7 +457,14 @@ protected: */ virtual int8 mapSourceChannel(uint8 source, uint8 dataChannel); - Common::Mutex _fadingMutex; // For operations on fades + void applySourceVolume(uint8 source) override; + void stopAllNotes(uint8 source, uint8 channel) override; + /** + * Runs the MIDI driver's timer related functionality. Will update volume + * fades and sends messages from the SysEx queue if necessary. + */ + void onTimer() override; + Common::Mutex _allocationMutex; // For operations on MIDI channel allocation Common::Mutex _activeNotesMutex; // For operations on active notes registration @@ -616,26 +488,18 @@ protected: // True if GS percussion channel volume should be scaled to match MT-32 volume. bool _scaleGSPercussionVolumeToMT32; - // True if the driver should scale MIDI channel volume to the user specified - // volume settings. - bool _userVolumeScaling; - - // User volume settings - uint16 _userMusicVolume; - uint16 _userSfxVolume; - bool _userMute; - // True if this MIDI driver has been opened. bool _isOpen; // Bitmask of the MIDI channels in use by the output device. uint16 _outputChannelMask; int _baseFreq; - uint32 _timerRate; // stores the controller values for each MIDI channel MidiChannelControlData *_controlData[MIDI_CHANNEL_COUNT]; - - MidiSource _sources[MAXIMUM_SOURCES]; + // The mapping of MIDI data channels to output channels for each source. + int8 _channelMap[MAXIMUM_SOURCES][MIDI_CHANNEL_COUNT]; + // Bitmask specifying which MIDI channels are available for use by each source. + uint16 _availableChannels[MAXIMUM_SOURCES]; // Maps used for MT-32 <> GM instrument mapping. Set these to an alternate // 128 byte array to customize the mapping. @@ -646,9 +510,6 @@ protected: // Active note registration ActiveNote *_activeNotes; - // The number of microseconds to wait before the next fading step. - uint16 _fadeDelay; - // The current number of microseconds that have to elapse before the next // SysEx message can be sent. uint32 _sysExDelay; @@ -656,21 +517,6 @@ protected: Common::Queue _sysExQueue; // Mutex for write access to the SysEx queue. Common::Mutex _sysExQueueMutex; - - // External timer callback - void *_timer_param; - Common::TimerManager::TimerProc _timer_proc; - -public: - // Callback hooked up to the driver wrapped by the MIDI driver - // object. Executes onTimer and the external callback set by - // the setTimerCallback function. - static void timerCallback(void *data) { - MidiDriver_MT32GM *driver = (MidiDriver_MT32GM *)data; - driver->onTimer(); - if (driver->_timer_proc && driver->_timer_param) - driver->_timer_proc(driver->_timer_param); - } }; /** @} */ #endif