LCOV - code coverage report
Current view: top level - muse/framework/vst/internal/synth - vstsequencer.cpp Coverage Total Hit
Test: filtered_coverage.info Lines: 0.0 % 226 0
Test Date: 2026-11-05 04:07:02

            Line data    Source code
       1              : /*
       2              :  * SPDX-License-Identifier: GPL-3.0-only
       3              :  * MuseScore-CLA-applies
       4              :  *
       5              :  * MuseScore Studio
       6              :  * Music Composition & Notation
       7              :  *
       8              :  * Copyright (C) 2022 MuseScore Limited and others
       9              :  *
      10              :  * This program is free software: you can redistribute it and/or modify
      11              :  * it under the terms of the GNU General Public License version 3 as
      12              :  * published by the Free Software Foundation.
      13              :  *
      14              :  * This program is distributed in the hope that it will be useful,
      15              :  * but WITHOUT ANY WARRANTY; without even the implied warranty of
      16              :  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
      17              :  * GNU General Public License for more details.
      18              :  *
      19              :  * You should have received a copy of the GNU General Public License
      20              :  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
      21              :  */
      22              : 
      23              : #include "vstsequencer.h"
      24              : 
      25              : #include "global/interpolation.h"
      26              : 
      27              : #include <map>
      28              : 
      29              : using namespace muse;
      30              : using namespace muse::vst;
      31              : 
      32              : static constexpr ControlIdx MODWHEEL_IDX = static_cast<ControlIdx>(Steinberg::Vst::kCtrlModWheel);
      33              : static constexpr ControlIdx SUSTAIN_IDX = static_cast<ControlIdx>(Steinberg::Vst::kCtrlSustainOnOff);
      34              : static constexpr ControlIdx SOSTENUTO_IDX = static_cast<ControlIdx>(Steinberg::Vst::kCtrlSustenutoOnOff);
      35              : static constexpr ControlIdx PITCH_BEND_IDX = static_cast<ControlIdx>(Steinberg::Vst::kPitchBend);
      36              : 
      37              : static const mpe::ArticulationTypeSet SUSTAIN_PEDAL_CC_SUPPORTED_TYPES {
      38              :     mpe::ArticulationType::Pedal,
      39              : };
      40              : 
      41              : static const mpe::ArticulationTypeSet SOSTENUTO_PEDAL_CC_SUPPORTED_TYPES {
      42              :     mpe::ArticulationType::LaissezVibrer,
      43              : };
      44              : 
      45              : static const mpe::ArticulationTypeSet BEND_SUPPORTED_TYPES {
      46              :     mpe::ArticulationType::Multibend, mpe::ArticulationType::ContinuousGlissando,
      47              : };
      48              : 
      49            0 : void VstSequencer::init(ParamsMapping&& mapping, bool useDynamicEvents)
      50              : {
      51            0 :     m_mapping = std::move(mapping);
      52            0 :     m_useDynamicEvents = useDynamicEvents;
      53            0 :     m_inited = true;
      54              : 
      55            0 :     updateMainStreamEvents(m_playbackData.originEvents, m_playbackData.dynamics);
      56            0 : }
      57              : 
      58            0 : void VstSequencer::updateMainStreamEvents(const mpe::PlaybackEventsMap& events, const mpe::DynamicAutomationLayers& dynamics)
      59              : {
      60            0 :     if (!m_inited) {
      61            0 :         return;
      62              :     }
      63              : 
      64            0 :     m_mainStreamEvents.clear();
      65              : 
      66            0 :     if (m_onMainStreamFlushed) {
      67            0 :         m_onMainStreamFlushed();
      68              :     }
      69              : 
      70            0 :     addPlaybackEvents(m_mainStreamEvents, events);
      71            0 :     sortNoteOnEventsByPitch(m_mainStreamEvents);
      72              : 
      73            0 :     if (m_useDynamicEvents) {
      74            0 :         addDynamicEvents(m_mainStreamEvents, dynamics);
      75              :     }
      76              : 
      77            0 :     updateMainSequenceIterator();
      78              : }
      79              : 
      80            0 : void VstSequencer::updateOffStreamEvents(const mpe::PlaybackEventsMap& events)
      81              : {
      82            0 :     addPlaybackEvents(m_offStreamEvents, events);
      83            0 :     updateOffSequenceIterator();
      84            0 : }
      85              : 
      86            0 : muse::audio::gain_t VstSequencer::currentGain() const
      87              : {
      88            0 :     if (!m_useDynamicEvents) {
      89            0 :         return 0.5f;
      90              :     }
      91              : 
      92            0 :     mpe::dynamic_level_t maxDynamicLevel = mpe::MIN_DYNAMIC_LEVEL;
      93            0 :     bool foundAnyLevel = false;
      94              : 
      95            0 :     for (const auto& [_, curve] : m_playbackData.dynamics) {
      96            0 :         if (curve.empty()) {
      97            0 :             continue;
      98              :         }
      99              : 
     100            0 :         foundAnyLevel = true;
     101            0 :         maxDynamicLevel = std::max(maxDynamicLevel, mpe::dynamicLevelFromNormalized(mpe::evaluateCurveAt(curve, m_playbackPosition)));
     102              :     }
     103              : 
     104            0 :     if (!foundAnyLevel) {
     105            0 :         maxDynamicLevel = mpe::dynamicLevelFromType(mpe::DynamicType::Natural);
     106              :     }
     107              : 
     108            0 :     return expressionLevel(maxDynamicLevel);
     109              : }
     110              : 
     111            0 : void VstSequencer::addPlaybackEvents(EventSequenceMap& destination, const mpe::PlaybackEventsMap& events)
     112              : {
     113            0 :     SostenutoTimeAndDurations sostenutoTimeAndDurations;
     114              : 
     115            0 :     for (const auto& evPair : events) {
     116            0 :         for (const mpe::PlaybackEvent& event : evPair.second) {
     117            0 :             if (std::holds_alternative<mpe::NoteEvent>(event)) {
     118            0 :                 addNoteEvent(destination, std::get<mpe::NoteEvent>(event), sostenutoTimeAndDurations);
     119            0 :             } else if (std::holds_alternative<mpe::ControllerChangeEvent>(event)) {
     120            0 :                 addControlChangeEvent(destination, evPair.first, std::get<mpe::ControllerChangeEvent>(event));
     121              :             }
     122              :         }
     123              :     }
     124              : 
     125            0 :     addSostenutoEvents(destination, sostenutoTimeAndDurations);
     126            0 : }
     127              : 
     128            0 : void VstSequencer::addDynamicEvents(EventSequenceMap& destination, const mpe::DynamicAutomationLayers& layers)
     129              : {
     130            0 :     constexpr mpe::timestamp_t STEP_INTERVAL_US = 30000;
     131              : 
     132              :     //! NOTE: VST instance has a single gain parameter, so merge layers by tracking each one's last-known level
     133            0 :     std::map<mpe::timestamp_t, std::vector<std::pair<mpe::layer_idx_t, mpe::dynamic_level_t> > > updatesAt;
     134            0 :     for (const auto& [layerIdx, curve] : layers) {
     135            0 :         mpe::resampleCurve(curve, STEP_INTERVAL_US, [&](mpe::timestamp_t t, muse::real_t normalized) {
     136            0 :             updatesAt[t].emplace_back(layerIdx, mpe::dynamicLevelFromNormalized(normalized));
     137            0 :         });
     138              :     }
     139              : 
     140            0 :     std::map<mpe::layer_idx_t, mpe::dynamic_level_t> currentLevel;
     141            0 :     std::optional<float> lastGain;
     142              : 
     143            0 :     for (const auto& [t, updates] : updatesAt) {
     144            0 :         for (const auto& [layerIdx, level] : updates) {
     145            0 :             currentLevel[layerIdx] = level;
     146              :         }
     147              : 
     148            0 :         mpe::dynamic_level_t maxDynamicLevel = mpe::MIN_DYNAMIC_LEVEL;
     149            0 :         for (const auto& [_, level] : currentLevel) {
     150            0 :             maxDynamicLevel = std::max(maxDynamicLevel, level);
     151              :         }
     152              : 
     153            0 :         const float gain = expressionLevel(maxDynamicLevel);
     154            0 :         if (lastGain.has_value() && muse::RealIsEqual(*lastGain, gain)) {
     155            0 :             continue;
     156              :         }
     157            0 :         lastGain = gain;
     158            0 :         destination[t].emplace_back(gain);
     159              :     }
     160            0 : }
     161              : 
     162            0 : void VstSequencer::addNoteEvent(EventSequenceMap& destination, const mpe::NoteEvent& noteEvent,
     163              :                                 SostenutoTimeAndDurations& sostenutoTimeAndDurations)
     164              : {
     165            0 :     const mpe::ArrangementContext& arrangementCtx = noteEvent.arrangementCtx();
     166            0 :     const int32_t noteId = noteIndex(noteEvent.pitchCtx().nominalPitchLevel);
     167            0 :     const float velocityFraction = noteVelocityFraction(noteEvent);
     168            0 :     const float tuning = noteTuning(noteEvent, noteId);
     169              : 
     170            0 :     if (arrangementCtx.hasStart()) {
     171            0 :         if (m_useDynamicEvents) {
     172            0 :             destination[arrangementCtx.actualTimestamp].emplace_back(expressionLevel(noteEvent.expressionCtx().nominalDynamicLevel));
     173              :         }
     174              : 
     175            0 :         destination[arrangementCtx.actualTimestamp].emplace_back(buildEvent(VstEvent::kNoteOnEvent, noteId, velocityFraction, tuning));
     176              :     }
     177              : 
     178            0 :     if (arrangementCtx.hasEnd()) {
     179            0 :         const mpe::timestamp_t timestampTo = arrangementCtx.actualTimestamp + noteEvent.arrangementCtx().actualDuration;
     180            0 :         destination[timestampTo].emplace_back(buildEvent(VstEvent::kNoteOffEvent, noteId, velocityFraction, tuning));
     181              :     }
     182              : 
     183            0 :     for (const auto& artPair : noteEvent.expressionCtx().articulations) {
     184            0 :         if (artPair.first == mpe::ArticulationType::Standard) {
     185            0 :             continue;
     186              :         }
     187              : 
     188            0 :         const mpe::ArticulationMeta& meta = artPair.second.meta;
     189              : 
     190            0 :         if (!noteEvent.pitchCtx().pitchCurve.empty() && muse::contains(BEND_SUPPORTED_TYPES, meta.type)) {
     191            0 :             addPitchCurve(destination, noteEvent, meta);
     192            0 :             continue;
     193              :         }
     194              : 
     195            0 :         if (muse::contains(SUSTAIN_PEDAL_CC_SUPPORTED_TYPES, meta.type)) {
     196            0 :             addPedalEvent(destination, meta);
     197            0 :             continue;
     198              :         }
     199              : 
     200            0 :         if (muse::contains(SOSTENUTO_PEDAL_CC_SUPPORTED_TYPES, meta.type)) {
     201            0 :             const mpe::timestamp_t timestamp = arrangementCtx.actualTimestamp + noteEvent.arrangementCtx().actualDuration * 0.1; // add offset for Sostenuto to take effect
     202            0 :             sostenutoTimeAndDurations.push_back(mpe::TimestampAndDuration { timestamp, meta.overallDuration });
     203            0 :             continue;
     204            0 :         }
     205              :     }
     206            0 : }
     207              : 
     208            0 : void VstSequencer::addPedalEvent(EventSequenceMap& destination, const mpe::ArticulationMeta& meta)
     209              : {
     210            0 :     if (meta.hasStart()) {
     211            0 :         addParamChange(destination, meta.timestamp, SUSTAIN_IDX, 1);
     212              :     }
     213              : 
     214            0 :     if (meta.hasEnd()) {
     215            0 :         addParamChange(destination, meta.timestamp + meta.overallDuration, SUSTAIN_IDX, 0);
     216              :     }
     217            0 : }
     218              : 
     219            0 : void VstSequencer::addControlChangeEvent(EventSequenceMap& destination, const mpe::timestamp_t timestamp,
     220              :                                          const mpe::ControllerChangeEvent& event)
     221              : {
     222            0 :     switch (event.type) {
     223            0 :     case mpe::ControllerChangeEvent::Modulation:
     224            0 :         addParamChange(destination, timestamp, MODWHEEL_IDX, event.val);
     225            0 :         break;
     226            0 :     case mpe::ControllerChangeEvent::SustainPedalOnOff:
     227            0 :         addParamChange(destination, timestamp, SUSTAIN_IDX, event.val);
     228            0 :         break;
     229            0 :     case mpe::ControllerChangeEvent::PitchBend:
     230            0 :         addParamChange(destination, timestamp, PITCH_BEND_IDX, event.val);
     231            0 :         break;
     232            0 :     case mpe::ControllerChangeEvent::Undefined:
     233            0 :         break;
     234              :     }
     235            0 : }
     236              : 
     237            0 : void VstSequencer::addParamChange(EventSequenceMap& destination, const mpe::timestamp_t timestamp,
     238              :                                   const ControlIdx controlIdx, const PluginParamValue value)
     239              : {
     240            0 :     auto controlIt = m_mapping.find(controlIdx);
     241            0 :     if (controlIt == m_mapping.cend()) {
     242            0 :         return;
     243              :     }
     244              : 
     245            0 :     const PluginParamId paramId = controlIt->second;
     246            0 :     EventSequence& events = destination[timestamp];
     247              : 
     248            0 :     for (const EventType& e : events) {
     249            0 :         if (!std::holds_alternative<ParamChangeEvent>(e)) {
     250            0 :             continue;
     251              :         }
     252              : 
     253            0 :         const ParamChangeEvent& pce = std::get<ParamChangeEvent>(e);
     254            0 :         if (pce.paramId == paramId && RealIsEqual(pce.value, value)) {
     255            0 :             return;
     256              :         }
     257              :     }
     258              : 
     259            0 :     events.emplace_back(ParamChangeEvent { paramId, value });
     260              : }
     261              : 
     262            0 : void VstSequencer::addPitchCurve(EventSequenceMap& destination, const mpe::NoteEvent& noteEvent,
     263              :                                  const mpe::ArticulationMeta& artMeta)
     264              : {
     265            0 :     auto pitchBendIt = m_mapping.find(PITCH_BEND_IDX);
     266            0 :     if (pitchBendIt == m_mapping.cend()) {
     267            0 :         return;
     268              :     }
     269              : 
     270            0 :     const mpe::timestamp_t noteTimestampTo = noteEvent.arrangementCtx().actualTimestamp + noteEvent.arrangementCtx().actualDuration;
     271            0 :     const mpe::timestamp_t pitchBendTimestampTo = std::min(artMeta.timestamp + artMeta.overallDuration, noteTimestampTo);
     272              : 
     273            0 :     ParamChangeEvent event;
     274            0 :     event.paramId = pitchBendIt->second;
     275            0 :     event.value = 0.5f;
     276            0 :     destination[pitchBendTimestampTo].push_back(event);
     277              : 
     278            0 :     auto currIt = noteEvent.pitchCtx().pitchCurve.cbegin();
     279            0 :     auto nextIt = std::next(currIt);
     280            0 :     auto endIt = noteEvent.pitchCtx().pitchCurve.cend();
     281              : 
     282            0 :     float prevBendValue = -1.f;
     283              : 
     284            0 :     for (; nextIt != endIt; currIt = nextIt, nextIt = std::next(currIt)) {
     285            0 :         const float currValue = pitchBendLevel(currIt->second);
     286            0 :         const float nextValue = pitchBendLevel(nextIt->second);
     287              : 
     288            0 :         const mpe::timestamp_t currTime = artMeta.timestamp + artMeta.overallDuration * mpe::percentageToFactor(currIt->first);
     289            0 :         const mpe::timestamp_t nextTime = artMeta.timestamp + artMeta.overallDuration * mpe::percentageToFactor(nextIt->first);
     290              : 
     291              :         using namespace muse::interpolation;
     292            0 :         const Point currPoint { static_cast<double>(currTime), currValue };
     293            0 :         const Point nextPoint { static_cast<double>(nextTime), nextValue };
     294              : 
     295              :         //! NOTE: Increasing this number results in fewer points being interpolated
     296            0 :         constexpr mpe::pitch_level_t POINT_WEIGHT = mpe::PITCH_LEVEL_STEP / 25;
     297            0 :         size_t pointCount = std::abs(nextIt->second - currIt->second) / POINT_WEIGHT;
     298            0 :         pointCount = std::max(pointCount, size_t(1));
     299              : 
     300            0 :         const std::vector<Point> points = lerp(currPoint, nextPoint, pointCount);
     301              : 
     302            0 :         for (const Point& point : points) {
     303            0 :             const mpe::timestamp_t time = static_cast<mpe::timestamp_t>(std::round(point.x));
     304            0 :             const float bendValue = static_cast<float>(point.y);
     305              : 
     306            0 :             if (time < pitchBendTimestampTo && !RealIsEqual(prevBendValue, bendValue)) {
     307            0 :                 event.value = bendValue;
     308            0 :                 destination[time].push_back(event);
     309              :             }
     310              : 
     311            0 :             prevBendValue = bendValue;
     312              :         }
     313            0 :     }
     314              : }
     315              : 
     316            0 : void VstSequencer::addSostenutoEvents(EventSequenceMap& destination, const SostenutoTimeAndDurations& sostenutoTimeAndDurations)
     317              : {
     318            0 :     for (size_t i = 0; i < sostenutoTimeAndDurations.size(); ++i) {
     319            0 :         const mpe::TimestampAndDuration& currentTnD = sostenutoTimeAndDurations.at(i);
     320            0 :         const mpe::timestamp_t timestampTo = currentTnD.timestamp + currentTnD.duration;
     321              : 
     322            0 :         addParamChange(destination, currentTnD.timestamp, SOSTENUTO_IDX, 1);
     323              : 
     324            0 :         if (i == sostenutoTimeAndDurations.size() - 1) {
     325            0 :             addParamChange(destination, timestampTo, SOSTENUTO_IDX, 0);
     326            0 :             continue;
     327              :         }
     328              : 
     329            0 :         const mpe::TimestampAndDuration& nextTnD = sostenutoTimeAndDurations.at(i + 1);
     330            0 :         if (timestampTo <= nextTnD.timestamp) { // handle potential overlap
     331            0 :             addParamChange(destination, timestampTo, SOSTENUTO_IDX, 0);
     332              :         }
     333              :     }
     334            0 : }
     335              : 
     336              : //! Hack to make keyswitches work until we have proper UI support
     337              : //! see: https://github.com/musescore/MuseScore/issues/32150
     338            0 : void VstSequencer::sortNoteOnEventsByPitch(EventSequenceMap& destination)
     339              : {
     340            0 :     for (auto& [_, seq] : destination) {
     341            0 :         if (seq.size() <= 1) {
     342            0 :             continue;
     343              :         }
     344              : 
     345            0 :         std::stable_sort(seq.begin(), seq.end(), [](const EventType& e1, const EventType& e2) {
     346            0 :             if (!std::holds_alternative<VstEvent>(e1) || !std::holds_alternative<VstEvent>(e2)) {
     347            0 :                 return false;
     348              :             }
     349              : 
     350            0 :             const VstEvent& ve1 = std::get<VstEvent>(e1);
     351            0 :             const VstEvent& ve2 = std::get<VstEvent>(e2);
     352              : 
     353            0 :             if (ve1.type == VstEvent::kNoteOnEvent && ve2.type == VstEvent::kNoteOnEvent) {
     354            0 :                 return ve1.noteOn.pitch < ve2.noteOn.pitch;
     355              :             }
     356              : 
     357            0 :             return false;
     358              :         });
     359              :     }
     360            0 : }
     361              : 
     362            0 : VstEvent VstSequencer::buildEvent(const VstEvent::EventTypes type, const int32_t noteIdx, const float velocityFraction,
     363              :                                   const float tuning) const
     364              : {
     365              :     VstEvent result;
     366              : 
     367            0 :     result.busIndex = 0;
     368            0 :     result.sampleOffset = 0;
     369            0 :     result.ppqPosition = 0;
     370            0 :     result.flags = VstEvent::kIsLive;
     371            0 :     result.type = type;
     372              : 
     373            0 :     if (type == VstEvent::kNoteOnEvent) {
     374            0 :         result.noteOn.noteId = -1;
     375            0 :         result.noteOn.channel = 0;
     376            0 :         result.noteOn.pitch = noteIdx;
     377            0 :         result.noteOn.tuning = tuning;
     378            0 :         result.noteOn.velocity = velocityFraction;
     379              :     } else {
     380            0 :         result.noteOff.noteId = -1;
     381            0 :         result.noteOff.channel = 0;
     382            0 :         result.noteOff.pitch = noteIdx;
     383            0 :         result.noteOff.tuning = tuning;
     384            0 :         result.noteOff.velocity = velocityFraction;
     385              :     }
     386              : 
     387            0 :     return result;
     388              : }
     389              : 
     390            0 : int32_t VstSequencer::noteIndex(const mpe::pitch_level_t pitchLevel) const
     391              : {
     392            0 :     float stepCount = mpe::ZERO_PITCH_LEVEL_MIDI_EQUIVALENT + pitchLevel / static_cast<float>(mpe::PITCH_LEVEL_STEP);
     393              : 
     394            0 :     return std::clamp(stepCount, 0.f, 127.f);
     395              : }
     396              : 
     397            0 : float VstSequencer::noteTuning(const mpe::NoteEvent& noteEvent, const int noteIdx) const
     398              : {
     399            0 :     int semitonesCount = noteIdx - mpe::ZERO_PITCH_LEVEL_MIDI_EQUIVALENT;
     400              : 
     401            0 :     mpe::pitch_level_t tuningPitchLevel = noteEvent.pitchCtx().nominalPitchLevel - semitonesCount * mpe::PITCH_LEVEL_STEP;
     402              : 
     403            0 :     return (tuningPitchLevel / static_cast<float>(mpe::PITCH_LEVEL_STEP)) * 100.f;
     404              : }
     405              : 
     406            0 : float VstSequencer::noteVelocityFraction(const mpe::NoteEvent& noteEvent) const
     407              : {
     408            0 :     const mpe::ExpressionContext& expressionCtx = noteEvent.expressionCtx();
     409              : 
     410            0 :     if (expressionCtx.velocityOverride.has_value()) {
     411            0 :         return std::clamp(expressionCtx.velocityOverride.value(), 0.f, 1.f);
     412              :     }
     413              : 
     414            0 :     mpe::dynamic_level_t dynamicLevel = expressionCtx.expressionCurve.empty()
     415            0 :                                         ? expressionCtx.nominalDynamicLevel : expressionCtx.expressionCurve.maxAmplitudeLevel();
     416            0 :     return expressionLevel(dynamicLevel);
     417              : }
     418              : 
     419            0 : float VstSequencer::expressionLevel(const mpe::dynamic_level_t dynamicLevel) const
     420              : {
     421              :     static constexpr mpe::dynamic_level_t MIN_SUPPORTED_DYNAMIC_LEVEL = mpe::dynamicLevelFromType(mpe::DynamicType::ppp);
     422              :     static constexpr mpe::dynamic_level_t MAX_SUPPORTED_DYNAMIC_LEVEL = mpe::dynamicLevelFromType(mpe::DynamicType::fff);
     423              :     static constexpr mpe::dynamic_level_t AVAILABLE_RANGE = MAX_SUPPORTED_DYNAMIC_LEVEL - MIN_SUPPORTED_DYNAMIC_LEVEL;
     424              : 
     425            0 :     if (dynamicLevel <= MIN_SUPPORTED_DYNAMIC_LEVEL) {
     426            0 :         return (0.5f * mpe::ONE_PERCENT) / AVAILABLE_RANGE;
     427              :     }
     428              : 
     429            0 :     if (dynamicLevel >= MAX_SUPPORTED_DYNAMIC_LEVEL) {
     430            0 :         return 1.f;
     431              :     }
     432              : 
     433            0 :     return RealRound((dynamicLevel - MIN_SUPPORTED_DYNAMIC_LEVEL) / static_cast<float>(AVAILABLE_RANGE), 2);
     434              : }
     435              : 
     436            0 : float VstSequencer::pitchBendLevel(const mpe::pitch_level_t pitchLevel) const
     437              : {
     438              :     static constexpr float SEMITONE_RANGE = 2.f;
     439              :     static constexpr float PITCH_BEND_SEMITONE_STEP = 0.5f / SEMITONE_RANGE;
     440              : 
     441            0 :     float pitchLevelSteps = pitchLevel / static_cast<float>(mpe::PITCH_LEVEL_STEP);
     442            0 :     float offset = pitchLevelSteps * PITCH_BEND_SEMITONE_STEP;
     443              : 
     444            0 :     return std::clamp(0.5f + offset, 0.f, 1.f);
     445              : }
        

Generated by: LCOV version 2.0-1