//******************************************************************************
//
// Simple MIDI Library / SMOutDevCtrl
//
// MIDI出力デバイス制御クラス
//
// Copyright (C) 2010-2021 WADA Masashi. All Rights Reserved.
//
//******************************************************************************

#import "YNBaseLib.h"
#import "SMOutDevCtrl.h"
#import "SMEventMIDI.h"


//******************************************************************************
// グローバル定義
//******************************************************************************
//MIDISendSysex処理完了コールバック関数
void SysexSendcompletionProc(MIDISysexSendRequest *request);

//スレッド同期用
static NSCondition* g_pSendCompleteCondition = nil;
static int g_RefCount = 0;


//******************************************************************************
// コンストラクタ
//******************************************************************************
SMOutDevCtrl::SMOutDevCtrl(void)
{
	unsigned char portNo = 0;
	
	//ポート情報
	for (portNo = 0; portNo < SM_MIDIOUT_PORT_NUM_MAX; portNo++) {
		m_PortInfo[portNo].isExist = NO;
		m_PortInfo[portNo].endpointRef = 0;
		m_PortInfo[portNo].portRef = 0;
	}
	
	//MIDIクライアント
	m_ClientRef = 0;
	
	//スレッド同期オブジェクト
	if (g_RefCount == 0) {
		g_pSendCompleteCondition = [[NSCondition alloc] init];
	}
	g_RefCount++;
}

//******************************************************************************
// デストラクタ
//******************************************************************************
SMOutDevCtrl::~SMOutDevCtrl()
{
	SMDevInfo* pDevInfo = NULL;
	SMOutDevListItr itr;
	
	for (itr = m_OutDevList.begin(); itr != m_OutDevList.end(); itr++) {
		pDevInfo = *itr;
		delete pDevInfo;
	}
	m_OutDevList.clear();
	
	g_RefCount--;
	if (g_RefCount == 0) {
		[g_pSendCompleteCondition release];
	}
	MIDIClientDispose(m_ClientRef);
}

//******************************************************************************
// 初期化
//******************************************************************************
int SMOutDevCtrl::Initialize()
{
	int result = 0;
	OSStatus err;
	
	//ハードウェア再スキャン
	//MIDIRestart();
	
	//ポート情報クリア
	result = ClearPortInfo();
	if (result != 0) goto EXIT;
	
	//MIDI出力デバイス一覧を作成
	//  MIDIGetNumberOfDevices, MIDIGetDevice を用いてエンドポイントを検索する
	//  ・オフラインのデバイス：○検索結果に含まれる
	//  ・仮想ポート        ：×検索結果に含まれない
	//result = _InitDevList();
	//if (result != 0) goto EXIT;
	
	//MIDI出力デバイス一覧を作成
	//  MIDIGetNumberOfDestinations, MIDIGetDestination を用いてエンドポイントを検索する
	//  ・オフラインのデバイス：×検索結果に含まれない
	//  ・仮想ポート        ：○検索結果に含まれる
	result = _InitDevListWithVitualDest();
	if (result != 0) goto EXIT;
	
	//MIDIクライアント生成
	if (m_ClientRef != 0) {
		MIDIClientDispose(m_ClientRef);
		m_ClientRef = 0;
	}
	err = MIDIClientCreate(
				CFSTR("MIDITrail Client"),	//クライアント名称
				NULL,					//MIDIシステム変更通知用コールバック関数
				NULL,					//通知オブジェクト
				&m_ClientRef			//作成されたクライアント
			);
	if (err != noErr) {
		result = YN_SET_ERR(@"MIDI client create error.", 0, 0);
		goto EXIT;
	}
	
EXIT:;
	return result;
}

//******************************************************************************
// デバイスリスト初期化
//******************************************************************************
int SMOutDevCtrl::_InitDevList()
{
	int result = 0;
	ItemCount devNum = 0;
	ItemCount devIndex = 0;
	SMOutDevListItr itr;
	SMDevInfo* pDevInfo = NULL;
	
	//デバイスリストクリア
	for (itr = m_OutDevList.begin(); itr != m_OutDevList.end(); itr++) {
		pDevInfo = *itr;
		delete pDevInfo;
	}
	m_OutDevList.clear();
	
	//デバイスの数
	//  プロセス起動後の初回API呼び出しで1秒以上引っかかる
	//  2回目以降は瞬時に終わる
	devNum = MIDIGetNumberOfDevices();
	
	//デバイスごとにループして出力先情報を取得する
	for (devIndex = 0; devIndex < devNum; devIndex++) {
		result = _CheckDev(devIndex);
		if (result != 0) goto EXIT;
	}
	
EXIT:;
	return result;
}

//******************************************************************************
// デバイスリスト初期化：デバイスチェック
//******************************************************************************
int SMOutDevCtrl::_CheckDev(
		ItemCount index
	)
{
	int result = 0;
	ItemCount entNum = 0;
	ItemCount entIndex = 0;
	MIDIDeviceRef devRef = 0;
	
	//デバイス取得
	devRef = MIDIGetDevice(index);
	
	//デバイスが保有するエンティティの数
	entNum = MIDIDeviceGetNumberOfEntities(devRef);
	
	//エンティティごとにループして出力先情報を取得する
	for (entIndex = 0; entIndex < entNum; entIndex++) {
		result = _CheckEnt(entIndex, devRef);
		if (result != 0) goto EXIT;
	}
	
EXIT:;
	return result;
}

//******************************************************************************
// デバイスリスト初期化：エンティティチェック
//******************************************************************************
int SMOutDevCtrl::_CheckEnt(
		ItemCount index,
		MIDIDeviceRef devRef
	)
{
	int result = 0;
	ItemCount destNum = 0;
	ItemCount destIndex = 0;
	MIDIEntityRef entityRef = 0;
	MIDIEndpointRef endpointRef = 0;
	
	//エンティティ取得
	entityRef = MIDIDeviceGetEntity(devRef, index);
	
	//出力先の数
	destNum= MIDIEntityGetNumberOfDestinations(entityRef);
	
	//出力先ごとにループして出力先情報を取得する
	for (destIndex = 0; destIndex < destNum; destIndex++) {
		//エンドポイント取得
		endpointRef = MIDIEntityGetDestination(entityRef, index);
		
		//エンドポイント確認
		result = _CheckEnd(endpointRef);
		if (result != 0) goto EXIT;
	}
	
EXIT:;
	return result;
}

//******************************************************************************
// デバイスリスト初期化：エンドポイントチェック
//******************************************************************************
int SMOutDevCtrl::_CheckEnd(
		MIDIEndpointRef endpointRef
	)
{
	int result = 0;
	OSStatus err;
	CFStringRef strDisplayNameRef = NULL;
	CFStringRef strManufacturerRef = NULL;
	CFStringRef strModelRef = NULL;
	CFStringRef strNameRef = NULL;
	SInt32 isOffline = YES;
	NSString* pDisplayName = nil;
	NSString* pIdName = nil;
	NSString* pManufacturerName = nil;
	SMDevInfo* pDevInfo = NULL;
	
	//表示名称 ex. "UM-2G 1" ＜「Audio MIDI 設定」でユーザが自由に変更できる
	err = MIDIObjectGetStringProperty(endpointRef, kMIDIPropertyDisplayName, &strDisplayNameRef);
	if (err != noErr) {
		result = YN_SET_ERR(@"CoreMIDI API Error", err, 0);
		goto EXIT;
	}
	//メーカー名 ex."Roland"
	err = MIDIObjectGetStringProperty(endpointRef, kMIDIPropertyManufacturer, &strManufacturerRef);
	if (err != noErr) {
		result = YN_SET_ERR(@"CoreMIDI API Error", err, 0);
		goto EXIT;
	}
	//モデル ex. "UM-2G"
	err = MIDIObjectGetStringProperty(endpointRef, kMIDIPropertyModel, &strModelRef);
	if (err != noErr) {
		result = YN_SET_ERR(@"CoreMIDI API Error", err, 0);
		goto EXIT;
	}
	//エンドポイント名 ex. "1"
	err = MIDIObjectGetStringProperty(endpointRef, kMIDIPropertyName, &strNameRef);
	if (err != noErr) {
		result = YN_SET_ERR(@"CoreMIDI API Error", err, 0);
		goto EXIT;
	}
	
	//出力先の接続状態
	err = MIDIObjectGetIntegerProperty(endpointRef, kMIDIPropertyOffline, &isOffline);
	if (err == kMIDIUnknownProperty) {
		//プロパティが存在しない場合はオンラインとみなす
		isOffline = NO;
	}
	else if (err != noErr) {
		result = YN_SET_ERR(@"CoreMIDI API Error", err, 0);
		goto EXIT;
	}
	
	//表示名を作成：オフラインの場合は末尾に(offline)を追加する
	pDisplayName = (NSString*)strDisplayNameRef;
	if (isOffline) {
		pDisplayName = [NSString stringWithFormat:@"%@ (offline)", (NSString*)strDisplayNameRef];
	}
	
	//識別名を作成 ex. "Roland/UM-2G/1"
	pIdName = [NSString stringWithFormat:@"%@/%@/%@",
					(NSString*)strManufacturerRef,
					(NSString*)strModelRef,
					(NSString*)strNameRef];
	
	//NSLog(@"MIDI OUT Device IdName: %@", pIdName);
	
	//メーカー名
	pManufacturerName = (NSString*)strManufacturerRef;
	
	//デバイス情報の作成
	try {
		pDevInfo = new SMDevInfo();
	}
	catch (std::bad_alloc) {
		result = YN_SET_ERR(@"Could not allocate memory.", 0, 0);
		goto EXIT;
	}
	pDevInfo->SetDisplayName(pDisplayName);
	pDevInfo->SetIdName(pIdName);
	pDevInfo->SetManufacturerName(pManufacturerName);
	pDevInfo->SetEndpointRef(endpointRef);
	pDevInfo->SetOnline(!isOffline);
	
	//デバイスリストに登録
	m_OutDevList.push_back(pDevInfo);
	pDevInfo = NULL;
	
EXIT:;
	if (strDisplayNameRef != NULL) CFRelease(strDisplayNameRef);
	if (strManufacturerRef != NULL) CFRelease(strManufacturerRef);
	if (strModelRef != NULL) CFRelease(strModelRef);
	if (strNameRef != NULL) CFRelease(strNameRef);
	return result;
}

//******************************************************************************
// デバイスリスト初期化：オフラインデバイスなし＋仮想ポートあり
//******************************************************************************
int SMOutDevCtrl::_InitDevListWithVitualDest()
{
	int result = 0;
	ItemCount destNum = 0;
	ItemCount index = 0;
	SMOutDevListItr itr;
	SMDevInfo* pDevInfo = NULL;
	MIDIEndpointRef endpointRef = 0;
	
	//デバイスリストクリア
	for (itr = m_OutDevList.begin(); itr != m_OutDevList.end(); itr++) {
		pDevInfo = *itr;
		delete pDevInfo;
	}
	m_OutDevList.clear();
	
	//出力先ポート数の取得
	destNum = MIDIGetNumberOfDestinations();
	
	//デバイスごとにループして出力先情報を取得する
	for (index = 0; index < destNum; index++) {
		//エンドポイント取得
		endpointRef = MIDIGetDestination(index);
		
		//エンドポイント確認		
		result = _CheckDest(endpointRef);
		if (result != 0) goto EXIT;
	}
	
EXIT:;
	return result;
}

//******************************************************************************
// デバイスリスト初期化：出力ポート確認
//******************************************************************************
int SMOutDevCtrl::_CheckDest(
		MIDIEndpointRef endpointRef
	)
{
	int result = 0;
	OSStatus err;
	CFStringRef strDisplayNameRef = NULL;
	CFStringRef strManufacturerRef = NULL;
	CFStringRef strModelRef = NULL;
	CFStringRef strNameRef = NULL;
	SInt32 isOffline = YES;
	NSString* pDisplayName = nil;
	NSString* pIdName = nil;
	NSString* pManufacturerName = nil;
	SMDevInfo* pDevInfo = NULL;
	
	//表示名称 ex. "UM-2G 1" ＜「Audio MIDI 設定」でユーザが自由に変更できる
	err = MIDIObjectGetStringProperty(endpointRef, kMIDIPropertyDisplayName, &strDisplayNameRef);
	if (err != noErr) {
		result = YN_SET_ERR(@"CoreMIDI API Error", err, 0);
		goto EXIT;
	}
	
	//メーカー名 ex."Roland"
	err = MIDIObjectGetStringProperty(endpointRef, kMIDIPropertyManufacturer, &strManufacturerRef);
	if (err != noErr) {
		if (err == kMIDIUnknownProperty) {
			//仮想ポートの場合は取得できない場合がある
			strManufacturerRef = CFSTR("");
		}
		else {
			result = YN_SET_ERR(@"CoreMIDI API Error", err, 0);
			goto EXIT;
		}
	}
	//モデル ex. "UM-2G"
	err = MIDIObjectGetStringProperty(endpointRef, kMIDIPropertyModel, &strModelRef);
	if (err != noErr) {
		if (err == kMIDIUnknownProperty) {
			//仮想ポートの場合は取得できない場合がある
			strModelRef = CFSTR("");
		}
		else {
			result = YN_SET_ERR(@"CoreMIDI API Error", err, 0);
			goto EXIT;
		}
	}
	//エンドポイント名 ex. "1"
	err = MIDIObjectGetStringProperty(endpointRef, kMIDIPropertyName, &strNameRef);
	if (err != noErr) {
		result = YN_SET_ERR(@"CoreMIDI API Error", err, 0);
		goto EXIT;
	}
	
	//出力先の接続状態
	err = MIDIObjectGetIntegerProperty(endpointRef, kMIDIPropertyOffline, &isOffline);
	if (err != noErr) {
		if (err == kMIDIUnknownProperty) {
			//仮想ポートの場合は取得できない場合がある
			isOffline = NO;
		}
		else {
			result = YN_SET_ERR(@"CoreMIDI API Error", err, 0);
			goto EXIT;
		}
	}
	
	//表示名を作成：オフラインの場合は末尾に(offline)を追加する
	pDisplayName = (NSString*)strDisplayNameRef;
	if (isOffline) {
		pDisplayName = [NSString stringWithFormat:@"%@ (offline)", (NSString*)strDisplayNameRef];
	}
	
	//識別名を作成 ex. "Roland/UM-2G/1"
	pIdName = [NSString stringWithFormat:@"%@/%@/%@",
			   (NSString*)strManufacturerRef,
			   (NSString*)strModelRef,
			   (NSString*)strNameRef];
	
	//NSLog(@"MIDI OUT Device IdName: %@", pIdName);
	
	//メーカー名
	pManufacturerName = (NSString*)strManufacturerRef;
	
	//デバイス情報の作成
	try {
		pDevInfo = new SMDevInfo();
	}
	catch (std::bad_alloc) {
		result = YN_SET_ERR(@"Could not allocate memory.", 0, 0);
		goto EXIT;
	}
	pDevInfo->SetDisplayName(pDisplayName);
	pDevInfo->SetIdName(pIdName);
	pDevInfo->SetManufacturerName(pManufacturerName);
	pDevInfo->SetEndpointRef(endpointRef);
	pDevInfo->SetOnline(!isOffline);
	
	//デバイスリストに登録
	m_OutDevList.push_back(pDevInfo);
	pDevInfo = NULL;
	
EXIT:;
	if (strDisplayNameRef != NULL) CFRelease(strDisplayNameRef);
	if (strManufacturerRef != NULL) CFRelease(strManufacturerRef);
	if (strModelRef != NULL) CFRelease(strModelRef);
	if (strNameRef != NULL) CFRelease(strNameRef);
	return result;
}

//******************************************************************************
// デバイス数取得
//******************************************************************************
unsigned int SMOutDevCtrl::GetDevNum()
{
	return (unsigned int)m_OutDevList.size();
}

//******************************************************************************
// デバイス表示名称取得
//******************************************************************************
NSString* SMOutDevCtrl::GetDevDisplayName(
		unsigned int index
	)
{
	SMDevInfo* pDevInfo = NULL;
	NSString* pDisplayName = nil;
	SMOutDevListItr itr;
	
	if (index < m_OutDevList.size()) {
		itr = m_OutDevList.begin();
		advance(itr, index);
		pDevInfo = *itr;
		pDisplayName = pDevInfo->GetDisplayName();
	}
	
	return pDisplayName;
}

//******************************************************************************
// デバイス識別名称取得
//******************************************************************************
NSString* SMOutDevCtrl::GetDevIdName(
		unsigned int index
	)
{
	SMDevInfo* pDevInfo = NULL;
	NSString* pIdName = nil;
	SMOutDevListItr itr;
	
	if (index < m_OutDevList.size()) {
		itr = m_OutDevList.begin();
		advance(itr, index);
		pDevInfo = *itr;
		pIdName = pDevInfo->GetIdName();
	}
	
	return pIdName;
}

//******************************************************************************
// メーカー名取得
//******************************************************************************
NSString* SMOutDevCtrl::GetManufacturerName(
		unsigned int index
	)
{
	SMDevInfo* pDevInfo = NULL;
	NSString* pManufacturerName = nil;
	SMOutDevListItr itr;
	
	if (index < m_OutDevList.size()) {
		itr = m_OutDevList.begin();
		advance(itr, index);
		pDevInfo = *itr;
		pManufacturerName = pDevInfo->GetManufacturerName();
	}
	
	return pManufacturerName;
}

//******************************************************************************
// オンライン状態取得
//******************************************************************************
bool SMOutDevCtrl::IsOnline(
		unsigned int index
	)
{
	SMDevInfo* pDevInfo = NULL;
	bool isOnline = false;
	SMOutDevListItr itr;
	
	if (index < m_OutDevList.size()) {
		itr = m_OutDevList.begin();
		advance(itr, index);
		pDevInfo = *itr;
		isOnline = pDevInfo->IsOnline();
	}
	
	return isOnline;
}

//******************************************************************************
// ポートに対応するデバイスを設定
//******************************************************************************
int SMOutDevCtrl::SetDevForPort(
		unsigned char portNo,
		NSString* pIdName,
		NSString* pDisplayName
	)
{
	int result = 0;
	bool isFound = false;
	SMDevInfo* pDevInfo = NULL;
	SMOutDevListItr itr;
	
	if (portNo >= SM_MIDIOUT_PORT_NUM_MAX) {
		result = YN_SET_ERR(@"Program error.", 0, 0);
		goto EXIT;
	}
	
	//出力デバイスリストから指定デバイスを探す
	for (itr = m_OutDevList.begin(); itr != m_OutDevList.end(); itr++) {
		pDevInfo = *itr;
		
		//デバイスがオフラインであれば無視する
		if (!(pDevInfo->IsOnline())) continue;
		
		//デバイス識別名称が一致する場合
		if ([pIdName isEqualToString:(pDevInfo->GetIdName())]) {
			//デバイス表示名が未指定の場合はデバイス識別名称のみで判定
			if ([pDisplayName length] == 0) {
				//指定デバイスが見つかったのでポートに情報を登録する
				m_PortInfo[portNo].isExist = true;
				m_PortInfo[portNo].endpointRef = pDevInfo->GetEndpointRef();
				m_PortInfo[portNo].portRef = 0;
				isFound = true;
				break;
			}
			//デバイス表示名が指定されている場合はデバイス表示名の一致も判定
			else if ([pDisplayName isEqualToString:(pDevInfo->GetDisplayName())]) {
				//指定デバイスが見つかったのでポートに情報を登録する
				m_PortInfo[portNo].isExist = true;
				m_PortInfo[portNo].endpointRef = pDevInfo->GetEndpointRef();
				m_PortInfo[portNo].portRef = 0;
				isFound = true;
				break;
			}
		}
	}
	
	//指定デバイスが見つからないかオフラインの場合は何もしない
	if (!isFound) {
		//result = YN_SET_ERR(@"Program error.", portNo, 0);
		//goto EXIT;
		NSLog(@"MIDI OUT - Device not found.");
	}
	
EXIT:;
	return result;
}

//******************************************************************************
// 全ポートに対応するデバイスを開く
//******************************************************************************
int SMOutDevCtrl::OpenPortDevAll()
{
	int result = 0;
	OSStatus err;
	unsigned char portNo = 0;
	unsigned char prevPortNo = 0;
	bool isOpen = false;
	MIDIPortRef portRef = 0;
	
	result = ClosePortDevAll();
	if (result != 0) goto EXIT;
	
	for (portNo = 0; portNo < SM_MIDIOUT_PORT_NUM_MAX; portNo++) {
		
		//ポートが存在しなければスキップ
		if (!m_PortInfo[portNo].isExist) continue;
		
		//別のポートで同じデバイスをすでに開いている場合の対処
		isOpen = false;
		for (prevPortNo = 0; prevPortNo < portNo; prevPortNo++) {
			if (m_PortInfo[portNo].endpointRef == m_PortInfo[prevPortNo].endpointRef) {
				m_PortInfo[portNo].portRef = m_PortInfo[prevPortNo].portRef;
				isOpen = true;
				break;
			}
		}
		
		//新規にポートを開く
		if (!isOpen) {
			err = MIDIOutputPortCreate(
						m_ClientRef,		//MIDIクライアント
						CFSTR("MIDITrail Port"),	//ポート名称
						&portRef			//作成されたポート
					);
			if (err != noErr) {
				result = YN_SET_ERR(@"MIDI port open error.", 0, 0);
			}
			m_PortInfo[portNo].portRef = portRef;
		}
	}

EXIT:;
	return result;
}

//******************************************************************************
// 全ポートに対応するデバイスを閉じる
//******************************************************************************
int SMOutDevCtrl::ClosePortDevAll()
{
	int result = 0;
	OSStatus err;
	unsigned char portNo = 0;
	unsigned char nextPortNo = 0;
	
	for (portNo = 0; portNo < SM_MIDIOUT_PORT_NUM_MAX; portNo++) {
		
		//ポートが存在しなければスキップ
		if (!m_PortInfo[portNo].isExist) continue;
		
		//ポートを開いてなければスキップ
		if (m_PortInfo[portNo].portRef == 0) continue;
		
		//ポートを閉じる
		err = MIDIPortDispose(m_PortInfo[portNo].portRef);
		if (err != noErr) {
			result = YN_SET_ERR(@"MIDI port close error.", 0, 0);
			goto EXIT;
		}
		m_PortInfo[portNo].portRef = 0;
		
		//別のポートで同じデバイスを開いている場合の対処
		for (nextPortNo = portNo+1; nextPortNo < SM_MIDIOUT_PORT_NUM_MAX; nextPortNo++) {
			if (m_PortInfo[portNo].endpointRef == m_PortInfo[nextPortNo].endpointRef) {
				m_PortInfo[nextPortNo].portRef = 0;
			}
		}
	}
	
EXIT:;
	return result;
}

//******************************************************************************
// ポート情報クリア
//******************************************************************************
int SMOutDevCtrl::ClearPortInfo()
{
	int result = 0;
	unsigned char portNo = 0;
	
	result = ClosePortDevAll();
	if (result != 0) goto EXIT;
	
	for (portNo = 0; portNo < SM_MIDIOUT_PORT_NUM_MAX; portNo++) {
		m_PortInfo[portNo].isExist = NO;
		m_PortInfo[portNo].endpointRef = 0;
		m_PortInfo[portNo].portRef = 0;
	}
	
EXIT:;
	return result;
}

//******************************************************************************
// MIDIデータ送信（ショートメッセージ）
//******************************************************************************
int SMOutDevCtrl::SendShortMsg(
		unsigned char portNo,
		unsigned char* pMsg,
		unsigned int size
	)
{
	int result = 0;
	
	if (@available(macOS 11, *)) {
		result = _SendShortMsg2(portNo, pMsg, size);
	}
	else {
		result = _SendShortMsg1(portNo, pMsg, size);
	}
	
	return result;
}

//******************************************************************************
// MIDIデータ送信（ショートメッセージ） macOS 10.15(Catalina)以前
//******************************************************************************
int SMOutDevCtrl::_SendShortMsg1(
		unsigned char portNo,
		unsigned char* pMsg,
		unsigned int size
	)
{
	int result = 0;
	OSStatus err;
	ByteCount bufSize = 1024;
	Byte packetListBuf[1024];
	MIDIPacketList* pPacketList = NULL;
	MIDIPacket* pPacket = NULL;
	
	//パラメータチェック
	if ((portNo >= SM_MIDIOUT_PORT_NUM_MAX) || (size == 0) || (size > 4)) {
		result = YN_SET_ERR(@"Program error.", portNo, size);
		goto EXIT;
	}
	
	//ポートが存在しなければ何もしない
	if (!m_PortInfo[portNo].isExist) goto EXIT;
	
	//ポートが開かれていなければエラー
	if (m_PortInfo[portNo].portRef == 0) {
		result = YN_SET_ERR(@"Program error.", portNo, size);
		goto EXIT;
	}
	
	//パケットリスト初期化
	pPacketList = (MIDIPacketList*)packetListBuf;
	pPacket = MIDIPacketListInit(pPacketList);
	if (pPacket == NULL) {
		result = YN_SET_ERR(@"Program error.", (unsigned long)pPacketList, size);
		goto EXIT;
	}
	//パケットリスト作成：1パケットのみ
	//  イベント発生時刻に将来の時間を設定することにより、
	//  イベント送信処理のスケジューリングをOSに任せることが推奨されているが、
	//  下記理由により即時送信とする。（スケジューリングはアプリで実施する）
	//  よってこの送信方法はトリッキーである。
	//  (1) スケジューリングをOSに任せると動画との同期方式が変わるため修正範囲が広がる
	//  (2) Windows版の実装とできるだけ合わせることにより工数を削減する
	//  スケジューリングをアプリで行う場合は現在時刻を取得して未来の時刻を作成する必要がある
	//  MIDITimeStamp time = AudioGetCurrentHostTime();
	pPacket = MIDIPacketListAdd(
					pPacketList,	//パケットリストバッファ
					bufSize,		//バッファサイズ
					pPacket,		//現在のパケット
					0,				//イベント発生時刻：即時
					size,			//イベントデータサイズ（バイト）
					(Byte*)pMsg		//イベントデータ（ランニングステータスは許されない）
				);
	if (pPacket == NULL) {
		result = YN_SET_ERR(@"Program error.", (unsigned long)pMsg[0], size);
		goto EXIT;
	}
	
	//メッセージ出力
	//  出力先デバイスがオフラインであってもAPIは正常終了する
	err = MIDISend(
				m_PortInfo[portNo].portRef,
				m_PortInfo[portNo].endpointRef,
				pPacketList
			);
	if (err != noErr) {
		result = YN_SET_ERR(@"MIDI OUT device output error.", (unsigned long)pMsg[0], size);
		goto EXIT;
	}
	
EXIT:;
	return result;
}

//******************************************************************************
// MIDIデータ送信（ショートメッセージ） macOS 11(Big Sur)以降
//******************************************************************************
int SMOutDevCtrl::_SendShortMsg2(
		unsigned char portNo,
		unsigned char* pMsg,
		unsigned int size
	)
{
	int result = 0;
	OSStatus err;
	ByteCount eventListBufSize = 1024;
	Byte eventListBuf[1024];
	MIDIEventList* pEventList = NULL;
	MIDIEventPacket* pEventPacket = NULL;
	SMEvent event;
	SMEventMIDI eventMIDI;
	MIDIMessage_32 midiMsg32 = 0;
	UInt8 group = 0;
	unsigned char* pData = NULL;
	
	//パラメータチェック
	if ((portNo >= SM_MIDIOUT_PORT_NUM_MAX) || (size == 0) || (size > 4)) {
		result = YN_SET_ERR(@"Program error.", portNo, size);
		goto EXIT;
	}
	
	//ポートが存在しなければ何もしない
	if (!m_PortInfo[portNo].isExist) goto EXIT;
	
	//ポートが開かれていなければエラー
	if (m_PortInfo[portNo].portRef == 0) {
		result = YN_SET_ERR(@"Program error.", portNo, size);
		goto EXIT;
	}
	
	//メッセージ読み込み
	event.SetMIDIData(pMsg[0], &(pMsg[1]), size - 1);
	eventMIDI.Attach(&event);
	
	//メッセージ作成
	//  MIDIイベントを Universal MIDI Packet (MIDI 1.0 Channel Voice Messages) に変換
	switch (eventMIDI.GetChMsg()) {
		case SMEventMIDI::NoteOff:
			midiMsg32 = MIDI1UPNoteOff(group, eventMIDI.GetChNo(), eventMIDI.GetNoteNo(), eventMIDI.GetVelocity());
			break;
		case SMEventMIDI::NoteOn:
			midiMsg32 = MIDI1UPNoteOn(group, eventMIDI.GetChNo(), eventMIDI.GetNoteNo(), eventMIDI.GetVelocity());
			break;
		case SMEventMIDI::PolyphonicKeyPressure:
			midiMsg32 = MIDI1UPNoteOn(group, eventMIDI.GetChNo(), eventMIDI.GetNoteNo(), eventMIDI.GetVelocity());
			break;
		case SMEventMIDI::ControlChange:
			midiMsg32 = MIDI1UPControlChange(group, eventMIDI.GetChNo(), eventMIDI.GetCCNo(), eventMIDI.GetCCValue());
			break;
		case SMEventMIDI::ProgramChange:
			midiMsg32 = MIDI1UPChannelVoiceMessage(group, kMIDICVStatusProgramChange, eventMIDI.GetChNo(), eventMIDI.GetProgramNo(), 0);
			break;
		case SMEventMIDI::ChannelPressure:
			midiMsg32 = MIDI1UPChannelVoiceMessage(group, kMIDICVStatusChannelPressure, eventMIDI.GetChNo(), eventMIDI.GetPressureValue(), 0);
			break;
		case SMEventMIDI::PitchBend:
			pData = event.GetDataPtr();
			midiMsg32 = MIDI1UPPitchBend(group, eventMIDI.GetChNo(), pData[0], pData[1]);
			break;
		case SMEventMIDI::None:
		default:
			result = YN_SET_ERR(@"Program error.", (unsigned long)pMsg[0], size);
			goto EXIT;
			break;
	}
	
	//イベントリスト初期化
	pEventList = (MIDIEventList*)eventListBuf;
	pEventPacket = MIDIEventListInit(pEventList, kMIDIProtocol_1_0);
	if (pEventPacket == NULL) {
		result = YN_SET_ERR(@"Program error.", (unsigned long)pEventList, size);
		goto EXIT;
	}
	//イベントリストにメッセージを追加
	pEventPacket = MIDIEventListAdd(
					pEventList,			//イベントリストバッファ
					eventListBufSize,	//バッファサイズ
					pEventPacket,		//現在のパケット
					0,					//イベント発生時刻：即時
					1,					//ワード数（32bit単位）
					&midiMsg32			//メッセージデータ（ランニングステータスは許されない）
				);
	if (pEventPacket == NULL) {
		result = YN_SET_ERR(@"Program error.", (unsigned long)midiMsg32, size);
		goto EXIT;
	}
	
	//イベントリスト送信
	err = MIDISendEventList(
				m_PortInfo[portNo].portRef,
				m_PortInfo[portNo].endpointRef,
				pEventList
			);
	if (err != noErr) {
		result = YN_SET_ERR(@"MIDI OUT device output error.", (unsigned long)midiMsg32, size);
		goto EXIT;
	}
	
EXIT:;
	return result;
}

//******************************************************************************
// MIDIデータ送信（ロングメッセージ）
//******************************************************************************
int SMOutDevCtrl::SendLongMsg(
		unsigned char portNo,
		unsigned char* pMsg,
		unsigned int size
	)
{
	int result = 0;
	
	if (@available(macOS 11, *)) {
		result = _SendLongMsg2(portNo, pMsg, size);
	}
	else {
		result = _SendLongMsg1(portNo, pMsg, size);
	}
	
	return result;
}

//******************************************************************************
// MIDIデータ送信（ロングメッセージ） macOS 10.15(Catalina)以前
//******************************************************************************
int SMOutDevCtrl::_SendLongMsg1(
		unsigned char portNo,
		unsigned char* pMsg,
		unsigned int size
	)
{
	int result = 0;
	OSStatus err;
	MIDISysexSendRequest sysexSendReq;
	
	//パラメータチェック
	if (portNo >= SM_MIDIOUT_PORT_NUM_MAX) {
		result = YN_SET_ERR(@"Program error.", portNo, size);
		goto EXIT;
	}
	if ((pMsg == NULL) || (size == 0)) {
		result = YN_SET_ERR(@"Program error.", portNo, size);
		goto EXIT;
	}
	
	//ポートが存在しなければ何もしない
	if (!m_PortInfo[portNo].isExist) goto EXIT;
	
	//エンドポイントが開かれていなければエラー
	if (m_PortInfo[portNo].endpointRef == 0) {
		result = YN_SET_ERR(@"Program error.", portNo, size);
		goto EXIT;
	}
	
	//ポートが開かれていなければエラー
	if (m_PortInfo[portNo].portRef == 0) {
		result = YN_SET_ERR(@"Program error.", portNo, size);
		goto EXIT;
	}
	
	//システムエクスクルーシブ要求生成
	memset(&sysexSendReq, 0, sizeof(MIDISysexSendRequest));
	//エンドポイント
	sysexSendReq.destination = m_PortInfo[portNo].endpointRef;
	//メッセージ位置
	sysexSendReq.data = (Byte*)pMsg;
	//メッセージサイズ
	sysexSendReq.bytesToSend = (UInt32)size;
	//送信終了時にtrueになる（利用者がtrueを設定することによって送信を中止できる）
	sysexSendReq.complete = false;
	//送信終了時のコールバック関数
	sysexSendReq.completionProc = SysexSendcompletionProc;
	//コールバック関数に渡す参照
	//  サンプルによると構造体自身のポインタを設定するのが普通のようだ
	//  API呼び出し時に自明である値をなぜわざわざ設定する必要があるのか？
	//  別の用途で使えるのだろうか？
	sysexSendReq.completionRefCon = (void*)&sysexSendReq;
	
	//メッセージ出力
	//  MIDISendSysex は MIDISend と以下の点が異なる
	//    ポートを指定する必要がない
	//    送信時刻を指定できない（即時送信のみ）
	err = MIDISendSysex(&sysexSendReq);
	if (err != noErr) {
		result = YN_SET_ERR(@"MIDI OUT device output error.", portNo, size);
		goto EXIT;
	}
	
	//送信処理完了まで待ち合わせる
	[g_pSendCompleteCondition lock];
	[g_pSendCompleteCondition wait];
	[g_pSendCompleteCondition unlock];

EXIT:;
	return result;
}

//******************************************************************************
// MIDIデータ送信（ロングメッセージ） macOS 11(Big Sur)以降
//******************************************************************************
int SMOutDevCtrl::_SendLongMsg2(
		unsigned char portNo,
		unsigned char* pMsg,
		unsigned int size
	)
{
	int result = 0;
	OSStatus err;
	unsigned char* pSysExMsg = NULL;
	unsigned int sysExMsgSize = 0;
	unsigned int eventListBufSize = SM_MIDI_OUT_SYSEX_DATA_MAX_SIZE * 2;
	unsigned char eventListBuf[SM_MIDI_OUT_SYSEX_DATA_MAX_SIZE * 2];
	MIDIEventList* pEventList = NULL;
	MIDIEventPacket* pEventPacket = NULL;
	unsigned long msgCount = 0;
	MIDIMessage_64 midiMsg64[SM_MIDI_OUT_SYSEX_DATA_MAX_SIZE / 6 + 10];  //1メッセージあたり6byteのデータを格納
	UInt8 group = 0;
	unsigned char status = 0;
	unsigned char ofBytes = 0;
	unsigned char data[6] = {0, 0, 0, 0, 0, 0};
	unsigned int i = 0;
	bool isSysExStart = false;
	bool isSysExEnd = false;

	//パラメータチェック
	if (portNo >= SM_MIDIOUT_PORT_NUM_MAX) {
		result = YN_SET_ERR(@"Program error.", portNo, size);
		goto EXIT;
	}
	if ((pMsg == NULL) || (size == 0)) {
		result = YN_SET_ERR(@"Program error.", portNo, size);
		goto EXIT;
	}
	
	//ポートが存在しなければ何もしない
	if (!m_PortInfo[portNo].isExist) goto EXIT;
	
	//エンドポイントが開かれていなければエラー
	if (m_PortInfo[portNo].endpointRef == 0) {
		result = YN_SET_ERR(@"Program error.", portNo, size);
		goto EXIT;
	}
	
	//ポートが開かれていなければエラー
	if (m_PortInfo[portNo].portRef == 0) {
		result = YN_SET_ERR(@"Program error.", portNo, size);
		goto EXIT;
	}
	
	//システムエクスクルーシブデータ位置とサイズ
	pSysExMsg = pMsg;
	sysExMsgSize = size;
	
	//先頭0XF0は送信しない
	if (pMsg[0] == 0xF0) {
		isSysExStart = true;
		pSysExMsg += 1;
		sysExMsgSize -= 1;
	}
	
	//末尾0XF7は送信しない
	if (pMsg[size - 1] == 0xF7) {
		isSysExEnd = true;
		sysExMsgSize -= 1;
	}
	
	//データサイズのチェック
	if (sysExMsgSize == 0) {
		//送信するデータがない場合は何もせずに終了
		//指定されたメッセージが"F0 F7"の場合に該当
		goto EXIT;
	}
	if (sysExMsgSize > SM_MIDI_OUT_SYSEX_DATA_MAX_SIZE) {
		//1024byteより大きいサイズのシステムエクスクルーシブの送信はサポートしない
		NSLog(@"WARNING: Ignore system exclusive message. The message is too long.");
		goto EXIT;
	}
	
	//メッセージ数（1メッセージあたりシステムエクスクルーシブデータ6byteを組み込める）
	msgCount = (sysExMsgSize + 5) / 6;
	
	//イベントリスト初期化
	pEventList = (MIDIEventList*)eventListBuf;
	pEventPacket = MIDIEventListInit(pEventList, kMIDIProtocol_1_0);
	if (pEventPacket == NULL) {
		result = YN_SET_ERR(@"Program error.", (unsigned long)pEventList, size);
		goto EXIT;
	}
	
	//メッセージ作成
	//  システムエクスクルーシブデータを Universal MIDI Packet (System Exclusive (7-Bit) Messages) に変換
	//  1メッセージにデータを6byteまで組み込み、7byte以上のデータは複数メッセージに分割する
	for (i = 0; i < msgCount; i++) {
		//1メッセージ単独の場合
		if (msgCount == 1) {
			if (isSysExStart && isSysExEnd) {
				status = kMIDISysExStatusComplete;
			}
			else if (isSysExStart) {
				status = kMIDISysExStatusStart;
			}
			else if (isSysExEnd) {
				status = kMIDISysExStatusEnd;
			}
			else {
				status = kMIDISysExStatusContinue;
			}
			ofBytes = sysExMsgSize;
		}
		//2メッセージ以上の場合
		else {
			//先頭メッセージの場合
			if (i == 0) {
				if (isSysExStart) {
					status = kMIDISysExStatusStart;
				}
				else {
					status = kMIDISysExStatusContinue;
				}
				ofBytes = 6;
			}
			//最終メッセージの場合
			else if (i == (msgCount - 1)) {
				if (isSysExEnd) {
					status = kMIDISysExStatusEnd;
				}
				else {
					status = kMIDISysExStatusContinue;
				}
				ofBytes = sysExMsgSize - (6 * (msgCount - 1));
			}
			//中間メッセージの場合
			else {
				status = kMIDISysExStatusContinue;
				ofBytes = 6;
			}
		}
		
		//メッセージ作成
		//  メッセージフォーマット(64bit)
		//  word0: [mmmmgggg][ssssoooo][dddddddd][dddddddd] 32bit
		//  word1: [dddddddd][dddddddd][dddddddd][dddddddd] 32bit
		//    mmmm     メッセージタイプ：データメッセージ 0x3
		//    gggg     グループ：0固定とする
		//    ssss     ステータス：Complete 0x0, Start 0x1, Continue 0x2, End 0x3
		//    oooo     データサイズ：0x0〜0x6（メッセージ内のデータサイズ）
		//    dddddddd データ：先頭0xF0と末尾0xF7を含まない
		memset(data, 0, 6);
		memcpy(data, pSysExMsg + (6 * i), ofBytes);
		midiMsg64[i] = (MIDIMessage_64){
						(UInt32)((kMIDIMessageTypeSysEx << 4) | (group & 0xF)) << 24
						| (UInt32)(status << 4 | (ofBytes & 0xF)) << 16
						| (UInt32)data[0] << 8
						| (UInt32)data[1],
						(UInt32)data[2] << 24
						| (UInt32)data[3] << 16
						| (UInt32)data[4] << 8
						| (UInt32)data[5]
					};
	}
	
	//イベントリストにメッセージを追加（イベントリストの最大サイズは65,536byte）
	pEventPacket = MIDIEventListAdd(
					pEventList,			//イベントリストバッファ
					eventListBufSize,	//バッファサイズ
					pEventPacket,		//現在のパケット
					0,					//イベント発生時刻：即時
					msgCount * 2,		//ワード数（32bit単位）
					(UInt32*)&(midiMsg64[0])	//メッセージデータ（ランニングステータスは許されない）
				);
	if (pEventPacket == NULL) {
		result = YN_SET_ERR(@"Program error.", (unsigned long)midiMsg64, msgCount);
		goto EXIT;
	}
	
	//イベントリスト送信
	err = MIDISendEventList(
				m_PortInfo[portNo].portRef,
				m_PortInfo[portNo].endpointRef,
				pEventList
			);
	if (err != noErr) {
		result = YN_SET_ERR(@"MIDI OUT device output error.", (unsigned long)midiMsg64, msgCount);
		goto EXIT;
	}
	
EXIT:;
	return result;
}

//******************************************************************************
// 全ポートノートオフ
//******************************************************************************
int SMOutDevCtrl::NoteOffAll()
{
	int result = 0;
	unsigned char i = 0;
	unsigned char msg[3];
	unsigned char portNo = 0;
	
	for (portNo = 0; portNo < SM_MIDIOUT_PORT_NUM_MAX; portNo++) {
		
		//ポートが存在しなければスキップ
		if (!m_PortInfo[portNo].isExist) continue;
		if (m_PortInfo[portNo].endpointRef == 0) continue;
		if (m_PortInfo[portNo].portRef == 0) continue;
		
		//全トラックノートオフ
		for (i = 0; i < 16; i++) {
			msg[0] = 0xB0 | i;
			msg[1] = 0x7B;
			msg[2] = 0x00;
			result = SendShortMsg(portNo, msg, 3);
			if (result != 0) goto EXIT;
		}
	}
	
EXIT:;
	return result;
}

//******************************************************************************
// 全ポートサウンドオフ
//******************************************************************************
int SMOutDevCtrl::SoundOffAll()
{
	int result = 0;
	unsigned char i = 0;
	unsigned char msg[3];
	unsigned char portNo = 0;
	
	for (portNo = 0; portNo < SM_MIDIOUT_PORT_NUM_MAX; portNo++) {
		
		//ポートが存在しなければスキップ
		if (!m_PortInfo[portNo].isExist) continue;
		if (m_PortInfo[portNo].endpointRef == 0) continue;
		if (m_PortInfo[portNo].portRef == 0) continue;
		
		//全トラックノートオフ
		for (i = 0; i < 16; i++) {
			msg[0] = 0xB0 | i;
			msg[1] = 0x78;
			msg[2] = 0x00;
			result = SendShortMsg(portNo, msg, 3);
			if (result != 0) goto EXIT;
		}
	}
	
EXIT:;
	return result;
}

//******************************************************************************
// MIDISendSysex処理完了コールバック関数
//******************************************************************************
void SysexSendcompletionProc(
		MIDISysexSendRequest *request
	)
{
	//NSLog(@"SysexSendcompletionProc");
	
	//送信完了まで待機しているシーケンサスレッドを起こす
	[g_pSendCompleteCondition lock];
	[g_pSendCompleteCondition signal];
	[g_pSendCompleteCondition unlock];
}


