2009-06-25 07:16:22 -07:00
|
|
|
|
/*
|
|
|
|
|
* MidiFile.cs
|
|
|
|
|
* Copyright (c) 2009 kbinani
|
|
|
|
|
*
|
|
|
|
|
* This file is part of Boare.Lib.Vsq.
|
|
|
|
|
*
|
|
|
|
|
* Boare.Lib.Vsq is free software; you can redistribute it and/or
|
|
|
|
|
* modify it under the terms of the BSD License.
|
|
|
|
|
*
|
|
|
|
|
* Boare.Lib.Vsq 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.
|
|
|
|
|
*/
|
|
|
|
|
using System;
|
|
|
|
|
using System.IO;
|
|
|
|
|
using System.Collections.Generic;
|
|
|
|
|
|
|
|
|
|
namespace Boare.Lib.Vsq {
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// midiイベント。メタイベントは、メタイベントのデータ長をData[1]に格納せず、生のデータをDataに格納するので、注意が必要
|
|
|
|
|
/// </summary>
|
|
|
|
|
public struct MidiEvent : IComparable<MidiEvent> {
|
2009-07-30 08:02:59 -07:00
|
|
|
|
public long clock;
|
|
|
|
|
public byte firstByte;
|
|
|
|
|
public byte[] data;
|
|
|
|
|
|
|
|
|
|
public long Clock {
|
|
|
|
|
get {
|
|
|
|
|
return clock;
|
|
|
|
|
}
|
|
|
|
|
set {
|
|
|
|
|
clock = value;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public byte FirstByte {
|
|
|
|
|
get {
|
|
|
|
|
return firstByte;
|
|
|
|
|
}
|
|
|
|
|
set {
|
|
|
|
|
firstByte = value;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public byte[] Data {
|
|
|
|
|
get {
|
|
|
|
|
return data;
|
|
|
|
|
}
|
|
|
|
|
set {
|
|
|
|
|
data = value;
|
|
|
|
|
}
|
|
|
|
|
}
|
2009-06-25 07:16:22 -07:00
|
|
|
|
|
|
|
|
|
private static void writeDeltaClock( Stream stream, long number ) {
|
|
|
|
|
bool[] bits = new bool[64];
|
|
|
|
|
long val = 0x1;
|
|
|
|
|
bits[0] = (number & val) == val;
|
|
|
|
|
for ( int i = 1; i < 64; i++ ) {
|
|
|
|
|
val = val << 1;
|
|
|
|
|
bits[i] = (number & val) == val;
|
|
|
|
|
}
|
|
|
|
|
int first = 0;
|
|
|
|
|
for ( int i = 63; i >= 0; i-- ) {
|
|
|
|
|
if ( bits[i] ) {
|
|
|
|
|
first = i;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// 何バイト必要か?
|
|
|
|
|
int bytes = first / 7 + 1;
|
|
|
|
|
for ( int i = 1; i <= bytes; i++ ) {
|
|
|
|
|
uint num = 0;
|
|
|
|
|
uint count = 0x80;
|
|
|
|
|
for ( int j = (bytes - i + 1) * 7 - 1; j >= (bytes - i + 1) * 7 - 6 - 1; j-- ) {
|
|
|
|
|
count = count >> 1;
|
|
|
|
|
if ( bits[j] ) {
|
|
|
|
|
num += count;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if ( i != bytes ) {
|
|
|
|
|
num += 0x80;
|
|
|
|
|
}
|
|
|
|
|
stream.WriteByte( (byte)num );
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static long readDeltaClock( Stream stream ) {
|
|
|
|
|
long ret = 0;
|
|
|
|
|
while ( true ) {
|
2009-07-29 10:03:20 -07:00
|
|
|
|
int i = stream.ReadByte();
|
|
|
|
|
if ( i < 0 ) {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
byte d = (byte)i;
|
2009-06-25 07:16:22 -07:00
|
|
|
|
ret = (ret << 7) | ((long)d & 0x7f);
|
|
|
|
|
if ( (d & 0x80) == 0x00 ) {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return ret;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static MidiEvent read( Stream stream, ref long last_clock, ref byte last_status_byte ) {
|
|
|
|
|
long delta_clock = readDeltaClock( stream );
|
|
|
|
|
last_clock += delta_clock;
|
|
|
|
|
byte first_byte = (byte)stream.ReadByte();
|
|
|
|
|
if ( first_byte < 0x80 ) {
|
|
|
|
|
// ランニングステータスが適用される
|
|
|
|
|
stream.Seek( -1, SeekOrigin.Current );
|
|
|
|
|
first_byte = last_status_byte;
|
|
|
|
|
} else {
|
|
|
|
|
last_status_byte = first_byte;
|
|
|
|
|
}
|
|
|
|
|
byte ctrl = (byte)(first_byte & (byte)0xf0);
|
|
|
|
|
if ( ctrl == 0x80 || ctrl == 0x90 || ctrl == 0xA0 || ctrl == 0xB0 || ctrl == 0xE0 || first_byte == 0xF2 ) {
|
|
|
|
|
// 3byte使用するチャンネルメッセージ:
|
|
|
|
|
// 0x8*: ノートオフ
|
|
|
|
|
// 0x9*: ノートオン
|
|
|
|
|
// 0xA*: ポリフォニック・キープレッシャ
|
|
|
|
|
// 0xB*: コントロールチェンジ
|
|
|
|
|
// 0xE*: ピッチベンドチェンジ
|
|
|
|
|
// 3byte使用するシステムメッセージ
|
|
|
|
|
// 0xF2: ソングポジション・ポインタ
|
|
|
|
|
MidiEvent me = new MidiEvent();
|
2009-07-30 08:02:59 -07:00
|
|
|
|
me.clock = last_clock;
|
|
|
|
|
me.firstByte = first_byte;
|
|
|
|
|
me.data = new byte[2];
|
|
|
|
|
stream.Read( me.data, 0, 2 );
|
2009-06-25 07:16:22 -07:00
|
|
|
|
return me;
|
|
|
|
|
} else if ( ctrl == 0xC0 || ctrl == 0xD0 || first_byte == 0xF1 || first_byte == 0xF2 ) {
|
|
|
|
|
// 2byte使用するチャンネルメッセージ
|
|
|
|
|
// 0xC*: プログラムチェンジ
|
|
|
|
|
// 0xD*: チャンネルプレッシャ
|
|
|
|
|
// 2byte使用するシステムメッセージ
|
|
|
|
|
// 0xF1: クォータフレーム
|
|
|
|
|
// 0xF3: ソングセレクト
|
|
|
|
|
MidiEvent me = new MidiEvent();
|
2009-07-30 08:02:59 -07:00
|
|
|
|
me.clock = last_clock;
|
|
|
|
|
me.firstByte = first_byte;
|
|
|
|
|
me.data = new byte[1];
|
|
|
|
|
stream.Read( me.data, 0, 1 );
|
2009-06-25 07:16:22 -07:00
|
|
|
|
return me;
|
|
|
|
|
} else if ( first_byte == 0xF6 ) {
|
|
|
|
|
// 1byte使用するシステムメッセージ
|
|
|
|
|
// 0xF6: チューンリクエスト
|
|
|
|
|
// 0xF7: エンドオブエクスクルーシブ(このクラスではF0ステータスのSysExの一部として取り扱う)
|
|
|
|
|
// 0xF8: タイミングクロック
|
|
|
|
|
// 0xFA: スタート
|
|
|
|
|
// 0xFB: コンティニュー
|
|
|
|
|
// 0xFC: ストップ
|
|
|
|
|
// 0xFE: アクティブセンシング
|
|
|
|
|
// 0xFF: システムリセット
|
|
|
|
|
MidiEvent me = new MidiEvent();
|
2009-07-30 08:02:59 -07:00
|
|
|
|
me.clock = last_clock;
|
|
|
|
|
me.firstByte = first_byte;
|
|
|
|
|
me.data = new byte[0];
|
2009-06-25 07:16:22 -07:00
|
|
|
|
return me;
|
|
|
|
|
} else if ( first_byte == 0xff ) {
|
|
|
|
|
// メタイベント
|
|
|
|
|
byte meta_event_type = (byte)stream.ReadByte();
|
|
|
|
|
long meta_event_length = readDeltaClock( stream );
|
|
|
|
|
MidiEvent me = new MidiEvent();
|
2009-07-30 08:02:59 -07:00
|
|
|
|
me.clock = last_clock;
|
|
|
|
|
me.firstByte = first_byte;
|
|
|
|
|
me.data = new byte[meta_event_length + 1];
|
|
|
|
|
me.data[0] = meta_event_type;
|
|
|
|
|
stream.Read( me.data, 1, (int)meta_event_length );
|
2009-06-25 07:16:22 -07:00
|
|
|
|
return me;
|
|
|
|
|
} else if ( first_byte == 0xf0 ) {
|
|
|
|
|
// f0ステータスのSysEx
|
|
|
|
|
MidiEvent me = new MidiEvent();
|
2009-07-30 08:02:59 -07:00
|
|
|
|
me.clock = last_clock;
|
|
|
|
|
me.firstByte = first_byte;
|
2009-06-25 07:16:22 -07:00
|
|
|
|
long sysex_length = readDeltaClock( stream );
|
2009-07-30 08:02:59 -07:00
|
|
|
|
me.data = new byte[sysex_length + 1];
|
|
|
|
|
stream.Read( me.data, 0, (int)(sysex_length + 1) );
|
2009-06-25 07:16:22 -07:00
|
|
|
|
return me;
|
|
|
|
|
} else if ( first_byte == 0xf7 ) {
|
|
|
|
|
// f7ステータスのSysEx
|
|
|
|
|
MidiEvent me = new MidiEvent();
|
2009-07-30 08:02:59 -07:00
|
|
|
|
me.clock = last_clock;
|
|
|
|
|
me.firstByte = first_byte;
|
2009-06-25 07:16:22 -07:00
|
|
|
|
long sysex_length = readDeltaClock( stream );
|
2009-07-30 08:02:59 -07:00
|
|
|
|
me.data = new byte[sysex_length];
|
|
|
|
|
stream.Read( me.data, 0, (int)sysex_length );
|
2009-06-25 07:16:22 -07:00
|
|
|
|
return me;
|
|
|
|
|
} else {
|
|
|
|
|
throw new ApplicationException( "don't know how to process first_byte: 0x" + Convert.ToString( first_byte, 16 ) );
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void writeData( Stream stream ) {
|
2009-07-30 08:02:59 -07:00
|
|
|
|
stream.WriteByte( firstByte );
|
|
|
|
|
if ( firstByte == 0xff ) {
|
|
|
|
|
stream.WriteByte( data[0] );
|
|
|
|
|
writeDeltaClock( stream, data.Length - 1 );
|
2009-06-25 07:16:22 -07:00
|
|
|
|
//stream.WriteByte( (byte)(Data.Length - 1) );
|
2009-07-30 08:02:59 -07:00
|
|
|
|
stream.Write( data, 1, data.Length - 1 );
|
2009-06-25 07:16:22 -07:00
|
|
|
|
} else {
|
2009-07-30 08:02:59 -07:00
|
|
|
|
stream.Write( data, 0, data.Length );
|
2009-06-25 07:16:22 -07:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public int CompareTo( MidiEvent item ) {
|
2009-07-30 08:02:59 -07:00
|
|
|
|
return (int)(clock - item.clock);
|
2009-06-25 07:16:22 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static MidiEvent generateTimeSigEvent( int clock, int numerator, int denominator ) {
|
|
|
|
|
MidiEvent ret = new MidiEvent();
|
2009-07-30 08:02:59 -07:00
|
|
|
|
ret.clock = clock;
|
|
|
|
|
ret.firstByte = 0xff;
|
|
|
|
|
byte b_numer = (byte)(Math.Log( denominator, 2 ) + 0.1);
|
|
|
|
|
#if DEBUG
|
|
|
|
|
Console.WriteLine( "VsqEvent.generateTimeSigEvent; b_number=" + b_numer + "; denominator=" + denominator );
|
|
|
|
|
#endif
|
|
|
|
|
ret.data = new byte[5] { 0x58, (byte)numerator, b_numer, 0x18, 0x08 };
|
2009-06-25 07:16:22 -07:00
|
|
|
|
return ret;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static MidiEvent generateTempoChangeEvent( int clock, int tempo ) {
|
|
|
|
|
MidiEvent ret = new MidiEvent();
|
2009-07-30 08:02:59 -07:00
|
|
|
|
ret.clock = clock;
|
|
|
|
|
ret.firstByte = 0xff;
|
2009-06-25 07:16:22 -07:00
|
|
|
|
byte b1 = (byte)(tempo & 0xff);
|
|
|
|
|
tempo = tempo >> 8;
|
|
|
|
|
byte b2 = (byte)(tempo & 0xff);
|
|
|
|
|
tempo = tempo >> 8;
|
|
|
|
|
byte b3 = (byte)(tempo & 0xff);
|
2009-07-30 08:02:59 -07:00
|
|
|
|
ret.data = new byte[4] { 0x51, b3, b2, b1 };
|
2009-06-25 07:16:22 -07:00
|
|
|
|
return ret;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public class MidiFile : IDisposable {
|
|
|
|
|
private List<List<MidiEvent>> m_events;
|
|
|
|
|
private ushort m_format;
|
|
|
|
|
private ushort m_time_format;
|
|
|
|
|
|
2009-07-29 10:03:20 -07:00
|
|
|
|
public MidiFile( string path ){
|
|
|
|
|
Stream stream = new FileStream( path, FileMode.Open, FileAccess.Read );
|
2009-06-25 07:16:22 -07:00
|
|
|
|
// ヘッダ
|
|
|
|
|
byte[] byte4 = new byte[4];
|
|
|
|
|
stream.Read( byte4, 0, 4 );
|
|
|
|
|
if ( makeUInt32( byte4 ) != 0x4d546864 ) {
|
|
|
|
|
throw new ApplicationException( "header error: MThd" );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// データ長
|
|
|
|
|
stream.Read( byte4, 0, 4 );
|
|
|
|
|
uint length = makeUInt32( byte4 );
|
|
|
|
|
|
|
|
|
|
// フォーマット
|
|
|
|
|
stream.Read( byte4, 0, 2 );
|
|
|
|
|
m_format = makeUint16( byte4 );
|
|
|
|
|
|
|
|
|
|
// トラック数
|
|
|
|
|
int tracks = 0;
|
|
|
|
|
stream.Read( byte4, 0, 2 );
|
|
|
|
|
tracks = (int)makeUint16( byte4 );
|
|
|
|
|
|
|
|
|
|
// 時間分解能
|
|
|
|
|
stream.Read( byte4, 0, 2 );
|
|
|
|
|
m_time_format = makeUint16( byte4 );
|
|
|
|
|
|
|
|
|
|
// 各トラックを読込み
|
|
|
|
|
m_events = new List<List<MidiEvent>>();
|
|
|
|
|
for ( int track = 0; track < tracks; track++ ) {
|
|
|
|
|
// ヘッダー
|
|
|
|
|
stream.Read( byte4, 0, 4 );
|
|
|
|
|
if ( makeUInt32( byte4 ) != 0x4d54726b ) {
|
|
|
|
|
throw new ApplicationException( "header error; MTrk" );
|
|
|
|
|
}
|
|
|
|
|
m_events.Add( new List<MidiEvent>() );
|
|
|
|
|
|
|
|
|
|
// チャンクサイズ
|
|
|
|
|
stream.Read( byte4, 0, 4 );
|
|
|
|
|
long size = (long)makeUInt32( byte4 );
|
|
|
|
|
long startpos = stream.Position;
|
|
|
|
|
|
|
|
|
|
// チャンクの終わりまで読込み
|
|
|
|
|
long clock = 0;
|
|
|
|
|
byte last_status_byte = 0x00;
|
|
|
|
|
while ( stream.Position < startpos + size ) {
|
2009-07-29 10:03:20 -07:00
|
|
|
|
MidiEvent mi = MidiEvent.read( stream, ref clock, ref last_status_byte );
|
|
|
|
|
m_events[track].Add( mi );
|
|
|
|
|
}
|
|
|
|
|
if ( m_time_format != 480 ) {
|
|
|
|
|
int count = m_events[track].Count;
|
|
|
|
|
for ( int i = 0; i < count; i++ ) {
|
|
|
|
|
MidiEvent mi = m_events[track][i];
|
2009-07-30 08:02:59 -07:00
|
|
|
|
mi.clock = mi.clock * 480 / m_time_format;
|
2009-07-29 10:03:20 -07:00
|
|
|
|
m_events[track][i] = mi;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
m_time_format = 480;
|
|
|
|
|
#if DEBUG
|
|
|
|
|
string dbg = Path.Combine( Path.GetDirectoryName( path ), Path.GetFileNameWithoutExtension( path ) + ".txt" );
|
|
|
|
|
using ( StreamWriter sw = new StreamWriter( dbg ) ) {
|
|
|
|
|
const string format = " {0,8} 0x{1:X4} {2,-35} 0x{3:X2} 0x{4:X2}";
|
|
|
|
|
const string format0 = " {0,8} 0x{1:X4} {2,-35} 0x{3:X2}";
|
|
|
|
|
for ( int track = 1; track < m_events.Count; track++ ) {
|
|
|
|
|
sw.WriteLine( "MidiFile..ctor; track=" + track );
|
|
|
|
|
byte msb, lsb, data_msb, data_lsb;
|
|
|
|
|
msb = lsb = data_msb = data_lsb = 0x0;
|
|
|
|
|
for ( int i = 0; i < m_events[track].Count; i++ ) {
|
2009-07-30 08:02:59 -07:00
|
|
|
|
if ( m_events[track][i].firstByte == 0xb0 ) {
|
|
|
|
|
switch ( m_events[track][i].data[0] ) {
|
2009-07-29 10:03:20 -07:00
|
|
|
|
case 0x63:
|
2009-07-30 08:02:59 -07:00
|
|
|
|
msb = m_events[track][i].data[1];
|
2009-07-29 10:03:20 -07:00
|
|
|
|
lsb = 0x0;
|
|
|
|
|
break;
|
|
|
|
|
case 0x62:
|
2009-07-30 08:02:59 -07:00
|
|
|
|
lsb = m_events[track][i].data[1];
|
2009-07-29 10:03:20 -07:00
|
|
|
|
break;
|
|
|
|
|
case 0x06:
|
2009-07-30 08:02:59 -07:00
|
|
|
|
data_msb = m_events[track][i].data[1];
|
2009-07-29 10:03:20 -07:00
|
|
|
|
ushort nrpn = (ushort)(msb << 8 | lsb);
|
|
|
|
|
string name = NRPN.getName( nrpn );
|
|
|
|
|
if ( name == "" ) {
|
|
|
|
|
name = "* * UNKNOWN * *";
|
2009-07-30 08:02:59 -07:00
|
|
|
|
sw.WriteLine( string.Format( format0, m_events[track][i].clock, nrpn, name, data_msb ) );
|
2009-07-29 10:03:20 -07:00
|
|
|
|
} else {
|
|
|
|
|
//if ( !NRPN.is_require_data_lsb( nrpn ) ) {
|
2009-07-30 08:02:59 -07:00
|
|
|
|
sw.WriteLine( string.Format( format0, m_events[track][i].clock, nrpn, name, data_msb ) );
|
2009-07-29 10:03:20 -07:00
|
|
|
|
//}
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
case 0x26:
|
2009-07-30 08:02:59 -07:00
|
|
|
|
data_lsb = m_events[track][i].data[1];
|
2009-07-29 10:03:20 -07:00
|
|
|
|
ushort nrpn2 = (ushort)(msb << 8 | lsb);
|
|
|
|
|
string name2 = NRPN.getName( nrpn2 );
|
|
|
|
|
if ( name2 == "" ) {
|
|
|
|
|
name2 = "* * UNKNOWN * *";
|
|
|
|
|
}
|
2009-07-30 08:02:59 -07:00
|
|
|
|
sw.WriteLine( string.Format( format, m_events[track][i].clock, nrpn2, name2, data_msb, data_lsb ) );
|
2009-07-29 10:03:20 -07:00
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2009-06-25 07:16:22 -07:00
|
|
|
|
}
|
|
|
|
|
}
|
2009-07-29 10:03:20 -07:00
|
|
|
|
#endif
|
2009-06-25 07:16:22 -07:00
|
|
|
|
stream.Close();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/*public void Write( string path ) {
|
|
|
|
|
}*/
|
|
|
|
|
|
|
|
|
|
public List<MidiEvent> getMidiEventList( int track ) {
|
|
|
|
|
if ( m_events == null ) {
|
|
|
|
|
return new List<MidiEvent>();
|
|
|
|
|
} else if ( 0 <= track && track < m_events.Count ) {
|
|
|
|
|
return m_events[track];
|
|
|
|
|
} else {
|
|
|
|
|
return new List<MidiEvent>();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public int getTrackCount() {
|
|
|
|
|
if ( m_events == null ) {
|
|
|
|
|
return 0;
|
|
|
|
|
} else {
|
|
|
|
|
return m_events.Count;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void Dispose() {
|
|
|
|
|
Close();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void Close() {
|
|
|
|
|
if ( m_events != null ) {
|
|
|
|
|
for ( int i = 0; i < m_events.Count; i++ ) {
|
|
|
|
|
m_events[i].Clear();
|
|
|
|
|
}
|
|
|
|
|
m_events.Clear();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static UInt32 makeUInt32( byte[] value ) {
|
|
|
|
|
return (uint)((uint)((uint)((uint)(value[0] << 8) | value[1]) << 8 | value[2]) << 8 | value[3]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static UInt16 makeUint16( byte[] value ) {
|
|
|
|
|
return (ushort)((ushort)(value[0] << 8) | value[1]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static long readDeltaClock( Stream stream ) {
|
|
|
|
|
byte[] b;
|
|
|
|
|
long ret = 0;
|
|
|
|
|
while ( true ) {
|
|
|
|
|
byte d = (byte)stream.ReadByte();
|
|
|
|
|
ret = (ret << 7) | ((long)d & 0x7f);
|
|
|
|
|
if ( (d & 0x80) == 0x00 ) {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return ret;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|