Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(1983)

Unified Diff: media/midi/midi_manager_winrt.cc

Issue 2243183002: Web MIDI backend for Windows 10 (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: rebase, revise thread-safety Created 4 years, 4 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
Index: media/midi/midi_manager_winrt.cc
diff --git a/media/midi/midi_manager_winrt.cc b/media/midi/midi_manager_winrt.cc
new file mode 100644
index 0000000000000000000000000000000000000000..016241b089bfc2d5a01ca4f497cf32d551d2f4ca
--- /dev/null
+++ b/media/midi/midi_manager_winrt.cc
@@ -0,0 +1,625 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "media/midi/midi_manager_winrt.h"
+
+#include <robuffer.h>
+#include <windows.devices.enumeration.h>
+#include <windows.devices.midi.h>
+#include <wrl/event.h>
+
+#include "base/bind.h"
+#include "base/containers/hash_tables.h"
+#include "base/strings/string16.h"
+#include "base/strings/utf_string_conversions.h"
+#include "base/timer/timer.h"
+#include "base/win/windows_version.h"
+
+namespace media {
+namespace midi {
+namespace {
+
+namespace WRL = Microsoft::WRL;
+
+using namespace ABI::Windows::Devices::Enumeration;
+using namespace ABI::Windows::Devices::Midi;
+using namespace ABI::Windows::Foundation;
+using namespace ABI::Windows::Storage::Streams;
+
+// Factory functions that activate and create WinRT components. The caller takes
+// ownership of the returning ComPtr.
+template <typename InterfaceType, base::char16 const* runtime_class_id>
+WRL::ComPtr<InterfaceType> WrlStaticsFactory() {
+ WRL::ComPtr<InterfaceType> com_ptr;
+
+ HRESULT hr = GetActivationFactory(
+ WRL::Wrappers::HStringReference(runtime_class_id).Get(), &com_ptr);
+ DCHECK(SUCCEEDED(hr));
+
+ return com_ptr;
+}
+
+WRL::ComPtr<IBufferFactory> GetBufferFactory() {
+ return WrlStaticsFactory<IBufferFactory,
+ RuntimeClass_Windows_Storage_Streams_Buffer>();
+}
+
+WRL::ComPtr<IDeviceInformationStatics> GetDeviceInformationStatics() {
+ return WrlStaticsFactory<
+ IDeviceInformationStatics,
+ RuntimeClass_Windows_Devices_Enumeration_DeviceInformation>();
+}
+
+template <typename T, HRESULT (T::*method)(HSTRING*)>
+std::string GetStringFromObjectMethod(T* obj) {
+ HSTRING result;
+ HRESULT hr = (obj->*method)(&result);
+ DCHECK(SUCCEEDED(hr));
+
+ const base::char16* buffer = WindowsGetStringRawBuffer(result, nullptr);
+ if (buffer)
+ return base::WideToUTF8(buffer);
+ return std::string();
+}
+
+template <typename T>
+std::string GetIdString(T* obj) {
+ return GetStringFromObjectMethod<T, &T::get_Id>(obj);
+}
+
+template <typename T>
+std::string GetDeviceIdString(T* obj) {
+ return GetStringFromObjectMethod<T, &T::get_DeviceId>(obj);
+}
+
+std::string GetNameString(IDeviceInformation* info) {
+ return GetStringFromObjectMethod<IDeviceInformation,
+ &IDeviceInformation::get_Name>(info);
+}
+
+uint8_t* GetPointerToBufferData(IBuffer* buffer) {
+ WRL::ComPtr<IInspectable> inspectable(
+ reinterpret_cast<IInspectable*>(buffer));
+ WRL::ComPtr<Windows::Storage::Streams::IBufferByteAccess> buffer_byte_access;
+
+ HRESULT hr = inspectable.As(&buffer_byte_access);
+ DCHECK(SUCCEEDED(hr));
+
+ uint8_t* ptr = nullptr;
+ hr = buffer_byte_access->Buffer(&ptr);
+ DCHECK(SUCCEEDED(hr));
+
+ // Lifetime of the pointing buffer is controlled by the buffer object.
+ return ptr;
+}
+
+template <typename InterfaceType>
+struct MidiPort {
+ MidiPort() = default;
+
+ uint32_t index;
+ WRL::ComPtr<InterfaceType> handle;
+ base::TimeTicks start_time;
+
+ private:
+ DISALLOW_COPY_AND_ASSIGN(MidiPort);
+};
+
+template <typename InterfaceType,
+ typename RuntimeType,
+ typename StaticsInterfaceType,
+ base::char16 const* runtime_class_id>
+class MidiPortManager {
+ public:
+ // MidiPortManager instances should be constructed on the COM thread.
+ MidiPortManager(MidiManagerWinrt* midi_manager)
+ : midi_manager_(midi_manager),
+ task_runner_(base::ThreadTaskRunnerHandle::Get()) {}
+
+ void StartWatcher() {
Shao-Chuan Lee 2016/08/23 04:33:29 Missing |thread_checker_| check.
+ HRESULT hr;
+
+ midi_port_statics_ =
+ WrlStaticsFactory<StaticsInterfaceType, runtime_class_id>();
+
+ HSTRING device_selector = nullptr;
+ hr = midi_port_statics_->GetDeviceSelector(&device_selector);
+ DCHECK(SUCCEEDED(hr));
+
+ hr = GetDeviceInformationStatics()->CreateWatcherAqsFilter(device_selector,
+ &watcher_);
+ DCHECK(SUCCEEDED(hr));
+
+ // Register callbacks to WinRT that post state-modifying jobs back to COM
+ // thread. |weak_ptr| and |task_runner| are captured by lambda callbacks for
+ // posting jobs. Note that WinRT callback arguments should not be passed
+ // outside the callback since the pointers may be unavailable afterwards.
+ base::WeakPtr<MidiPortManager> weak_ptr = GetWeakPtrFromFactory();
+ scoped_refptr<base::SingleThreadTaskRunner> task_runner = task_runner_;
+
+ hr = watcher_->add_Added(
+ WRL::Callback<ITypedEventHandler<DeviceWatcher*, DeviceInformation*>>(
+ [weak_ptr, task_runner](IDeviceWatcher* watcher,
+ IDeviceInformation* info) {
+ std::string dev_id = GetIdString(info),
+ dev_name = GetNameString(info);
+
+ task_runner->PostTask(
+ FROM_HERE, base::Bind(&MidiPortManager::OnAdded, weak_ptr,
+ dev_id, dev_name));
+
+ return S_OK;
+ })
+ .Get(),
+ &token_Added_);
+ DCHECK(SUCCEEDED(hr));
+
+ hr = watcher_->add_EnumerationCompleted(
+ WRL::Callback<ITypedEventHandler<DeviceWatcher*, IInspectable*>>(
+ [weak_ptr, task_runner](IDeviceWatcher* watcher,
+ IInspectable* insp) {
+ task_runner->PostTask(
+ FROM_HERE,
+ base::Bind(&MidiPortManager::OnEnumerationCompleted,
+ weak_ptr));
+
+ return S_OK;
+ })
+ .Get(),
+ &token_EnumerationCompleted_);
+ DCHECK(SUCCEEDED(hr));
+
+ hr = watcher_->add_Removed(
+ WRL::Callback<
+ ITypedEventHandler<DeviceWatcher*, DeviceInformationUpdate*>>(
+ [weak_ptr, task_runner](IDeviceWatcher* watcher,
+ IDeviceInformationUpdate* update) {
+ std::string dev_id = GetIdString(update);
+
+ task_runner->PostTask(
+ FROM_HERE,
+ base::Bind(&MidiPortManager::OnRemoved, weak_ptr, dev_id));
+
+ return S_OK;
+ })
+ .Get(),
+ &token_Removed_);
+ DCHECK(SUCCEEDED(hr));
+
+ hr = watcher_->add_Stopped(
+ WRL::Callback<ITypedEventHandler<DeviceWatcher*, IInspectable*>>(
+ [](IDeviceWatcher* watcher, IInspectable* insp) {
+ // Placeholder, does nothing for now.
+ return S_OK;
+ })
+ .Get(),
+ &token_Stopped_);
+ DCHECK(SUCCEEDED(hr));
+
+ hr = watcher_->add_Updated(
+ WRL::Callback<
+ ITypedEventHandler<DeviceWatcher*, DeviceInformationUpdate*>>(
+ [](IDeviceWatcher* watcher, IDeviceInformationUpdate* update) {
+ // TODO(shaochuan): Check for fields to be updated here.
+ return S_OK;
+ })
+ .Get(),
+ &token_Updated_);
+ DCHECK(SUCCEEDED(hr));
+
+ hr = watcher_->Start();
+ DCHECK(SUCCEEDED(hr));
+ }
+
+ ~MidiPortManager() {
+ DCHECK(thread_checker_.CalledOnValidThread());
+
+ watcher_->remove_Added(token_Added_);
+ watcher_->remove_EnumerationCompleted(token_EnumerationCompleted_);
+ watcher_->remove_Removed(token_Removed_);
+ watcher_->remove_Stopped(token_Stopped_);
+ watcher_->remove_Updated(token_Updated_);
+
+ watcher_->Stop();
+ }
+
+ MidiPort<InterfaceType>* GetPortByDeviceId(std::string dev_id) {
+ DCHECK(thread_checker_.CalledOnValidThread());
+
+ auto it = ports_.find(dev_id);
+ if (it == ports_.end())
+ return nullptr;
+ return it->second.get();
+ }
+
+ MidiPort<InterfaceType>* GetPortByIndex(uint32_t port_index) {
+ DCHECK(thread_checker_.CalledOnValidThread());
+
+ auto it = ports_.find(port_ids_[port_index]);
+ if (it == ports_.end())
+ return nullptr;
+ return it->second.get();
+ }
+
+ protected:
+ // The owning MidiManagerWinrt.
+ MidiManagerWinrt* midi_manager_;
+
+ // Task runner of the COM thread.
+ scoped_refptr<base::SingleThreadTaskRunner> task_runner_;
+
+ // Ensures all methods are called on the COM thread.
+ base::ThreadChecker thread_checker_;
+
+ private:
+ // DeviceWatcher callbacks:
+ void OnAdded(std::string dev_id, std::string dev_name) {
+ DCHECK(thread_checker_.CalledOnValidThread());
+
+ // TODO(shaochuan): Disable Microsoft GS Wavetable Synth due to security
+ // reasons. http://crbug.com/499279
+
+ port_names_[dev_id] = dev_name;
+
+ base::string16 dev_id_string16 = base::UTF8ToWide(dev_id);
+ HSTRING dev_id_hstring;
+ HRESULT hr = WindowsCreateString(
+ dev_id_string16.c_str(), static_cast<UINT32>(dev_id_string16.length()),
+ &dev_id_hstring);
+ DCHECK(SUCCEEDED(hr));
+
+ WRL::ComPtr<IAsyncOperation<RuntimeType*>> async_op;
+
+ hr = midi_port_statics_->FromIdAsync(dev_id_hstring, &async_op);
+ DCHECK(SUCCEEDED(hr));
+
+ WindowsDeleteString(dev_id_hstring);
+ dev_id_hstring = nullptr;
+
+ base::WeakPtr<MidiPortManager> weak_ptr = GetWeakPtrFromFactory();
+ scoped_refptr<base::SingleThreadTaskRunner> task_runner = task_runner_;
+
+ hr = async_op->put_Completed(
+ WRL::Callback<IAsyncOperationCompletedHandler<RuntimeType*>>(
+ [weak_ptr, task_runner](IAsyncOperation<RuntimeType*>* async_op,
+ AsyncStatus status) {
+ // TODO(shaochuan): Check if port open time is accurate.
+ const auto now = base::TimeTicks::Now();
+
+ InterfaceType* handle;
+ HRESULT hr = async_op->GetResults(&handle);
+ DCHECK(SUCCEEDED(hr));
+
+ // |async_op| is owned by |async_ops_|, safe to pass outside.
+ task_runner->PostTask(
+ FROM_HERE,
+ base::Bind(&MidiPortManager::OnCompletedGetPortFromIdAsync,
+ weak_ptr, handle, now, async_op));
+
+ return S_OK;
+ })
+ .Get());
+ DCHECK(SUCCEEDED(hr));
+
+ // Keep a reference to incompleted |async_op| to ensure lifetime.
+ async_ops_.insert(std::move(async_op));
+ }
+
+ void OnEnumerationCompleted() {
+ DCHECK(thread_checker_.CalledOnValidThread());
+
+ midi_manager_->OnPortManagerReady();
+ }
+
+ void OnRemoved(std::string dev_id) {
+ DCHECK(thread_checker_.CalledOnValidThread());
+
+ MidiPort<InterfaceType>* port = GetPortByDeviceId(dev_id);
+ DCHECK(port != nullptr);
+
+ SetPortState(port->index, MIDI_PORT_DISCONNECTED);
+
+ port->handle = nullptr;
+ }
+
+ void OnCompletedGetPortFromIdAsync(InterfaceType* handle,
+ base::TimeTicks start_time,
+ IAsyncOperation<RuntimeType*>* async_op) {
+ DCHECK(thread_checker_.CalledOnValidThread());
+
+ RegisterOnMessageReceived(handle);
+
+ std::string dev_id = GetDeviceIdString(handle);
+
+ MidiPort<InterfaceType>* port = GetPortByDeviceId(dev_id);
+
+ if (port == nullptr) {
+ // TODO(shaochuan): Fill in manufacturer and driver version.
+ AddPort(MidiPortInfo(dev_id, std::string("Manufacturer"),
+ port_names_[dev_id], std::string("DriverVersion"),
+ MIDI_PORT_OPENED));
+
+ port = new MidiPort<InterfaceType>;
+ port->index = static_cast<uint32_t>(port_ids_.size());
+
+ ports_[dev_id].reset(port);
+ port_ids_.push_back(dev_id);
+ } else {
+ SetPortState(port->index, MIDI_PORT_CONNECTED);
+ }
+
+ port->handle = handle;
+ port->start_time = start_time;
+
+ // Remove reference to completed |async_op|.
+ auto it = async_ops_.find(async_op);
+ DCHECK(it != async_ops_.end());
+ async_ops_.erase(it);
+ }
+
+ // Overrided by MidiInPortManager to listen to input ports.
+ virtual void RegisterOnMessageReceived(InterfaceType* handle) {}
+
+ // Calls midi_manager_->Add{Input,Output}Port.
+ virtual void AddPort(MidiPortInfo info) = 0;
+
+ // Calls midi_manager_->Set{Input,Output}PortState.
+ virtual void SetPortState(uint32_t port_index, MidiPortState state) = 0;
+
+ // WeakPtrFactory has to be declared in derived class, use this method to
+ // retrieve upcasted WeakPtr for posting tasks.
+ virtual base::WeakPtr<MidiPortManager> GetWeakPtrFromFactory() = 0;
+
+ // Midi{In,Out}PortStatics instance.
+ WRL::ComPtr<StaticsInterfaceType> midi_port_statics_;
+
+ // DeviceWatcher instance and event registration tokens for unsubscribing
+ // events in destructor.
+ WRL::ComPtr<IDeviceWatcher> watcher_;
+ EventRegistrationToken token_Added_, token_EnumerationCompleted_,
+ token_Removed_, token_Stopped_, token_Updated_;
+
+ // All manipulations to these fields should be done on COM thread.
+ base::hash_map<std::string, std::unique_ptr<MidiPort<InterfaceType>>> ports_;
+ std::vector<std::string> port_ids_;
+ base::hash_map<std::string, std::string> port_names_;
+
+ // Keeps AsyncOperation objects before the operation completes.
+ std::set<WRL::ComPtr<IAsyncOperation<RuntimeType*>>> async_ops_;
+};
+
+} // namespace
+
+class MidiManagerWinrt::MidiInPortManager final
+ : public MidiPortManager<IMidiInPort,
+ MidiInPort,
+ IMidiInPortStatics,
+ RuntimeClass_Windows_Devices_Midi_MidiInPort> {
+ public:
+ MidiInPortManager(MidiManagerWinrt* midi_manager)
+ : MidiPortManager(midi_manager), weak_factory_(this) {}
+
+ private:
+ // MidiPortManager overrides:
+ void RegisterOnMessageReceived(IMidiInPort* handle) override {
+ DCHECK(thread_checker_.CalledOnValidThread());
+
+ EventRegistrationToken& token = tokens_[GetDeviceIdString(handle)];
+
+ base::WeakPtr<MidiInPortManager> weak_ptr = weak_factory_.GetWeakPtr();
+ scoped_refptr<base::SingleThreadTaskRunner> task_runner = task_runner_;
+
+ handle->add_MessageReceived(
+ WRL::Callback<
+ ITypedEventHandler<MidiInPort*, MidiMessageReceivedEventArgs*>>(
+ [weak_ptr, task_runner](IMidiInPort* handle,
+ IMidiMessageReceivedEventArgs* args) {
+ std::string dev_id = GetDeviceIdString(handle);
+
+ WRL::ComPtr<IMidiMessage> message;
+ HRESULT hr = args->get_Message(&message);
+ DCHECK(SUCCEEDED(hr));
+
+ WRL::ComPtr<IBuffer> buffer;
+ hr = message->get_RawData(&buffer);
+ DCHECK(SUCCEEDED(hr));
+
+ uint8_t* p_buffer_data = GetPointerToBufferData(buffer.Get());
+
+ uint32_t data_length;
+ hr = buffer->get_Length(&data_length);
+ DCHECK(SUCCEEDED(hr));
+ DCHECK(data_length != 0);
+
+ std::vector<uint8_t> data(p_buffer_data,
+ p_buffer_data + data_length);
+
+ // Time since port opened in 100-nanosecond units.
+ TimeSpan time_span;
+ hr = message->get_Timestamp(&time_span);
+ DCHECK(SUCCEEDED(hr));
+
+ task_runner->PostTask(
+ FROM_HERE,
+ base::Bind(&MidiInPortManager::OnMessageReceived, weak_ptr,
+ dev_id, data, base::TimeDelta::FromMicroseconds(
+ time_span.Duration / 10)));
+
+ return S_OK;
+ })
+ .Get(),
+ &token);
+ }
+
+ void AddPort(MidiPortInfo info) final { midi_manager_->AddInputPort(info); }
+
+ void SetPortState(uint32_t port_index, MidiPortState state) final {
+ midi_manager_->SetInputPortState(port_index, state);
+ }
+
+ base::WeakPtr<MidiPortManager> GetWeakPtrFromFactory() final {
+ DCHECK(thread_checker_.CalledOnValidThread());
+
+ return weak_factory_.GetWeakPtr();
+ }
+
+ // Callback on receiving MIDI input message.
+ void OnMessageReceived(std::string dev_id,
+ std::vector<uint8_t> data,
+ base::TimeDelta time_since_start) {
+ DCHECK(thread_checker_.CalledOnValidThread());
+
+ MidiPort<IMidiInPort>* port = GetPortByDeviceId(dev_id);
+ DCHECK(port != nullptr);
+
+ midi_manager_->ReceiveMidiData(port->index, &data[0], data.size(),
+ port->start_time + time_since_start);
+ }
+
+ // Event tokens for input message received events.
+ base::hash_map<std::string, EventRegistrationToken> tokens_;
+
+ // Last member to ensure destructed first.
+ base::WeakPtrFactory<MidiInPortManager> weak_factory_;
+
+ DISALLOW_COPY_AND_ASSIGN(MidiInPortManager);
+};
+
+class MidiManagerWinrt::MidiOutPortManager final
+ : public MidiPortManager<IMidiOutPort,
+ IMidiOutPort,
+ IMidiOutPortStatics,
+ RuntimeClass_Windows_Devices_Midi_MidiOutPort> {
+ public:
+ MidiOutPortManager(MidiManagerWinrt* midi_manager)
+ : MidiPortManager(midi_manager), weak_factory_(this) {}
+
+ private:
+ // MidiPortManager overrides:
+ void AddPort(MidiPortInfo info) final { midi_manager_->AddOutputPort(info); }
+
+ void SetPortState(uint32_t port_index, MidiPortState state) final {
+ midi_manager_->SetOutputPortState(port_index, state);
+ }
+
+ base::WeakPtr<MidiPortManager> GetWeakPtrFromFactory() final {
+ DCHECK(thread_checker_.CalledOnValidThread());
+
+ return weak_factory_.GetWeakPtr();
+ }
+
+ // Last member to ensure destructed first.
+ base::WeakPtrFactory<MidiOutPortManager> weak_factory_;
+
+ DISALLOW_COPY_AND_ASSIGN(MidiOutPortManager);
+};
+
+MidiManagerWinrt::MidiManagerWinrt() : com_thread_("Windows MIDI COM Thread") {}
+
+MidiManagerWinrt::~MidiManagerWinrt() {}
+
+void MidiManagerWinrt::StartInitialization() {
+ DCHECK(base::win::GetVersion() >= base::win::VERSION_WIN10);
+
+ com_thread_.init_com_with_mta(true);
+ com_thread_.Start();
+
+ com_thread_.task_runner()->PostTask(
+ FROM_HERE, base::Bind(&MidiManagerWinrt::InitializeOnComThread,
+ base::Unretained(this)));
+}
+
+void MidiManagerWinrt::Finalize() {
+ com_thread_.task_runner()->PostTask(
+ FROM_HERE, base::Bind(&MidiManagerWinrt::FinalizeOnComThread,
+ base::Unretained(this)));
+
+ // Blocks until FinalizeOnComThread() returns. Delayed MIDI send data tasks
+ // will be ignored.
+ com_thread_.Stop();
+
+ port_manager_ready_count_ = 0;
+}
+
+void MidiManagerWinrt::DispatchSendMidiData(MidiManagerClient* client,
+ uint32_t port_index,
+ const std::vector<uint8_t>& data,
+ double timestamp) {
+ base::AutoLock auto_lock(scheduler_lock_);
+ if (!scheduler_)
+ return;
+
+ scheduler_->PostSendDataTask(
+ client, data.size(), timestamp,
+ base::Bind(&MidiManagerWinrt::SendOnComThread, base::Unretained(this),
+ port_index, data),
+ com_thread_.task_runner());
+}
+
+void MidiManagerWinrt::InitializeOnComThread() {
+ if (!com_thread_checker_)
+ com_thread_checker_.reset(new base::ThreadChecker);
+ DCHECK(com_thread_checker_->CalledOnValidThread());
+
+ port_manager_in_.reset(new MidiInPortManager(this));
+ port_manager_out_.reset(new MidiOutPortManager(this));
+
+ {
+ base::AutoLock auto_lock(scheduler_lock_);
+ scheduler_.reset(new MidiScheduler(this));
+ }
+
+ port_manager_in_->StartWatcher();
+ port_manager_out_->StartWatcher();
+}
+
+void MidiManagerWinrt::FinalizeOnComThread() {
+ DCHECK(com_thread_checker_->CalledOnValidThread());
+
+ {
+ base::AutoLock auto_lock(scheduler_lock_);
+ scheduler_.reset();
+ }
+
+ port_manager_in_.reset();
+ port_manager_out_.reset();
+}
+
+void MidiManagerWinrt::SendOnComThread(uint32_t port_index,
+ const std::vector<uint8_t>& data) {
+ DCHECK(com_thread_checker_->CalledOnValidThread());
+
+ WRL::ComPtr<IBuffer> buffer;
+ HRESULT hr =
+ GetBufferFactory()->Create(static_cast<UINT32>(data.size()), &buffer);
+ DCHECK(SUCCEEDED(hr));
+
+ hr = buffer->put_Length(static_cast<UINT32>(data.size()));
+ DCHECK(SUCCEEDED(hr));
+
+ uint8_t* p_buffer_data = GetPointerToBufferData(buffer.Get());
+
+ std::copy(data.begin(), data.end(), p_buffer_data);
+
+ MidiPort<IMidiOutPort>* port = port_manager_out_->GetPortByIndex(port_index);
+ DCHECK(port != nullptr);
+
+ hr = port->handle->SendBuffer(buffer.Get());
+ DCHECK(SUCCEEDED(hr));
+}
+
+void MidiManagerWinrt::OnPortManagerReady() {
+ DCHECK(com_thread_checker_->CalledOnValidThread());
+ DCHECK(port_manager_ready_count_ < 2);
+
+ if (++port_manager_ready_count_ == 2)
+ CompleteInitialization(Result::OK);
+}
+
+MidiManager* MidiManager::Create() {
+ return new MidiManagerWinrt();
+}
+
+} // namespace midi
+} // namespace media

Powered by Google App Engine
This is Rietveld 408576698