├── .gitignore ├── AUTHORS ├── COPYING ├── rtpmidi ├── __init__.py ├── engines │ ├── __init__.py │ └── midi │ │ ├── __init__.py │ │ ├── list_circ.py │ │ ├── midi_in.py │ │ ├── midi_object.py │ │ ├── midi_out.py │ │ ├── midi_session.py │ │ ├── recovery_journal.py │ │ ├── recovery_journal_chapters.py │ │ └── ringBuffer.py ├── protocols │ ├── __init__.py │ └── rtp │ │ ├── README │ │ ├── __init__.py │ │ ├── defcache.py │ │ ├── formats.py │ │ ├── jitter_buffer.py │ │ ├── list_circ.py │ │ ├── nat.py │ │ ├── packets.py │ │ ├── protocol.py │ │ ├── rtcp.py │ │ ├── rtp_control.py │ │ ├── rtp_session.py │ │ ├── sdp.py │ │ ├── stun.py │ │ └── utils.py ├── runner.py ├── test │ ├── NOMENCLATURE.txt │ ├── __init__.py │ ├── test_jitter_buffer.py │ ├── test_listcirc.py │ ├── test_midi_in.py │ ├── test_midi_object.py │ ├── test_midi_out.py │ ├── test_packets.py │ ├── test_protocol.py │ ├── test_recovery_journal.py │ ├── test_recovery_journal_chapters.py │ ├── test_rtcp.py │ └── test_rtp_control.py └── utils.py ├── run-midi ├── scripts └── midistream.in └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | .idea 3 | build/ 4 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Authors: 2 | Tristan Matthews 3 | Alexandre Quessy 4 | Simon Piette 5 | Philippe Chevry 6 | Koya Charles 7 | Antoine Collet 8 | Sylvain Cormier 9 | Étienne Désautels 10 | Hugo Boyer 11 | 12 | -------------------------------------------------------------------------------- /rtpmidi/__init__.py: -------------------------------------------------------------------------------- 1 | __all__ = [ 2 | 'protocols', 3 | 'engines' 4 | ] 5 | -------------------------------------------------------------------------------- /rtpmidi/engines/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/avsaj/rtpmidi/622a8cffeb3b4e67b66cbaf63a6432aef9fb6984/rtpmidi/engines/__init__.py -------------------------------------------------------------------------------- /rtpmidi/engines/midi/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/avsaj/rtpmidi/622a8cffeb3b4e67b66cbaf63a6432aef9fb6984/rtpmidi/engines/midi/__init__.py -------------------------------------------------------------------------------- /rtpmidi/engines/midi/list_circ.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | #motherclass 4 | class ListCirc(object): 5 | 6 | def __init__(self, listeSize): 7 | self.index = 0 8 | self.list = [] 9 | self.round = 0 10 | self.listeSize = listeSize 11 | 12 | def to_list(self, note): 13 | if self.round == 1: 14 | self._replace_note(note) 15 | else: 16 | self._add_note(note) 17 | 18 | def _add_note(self, note): 19 | self.list.append(note) 20 | 21 | if self.index == self.listeSize-1: 22 | self.round = 1 23 | self.index = 0 24 | else: 25 | self.index += 1 26 | 27 | def _replace_note(self, note): 28 | if (self.index == self.listeSize): 29 | self.index = 0 30 | self.list[self.index] = note 31 | else: 32 | self.list[self.index] = note 33 | self.index += 1 34 | 35 | def flush(self): 36 | self.list = [] 37 | self.round = 0 38 | self.index = 0 39 | 40 | def __getitem__(self,key): 41 | return self.list[key] 42 | 43 | def __len__(self): 44 | return len(self.list) 45 | 46 | class PacketCirc(ListCirc): 47 | 48 | #Recupere une note avec son compteur (numero) 49 | def find_packet(self, seqNo): 50 | """Return emplacement of selected packet index the list""" 51 | res = -1 52 | 53 | for i in range(len(self.list)): 54 | if (self.list[i].seqNo == seqNo): 55 | res = i 56 | break 57 | 58 | return res 59 | 60 | def get_packets(self, checkpoint, act_seq ): 61 | """Get packets from checkpoint to act seq 62 | this function is for simplify create_recovery_journal""" 63 | midi_cmd = [] 64 | #getting pos of element 65 | checkpoint_pos = self.find_packet(checkpoint) 66 | act_seq_pos = self.find_packet(act_seq) 67 | 68 | #Take care of wrap around in seqNo 69 | 70 | if checkpoint >= act_seq: 71 | if checkpoint_pos < act_seq_pos: 72 | midi_cmd = self.list[checkpoint_pos+1:act_seq_pos+1] 73 | else: 74 | #getting checkpoint -> 0 75 | midi_cmd = self.list[checkpoint_pos+1:] 76 | 77 | #getting 0 -> act_seq 78 | midi_cmd += self.list[:act_seq_pos+1] 79 | else: 80 | #getting checkpoint -> act_seq 81 | [midi_cmd.append(self.list[i]) \ 82 | for i in range(len(self.list)) \ 83 | if self.list[i].seqNo > checkpoint and \ 84 | self.list[i].seqNo <= act_seq] 85 | 86 | 87 | return midi_cmd 88 | 89 | 90 | 91 | #list circ to stock time 92 | class DelayCirc(ListCirc): 93 | 94 | def __init__(self, listeSize): 95 | ListCirc.__init__(self, listeSize) 96 | self.lastSync = 0 97 | 98 | def to_list(self,note): 99 | ListCirc.to_list(self,note) 100 | self.lastSync = time.time() 101 | 102 | def average(self): 103 | if (len(self.list)>0): 104 | average = float(sum(self.list)/len(self.list)) 105 | return average 106 | 107 | def __repr__(self): 108 | return str(self.list) 109 | 110 | #list circ to stock last miditime difference 111 | class MidiTimeCirc(DelayCirc): 112 | 113 | def split(self): 114 | if ( len(self.list)>0): 115 | split = max(self.list) - min(self.list) 116 | return abs(split) 117 | else: 118 | return 0 119 | -------------------------------------------------------------------------------- /rtpmidi/engines/midi/midi_in.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Scenic 5 | # Copyright (C) 2008 Société des arts technologiques (SAT) 6 | # http://www.sat.qc.ca 7 | # All rights reserved. 8 | # 9 | # This file is free software: you can redistribute it and/or modify 10 | # it under the terms of the GNU General Public License as published by 11 | # the Free Software Foundation, either version 2 of the License, or 12 | # (at your option) any later version. 13 | # 14 | # Scenic 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 Scenic. If not, see . 21 | """ 22 | MIDI input device manager. 23 | """ 24 | from twisted.internet import reactor 25 | from twisted.internet import task 26 | from twisted.internet import defer 27 | import time 28 | try: 29 | import pypm 30 | except ImportError: 31 | from pygame import pypm 32 | 33 | # FIXME: what are those two vars? 34 | INPUT = 0 35 | OUTPUT = 1 36 | 37 | class MidiIn(object): 38 | """ 39 | Midi device input. 40 | 41 | Manages a single MIDI input (source) device. 42 | Can list all input devices. 43 | """ 44 | def __init__(self, client, verbose=0): 45 | self.verbose = verbose 46 | #Init var 47 | self.midi_device_list = [] 48 | self.midi_device = None 49 | self.midi_in = None 50 | #setting looping task 51 | self.releaser = task.LoopingCall(self._get_input) 52 | #Launching RTP Client to call after midi time start in order to sync TS 53 | self.client = client 54 | #stats 55 | self.nbNotes = 0 56 | self.fps_note = 0 57 | #flag ( 1 == syncronyse with remote peer ) 58 | self.sync = 0 59 | self.end_flag = True 60 | #check activity 61 | self.last_activity = 0 62 | self.in_activity = False 63 | #value to set 64 | #in s (increase it when note frequency is high in order to reduce packet number) 65 | self.polling_interval = 0.015 66 | #in ms 67 | #Time out must be > to polling 68 | self.time_out = 5 69 | #Init time is for timestamp in RTP 70 | self.init_time = pypm.Time() 71 | 72 | def start(self): 73 | """ 74 | Starts polling the selected device. 75 | One must first select a device ! 76 | @rtype: bool 77 | Returns success. 78 | """ 79 | if self.end_flag : 80 | if self.midi_in is not None: 81 | self.end_flag = False 82 | reactor.callInThread(self._polling) 83 | return True 84 | else: 85 | line = "INPUT: you have to set a midi device before start " 86 | line += "sending midi data" 87 | print line 88 | return False 89 | else: 90 | line = "INPUT: already sending midi data" 91 | return False 92 | 93 | def stop(self): 94 | """ 95 | Stops polling the selected device. 96 | """ 97 | if not self.end_flag: 98 | self.end_flag = True 99 | 100 | def _polling(self): 101 | """ 102 | Starts a never ending loop in a python thread. 103 | @rtype: Deferred 104 | """ 105 | #need by twisted to stop properly the thread 106 | d = defer.Deferred() 107 | #setting new scopes 108 | last_poll = self.last_activity 109 | midi_in = self.midi_in 110 | in_activity = self.in_activity 111 | while not self.end_flag: 112 | # hasattr is workaround for weird race condition on stop whereby midi_in is an int 113 | if hasattr(midi_in, 'Poll') and midi_in.Poll(): 114 | last_poll = time.time() * 1000 115 | reactor.callInThread(self._get_input) 116 | in_activity = True 117 | if in_activity and ((time.time() * 1000) - last_poll >= self.time_out): 118 | #send silent packet after 3ms of inactivity 119 | self.client.send_silence() 120 | in_activity = False 121 | time.sleep(self.polling_interval) 122 | return d 123 | 124 | def get_devices(self): 125 | """ 126 | Returns the list of MIDI input devices on this computer. 127 | @rtype: list 128 | """ 129 | self.midi_device_list = [] 130 | for loop in range(pypm.CountDevices()): 131 | interf, name, inp, outp, opened = pypm.GetDeviceInfo(loop) 132 | if inp == 1: 133 | self.midi_device_list.append([loop,name, opened]) 134 | return self.midi_device_list 135 | 136 | def get_device_info(self): 137 | """ 138 | Returns info about the currently selected device 139 | """ 140 | return pypm.GetDeviceInfo(self.midi_device) 141 | 142 | def set_device(self, device): 143 | """ 144 | Selects the MIDI device to be polled. 145 | 146 | @param device: The device number to choose. 147 | @type device: int 148 | @rtype: bool 149 | @return: Success or not 150 | """ 151 | #check if device exist 152 | dev_list = [self.midi_device_list[i][0] for i in range(len(self.midi_device_list))] 153 | if device in dev_list: # if the number is not in list of input devices 154 | self.midi_device = device 155 | if self.midi_in is not None: 156 | #delete old midi device if present 157 | del self.midi_in 158 | #Initializing midi input stream 159 | self.midi_in = pypm.Input(self.midi_device) 160 | if self.verbose: 161 | line = " Midi device in: " + str(self.get_device_info()[1]) 162 | print line 163 | return True 164 | else: 165 | print "INPUT: Invalid midi device selected" 166 | print dev_list 167 | return False 168 | 169 | def _get_input(self): 170 | """ 171 | Get input from selected device 172 | """ 173 | current_time = pypm.Time() 174 | #Reading Midi Input 175 | midi_data = self.midi_in.Read(1024) 176 | if self.verbose: 177 | print midi_data 178 | if len(midi_data) > 0: 179 | reactor.callFromThread(self.client.send_midi_data, midi_data, current_time) 180 | 181 | def __del__(self): 182 | #deleting objects 183 | del self.client 184 | del self.midi_in 185 | -------------------------------------------------------------------------------- /rtpmidi/engines/midi/midi_object.py: -------------------------------------------------------------------------------- 1 | from struct import pack 2 | from struct import unpack 3 | 4 | TWO_TO_THE_32ND = 2L<<32 5 | 6 | class MidiCommand(object): 7 | """ 8 | Figure 2 shows the format of the MIDI command section. 9 | rfc 4695 10 | 11 | 0 1 2 3 12 | 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 0 13 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 14 | |B|J|Z|P|LEN... | MIDI list ... | 15 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 16 | """ 17 | def __init__(self): 18 | pass 19 | 20 | def header(self, marker_b, recovery, timestamp, phantom, length): 21 | """Create header for MIDI Command part""" 22 | #MIDI PAYLOAD 23 | marker_b = 0 << 7 24 | 25 | #Recovery Journal 1 if present 26 | marker_j = recovery << 6 27 | 28 | #if Z = 1 of packet are play with the same timestamp 29 | #( no delay between notes => no delta field) 30 | marker_z = timestamp << 5 31 | 32 | #Phantom 33 | marker_p = phantom << 4 34 | 35 | #Check length (max 255) 36 | 37 | first = marker_b | marker_j | marker_z | marker_p | length 38 | header = pack('!B',first) 39 | 40 | return header 41 | 42 | def parse_header(self, header_to_parse): 43 | """Parsing header for MIDI Command part""" 44 | #Heading 45 | header = unpack('!B', header_to_parse) 46 | 47 | #Parsing RTP MIDI Header 48 | marker_b = (header[0] & 128) and 1 or 0 49 | marker_j = (header[0] & 64) and 1 or 0 50 | marker_z = (header[0] & 32) and 1 or 0 51 | marker_p = (header[0] & 16) and 1 or 0 52 | length = header[0] & 15 53 | 54 | return marker_b, marker_j, marker_z, marker_p, length 55 | 56 | 57 | def encode_midi_commands(self, commands): 58 | """Take a list of cmd in argument, and provide a midi command list 59 | format for network 60 | 61 | Each element: 62 | | EVENT | NOTE | VELOCITY | 63 | 64 | Notes must be chronologically ordered oldest first 65 | 66 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 67 | |Delta T 4 octets long, 0 octets if Z = 1 | 68 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 69 | | MIDI Command 0 (3 octets long) | 70 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 71 | """ 72 | 73 | if len(commands) > 0: 74 | #Ordering notes 75 | #Order history by channels midi command and pitch 76 | decorate = [((x[0][0]&15), (x[0][0]&240), x[0][1], x) 77 | for x in commands] 78 | decorate.sort() 79 | commands = [x[3] for x in decorate] 80 | #commands = bubble_sort(commands,len(commands)) 81 | notes = "" 82 | nb_notes = 0 83 | time_calc = [x[1] for x in commands] 84 | time_calc.sort() 85 | time_ref = time_calc[0] 86 | 87 | #Formating the list 88 | for i in range(len(commands)): 89 | #recup time 90 | timestamp = commands[i][1] - time_ref 91 | 92 | if timestamp >= TWO_TO_THE_32ND: 93 | timestamp = timestamp - TWO_TO_THE_32ND 94 | 95 | #print timestamp 96 | event = commands[i][0][0] 97 | note = commands[i][0][1] 98 | velocity = commands[i][0][2] 99 | 100 | tmp = pack('!BBB', event , note , velocity) 101 | tmp = pack('!I', timestamp) + tmp 102 | 103 | notes += tmp 104 | 105 | nb_notes += 1 106 | 107 | return notes, nb_notes 108 | else: 109 | return "", 0 110 | 111 | 112 | def decode_midi_commands(self, bytes, nb_notes): 113 | """decode a note list from bytes formated for network""" 114 | #iterator 115 | j = 0 116 | midi_list = [] 117 | for i in range(nb_notes): 118 | cmd = bytes[j:j+7] 119 | 120 | timestamp = unpack('!I', cmd[0:4]) 121 | event, note, velocity = unpack('!BBB', cmd[4:]) 122 | timestamp = timestamp[0] 123 | 124 | #applique modif ts 125 | midi_list.append([[event, note, velocity],timestamp]) 126 | j += 7 127 | 128 | return midi_list 129 | 130 | 131 | class MidiNote(object): #TODO: Maybe we could convert this object to a tuple to improve the performance 132 | 133 | def __init__(self, time, event, note, velocity, what=0): 134 | self.time = time 135 | self.event = event 136 | self.note = note 137 | self.velocity = velocity 138 | 139 | 140 | class OldPacket(object): 141 | 142 | def __init__(self, seqNo, packet, marker=0): 143 | self.seqNo = seqNo 144 | self.packet = packet 145 | self.marker = marker 146 | 147 | 148 | 149 | class SafeKeyboard(object): 150 | """This class is needed in the case where 151 | some note are invert (with pypm, sometime, when notes are played very 152 | fast i.e more than two notes in a ms the output of midi_in.Read() invert note off 153 | and note on. The result of that is an indefinite artefact midi_out side. 154 | """ 155 | def __init__(self): 156 | self.keyboard = [] 157 | 158 | #Building a map of all notes 159 | for i in range(16): 160 | note_list = [False for i in range(127)] 161 | self.keyboard.append(note_list) 162 | 163 | def note_index(self, chan, pitch, on, ind, midi_list): 164 | """Getting the nearest note for a given pitch and a given pitch 165 | ex : note_index(120, 1, 10, cmd_list) 166 | research a note on for 120 pitch near index 10 in cmd_list""" 167 | ind_list = [] 168 | value = 0 169 | res = -1 170 | 171 | if on == 1: 172 | value = 144 173 | else: 174 | value = 128 175 | 176 | #getting index of pitch (possibilit de stopper si trouver) 177 | for i in range(ind, len(midi_list)): 178 | if (midi_list[i][0][1] == pitch and midi_list[i][0][0]&240 == value 179 | and midi_list[i][0][0]&15 == chan): 180 | res = i 181 | break 182 | 183 | return res 184 | 185 | #Calcul the distance between note to check and values retreive 186 | #ind_list_calc = [[ind_list[i], ind_list[i] - ind] for i in range(len(ind_list)) 187 | #if (ind_list[i] - ind) > 0] 188 | #for i in range(len(ind_list_calc)): 189 | # if ind_list_calc[i][1] == 1: 190 | # return ind_list_calc[i][0] 191 | 192 | 193 | def check(self, midi_notes): 194 | """Verify that note off and note on alternate 195 | in a safe way (no double note off) 196 | """ 197 | 198 | #Checking if notes are playing 199 | i = 0 200 | j = 0 201 | long = len(midi_notes) 202 | while j < long: 203 | #Test for note on 204 | if midi_notes[i][0][0]&240 == 144: 205 | #getting channel 206 | chan = midi_notes[i][0][0]&15 207 | #if note is already playing (reorder neededor sup) 208 | if self.keyboard[chan][midi_notes[i][0][1]]: 209 | 210 | #Getting nearest good note 211 | ind_to_swap = self.note_index(chan, midi_notes[i][0][1], 0, i, midi_notes) 212 | 213 | #if the note paire is found after it 214 | if ind_to_swap != -1: 215 | if (midi_notes[i][1] <= midi_notes[ind_to_swap][1] + 1 216 | and midi_notes[i][1] >= midi_notes[ind_to_swap][1] - 1): 217 | tmp = midi_notes[i] 218 | midi_notes[i] = midi_notes[ind_to_swap] 219 | midi_notes[ind_to_swap] = tmp 220 | 221 | self.keyboard[chan][midi_notes[i][0][1]] = False 222 | i += 1 223 | 224 | else: 225 | #else we delete it 226 | #can't swap with different timestamp 227 | del midi_notes[i] 228 | 229 | else: 230 | #else we delete it 231 | del midi_notes[i] 232 | 233 | else: 234 | self.keyboard[chan][midi_notes[i][0][1]] = True 235 | i += 1 236 | 237 | 238 | elif midi_notes[i][0][0]&240 == 128: 239 | #getting channel 240 | chan = midi_notes[i][0][0]&15 241 | 242 | #Getting nearest good note 243 | if not self.keyboard[chan][midi_notes[i][0][1]]: 244 | ind_to_swap = self.note_index(chan, midi_notes[i][0][1], 1, i, midi_notes) 245 | #if the note paire is found 246 | if ind_to_swap != -1: 247 | if (midi_notes[i][1] <= midi_notes[ind_to_swap][1] + 1 248 | and midi_notes[i][1] >= midi_notes[ind_to_swap][1] - 1): 249 | tmp = midi_notes[i] 250 | midi_notes[i] = midi_notes[ind_to_swap] 251 | midi_notes[ind_to_swap] = tmp 252 | self.keyboard[chan][midi_notes[i][0][1]] = True 253 | i += 1 254 | 255 | else: 256 | #else we delete it 257 | #can't swap with different timestamp 258 | del midi_notes[i] 259 | else: 260 | #else we delete i 261 | del midi_notes[i] 262 | else: 263 | self.keyboard[chan][midi_notes[i][0][1]] = False 264 | i += 1 265 | 266 | j += 1 267 | return midi_notes 268 | -------------------------------------------------------------------------------- /rtpmidi/engines/midi/midi_out.py: -------------------------------------------------------------------------------- 1 | #Twisted 2 | from twisted.internet import defer 3 | from twisted.internet import reactor 4 | 5 | #Midi Streams 6 | from rtpmidi.engines.midi.ringBuffer import myRingBuffer 7 | from rtpmidi.engines.midi.midi_object import SafeKeyboard 8 | 9 | import Queue 10 | 11 | #Utils 12 | import time 13 | try: 14 | import pypm 15 | except ImportError: 16 | from pygame import pypm 17 | 18 | #Midi Commands 19 | NOTE_ON = 0x90 20 | NOTE_OFF = 0x80 21 | PRESSURE_CHANGE = 0xA0 22 | CONTROL_CHANGE = 0xB0 23 | PROGRAM_CHANGE = 0xC0 24 | CHANNEL_PRESSURE = 0xD0 25 | PITCH_CHANGE = 0xE0 26 | 27 | #Midi Constants 28 | NUM_MIDI_CHANS = 15 29 | MIDI_MAX = 2**7 - 1 30 | INPUT = 0 31 | OUTPUT = 1 32 | 33 | VERBOSE = 0 34 | 35 | class MidiOut(object): 36 | 37 | def __init__(self, permissif, latency, safe_keyboard=0, verbose=0): 38 | if verbose: 39 | global VERBOSE 40 | VERBOSE = 1 41 | 42 | self.midi_device_list = [] 43 | self.midi_out = None 44 | self.midi_device = None 45 | self.latency = latency 46 | self.tolerance = latency / float(2) 47 | 48 | #time for a round trip packet 49 | self.delay = 0 50 | 51 | #stat 52 | self.nb_notes = 0 53 | self.nb_lost = 0 54 | self.nb_xrun = 0 55 | self.start_chrono = 0 56 | 57 | #Struct 58 | #self.midi_cmd_list = myFIFO() 59 | self.midi_cmd_list = Queue.Queue(0) 60 | self.playing_buffer = myRingBuffer() 61 | 62 | #flag 63 | self.is_listening = 0 64 | self.publish_flag = False 65 | self.start_time = 0 66 | 67 | #checking 68 | self.keyboard = [False for i in range(127)] 69 | 70 | #Mode 71 | self.permissif = permissif 72 | 73 | #Checking non wanted artefact 74 | self.safe_k = 0 75 | if safe_keyboard: 76 | self.safe_keyboard = SafeKeyboard() 77 | self.safe_k = 1 78 | 79 | if VERBOSE: 80 | print " SafeKeyboard is running for Midi Out" 81 | 82 | 83 | def start(self): 84 | """Start publishing notes 85 | """ 86 | if not self.midi_out is None: 87 | #self.send_note_off() 88 | self.publish_flag = True 89 | reactor.callInThread(self.publish_midi_notes) 90 | if VERBOSE: 91 | print "OUTPUT: Start publish notes" 92 | 93 | return 1 94 | 95 | else: 96 | if VERBOSE: 97 | print "OUTPUT: Can not start publish without a midi device set" 98 | 99 | return 0 100 | 101 | 102 | def get_midi_time(self): 103 | return pypm.Time() 104 | 105 | def stop(self): 106 | """Stop publish 107 | """ 108 | self.publish_flag = False 109 | if not self.midi_out is None: 110 | #Why a midi problem with send note_off 111 | #(only when closing app ) <- patch it silly man ! 112 | self.send_note_off() 113 | 114 | def set_init_time(self): 115 | """Sync set the difference between local midi time and 116 | remote midi time in order to apply it to the notes 117 | """ 118 | self.init_time = pypm.Time() 119 | 120 | def get_devices(self): 121 | """list and set midi device 122 | """ 123 | self.midi_device_list = [] 124 | for loop in range(pypm.CountDevices()): 125 | interf, name, inp, outp, opened = pypm.GetDeviceInfo(loop) 126 | if outp == 1: 127 | self.midi_device_list.append([loop, name, opened]) 128 | 129 | return self.midi_device_list 130 | 131 | 132 | def set_device(self, device): 133 | """set output midi device 134 | """ 135 | #check if device exist 136 | dev_list = [self.midi_device_list[i][0] 137 | for i in range(len(self.midi_device_list))] 138 | 139 | if device in dev_list : 140 | self.midi_device = device 141 | 142 | if self.midi_out is not None : 143 | del self.midi_out # delete old midi device if present 144 | # Initializing midi input stream 145 | self.midi_out = pypm.Output(self.midi_device, 0) 146 | if VERBOSE: 147 | line = " Midi device out: " + str(self.get_device_info()[1]) 148 | print line 149 | return True 150 | 151 | else: 152 | print "OUTPUT: Invalid midi device selected" 153 | print dev_list 154 | return False 155 | 156 | 157 | def get_device_info(self): 158 | """print info of the current device 159 | """ 160 | res = pypm.GetDeviceInfo(self.midi_device) 161 | return res 162 | 163 | 164 | def send_note_off(self): 165 | """send Note Off all pitches and all channels 166 | """ 167 | midi_time = pypm.Time() 168 | #127 note off and 16 channels 169 | #TODO check problem: portMidi found host error (link to zynadd?) 170 | for i in range(NUM_MIDI_CHANS): 171 | for j in range(MIDI_MAX): 172 | self.midi_out.Write([[[NOTE_OFF + i,j,0],0]]) 173 | 174 | #Permisive Mode => joue toutes les notes meme si en retard de qq 175 | #milisecond en affichant 176 | #un erreur style xrun dans le fichier de log 177 | 178 | def play_midi_note(self): 179 | """PlayMidi Note 180 | Separate midi infos to choose the good function for 181 | the good action 182 | """ 183 | #getting time 184 | midi_time = pypm.Time() 185 | 186 | #getting notes 187 | midi_notes = self.playing_buffer.get_data(midi_time - self.latency, 188 | self.tolerance) 189 | 190 | self.nb_notes += len(midi_notes) 191 | 192 | if self.safe_k: 193 | midi_notes = self.safe_keyboard.check(midi_notes) 194 | 195 | if self.permissif : 196 | #Building list of lates notes in order to log it 197 | new_list = [midi_notes[i][1] 198 | for i in range(len(midi_notes)) 199 | if (midi_time > (midi_notes[i][1] + self.latency))] 200 | 201 | if (len(new_list) > 0) : 202 | self.nb_xrun += 1 203 | 204 | if VERBOSE: 205 | line = "OUTPUT: time=" + str(midi_time) 206 | line += "ms can't play in time , " 207 | line += str(len(midi_notes)) 208 | line += " notes - late of " 209 | calc = ( midi_time - ( self.latency + new_list[0] )) 210 | line += str(calc) + " ms" 211 | print line 212 | 213 | note_filtered = midi_notes 214 | 215 | else: 216 | # filter note off program change for notes that are late 217 | # if mode non permissif is on skip late notes except 218 | # note off, notes with velocitiy 0 or program change 219 | note_filtered = [midi_notes[i] for i in range(len(midi_notes)) 220 | if midi_notes[i][1] + self.latency >= midi_time 221 | or ( midi_notes[i][0][0] == PROGRAM_CHANGE 222 | or midi_notes[i][0][2] == 0 223 | or midi_notes[i][0][0] == NOTE_OFF)] 224 | 225 | if (len(note_filtered) < len(midi_notes)): 226 | if VERBOSE: 227 | line = "OUTPUT: time=" + str(pypm.Time()) 228 | line += "ms can't play in time, " 229 | line += str(len(midi_notes) - len(note_filtered)) 230 | line += " note(s) skipped, late of: " 231 | calc = ( midi_time - ( self.latency + midi_notes[0][1] )) 232 | line += str(calc) + " ms" 233 | print line 234 | 235 | #Playing note on the midi device 236 | self.midi_out.Write(midi_notes) 237 | 238 | def publish_midi_notes(self): 239 | """Polling function for midi out""" 240 | def_p = defer.Deferred() 241 | # put in local scope to improve performance 242 | midi_cmd_list = self.midi_cmd_list 243 | play_midi_note = self.play_midi_note 244 | 245 | while self.publish_flag : 246 | """ if there are notes in the shared buffer 247 | Put the in the playing buffer """ 248 | while True: 249 | try: 250 | cur_data = midi_cmd_list.get_nowait() 251 | if VERBOSE: 252 | print cur_data 253 | self.playing_buffer.put(cur_data) 254 | except Queue.Empty: 255 | break 256 | 257 | if self.playing_buffer.len() > 0: 258 | current_time = pypm.Time() 259 | #if the first is in time 260 | #12 correspond to the polling interval on the 261 | #test machine and the jitter of thread 262 | #switching ( to test on others computers with 263 | #diff set up) 264 | #The problem is that scheduler taking time to 265 | #switch between process 266 | if ((self.playing_buffer.buffer[0][1] + self.latency - self.tolerance) <= current_time): 267 | reactor.callInThread(play_midi_note) 268 | #time.sleep(0.001) # this probably used to be the sleep below 269 | 270 | # don't hog the cpu 271 | time.sleep(0.001) 272 | 273 | 274 | return def_p 275 | 276 | 277 | def __del__(self): 278 | self.terminate = 1 279 | -------------------------------------------------------------------------------- /rtpmidi/engines/midi/midi_session.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Scenic 5 | # Copyright (C) 2008 Société des arts technologiques (SAT) 6 | # http://www.sat.qc.ca 7 | # All rights reserved. 8 | # 9 | # This file is free software: you can redistribute it and/or modify 10 | # it under the terms of the GNU General Public License as published by 11 | # the Free Software Foundation, either version 2 of the License, or 12 | # (at your option) any later version. 13 | # 14 | # Scenic 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 Scenic. If not, see . 21 | """ 22 | RTP MIDI session. 23 | """ 24 | #History Needs 25 | from midi_object import OldPacket 26 | from midi_object import MidiCommand 27 | from list_circ import PacketCirc 28 | #rtp import 29 | from recovery_journal import RecoveryJournal 30 | from recovery_journal import compare_history_with_recovery 31 | from rtpmidi.protocols.rtp.rtp_session import RTPSession 32 | #twisted import 33 | from twisted.internet import task 34 | #midi import 35 | from rtpmidi.engines.midi.midi_in import MidiIn 36 | from rtpmidi.engines.midi.midi_out import MidiOut 37 | try: 38 | import pypm 39 | except ImportError: 40 | from pygame import pypm 41 | from time import time 42 | 43 | #Constants 44 | DEFAULT_PORT = 44000 45 | PAYLOAD = 96 46 | #RTP Timeout is 60 (need it because 47 | #midi data are not sent when no note are played) 48 | MIDI_RTP_TIME_OUT = 40 49 | TOOL_NAME = "Sropulpof_midi" #FIXME: rename this 50 | HISTORY_SIZE = 1024 51 | DEBUG = 1 52 | 53 | class MidiSession(RTPSession): 54 | """ 55 | Control RTP for MIDI payload. 56 | """ 57 | def __init__(self, peer_address, sport=0, rport=0, latency=20, 58 | jitter_buffer_size=10, safe_keyboard=0, recovery=1, 59 | follow_standard=0, verbose=0): 60 | #Init mother class 61 | RTPSession.__init__(self, peer_address, sport, rport, PAYLOAD, 62 | jitter_buffer_size, TOOL_NAME) 63 | self.verbose = verbose 64 | if verbose: 65 | print "Your configuration:" 66 | print " Latency:", latency, "ms" 67 | print " Jitter Buffer Time:", jitter_buffer_size, "ms" 68 | #init midi 69 | if self.sport > 0: 70 | self.midi_in = MidiIn(self, verbose) 71 | #1 == Permisive mode (make this configurable) 72 | if self.rport > 0: 73 | self.midi_out = MidiOut(0, latency, safe_keyboard, verbose) 74 | #history of the feed 75 | self.packets_received_list = PacketCirc(HISTORY_SIZE) 76 | #Recovery utils 77 | self.recovery = 0 78 | self.recovery_journal_system = None 79 | if recovery: 80 | self.recovery = 1 81 | self.recovery_journal_system = RecoveryJournal(follow_standard) 82 | if verbose: 83 | print " Recovery journal is running" 84 | if follow_standard: 85 | print " Recovery is following standard" 86 | self.init_timestamp = None 87 | #Flag 88 | self.sending_data = 0 89 | self.receiving_data = 0 90 | #Timestamp story 91 | self.last_midi_time_sent = pypm.Time() 92 | self.timeouterLoop = None 93 | 94 | def start(self): 95 | """ 96 | Launch the MIDI session and the RTP session 97 | """ 98 | if self.sport > 0: 99 | self.midi_in.start() 100 | if self.rport > 0: 101 | self.midi_out.start() 102 | self._keep_alive() 103 | 104 | def stop(self): 105 | """ 106 | Stop midi session and the RTP session 107 | """ 108 | if self.sport > 0: 109 | self.midi_in.stop() 110 | if self.rport > 0: 111 | self.midi_out.stop() 112 | if not self.timeouterLoop is None: 113 | if self.timeouterLoop.running: 114 | self.timeouterLoop.stop() 115 | 116 | def _keep_alive(self): 117 | #FIXME: document this 118 | def check_timeout(): 119 | """ 120 | Sends an empty packet if there is no activity. 121 | """ 122 | if time.time() > self.last_send + MIDI_RTP_TIME_OUT: 123 | self.send_silence() 124 | #Deccomment following line if want to log keep aive packet 125 | #log.info("silent packet sent to keep alive the connection") 126 | self.timeouterLoop = task.LoopingCall(check_timeout, now=False) 127 | self.timeouterLoop.start(15) 128 | 129 | def incoming_rtp(self, cookie, timestamp, packet, read_recovery_journal=0): 130 | """ 131 | Function called by RTPControl when incoming 132 | data comes out from jitter buffer. 133 | """ 134 | #Parsing RTP MIDI Header 135 | marker_b, marker_j, marker_z, marker_p, length = \ 136 | MidiCommand().parse_header(packet.data[0]) 137 | if marker_p : 138 | #silent packet with recovery 139 | midi_list = [] 140 | else: 141 | #normal packet 142 | #Extract Midi Note (length en nb notes) 143 | midi_list = packet.data[1:length*7+1] 144 | #Decoding midi commands 145 | midi_list = MidiCommand().decode_midi_commands(midi_list, length) 146 | if DEBUG: 147 | print "receiving data", midi_list 148 | #Saving feed history 149 | packet_to_save = OldPacket(self.seq, midi_list, 0) 150 | self.packets_received_list.to_list(packet_to_save) 151 | #Extract Midi Recovery Journal if is present in the packet and 152 | #the previous packet has been lost 153 | if self.recovery: 154 | if marker_j and read_recovery_journal: 155 | if DEBUG: 156 | print "Read recovery journal" 157 | print packet.header.marker 158 | journal = packet.data[length*7+1:] 159 | if len(journal)>0: 160 | #Parse Recovery journal 161 | r_journal = self.recovery_journal_system.parse(journal) 162 | else: 163 | r_journal = [] 164 | #compare it with history feed 165 | #Extract midi notes from checkpoint sent to actual seq 166 | midi_history = self.packets_received_list.get_packets(self.last_checkpoint,self.seq) 167 | #Selecting only notes present in recovery 168 | #that are not in feed history 169 | midi_cmd_history = [] 170 | for i in range(len(midi_history)): 171 | cmd_tmp = midi_history[i].packet 172 | for j in range(len(cmd_tmp)): 173 | midi_cmd_history.append(cmd_tmp[j]) 174 | if DEBUG: 175 | rem = time() 176 | midi_from_history = compare_history_with_recovery(r_journal, midi_cmd_history) 177 | if DEBUG: 178 | print "tps for history compare:", str(time() - rem) 179 | #Initialize timestamp diff 180 | if self.init_timestamp is None: 181 | self.init_timestamp = timestamp 182 | #calculate delta midi 183 | self.midi_out.set_init_time() 184 | #adding recovery journal to the list 185 | if self.recovery: 186 | if marker_j and read_recovery_journal: 187 | midi_list.extend(midi_from_history) 188 | #profiter du parcours pour appliquer les timestamps 189 | for i in range(len(midi_list)): 190 | midi_list[i][1] = (timestamp - self.init_timestamp + self.midi_out.init_time) 191 | #Adding the note to the playing buffer 192 | for i in range(len(midi_list)): 193 | self.midi_out.midi_cmd_list.put(midi_list[i]) 194 | #switch off witness 195 | self.receiving_data = 0 196 | 197 | def send_silence(self): 198 | """ 199 | Sends empty packet to signal a silent period 200 | """ 201 | recovery_journal = "" 202 | marker_j = 0 203 | #Getting recovery 204 | if self.recovery: 205 | recovery_journal = self.recovery_journal_system.content 206 | if recovery_journal != "": 207 | #Recovery Journal 1 if present 208 | marker_j = 1 209 | #Creating empty midicommand filed 210 | header = MidiCommand().header(0, marker_j, 1, 1, 0) 211 | if recovery_journal != "": 212 | chunk = header + recovery_journal 213 | else: 214 | chunk = str(0) 215 | #sending silent packet with recovery journal 216 | RTPSession.send_empty_packet(self, chunk) 217 | #RTPControl().send_empty_packet(self.cookie, chunk) 218 | 219 | def send_midi_data(self, data, midi_time, recovery=1, timestamp=1): 220 | """ 221 | Sends MIDI data through the RTP session. 222 | """ 223 | if DEBUG: 224 | print "Sending data", data 225 | #Witness 226 | self.sending_data = 1 227 | #midi Cmd List 228 | midi_list = data 229 | #Saving packet 230 | packet = OldPacket(self.seq, midi_list, 0) 231 | chunk = "" 232 | recovery_journal = "" 233 | if recovery: 234 | #Recovery Journal (can be empty) 235 | #TODO customize it for each member of the feed 236 | if self.recovery_journal_system is not None: 237 | recovery_journal = self.recovery_journal_system.content 238 | if recovery_journal == "": 239 | recovery = 0 240 | #Packing All 241 | #Testing length of midi list ( in nb notes ) 242 | if len(midi_list) < 1: 243 | return 244 | #Formating commands for network 245 | midi_list_formated, length = \ 246 | MidiCommand().encode_midi_commands(midi_list) 247 | #Creating Header 248 | header = MidiCommand().header(0, recovery, 0, 0, length) 249 | #Building Chunk 250 | chunk = header + midi_list_formated + recovery_journal 251 | #Timestamp care (TS == temps midi ecouler depuis la creation de rtp) 252 | ts = midi_time - self.last_midi_time_sent 253 | self.last_midi_time_sent = midi_time 254 | #sending data to rtp session 255 | RTPSession.send_data(self, chunk, ts) 256 | self.sending_data = 0 257 | #Updating Recovery Journal 258 | if self.recovery: 259 | self.recovery_journal_system.update(packet) 260 | 261 | def update_checkpoint(self, new_checkpoint): 262 | """ 263 | Function called by RTCP to reduce size of recovery journal. 264 | """ 265 | if self.recovery: 266 | self.recovery_journal_system.trim(new_checkpoint) 267 | 268 | self.checkpoint = new_checkpoint 269 | 270 | def drop_connection(self, cookie=0): 271 | """ 272 | Called by RTP when the connection has been dropped. 273 | """ 274 | #Rename drop connection 275 | print "drop connexion for midi session" 276 | 277 | def get_devices(self): 278 | """ 279 | Lists MIDI devices on this computer. 280 | @rtype: tuple 281 | @return: Tuple of list of device info. Input devices first, then output devices. 282 | """ 283 | #FIXME: return a dict, not a tuple. 284 | if self.sport > 0: 285 | devices_in = self.midi_in.get_devices() 286 | else: 287 | devices_in = [] 288 | if self.rport > 0: 289 | devices_out = self.midi_out.get_devices() 290 | else: 291 | devices_out = [] 292 | return devices_in, devices_out 293 | 294 | def set_device_in(self, dev): 295 | """ 296 | Selects the MIDI device to be polled. 297 | 298 | @param dev: The device number to choose. 299 | @type dev: int 300 | @rtype: bool 301 | @return: Success or not 302 | """ 303 | return self.midi_in.set_device(dev) 304 | 305 | def set_device_out(self, dev): 306 | """ 307 | Selects the MIDI output device to send data to. 308 | 309 | @param dev: The device number to choose. 310 | @type dev: int 311 | @rtype: bool 312 | @return: Success or not 313 | """ 314 | return self.midi_out.set_device(dev) 315 | -------------------------------------------------------------------------------- /rtpmidi/engines/midi/ringBuffer.py: -------------------------------------------------------------------------------- 1 | from threading import Lock 2 | 3 | class myFIFO(object): 4 | def __init__(self): 5 | self.data = [] 6 | self.lock = Lock() 7 | 8 | def append(self, x): 9 | print "append" 10 | #self.lock.acquire() 11 | self.data.append(x) 12 | #self.lock.release() 13 | 14 | def get(self): 15 | if len(self.data) > 0: 16 | res = [] 17 | for i in range(len(self.data)): 18 | print "get" 19 | #self.lock.acquire() 20 | res.append(self.data.pop(0)) 21 | #self.lock.release() 22 | return res 23 | 24 | else: 25 | return None 26 | 27 | 28 | 29 | class myRingBuffer(object): 30 | def __init__(self): 31 | self.start = 0 32 | self.end = 0 33 | self.buffer = [] 34 | 35 | #get the total len of the ring 36 | def len(self): 37 | return len(self.buffer) 38 | 39 | def average(self): 40 | return sum(self.buffer) / len(self.buffer) 41 | 42 | #Write Data in the ring buffer 43 | def put(self, newNote): 44 | if len(self.buffer) > 0 : 45 | if self.buffer[-1][1] <= newNote[1]: 46 | #checking if the note is in the right place 47 | self.buffer.append(newNote) 48 | else: 49 | #sinon on regarde ou il faut l inserer 50 | if newNote[1] <= self.buffer[0][1]: 51 | self.buffer.insert(0,newNote) 52 | else: 53 | #Explication : ?? 54 | place = [ind for ind, obj in enumerate(self.buffer) \ 55 | if obj[1] < newNote[1]][-1] 56 | self.buffer.insert(place + 1, newNote) 57 | 58 | else: 59 | #simply adding the note to the buffer 60 | self.buffer.append(newNote) 61 | 62 | #getting data from the buffer 63 | def get(self): 64 | if len(self.buffer) > 100 : 65 | copied = self.buffer[:100] 66 | [self.buffer.pop(0) for i in range(0,100)] 67 | else: 68 | copied = self.buffer 69 | self.buffer = [] 70 | 71 | return copied 72 | 73 | 74 | def get_data(self, time, tolerance=0): 75 | data = [] 76 | ind = [] 77 | 78 | #attention part du principe que le buffer est trier en ordre croissant 79 | for i in range(len(self.buffer)): 80 | if self.buffer[i][1] <= time + tolerance: 81 | data.append(self.buffer[i]) 82 | ind.append(i) 83 | for i in range(len(ind)): 84 | self.buffer.pop(0) 85 | return data 86 | 87 | if __name__ == "__main__": 88 | r = myRingBuffer() 89 | r.put([['ob',1,1],10]) 90 | r.put([['ob',1,1],14]) 91 | r.put([['ob',1,1],12]) 92 | print r.buffer 93 | 94 | -------------------------------------------------------------------------------- /rtpmidi/protocols/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/avsaj/rtpmidi/622a8cffeb3b4e67b66cbaf63a6432aef9fb6984/rtpmidi/protocols/__init__.py -------------------------------------------------------------------------------- /rtpmidi/protocols/rtp/README: -------------------------------------------------------------------------------- 1 | How to use RTP to stream data: 2 | 3 | Class you have to know: 4 | 5 | RTPControl: manage several RTP session 6 | RTPSession: prototype of an RTP session 7 | 8 | In order to create a stream, create a class which inherited from RTPSession 9 | This class will be the interface between your device in and/or out and the network. 10 | 11 | The MIDI example: 12 | Imagine you have your MidiIn class which is reading data from a MIDI device, 13 | and a MidiOut class which is writting data on a MIDI device. 14 | 15 | Let's called our session MidiSession. 16 | 17 | There are some mandatory method to add to our session: 18 | incoming_rtp: This function will be called by the RTP protocol when a packet is received 19 | start: If you have some things to launch (for example a polling), you have to put it here. 20 | stop: Stop the things launched 21 | drop_call: Called by RTP protocol when something wrong append to the connection (timeout, network error, ...) 22 | 23 | Your session MUST have a sending port and/or a receiving port. But at least one of them. 24 | payload and host has to be specify at the initialization of the session (they are mandatory). 25 | 26 | Some others var can be set at this time like the jitter buffer size of the RTP and the tool name that will 27 | be stream in RTCP (is used to identify a source). 28 | 29 | midi_session.py: 30 | 31 | TOOLNAME = "ExampleStream" 32 | PAYLOAD = 96 33 | 34 | class MidiSession(RTPSession): 35 | """Control rtp for midi payload 36 | """ 37 | def __init__(self, peer_address, sending_port=0, receiving_port=0, jitter_buffer_size=10): 38 | #Init mother class 39 | RTPSession.__init__(self, peer_address, sending_port, receiving_port, PAYLOAD, jitter_buffer_size, TOOL_NAME) 40 | 41 | def start(self): 42 | #start your stuff 43 | self.midi_in.start() 44 | 45 | def stop(self): 46 | #stop your stuff 47 | self.midi_in.stop() 48 | 49 | def incoming_rtp(self, cookie, timestamp, packet, 50 | read_recovery_journal=0): 51 | 52 | pass 53 | 54 | 55 | The incoming function: 56 | 57 | cookie: Own cookie 58 | timestamp: Timestamp of the packet 59 | packet: Content of the packet 60 | read_recovery_journal: Is set to one if the previous packet has been lost else 0. 61 | 62 | A send function could also be helpfull ! 63 | Here is how to call the send data function of RTPControl: 64 | 65 | def send_data(self, data, time): 66 | #encrypt your data has you which to received them 67 | chunk = encrypt(data) 68 | #Normalize your time has you which to received it 69 | ts = normalize(time) 70 | 71 | #Send the data 72 | RTPSession.send_data(self, chunk, ts) 73 | 74 | 75 | Our class MidiSession has to be able to manage MidiIn and MidiOut, 76 | at least to list MIDI devices and set them. 77 | 78 | What is the link between MidiSession and MidiOut ? 79 | MidiSession send data to MidiOut buffer, 80 | MidiSession set latency of MidiOut class. 81 | 82 | What is the link between MidiSession and MidiIn ? 83 | MidiIn send data to MidiSession 84 | 85 | Once your custom session is created we have to integrate it to the streaming process. 86 | 87 | RTPControl is managing all RTP sessions of the software, 88 | here is how to initialize it and register our session in it. 89 | 90 | #Init RTPControl 91 | rtp_control = RTPControl() 92 | 93 | #Create midi_session 94 | midi_session = MidiSession(our_params) 95 | 96 | #Register the session 97 | cookie_session = rtp_control.add_session(midi_session) 98 | 99 | #start the session 100 | rtp_control.start_session(cookie_session) 101 | 102 | #Or start all session 103 | #rtp_control.start() 104 | 105 | #stop the session 106 | rtp_control.stop_session(cookie_session) 107 | 108 | #Or stop all session 109 | #rtp_control.stop() 110 | 111 | Note that RTPControl is a singleton. 112 | 113 | You can retreive you session object with your cookie like that: 114 | midi_session = rtp_control.get_session(midi_session) 115 | 116 | Then you can acces your session like an object. 117 | 118 | Now you know how to use this little library. Have fun! 119 | -------------------------------------------------------------------------------- /rtpmidi/protocols/rtp/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/avsaj/rtpmidi/622a8cffeb3b4e67b66cbaf63a6432aef9fb6984/rtpmidi/protocols/rtp/__init__.py -------------------------------------------------------------------------------- /rtpmidi/protocols/rtp/defcache.py: -------------------------------------------------------------------------------- 1 | 2 | from __future__ import nested_scopes 3 | 4 | from twisted.internet import defer 5 | import sys 6 | 7 | class _DeferredCache: 8 | """ Wraps a call that returns a deferred in a cache. Any subsequent 9 | calls with the same argument will wait for the first call to 10 | finish and return the same result (or errback). 11 | """ 12 | 13 | hashableArgs = False 14 | inProgressOnly = True 15 | 16 | def __init__(self, op, hashableArgs=None, inProgressOnly=None): 17 | self.op = op 18 | self.cache = {} 19 | if hashableArgs is not None: 20 | self.hashableArgs = hashableArgs 21 | if inProgressOnly is not None: 22 | self.inProgressOnly = inProgressOnly 23 | 24 | def cb_triggerUserCallback(self, res, deferred): 25 | #print "triggering", deferred 26 | deferred.callback(res) 27 | return res 28 | 29 | def cb_triggerUserErrback(self, failure, deferred): 30 | deferred.errback(failure) 31 | return failure 32 | 33 | def _genCache(self, args, kwargs): 34 | # This could be better, probably 35 | try: 36 | arghash = hash(args) 37 | except TypeError: 38 | return None 39 | kwit = kwargs.items() 40 | kwit.sort() 41 | try: 42 | kwhash = hash(tuple(kwit)) 43 | except TypeError: 44 | return None 45 | return (arghash, kwhash) 46 | 47 | def _removeCacheVal(self, res, cacheVal): 48 | del self.cache[cacheVal] 49 | return res 50 | 51 | def clearCache(self): 52 | self.cache = {} 53 | 54 | def call(self, *args, **kwargs): 55 | # Currently not in progress - start it 56 | #print "called with", args 57 | cacheVal = self._genCache(args, kwargs) 58 | if cacheVal is None and self.hashableArgs: 59 | raise TypeError('DeferredCache(%s) arguments must be hashable'%( 60 | self.op.func_name)) 61 | 62 | opdef = self.cache.get(cacheVal) 63 | if not opdef: 64 | # XXX assert that it returns a deferred? 65 | opdef = self.op(*args, **kwargs) 66 | if cacheVal is not None: 67 | self.cache[cacheVal] = opdef 68 | if self.inProgressOnly and cacheVal: 69 | opdef.addCallbacks(lambda x: self._removeCacheVal(x, cacheVal), 70 | lambda x: self._removeCacheVal(x, cacheVal)) 71 | 72 | userdef = defer.Deferred() 73 | opdef.addCallbacks(lambda x: self.cb_triggerUserCallback(x, userdef), 74 | lambda x: self.cb_triggerUserErrback(x, userdef)) 75 | return userdef 76 | 77 | 78 | def DeferredCache(op=None, hashableArgs=None, inProgressOnly=None): 79 | """ Use this as a decorator for a function or method that returns a 80 | deferred. Any subsequent calls using the same arguments will 81 | be all triggered off the original deferred, all returning the 82 | same result. 83 | """ 84 | if op is None: 85 | return lambda x: DeferredCache(x, hashableArgs, inProgressOnly) 86 | c = _DeferredCache(op, hashableArgs, inProgressOnly) 87 | def func(*args, **kwargs): 88 | return c.call(*args, **kwargs) 89 | if sys.version_info > (2,4): 90 | func.func_name = op.func_name 91 | func.clearCache = c.clearCache 92 | func.cache_hashableArgs = c.hashableArgs 93 | func.cache_inProgressOnly = c.inProgressOnly 94 | return func 95 | -------------------------------------------------------------------------------- /rtpmidi/protocols/rtp/formats.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2004 Anthony Baxter 2 | "This module contains the logic and classes for format negotiation" 3 | 4 | from twisted.python.util import OrderedDict 5 | #from xshtoom.avail import codecs 6 | 7 | 8 | class PTMarker: 9 | "A marker of a particular payload type" 10 | media = None 11 | 12 | def __init__(self, name, pt=None, clock=8000, params=1, fmtp=None): 13 | self.name = name 14 | self.pt = pt 15 | self.clock = clock 16 | self.params = params 17 | self.fmtp = fmtp 18 | 19 | def __repr__(self): 20 | if self.pt is None: 21 | pt = 'dynamic' 22 | else: 23 | pt = str(self.pt) 24 | cname = self.__class__.__name__ 25 | return "<%s %s(%s)/%s/%s at %x>"%(cname, self.name, pt, 26 | self.clock, self.params, id(self)) 27 | 28 | class AudioPTMarker(PTMarker): 29 | "An audio payload type" 30 | media = 'audio' 31 | 32 | class VideoPTMarker(PTMarker): 33 | "A video payload type" 34 | media = 'video' 35 | 36 | #Midi Codec 37 | #MIDI = AudioPTMarker('mpeg4-generic', clock=44100, params=1, pt=96) 38 | 39 | PT_PCMU = AudioPTMarker('PCMU', clock=8000, params=1, pt=0) 40 | PT_GSM = AudioPTMarker('GSM', clock=8000, params=1, pt=3) 41 | # G723 is actually G.723.1, but is the same as G.723. XXX test against cisco 42 | PT_G723 = AudioPTMarker('G723', clock=8000, params=1, pt=4) 43 | PT_DVI4 = AudioPTMarker('DVI4', clock=8000, params=1, pt=5) 44 | PT_DVI4_16K = AudioPTMarker('DVI4', clock=16000, params=1, pt=6) 45 | PT_LPC = AudioPTMarker('LPC', clock=8000, params=1, pt=7) 46 | PT_PCMA = AudioPTMarker('PCMA', clock=8000, params=1, pt=8) 47 | PT_G722 = AudioPTMarker('G722', clock=8000, params=1, pt=9) 48 | PT_L16_STEREO = AudioPTMarker('L16', clock=44100, params=2, pt=10) 49 | PT_L16 = AudioPTMarker('L16', clock=44100, params=1, pt=11) 50 | PT_QCELP = AudioPTMarker('QCELP', clock=8000, params=1, pt=12) 51 | PT_CN = AudioPTMarker('CN', clock=8000, params=1, pt=13) 52 | PT_G728 = AudioPTMarker('G728', clock=8000, params=1, pt=15) 53 | PT_DVI4_11K = AudioPTMarker('DVI4', clock=11025, params=1, pt=16) 54 | PT_DVI4_22K = AudioPTMarker('DVI4', clock=22050, params=1, pt=17) 55 | PT_G729 = AudioPTMarker('G729', clock=8000, params=1, pt=18) 56 | PT_xCN = AudioPTMarker('xCN', clock=8000, params=1, pt=19) 57 | 58 | #FOR RTP MIDI 59 | PT_AVP = AudioPTMarker('mpeg4-generic', clock=44100, params=1, pt=96) 60 | 61 | PT_SPEEX = AudioPTMarker('speex', clock=8000, params=1) 62 | PT_SPEEX_16K = AudioPTMarker('speex', clock=16000, params=1) 63 | PT_G726_40 = AudioPTMarker('G726-40', clock=8000, params=1) 64 | # Deprecated - gone from RFC3551 65 | PT_1016 = AudioPTMarker('1016', clock=8000, params=1, pt=1) 66 | # aka G723-40 (5 bit data) 67 | PT_G726_40 = AudioPTMarker('G726-40', clock=8000, params=1) 68 | # G726-32 aka G721-32 (4 bit data) 69 | PT_G726_32 = AudioPTMarker('G726-32', clock=8000, params=1) 70 | # Deprecated spelling for G726-32 - gone from RFC3551 71 | PT_G721 = AudioPTMarker('G721', clock=8000, params=1, pt=2) 72 | # G726-24 aka G723-24 (3 bit data) 73 | PT_G726_24 = AudioPTMarker('G726-24', clock=8000, params=1) 74 | PT_G726_16 = AudioPTMarker('G726-16', clock=8000, params=1) 75 | PT_G729D = AudioPTMarker('G729D', clock=8000, params=1) 76 | PT_G729E = AudioPTMarker('G729E', clock=8000, params=1) 77 | PT_GSM_EFR = AudioPTMarker('GSM-EFR', clock=8000, params=1) 78 | PT_ILBC = AudioPTMarker('iLBC', clock=8000, params=1) 79 | #PT_L8 = AudioPTMarker('L8', clock=None, params=1) 80 | #PT_RED = AudioPTMarker('RED', clock=8000, params=1) 81 | #PT_VDVI = AudioPTMarker('VDVI', clock=None, params=1) 82 | PT_NTE = PTMarker('telephone-event', clock=8000, params=None, 83 | fmtp='0-16') 84 | # Internal shtoom codec. Note that the L16 format, above, is at 44100 KHz. 85 | PT_RAW = AudioPTMarker('RAW_L16', clock=8000, params=1) 86 | 87 | PT_CELB = VideoPTMarker('CelB', clock=90000, pt=25) 88 | PT_JPEG = VideoPTMarker('JPEG', clock=90000, pt=26) 89 | PT_NV = VideoPTMarker('nv', clock=90000, pt=28) 90 | PT_H261 = VideoPTMarker('H261', clock=90000, pt=31) 91 | PT_MPV = VideoPTMarker('MPV', clock=90000, pt=32) 92 | PT_MP2T = VideoPTMarker('MP2T', clock=90000, pt=33) 93 | PT_H263 = VideoPTMarker('H263', clock=90000, pt=34) 94 | 95 | 96 | TryCodecs = OrderedDict() 97 | #TryCodecs[PT_GSM] = codecs.gsm 98 | #TryCodecs[PT_SPEEX] = codecs.speex 99 | #TryCodecs[PT_DVI4] = codecs.dvi4 100 | #TryCodecs[PT_ILBC] = codecs.ilbc 101 | 102 | class SDPGenerator: 103 | "Responsible for generating SDP for the RTPProtocol" 104 | 105 | def getSDP(self, rtp, extrartp=None): 106 | from sdp import SDP, MediaDescription 107 | if extrartp: 108 | raise ValueError("can't handle multiple RTP streams in a call yet") 109 | s = SDP() 110 | addr = rtp.getVisibleAddress() 111 | s.setServerIP(addr[0]) 112 | md = MediaDescription() # defaults to type 'audio' 113 | s.addMediaDescription(md) 114 | md.setServerIP(addr[0]) 115 | md.setLocalPort(addr[1]) 116 | for pt, test in TryCodecs.items(): 117 | if test is not None: 118 | md.addRtpMap(pt) 119 | 120 | md.addRtpMap(PT_AVP) 121 | md.addFMTP(5) 122 | return s 123 | 124 | RTPDict = {} 125 | all = globals() 126 | for key,val in all.items(): 127 | if isinstance(val, PTMarker): 128 | # By name 129 | RTPDict[key] = val 130 | # By object 131 | if val.pt is not None: 132 | RTPDict[val] = val.pt 133 | # By PT 134 | if val.pt is not None: 135 | RTPDict[val.pt] = val 136 | # By name/clock/param 137 | RTPDict[(val.name.lower(),val.clock,val.params or 1)] = val 138 | 139 | del all, key, val 140 | -------------------------------------------------------------------------------- /rtpmidi/protocols/rtp/jitter_buffer.py: -------------------------------------------------------------------------------- 1 | from threading import Lock 2 | 3 | def packet_seq_compare(x, y): 4 | if x.header.seq>y.header.seq: 5 | return 1 6 | elif x.header.seq==y.header.seq: 7 | return 0 8 | else: # x= 1: 38 | if self.buffer[i][0].header.seq > to_add[0].header.seq: 39 | self.buffer.insert(0, to_add) 40 | else: 41 | self.buffer.append(to_add) 42 | else: 43 | self.buffer.append(to_add) 44 | 45 | self.lock.release() 46 | 47 | 48 | def has_seq(self, seq): 49 | """Looking for a special seqNum""" 50 | res = False 51 | for i in range(len(self.buffer)): 52 | if self.buffer[i][0].header.seq == seq: 53 | res = True 54 | break 55 | return res 56 | 57 | def get_packets(self, time): 58 | """Getting all the packet with continus seq num from packet seq""" 59 | to_send = [] 60 | i = 0 61 | last_time = 0 62 | self.lock.acquire() 63 | while ( i < len(self.buffer)): 64 | #if buffer time passed or 65 | if self.buffer[i][1] <= time: 66 | to_send.append(self.buffer[i][0]) 67 | del self.buffer[i] 68 | else: 69 | break 70 | 71 | self.lock.release() 72 | 73 | return to_send 74 | 75 | 76 | if __name__ == "__main__": 77 | j = JitterBuffer() 78 | -------------------------------------------------------------------------------- /rtpmidi/protocols/rtp/list_circ.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | #motherclass 4 | class ListCirc(object): 5 | 6 | def __init__(self, listeSize): 7 | self.index = 0 8 | self.list = [] 9 | self.round = 0 10 | self.listeSize = listeSize 11 | 12 | def to_list(self, note): 13 | if self.round == 1: 14 | self._replace_note(note) 15 | else: 16 | self._add_note(note) 17 | 18 | def _add_note(self, note): 19 | self.list.append(note) 20 | 21 | if self.index == self.listeSize-1: 22 | self.round = 1 23 | self.index = 0 24 | else: 25 | self.index += 1 26 | 27 | def _replace_note(self, note): 28 | if (self.index == self.listeSize): 29 | self.index = 0 30 | self.list[self.index] = note 31 | else: 32 | self.list[self.index] = note 33 | self.index += 1 34 | 35 | def flush(self): 36 | self.list = [] 37 | self.round = 0 38 | self.index = 0 39 | 40 | def __getitem__(self,key): 41 | return self.list[key] 42 | 43 | def __len__(self): 44 | return len(self.list) 45 | 46 | 47 | #list circ to stock time 48 | class DelayCirc(ListCirc): 49 | 50 | def __init__(self, listeSize): 51 | ListCirc.__init__(self, listeSize) 52 | self.lastSync = 0 53 | 54 | def to_list(self,note): 55 | ListCirc.to_list(self,note) 56 | self.lastSync = time.time() 57 | 58 | def average(self): 59 | if (len(self.list)>0): 60 | average = float(sum(self.list)/len(self.list)) 61 | return average 62 | 63 | def __repr__(self): 64 | return str(self.list) 65 | -------------------------------------------------------------------------------- /rtpmidi/protocols/rtp/nat.py: -------------------------------------------------------------------------------- 1 | 2 | # Code for NATs and the like. Also includes code for determining local IP 3 | # address (suprisingly tricky, in the presence of STUPID STUPID STUPID 4 | # networking stacks) 5 | 6 | from twisted.internet import defer 7 | from twisted.internet.protocol import DatagramProtocol 8 | import socket 9 | import random 10 | from twisted.python import log 11 | from defcache import DeferredCache 12 | 13 | 14 | _Debug = False 15 | 16 | class LocalNetworkMulticast(DatagramProtocol, object): 17 | 18 | def __init__(self, *args, **kwargs): 19 | self.compDef = defer.Deferred() 20 | self.completed = False 21 | super(LocalNetworkMulticast,self).__init__(*args, **kwargs) 22 | 23 | def listenMulticast(self): 24 | from twisted.internet import reactor 25 | from twisted.internet.error import CannotListenError 26 | attempt = 0 27 | port = 11000 + random.randint(0,5000) 28 | while True: 29 | try: 30 | mcast = reactor.listenMulticast(port, self) 31 | break 32 | except CannotListenError: 33 | port = 11000 + random.randint(0,5000) 34 | attempt += 1 35 | print "listenmulticast failed, trying", port 36 | if attempt > 5: 37 | log.msg("warning: couldn't listen ony mcast port", system='network') 38 | d, self.compDef = self.compDef, None 39 | d.callback(None) 40 | mcast.joinGroup('239.255.255.250', socket.INADDR_ANY) 41 | self.mcastPort = port 42 | 43 | def blatMCast(self): 44 | # XXX might need to set an option to make sure we see our own packets 45 | self.transport.write('ping', ('239.255.255.250', self.mcastPort)) 46 | self.transport.write('ping', ('239.255.255.250', self.mcastPort)) 47 | self.transport.write('ping', ('239.255.255.250', self.mcastPort)) 48 | 49 | def datagramReceived(self, dgram, addr): 50 | if self.completed: 51 | return 52 | elif dgram != 'ping': 53 | return 54 | else: 55 | self.completed = True 56 | d, self.compDef = self.compDef, None 57 | d.callback(addr[0]) 58 | 59 | _cachedLocalIP = None 60 | def _cacheLocalIP(res): 61 | global _cachedLocalIP 62 | if _Debug: print "caching value", res 63 | _cachedLocalIP = res 64 | return res 65 | 66 | # If there's a need to clear the cache, call this method (e.g. DHCP client) 67 | def _clearCachedLocalIP(): 68 | _cacheLocalIP(None) 69 | 70 | def _getLocalIPAddress(): 71 | # So much pain. Don't even bother with 72 | # socket.gethostbyname(socket.gethostname()) - the number of ways this 73 | # is broken is beyond belief. socket.getfqdn() is even worse. 74 | from twisted.internet import reactor 75 | global _cachedLocalIP 76 | if _cachedLocalIP is not None: 77 | return defer.succeed(_cachedLocalIP) 78 | # first we try a connected udp socket 79 | if _Debug: print "resolving A.ROOT-SERVERS.NET" 80 | d = reactor.resolve('A.ROOT-SERVERS.NET') 81 | d.addCallbacks(_getLocalIPAddressViaConnectedUDP, _noDNSerrback) 82 | return d 83 | 84 | getLocalIPAddress = DeferredCache(_getLocalIPAddress) 85 | 86 | def clearCache(): 87 | "Clear cached NAT settings (e.g. when moving to a different network)" 88 | from shtoom.stun import clearCache as sClearCache 89 | print "clearing all NAT caches" 90 | getLocalIPAddress.clearCache() 91 | getMapper.clearCache() 92 | sClearCache() 93 | 94 | def _noDNSerrback(failure): 95 | # No global DNS? What the heck, it's possible, I guess. 96 | if _Debug: print "no DNS, trying multicast" 97 | return _getLocalIPAddressViaMulticast() 98 | 99 | def _getLocalIPAddressViaConnectedUDP(ip): 100 | from twisted.internet import reactor 101 | from twisted.internet.protocol import DatagramProtocol 102 | if _Debug: print "connecting UDP socket to", ip 103 | prot = DatagramProtocol() 104 | p = reactor.listenUDP(0, prot) 105 | res = prot.transport.connect(ip, 7) 106 | locip = prot.transport.getHost().host 107 | p.stopListening() 108 | del prot, p 109 | 110 | if _Debug: print "connected UDP socket says", locip 111 | if isBogusAddress(locip): 112 | # #$#*(&??!@#$!!! 113 | if _Debug: print "connected UDP socket gives crack, trying mcast instead" 114 | return _getLocalIPAddressViaMulticast() 115 | else: 116 | return locip 117 | 118 | 119 | def _getLocalIPAddressViaMulticast(): 120 | # We listen on a new multicast address (using UPnP group, and 121 | # a random port) and send out a packet to that address - we get 122 | # our own packet back and get the address from it. 123 | from twisted.internet import reactor 124 | from twisted.internet.interfaces import IReactorMulticast 125 | try: 126 | IReactorMulticast(reactor) 127 | except: 128 | if _Debug: print "no multicast support in reactor" 129 | log.msg("warning: no multicast in reactor", system='network') 130 | return None 131 | locprot = LocalNetworkMulticast() 132 | if _Debug: print "listening to multicast" 133 | locprot.listenMulticast() 134 | if _Debug: print "sending multicast packets" 135 | locprot.blatMCast() 136 | locprot.compDef.addCallback(_cacheLocalIP) 137 | return locprot.compDef 138 | 139 | def cb_detectNAT(res): 140 | (ufired,upnp), (sfired,stun) = res 141 | if not ufired and not sfired: 142 | log.msg("no STUN or UPnP results", system="nat") 143 | return None 144 | if ufired: 145 | return upnp 146 | return stun 147 | 148 | def detectNAT(): 149 | # We prefer UPnP when available, as it's less pissing about (ha!) 150 | from xshtoom.stun import getSTUN 151 | ud = defer.succeed([None, None]) 152 | sd = getSTUN() 153 | dl = defer.DeferredList([ud, sd]) 154 | dl.addCallback(cb_detectNAT).addErrback(log.err) 155 | return dl 156 | 157 | def cb_getMapper(res): 158 | from xshtoom.stun import getMapper as getSTUNMapper 159 | (ufired,upnp), (sfired,stun) = res 160 | log.msg("detectNAT got %r"%res, system="nat") 161 | if not upnp and not stun: 162 | log.msg("no STUN or UPnP results", system="nat") 163 | return getNullMapper() 164 | if stun.useful: 165 | log.msg("using STUN mapper", system="nat") 166 | return getSTUNMapper() 167 | log.msg("No UPnP, and STUN is useless", system="nat") 168 | return getNullMapper() 169 | 170 | _forcedMapper = None 171 | 172 | _installedShutdownHook = False 173 | def getMapper(): 174 | # We prefer UPnP when available, as it's more robust 175 | global _installedShutdownHook 176 | if not _installedShutdownHook: 177 | from twisted.internet import reactor 178 | t = reactor.addSystemEventTrigger('after', 179 | 'shutdown', 180 | clearCache) 181 | _installedShutdownHook = True 182 | try: 183 | from __main__ import app 184 | except: 185 | app = None 186 | natPref = 'both' 187 | if app is not None: 188 | print "app is", app 189 | natPref = app.getPref('nat') 190 | log.msg('NAT preference says to use %s'%(natPref)) 191 | if _forcedMapper is not None: 192 | return defer.succeed(_forcedMapper) 193 | from xshtoom.stun import getSTUN 194 | if natPref == 'stun': 195 | ud = getSTUN() 196 | d = defer.DeferredList([defer.succeed(None), ud]) 197 | else: 198 | nm = NullMapper() 199 | d = defer.DeferredList([defer.succeed(None), 200 | defer.succeed(None)]) 201 | d.addCallback(cb_getMapper).addErrback(log.err) 202 | return d 203 | getMapper = DeferredCache(getMapper, inProgressOnly=False) 204 | 205 | def _forceMapper(mapper): 206 | global _forcedMapper 207 | _forcedMapper = mapper 208 | 209 | def isBogusAddress(addr): 210 | """ Returns true if the given address is bogus, i.e. 0.0.0.0 or 211 | 127.0.0.1. Additional forms of bogus might be added later. 212 | """ 213 | if addr.startswith('0.') or addr.startswith('127.'): 214 | return True 215 | return False 216 | 217 | class BaseMapper: 218 | "Base class with useful functionality for Mappers" 219 | _ptypes = [] 220 | 221 | def _checkValidPort(self, port): 222 | from twisted.internet.base import BasePort 223 | # Ugh. Why is there no IPort ? 224 | if not isinstance(port, BasePort): 225 | raise ValueError("expected a Port, got %r"%(port)) 226 | # XXX Check it's listening! How??? 227 | if not hasattr(port, 'socket'): 228 | raise ValueError("Port %r appears to be closed"%(port)) 229 | 230 | locAddr = port.getHost() 231 | if locAddr.type not in self._ptypes: 232 | raise ValueError("can only map %s, not %s"% 233 | (', '.join(self._ptypes),locAddr.type)) 234 | if locAddr.port == 0: 235 | raise ValueError("Port %r has port number of 0"%(port)) 236 | 237 | if not port.connected: 238 | raise ValueError("Port %r is not listening"%(port)) 239 | 240 | class NullMapper(BaseMapper): 241 | "Mapper that does nothing" 242 | 243 | _ptypes = ( 'TCP', 'UDP' ) 244 | 245 | def __init__(self): 246 | self._mapped = {} 247 | 248 | def map(self, port): 249 | "See shtoom.interfaces.NATMapper.map" 250 | self._checkValidPort(port) 251 | if port in self._mapped: 252 | return defer.succeed(self._mapped[port]) 253 | cd = defer.Deferred() 254 | self._mapped[port] = cd 255 | locAddr = port.getHost().host 256 | if isBogusAddress(locAddr): 257 | # lookup local IP. 258 | d = getLocalIPAddress() 259 | d.addCallback(lambda x: self._cb_map_gotLocalIP(x, port)) 260 | else: 261 | reactor.callLater(0, lambda: self._cb_map_gotLocalIP(locAddr, port)) 262 | return cd 263 | map = DeferredCache(map, inProgressOnly=True) 264 | 265 | def _cb_map_gotLocalIP(self, locIP, port): 266 | cd = self._mapped[port] 267 | self._mapped[port] = (locIP, port.getHost().port) 268 | cd.callback(self._mapped[port]) 269 | 270 | def info(self, port): 271 | "See shtoom.interfaces.NATMapper.info" 272 | if port in self._mapped: 273 | return self._mapped[port] 274 | else: 275 | raise ValueError('Port %r is not currently mapped'%(port)) 276 | 277 | def unmap(self, port): 278 | "See shtoom.interfaces.NATMapper.unmap" 279 | # A no-op for NullMapper 280 | if port not in self._mapped: 281 | raise ValueError('Port %r is not currently mapped'%(port)) 282 | del self._mapped[port] 283 | return defer.succeed(None) 284 | 285 | _cached_nullmapper = None 286 | def getNullMapper(): 287 | global _cached_nullmapper 288 | if _cached_nullmapper is None: 289 | _cached_nullmapper = NullMapper() 290 | return _cached_nullmapper 291 | 292 | class NetAddress: 293 | """ A class that represents a net address of the form 294 | foo/nbits, e.g. 10/8, or 192.168/16, or whatever 295 | """ 296 | def __init__(self, netaddress): 297 | parts = netaddress.split('/') 298 | if len(parts) > 2: 299 | raise ValueError, "should be of form address/mask" 300 | if len(parts) == 1: 301 | ip, mask = parts[0], 32 302 | else: 303 | ip, mask = parts[0], int(parts[1]) 304 | if mask < 0 or mask > 32: 305 | raise ValueError, "mask should be between 0 and 32" 306 | 307 | self.net = self.inet_aton(ip) 308 | self.mask = ( 2L**32 -1 ) ^ ( 2L**(32-mask) - 1 ) 309 | self.start = self.net 310 | self.end = self.start | (2L**(32-mask) - 1) 311 | 312 | def inet_aton(self, ipstr): 313 | "A sane inet_aton" 314 | if ':' in ipstr: 315 | return 316 | net = [ int(x) for x in ipstr.split('.') ] + [ 0,0,0 ] 317 | net = net[:4] 318 | return ((((((0L+net[0])<<8) + net[1])<<8) + net[2])<<8) +net[3] 319 | 320 | def inet_ntoa(self, ip): 321 | import socket, struct 322 | return socket.inet_ntoa(struct.pack('!I',ip)) 323 | 324 | def __repr__(self): 325 | return ''%(self.inet_ntoa(self.net), 326 | self.inet_ntoa(self.mask), 327 | self.inet_ntoa(self.start), 328 | self.inet_ntoa(self.end), 329 | id(self)) 330 | 331 | def check(self, ip): 332 | "Check if an IP or network is contained in this network address" 333 | if isinstance(ip, NetAddress): 334 | return self.check(ip.start) and self.check(ip.end) 335 | if isinstance(ip, basestring): 336 | ip = self.inet_aton(ip) 337 | if ip is None: 338 | return False 339 | if ip & self.mask == self.net: 340 | return True 341 | else: 342 | return False 343 | 344 | __contains__ = check 345 | 346 | 347 | class AlwaysNAT: 348 | 349 | def checkNAT(self, localip, remoteip): 350 | return True 351 | 352 | class NeverNAT: 353 | 354 | def checkNAT(self, localip, remoteip): 355 | return False 356 | 357 | class RFC1918NAT: 358 | "A sane default policy" 359 | 360 | addresses = ( NetAddress('10/8'), 361 | NetAddress('172.16/12'), 362 | NetAddress('192.168/16'), 363 | NetAddress('127/8') ) 364 | localhost = NetAddress('127/8') 365 | 366 | def checkNAT(self, localip, remoteip): 367 | localIsRFC1918 = False 368 | remoteIsRFC1918 = False 369 | remoteIsLocalhost = False 370 | # Yay. getPeer() returns a name, not an IP 371 | # XXX tofix: grab radix's goodns.py until it 372 | # lands in twisted proper. 373 | # Until then, use this getaddrinfo() hack. 374 | if not remoteip: 375 | return None 376 | if remoteip[0] not in '0123456789': 377 | import socket 378 | try: 379 | ai = socket.getaddrinfo(remoteip, None) 380 | except (socket.error, socket.gaierror): 381 | return None 382 | remoteips = [x[4][0] for x in ai] 383 | else: 384 | remoteips = [remoteip,] 385 | for net in self.addresses: 386 | if localip in net: 387 | localIsRFC1918 = True 388 | # See comments above. Worse, if the host has an address that's 389 | # RFC1918, and externally advertised (which is wrong, and broken), 390 | # the STUN check will be incorrect. Bah. 391 | for remoteip in remoteips: 392 | if remoteip in net: 393 | remoteIsRFC1918 = True 394 | if remoteip in self.localhost: 395 | remoteIsLocalhost = True 396 | if localIsRFC1918 and not (remoteIsRFC1918 or remoteIsLocalhost): 397 | return True 398 | else: 399 | return False 400 | 401 | _defaultPolicy = RFC1918NAT() 402 | def installPolicy(policy): 403 | global _defaultPolicy 404 | _defaultPolicy = policy 405 | 406 | def getPolicy(): 407 | return _defaultPolicy 408 | 409 | 410 | 411 | if __name__ == "__main__": 412 | from twisted.internet import gtk2reactor 413 | gtk2reactor.install() 414 | from twisted.internet import reactor 415 | import sys 416 | 417 | log.FileLogObserver.timeFormat = "%H:%M:%S" 418 | log.startLogging(sys.stdout) 419 | 420 | def cb_gotip(addr): 421 | print "got local IP address of", addr 422 | def cb_gotnat(res): 423 | print "got NAT of", res 424 | d1 = getLocalIPAddress().addCallback(cb_gotip) 425 | d2 = detectNAT().addCallback(cb_gotnat) 426 | dl = defer.DeferredList([d1,d2]) 427 | dl.addCallback(lambda x:reactor.stop()) 428 | reactor.run() 429 | -------------------------------------------------------------------------------- /rtpmidi/protocols/rtp/packets.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2004 Anthony Baxter 2 | 3 | import struct 4 | from time import time 5 | 6 | # This class supports extension headers, but only one per packet. 7 | class RTPPacket: 8 | """ Contains RTP data. """ 9 | class Header: 10 | def __init__(self, ssrc, pt, ct, seq, ts, marker=0, 11 | xhdrtype=None, xhdrdata=''): 12 | """ 13 | If xhdrtype is not None then it is required to be an 14 | int >= 0 and < 2**16 and xhdrdata is required to be 15 | a string whose length is a multiple of 4. 16 | """ 17 | assert isinstance(ts, (int, long,)), "ts: %s :: %s" % (ts, type(ts)) 18 | assert isinstance(ssrc, (int, long,)) 19 | assert xhdrtype is None or isinstance(xhdrtype, int) \ 20 | and xhdrtype >= 0 and xhdrtype < 2**16 21 | # Sorry, RFC standard specifies that len is in 4-byte words, 22 | # and I'm not going to do the padding and unpadding for you. 23 | assert xhdrtype is None or (isinstance(xhdrdata, str) and \ 24 | len(xhdrdata) % 4 == 0), \ 25 | "xhdrtype: %s, len(xhdrdata): %s, xhdrdata: %s" % ( 26 | xhdrtype, len(xhdrdata), `xhdrdata`,) 27 | 28 | (self.ssrc, self.pt, self.ct, self.seq, self.ts, 29 | self.marker, self.xhdrtype, self.xhdrdata) = ( 30 | ssrc, pt, ct, seq, ts, 31 | marker, xhdrtype, xhdrdata) 32 | 33 | def netbytes(self): 34 | "Return network-formatted header." 35 | assert isinstance(self.pt, int) and self.pt >= 0 and \ 36 | self.pt < 2**8, \ 37 | "pt is required to be a simple byte, suitable " + \ 38 | "for stuffing into an RTP packet and sending. pt: %s" % self.pt 39 | if self.xhdrtype is not None: 40 | firstbyte = 0x90 41 | xhdrnetbytes = struct.pack('!HH', self.xhdrtype, 42 | len(self.xhdrdata)/4) + self.xhdrdata 43 | else: 44 | firstbyte = 0x80 45 | xhdrnetbytes = '' 46 | return struct.pack('!BBHII', firstbyte, 47 | self.pt | self.marker << 7, 48 | self.seq % 2**16, 49 | self.ts, self.ssrc) + xhdrnetbytes 50 | 51 | def __init__(self, ssrc, seq, ts, data, pt=None, ct=None, marker=0, 52 | authtag='', xhdrtype=None, xhdrdata=''): 53 | assert pt is None or isinstance(pt, int) and pt >= 0 and pt < 2**8, \ 54 | "pt is required to be a simple byte, suitable for stuffing " + \ 55 | "into an RTP packet and sending. pt: %s" % pt 56 | self.header = RTPPacket.Header(ssrc, pt, ct, seq, ts, marker, 57 | xhdrtype, xhdrdata) 58 | self.data = data 59 | # please leave this alone even if it appears unused -- 60 | # it is required for SRTP 61 | self.authtag = authtag 62 | 63 | def __repr__(self): 64 | if self.header.ct is not None: 65 | ptrepr = "%r" % (self.header.ct,) 66 | else: 67 | ptrepr = "pt %s" % (self.header.pt,) 68 | 69 | if self.header.xhdrtype is not None: 70 | return "<%s #%d (%s) %s [%s] at %x>"%(self.__class__.__name__, 71 | self.header.seq, 72 | self.header.xhdrtype, 73 | ptrepr, 74 | repr(self.header.xhdrdata), 75 | id(self)) 76 | else: 77 | return "<%s #%d %s at %x>"%(self.__class__.__name__, 78 | self.header.seq, ptrepr, id(self)) 79 | 80 | def netbytes(self): 81 | "Return network-formatted packet." 82 | return self.header.netbytes() + self.data + self.authtag 83 | 84 | 85 | 86 | 87 | def parse_rtppacket(bytes, authtaglen=0): 88 | # Most variables are named for the fields in the RTP RFC. 89 | hdrpieces = struct.unpack('!BBHII', bytes[:12]) 90 | 91 | # Padding 92 | p = (hdrpieces[0] & 32) and 1 or 0 93 | # Extension header present 94 | x = (hdrpieces[0] & 16) and 1 or 0 95 | # CSRC Count 96 | cc = hdrpieces[0] & 15 97 | # Marker bit 98 | marker = hdrpieces[1] & 128 99 | # Payload type 100 | pt = hdrpieces[1] & 127 101 | # Sequence number 102 | seq = hdrpieces[2] 103 | # Timestamp 104 | ts = hdrpieces[3] 105 | ssrc = hdrpieces[4] 106 | headerlen = 12 + cc * 4 107 | # XXX throwing away csrc info for now 108 | bytes = bytes[headerlen:] 109 | if x: 110 | # Only one extension header 111 | (xhdrtype, xhdrlen,) = struct.unpack('!HH', bytes[:4]) 112 | xhdrdata = bytes[4:4+xhdrlen*4] 113 | bytes = bytes[xhdrlen*4 + 4:] 114 | else: 115 | xhdrtype, xhdrdata = None, None 116 | if authtaglen: 117 | authtag = bytes[-authtaglen:] 118 | bytes = bytes[:-authtaglen] 119 | else: 120 | authtag = '' 121 | if p: 122 | # padding 123 | padlen = struct.unpack('!B', bytes[-1])[0] 124 | if padlen: 125 | bytes = bytes[:-padlen] 126 | return RTPPacket(ssrc, seq, ts, bytes, marker=marker, pt=pt, 127 | authtag=authtag, xhdrtype=xhdrtype, xhdrdata=xhdrdata) 128 | 129 | 130 | class NTE: 131 | "An object representing an RTP NTE (rfc2833)" 132 | # XXX at some point, this should be hooked into the RTPPacketFactory. 133 | def __init__(self, key, startTS): 134 | self.startTS = startTS 135 | self.ending = False 136 | self.counter = 3 137 | self.key = key 138 | if key >= '0' and key <= '9': 139 | self._payKey = chr(int(key)) 140 | elif key == '*': 141 | self._payKey = chr(10) 142 | elif key == '#': 143 | self._payKey = chr(11) 144 | elif key >= 'A' and key <= 'D': 145 | # A - D are 12-15 146 | self._payKey = chr(ord(key)-53) 147 | elif key == 'flash': 148 | self._payKey = chr(16) 149 | else: 150 | raise ValueError, "%s is not a valid NTE"%(key) 151 | 152 | def getKey(self): 153 | return self.key 154 | 155 | def end(self): 156 | self.ending = True 157 | self.counter = 1 158 | 159 | def getPayload(self, ts): 160 | if self.counter > 0: 161 | if self.ending: 162 | end = 128 163 | else: 164 | end = 0 165 | payload = self._payKey + chr(10|end) + \ 166 | struct.pack('!H', ts - self.startTS) 167 | self.counter -= 1 168 | return payload 169 | else: 170 | return None 171 | 172 | def isDone(self): 173 | if self.ending and self.counter < 1: 174 | return True 175 | else: 176 | return False 177 | 178 | def __repr__(self): 179 | return ''%(self.key, self.ending and ' (ending)' or '') 180 | 181 | 182 | ####################################RTCP Packets############################ 183 | #Constants 184 | RTCP_PT_SR = 200 185 | RTCP_PT_RR = 201 186 | RTCP_PT_SDES = 202 187 | RTCP_PT_BYE = 203 188 | RTCP_PT_APP = 204 189 | rtcpPTdict = {RTCP_PT_SR: 'SR', RTCP_PT_RR: 'RR', RTCP_PT_SDES:'SDES', \ 190 | RTCP_PT_BYE:'BYE'} 191 | 192 | for k,v in rtcpPTdict.items(): 193 | rtcpPTdict[v] = k 194 | 195 | RTCP_SDES_CNAME = 1 196 | RTCP_SDES_NAME = 2 197 | RTCP_SDES_EMAIL = 3 198 | RTCP_SDES_PHONE = 4 199 | RTCP_SDES_LOC = 5 200 | RTCP_SDES_TOOL = 6 201 | RTCP_SDES_NOTE = 7 202 | RTCP_SDES_PRIV = 8 203 | 204 | RTP_VERSION = 2 205 | 206 | rtcpSDESdict = {RTCP_SDES_CNAME: 'CNAME', 207 | RTCP_SDES_NAME: 'NAME', 208 | RTCP_SDES_EMAIL: 'EMAIL', 209 | RTCP_SDES_PHONE: 'PHONE', 210 | RTCP_SDES_LOC: 'LOC', 211 | RTCP_SDES_TOOL: 'TOOL', 212 | RTCP_SDES_NOTE: 'NOTE', 213 | RTCP_SDES_PRIV: 'PRIV', 214 | } 215 | 216 | for k,v in rtcpSDESdict.items(): 217 | rtcpSDESdict[v] = k 218 | 219 | def hexrepr(bytes): 220 | out = '' 221 | bytes = bytes + '\0'* ( 8 - len(bytes)%8 ) 222 | for i in range(0,len(bytes), 8): 223 | out = out + " %02x%02x%02x%02x %02x%02x%02x%02x\n" \ 224 | % tuple([ord(bytes[i+x]) for x in range(8)]) 225 | return out 226 | 227 | def ext_32_out_of_64(value): 228 | """16 bits for int part and 16 bit for fractionnal part 229 | These two values are format in I (unsigned int)""" 230 | value_int = int(value) 231 | value_frac = value - value_int 232 | value_int = value_int & 65535 233 | 234 | if value_frac > 0: 235 | value_frac = value_frac * (10 ** 4) 236 | #Why all that needed... 237 | value_frac = int(float(str(value_frac))) 238 | 239 | else: 240 | value_frac = 0 241 | 242 | value_int = value_int << 16 243 | res = value_int | value_frac 244 | return res 245 | 246 | def unformat_from_32(value): 247 | value_int = value >> 16 248 | value_frac = value & 65535 249 | value = value_int + ( value_frac * (10 ** -4)) 250 | return value 251 | 252 | class RTCPPacket: 253 | def __init__(self, pt, contents=None, ptcode=None): 254 | self._pt = pt 255 | if ptcode is None: 256 | self._ptcode = rtcpPTdict.get(pt) 257 | else: 258 | self._ptcode = ptcode 259 | self._body = '' 260 | if contents: 261 | self._contents = contents 262 | else: 263 | self._contents = None 264 | 265 | def getPT(self): 266 | return self._pt 267 | 268 | def getContents(self): 269 | return self._contents 270 | 271 | def decode(self, count, body): 272 | self._count = count 273 | self._body = body 274 | getattr(self, 'decode_%s'%self._pt)() 275 | 276 | def encode(self): 277 | out = getattr(self, 'encode_%s'%self._pt)() 278 | return out 279 | 280 | def _padIfNeeded(self, packet): 281 | if len(packet)%4: 282 | pad = '\0' * (4-(len(packet)%4)) 283 | packet += pad 284 | 285 | return packet 286 | 287 | def _patchLengthHeader(self, packet): 288 | length = (len(packet)/4) - 1 289 | packet = packet[:2] + struct.pack('!H', length) + packet[4:] 290 | return packet 291 | 292 | def decode_SDES(self): 293 | for i in range(self._count): 294 | self._contents = [] 295 | ssrc, = struct.unpack('!I', self._body[:4]) 296 | self._contents.append((ssrc,[])) 297 | self._body = self._body[4:] 298 | 299 | off = 0 300 | while True: 301 | type, length = ord(self._body[0]), ord(self._body[1]) 302 | 303 | #Cumul length to check padding 304 | off += length+2 305 | 306 | maybepadlen = 4-((off)%4) 307 | 308 | body, maybepad = self._body[2:length+2], \ 309 | self._body[length+2:length+2+maybepadlen] 310 | 311 | #Flushing body 312 | self._body = self._body[length+2:] 313 | self._contents[-1][1].append((rtcpSDESdict[type], body)) 314 | 315 | if ord(maybepad[0]) == 0: 316 | # end of list. eat the padding. 317 | self._body = self._body[maybepadlen:] 318 | break 319 | 320 | 321 | def encode_SDES(self): 322 | """ 323 | 6.5 SDES: Source Description RTCP Packet 324 | 325 | 0 1 2 3 326 | 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 327 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 328 | |V=2|P| SC | PT=SDES=202 | length | 329 | +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+ 330 | | SSRC/CSRC_1 | 331 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 332 | | SDES items | 333 | | ... | 334 | +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+ 335 | """ 336 | blocks = self._contents 337 | packet = struct.pack('!BBH',len(blocks)|128, self._ptcode, 0) 338 | 339 | for ssrc,items in blocks: 340 | packet += struct.pack('!I', ssrc) 341 | 342 | for sdes, value in items: 343 | sdescode = rtcpSDESdict[sdes] 344 | packet += struct.pack('!BB', sdescode, len(value)) + value 345 | if len(packet)%4: 346 | packet = self._padIfNeeded(packet) 347 | else: 348 | packet += '\0\0\0\0' 349 | 350 | packet = self._patchLengthHeader(packet) 351 | return packet 352 | 353 | def decode_BYE(self): 354 | self._contents = [[],''] 355 | for i in range(self._count): 356 | ssrc, = struct.unpack('!I', self._body[:4]) 357 | self._contents[0].append(ssrc) 358 | self._body = self._body[4:] 359 | if self._body: 360 | # A reason! 361 | length = ord(self._body[0]) 362 | reason = self._body[1:length+1] 363 | self._contents[1] = reason 364 | self._body = '' 365 | 366 | def encode_BYE(self): 367 | """ 368 | 6.6 BYE: Goodbye RTCP Packet 369 | 370 | 0 1 2 3 371 | 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 372 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 373 | |V=2|P| SC | PT=BYE=203 | length | 374 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 375 | | SSRC/CSRC | 376 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 377 | : ... : 378 | +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+ 379 | | length | reason for leaving ... 380 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 381 | """ 382 | #the BYE packet MUST be padded with null octets to the next 32- 383 | #bit boundary. 384 | ssrcs, reason = self._contents 385 | packet = struct.pack('!BBH',len(ssrcs)|128, self._ptcode, 0) 386 | for ssrc in ssrcs: 387 | packet = packet + struct.pack('!I', ssrc) 388 | if reason: 389 | packet = packet + chr(len(reason)) + reason 390 | packet = self._padIfNeeded(packet) 391 | packet = self._patchLengthHeader(packet) 392 | return packet 393 | 394 | def decode_SR(self): 395 | self._contents = [] 396 | ssrc, = struct.unpack('!I', self._body[:4]) 397 | bits = struct.unpack('!IIIII', self._body[4:24]) 398 | names = 'ntpHi', 'ntpLo', 'rtpTS', 'packets', 'octets' 399 | sender = dict(zip(names, bits)) 400 | 401 | #ntpTS care 402 | sender['ntpTS'] = sender['ntpHi'] + sender['ntpLo'] * ( 10 ** -9) 403 | del sender['ntpHi'], sender['ntpLo'] 404 | 405 | self._body = self._body[24:] 406 | blocks = self._decodeRRSRReportBlocks() 407 | self._contents = [ssrc,sender,blocks] 408 | 409 | def encode_SR(self): 410 | """ 411 | Sender report 412 | 6.4.1 rfc 3550 413 | NTP Timestamps represents the ntp time in timestamp format 414 | 0 1 2 3 415 | 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 416 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 417 | |V=2|P| RC | PT=SR=200 | length | 418 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 419 | | SSRC of sender | 420 | +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+ 421 | | NTP timestamp, most significant word | 422 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 423 | | NTP timestamp, least significant word | 424 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 425 | | RTP timestamp | 426 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 427 | | sender's packet count | 428 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 429 | | sender's octet count | 430 | +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+ 431 | + blocks 432 | """ 433 | 434 | #header 435 | version = RTP_VERSION * (2**6) 436 | #Padding 0 or 1 437 | padding = 0 438 | #Count 0->31 Number of reception block contain in the packet 439 | 440 | #Packet type 441 | type = self._ptcode 442 | #Length 443 | length = 0 444 | 445 | #Building packet 446 | #Contents wait (order is important for what's above) 447 | #(ssrc, ts, total_packets, total_bytes, members) 448 | (ssrc, ts, total_packets, total_bytes, members) = self._contents 449 | 450 | #Take care about ntp 451 | ntp = time() 452 | ntp_m = int(ntp) 453 | ntp_l = ntp - ntp_m 454 | ntp_l = int(ntp_l * ( 10 ** 9)) 455 | 456 | #SR parts 457 | packet = struct.pack('!IIIIII', ssrc, ntp_m, ntp_l, ts, total_packets, total_bytes) 458 | 459 | #Processing blocks 460 | block_packets, count = self._encodeRRSRReportBlocks(ssrc, members) 461 | starter = version | padding | count 462 | packet += block_packets 463 | 464 | #+ 4 is for header (see page 36 of RFC 3550) 465 | #length = len(packet) + 4 466 | header = struct.pack('!BBH', starter, type, 0) 467 | 468 | packet = header + packet 469 | 470 | #Checking padding 471 | packet = self._padIfNeeded(packet) 472 | packet = self._patchLengthHeader(packet) 473 | 474 | return packet 475 | 476 | def decode_RR(self): 477 | ssrc, = struct.unpack('!I', self._body[:4]) 478 | self._body = self._body[4:] 479 | blocks = self._decodeRRSRReportBlocks() 480 | self._contents = [ssrc,blocks] 481 | 482 | def encode_RR(self): 483 | """ 484 | rfc 3550 485 | 6.4.2 RR: Receiver Report RTCP Packet 486 | 487 | 0 1 2 3 488 | 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 489 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 490 | |V=2|P| RC | PT=RR=201 | length | 491 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 492 | | SSRC of packet sender | 493 | +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+ 494 | + blocks 495 | """ 496 | 497 | #header 498 | #Version 1 ou 2 499 | version = RTP_VERSION * (2**6) 500 | #Padding 0 ou 1 501 | padding = 0 * (2**5) 502 | 503 | #Packet type 504 | type = self._ptcode 505 | #Length 506 | length = 0 507 | 508 | (ssrc, members) \ 509 | = self._contents 510 | 511 | #ount = len(members) 512 | packet = struct.pack('!I', ssrc) 513 | block_packets, count = self._encodeRRSRReportBlocks(ssrc, members) 514 | starter = version | padding | count 515 | packet += block_packets 516 | 517 | #+ 4 is for header (see page 36 of RFC 3550) 518 | length = len(packet) + 4 519 | header = struct.pack('!BBH', starter, type, length) 520 | packet = header + packet 521 | 522 | #Checking padding 523 | packet = self._padIfNeeded(packet) 524 | packet = self._patchLengthHeader(packet) 525 | 526 | return packet 527 | 528 | def _encodeRRSRReportBlocks(self, my_ssrc, members): 529 | """ 530 | +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+ 531 | | SSRC_1 (SSRC of first source) | 532 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 533 | | fraction lost | cumulative number of packets lost | 534 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 535 | | extended highest sequence number received | 536 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 537 | | interarrival jitter | 538 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 539 | | last SR (LSR) | 540 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 541 | | delay since last SR (DLSR) | 542 | +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+ 543 | """ 544 | packet = "" 545 | count = 0 546 | ref_time = time() 547 | for ssrc in members: 548 | if ssrc != my_ssrc: 549 | #ssrc_1, frac_lost, lost, highest, jitter, lsr, dlsr 550 | lost = members[ssrc]['lost'] 551 | if members[ssrc]['total_received_packets'] > 0: 552 | frac_lost = int(lost / float(members[ssrc]['total_received_packets'] + lost)) 553 | else: 554 | frac_lost = 0 555 | 556 | #Take care about fraction lost 557 | frac_lost = frac_lost << (3*8) 558 | lost_part = frac_lost | lost 559 | 560 | #Take care about lsr and dlsr 561 | lsr = members[ssrc]['lsr'] 562 | lsr = ext_32_out_of_64(lsr) 563 | 564 | if lsr != 0: 565 | dlsr = ref_time - lsr 566 | else: 567 | dlsr = 0 568 | 569 | dlsr = ext_32_out_of_64(dlsr) 570 | 571 | highest = members[ssrc]['last_seq'] 572 | jitter = members[ssrc]['jitter'] 573 | 574 | arg_list = (ssrc, lost_part, highest, jitter, lsr, dlsr) 575 | 576 | packet += struct.pack('!IIIIII', *arg_list) 577 | count += 1 578 | 579 | #Max 31 members in a report 580 | if count == 31: 581 | break 582 | 583 | return packet, count 584 | 585 | def _decodeRRSRReportBlocks(self): 586 | blocks = [] 587 | for i in range(self._count): 588 | bits = struct.unpack('!IIIIII', self._body[:24]) 589 | names = 'ssrc', 'lost', 'highest', 'jitter', 'lsr', 'dlsr' 590 | c = dict(zip(names,bits)) 591 | 592 | #Take care about lost part 593 | c['fraclost'] = c['lost'] >> 24 594 | c['packlost'] = (c['lost'] & 0x00FFFFFF) 595 | del c['lost'] 596 | 597 | c['lsr'] = unformat_from_32(c['lsr']) 598 | c['dlsr'] = unformat_from_32(c['dlsr']) 599 | 600 | blocks.append(c) 601 | self._body = self._body[24:] 602 | return blocks 603 | 604 | def decode_APP(self): 605 | self._contents = [] 606 | subtype = self._count 607 | ssrc, = struct.unpack('!I', self._body[:4]) 608 | name = self._body[4:8] 609 | value = self._body[8:] 610 | self._contents = [subtype,ssrc,name,value] 611 | 612 | def encode_APP(self): 613 | subtype,ssrc,name,value = self._contents 614 | packet = struct.pack('!BBHI',subtype|128, self._ptcode, 0, ssrc) 615 | packet = packet + name + value 616 | packet = self._padIfNeeded(packet) 617 | packet = self._patchLengthHeader(packet) 618 | return packet 619 | 620 | # We can at least roundtrip unknown RTCP PTs 621 | def decode_UNKNOWN(self): 622 | self._contents = (self._count, self._body) 623 | self._body = '' 624 | 625 | def encode_UKNOWN(self): 626 | count, body = self._contents 627 | packet = struct.pack('!BBH',count|128, self._ptcode, 0) 628 | packet = packet + body 629 | packet = self._padIfNeeded(packet) 630 | packet = self._patchLengthHeader(packet) 631 | return packet 632 | 633 | def __repr__(self): 634 | if self._body: 635 | leftover = ' '+repr(self._body) 636 | else: 637 | leftover = '' 638 | return ''%(self._pt, self._contents, leftover) 639 | 640 | 641 | class RTCPCompound: 642 | "A single RTCP packet can contain multiple RTCP items" 643 | def __init__(self, bytes=None): 644 | self._rtcp = [] 645 | if bytes: 646 | self.decode(bytes) 647 | 648 | def addPacket(self, packet): 649 | self._rtcp.append(packet) 650 | 651 | def decode(self, bytes): 652 | while bytes: 653 | #Getting infos to decode 654 | count = ord(bytes[0]) & 31 655 | pt = ord(bytes[1]) 656 | PT = rtcpPTdict.get(pt, "UNKNOWN") 657 | try: 658 | length, = struct.unpack('!H', bytes[2:4]) 659 | except struct.error: 660 | print "struct.unpack got bad number of bytes" 661 | return 662 | offset = 4*(length+1) 663 | body, bytes = bytes[4:offset], bytes[offset:] 664 | p = RTCPPacket(PT, ptcode=pt) 665 | p.decode(count, body) 666 | self._rtcp.append(p) 667 | 668 | return p 669 | 670 | def encode(self): 671 | return ''.join([x.encode() for x in self._rtcp]) 672 | 673 | def __len__(self): 674 | return len(self._rtcp) 675 | 676 | def __getitem__(self, i): 677 | return self._rtcp[i] 678 | 679 | def __repr__(self): 680 | return "" \ 681 | % (', '.join([x.getPT() for x in self._rtcp])) 682 | 683 | -------------------------------------------------------------------------------- /rtpmidi/protocols/rtp/rtp_control.py: -------------------------------------------------------------------------------- 1 | #utils import 2 | import time 3 | 4 | #data import 5 | from rtpmidi.protocols.rtp.utils import Singleton 6 | 7 | #rtp import 8 | from rtpmidi.protocols.rtp.protocol import RTPProtocol 9 | 10 | #RTP Timeout is 60 11 | RTP_TIME_OUT = 40 12 | 13 | class RTPControl(Singleton): 14 | """Control rtp for a payload to implement 15 | """ 16 | currentRecordings = {} 17 | cookies = 0 18 | 19 | def add_session(self, session): 20 | #Test type 21 | if (not(hasattr(session, "incoming_rtp") 22 | and hasattr(session, "payload") 23 | and hasattr(session, "host") 24 | and hasattr(session, "rport") 25 | and hasattr(session, "drop_call") 26 | and hasattr(session, "start") 27 | and hasattr(session, "stop"))): 28 | raise Exception, "This session type is not supported" 29 | return 30 | 31 | #Cookie related to the rtp session 32 | cookie = self.make_cookie() 33 | session.cookie = cookie 34 | 35 | #Creating RTP 36 | self._create_rtp_socket(cookie, session) 37 | return cookie 38 | 39 | def del_session(self, cookie): 40 | rtp, o = self.currentRecordings[cookie] 41 | rtp.stopSendingAndReceiving() 42 | del self.currentRecordings[cookie] 43 | 44 | def start_session(self, cookie): 45 | rtp, session = self.currentRecordings[cookie] 46 | rtp.start(session.host) 47 | session.start() 48 | 49 | def start(self): 50 | for cookie in self.currentRecordings: 51 | self.start_session(cookie) 52 | 53 | def _create_rtp_socket(self, cookie, session): 54 | 55 | #Init RTPProtocol 56 | rtp = RTPProtocol(self, cookie, session.payload, 57 | session.jitter_buffer_size, verbose=session.verbose) 58 | 59 | #Creating socket 60 | rtp.createRTPSocket(session.host, session.rport, session.sport, False) 61 | 62 | #Register the session 63 | self.currentRecordings[cookie] = (rtp, session) 64 | 65 | #Registering host and port in used(if default port is in used by another 66 | #application it will choose another 67 | if rtp.rport > 0: 68 | session.host = (session.host, rtp.rport) 69 | 70 | else: 71 | session.host = (session.host, rtp.sport) 72 | 73 | def incoming_rtp(self, cookie, timestamp, packet, 74 | read_recovery_journal = 0): 75 | """Function called by RTPProtocol when incoming 76 | data coming out from jitter buffer 77 | """ 78 | rtp, session = self.currentRecordings[cookie] 79 | session.incoming_rtp(cookie, timestamp, packet, 80 | read_recovery_journal) 81 | 82 | 83 | def send_empty_packet(self, cookie, chunk): 84 | #Selecting RTP session 85 | rtp, session = self.currentRecordings[cookie] 86 | 87 | #sending silent packet with recovery journal 88 | rtp.handle_data(session.payload, 0 , chunk, 1) 89 | self.last_send = time.time() 90 | 91 | 92 | def send_data_packet(self, cookie, data, ts): 93 | #Selecting RTP session 94 | rtp, session = self.currentRecordings[cookie] 95 | rtp.handle_data(session.payload, ts , data, 0) 96 | self.last_send = time.time() 97 | 98 | 99 | def stop_session(self, cookie): 100 | #Selecting RTP 101 | rtp, session = self.currentRecordings[cookie] 102 | rtp.stopSendingAndReceiving() 103 | session.stop() 104 | 105 | def stop(self): 106 | #Selecting RTP 107 | for cookie in self.currentRecordings: 108 | self.stop_session(cookie) 109 | 110 | def selectDefaultFormat(self, what): 111 | """link to sdp??""" 112 | #TODO pass SDP parameter for the session here ???? 113 | pass 114 | 115 | def drop_connection(self, cookie): 116 | #Selecting RTP session 117 | rtp, session = self.currentRecordings[cookie] 118 | session.drop_connection() 119 | 120 | 121 | def get_session(self, cookie): 122 | rtp, session = self.currentRecordings[cookie] 123 | return session 124 | 125 | def make_cookie(self): 126 | self.cookies += 1 127 | return "cookie%s" % (self.cookies,) 128 | 129 | 130 | 131 | -------------------------------------------------------------------------------- /rtpmidi/protocols/rtp/rtp_session.py: -------------------------------------------------------------------------------- 1 | #utils import 2 | import re 3 | import subprocess 4 | 5 | #rtp import 6 | from rtpmidi.protocols.rtp.rtp_control import RTPControl 7 | 8 | class RTPSession(object): 9 | """RTP Session prototype 10 | """ 11 | def __init__(self, peerAddress, sport, rport, payload, 12 | jitter_buffer_size, tool_name="", fqdn="", user_name=""): 13 | #RTP utils 14 | self.sport = sport 15 | self.rport = rport 16 | 17 | self.payload = payload 18 | self.host = peerAddress 19 | self.jitter_buffer_size = jitter_buffer_size 20 | 21 | if tool_name == "": 22 | self.tool_name = "Unknown tool" 23 | else: 24 | self.tool_name = tool_name 25 | 26 | if fqdn == "": 27 | self.fqdn = get_fqdn() 28 | else: 29 | self.fqdn = fqdn 30 | 31 | if user_name == "": 32 | self.user_name = get_name() 33 | else: 34 | self.user_name = user_name 35 | 36 | self.cookie = None 37 | 38 | #checkpoint received 39 | #A dict of that link with member table 40 | self.checkpoint = 0 41 | 42 | #checkpoint sent 43 | self.last_checkpoint = 0 44 | self.seq = 0 45 | 46 | #stat 47 | self.last_send = 0 48 | 49 | def start(self): 50 | """If override by daughters must call: 51 | """ 52 | RTPControl().start_session(self.cookie) 53 | 54 | def stop(self): 55 | """If override by daughters must call: 56 | """ 57 | RTPControl().stop_session(self.cookie) 58 | 59 | def incoming_rtp(self, cookie, timestamp, packet, 60 | read_recovery_journal = 0): 61 | """Function called by RTPProtocol when incoming 62 | data coming out from jitter buffer 63 | """ 64 | raise NotImplementedError 65 | 66 | def send_empty_packet(self, chunk=0): 67 | RTPControl().send_empty_packet(self.cookie, chunk) 68 | 69 | 70 | def send_data(self, data, ts): 71 | #Selecting RTP session 72 | RTPControl().send_data_packet(self.cookie, data, ts) 73 | 74 | 75 | def drop_call(self, cookie=0): 76 | #Rename drop connection 77 | raise NotImplementedError 78 | 79 | 80 | #Utilities 81 | def get_first_cmd_answer(cmd): 82 | proc = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, close_fds=True) 83 | (child_stdin, child_stdout_and_stderr) = (proc.stdin, proc.stdout) 84 | p = re.compile('\\n') 85 | 86 | for line in child_stdout_and_stderr.readlines(): 87 | print(line) 88 | res = p.sub('', line) 89 | return str(res) 90 | 91 | def get_fqdn(): 92 | login = get_first_cmd_answer("whoami") 93 | hostname = get_first_cmd_answer("hostname") 94 | return login + "@" + hostname 95 | 96 | def get_name(): 97 | login = get_first_cmd_answer("whoami") 98 | return login 99 | -------------------------------------------------------------------------------- /rtpmidi/protocols/rtp/sdp.py: -------------------------------------------------------------------------------- 1 | # -*- test-case-name: shtoom.test.test_new_sdp -*-# 2 | # Copyright (C) 2004 Anthony Baxter 3 | # Copyright (C) 2004 Jamey Hicks 4 | # 5 | 6 | from rtpmidi.protocols.rtp.formats import RTPDict, PTMarker, PT_AVP 7 | from twisted.python.util import OrderedDict 8 | 9 | class BadAnnounceError(Exception): 10 | "Bad Announcement" 11 | 12 | def get(obj,typechar,optional=0): 13 | return obj._d.get(typechar) 14 | 15 | def getA(obj, subkey): 16 | return obj._a.get(subkey) 17 | 18 | def parse_generic(obj, k, text): 19 | obj._d.setdefault(k, []).append(text) 20 | 21 | def unparse_generic(obj, k): 22 | if obj._d.has_key(k): 23 | return obj._d[k] 24 | else: 25 | return [] 26 | 27 | def parse_singleton(obj, k, text): 28 | obj._d[k] = text 29 | 30 | def unparse_singleton(obj, k): 31 | if obj._d.has_key(k): 32 | return [obj._d[k]] 33 | else: 34 | return [] 35 | 36 | def parse_o(obj, o, value): 37 | if value: 38 | l = value.split() 39 | if len(l) != 6: 40 | raise BadAnnounceError("wrong # fields in o=`%s'"%value) 41 | ( obj._o_username, obj._o_sessid, obj._o_version, 42 | obj._o_nettype, obj._o_addrfamily, obj._o_ipaddr ) = tuple(l) 43 | 44 | def unparse_o(obj, o): 45 | return ['%s %s %s %s %s %s' % ( obj._o_username, obj._o_sessid, 46 | obj._o_version, obj._o_nettype, 47 | obj._o_addrfamily, obj._o_ipaddr )] 48 | 49 | def parse_a(obj, a, text): 50 | words = text.split(':', 1) 51 | if len(words) > 1: 52 | # I don't know what is happening here, but I got a traceback here 53 | # because 'words' was too long before the ,1 was added. The value was: 54 | # ['alt', '1 1 ', ' 55A94DDE 98A2400C *ip address elided* 6086'] 55 | # Adding the ,1 seems to fix it but I don't know why. -glyph 56 | attr, attrvalue = words 57 | else: 58 | attr, attrvalue = text, None 59 | if attr == 'rtpmap': 60 | payload,info = attrvalue.split(' ') 61 | entry = rtpmap2canonical(int(payload), attrvalue) 62 | try: 63 | fmt = RTPDict[entry] 64 | except KeyError: 65 | name,clock,params = entry 66 | fmt = PTMarker(name, None, clock, params) 67 | obj.rtpmap[int(payload)] = (attrvalue, fmt) 68 | obj._a.setdefault(attr, OrderedDict())[int(payload)] = attrvalue 69 | else: 70 | obj._a.setdefault(attr, []).append(attrvalue) 71 | 72 | def unparse_a(obj, k): 73 | out = [] 74 | for (a,vs) in obj._a.items(): 75 | if isinstance(vs, OrderedDict): 76 | vs = vs.values() 77 | for v in vs: 78 | if v: 79 | out.append('%s:%s' % (a, v)) 80 | else: 81 | out.append(a) 82 | return out 83 | 84 | def parse_c(obj, c, text): 85 | words = text.split(' ') 86 | (obj.nettype, obj.addrfamily, obj.ipaddr) = words 87 | 88 | def unparse_c(obj, c): 89 | mds = getattr(obj, "mediaDescriptions", None) 90 | if mds and not [x for x in mds if not x.ipaddr]: #if every MediaDescription has an IP... 91 | return [] 92 | if obj.ipaddr: 93 | return ['%s %s %s' % (obj.nettype, obj.addrfamily, obj.ipaddr)] 94 | else: 95 | return [] 96 | def parse_m(obj, m, value): 97 | if value: 98 | els = value.split() 99 | (obj.media, port, obj.transport) = els[:3] 100 | obj.setFormats(els[3:]) 101 | obj.port = int(port) 102 | 103 | def unparse_m(obj, m): 104 | return ['%s %s %s %s' % (obj.media, str(obj.port), obj.transport, 105 | ' '.join(obj.formats))] 106 | 107 | parsers = [ 108 | ('v', 1, parse_singleton, unparse_singleton), 109 | ('o', 1, parse_o, unparse_o), 110 | ('s', 1, parse_singleton, unparse_singleton), 111 | ('i', 0, parse_generic, unparse_generic), 112 | ('u', 0, parse_generic, unparse_generic), 113 | ('e', 0, parse_generic, unparse_generic), 114 | ('p', 0, parse_generic, unparse_generic), 115 | ('c', 0, parse_c, unparse_c), 116 | ('b', 0, parse_generic, unparse_generic), 117 | ('t', 0, parse_singleton, unparse_singleton), 118 | ('r', 0, parse_generic, unparse_generic), 119 | ('k', 0, parse_generic, unparse_generic), 120 | ('a', 0, parse_a, unparse_a) 121 | ] 122 | 123 | mdparsers = [ 124 | ('m', 0, parse_m, unparse_m), 125 | ('i', 0, parse_generic, unparse_generic), 126 | ('c', 0, parse_c, unparse_c), 127 | ('b', 0, parse_generic, unparse_generic), 128 | ('k', 0, parse_generic, unparse_generic), 129 | ('a', 0, parse_a, unparse_a) 130 | ] 131 | 132 | parser = {} 133 | unparser = {} 134 | mdparser = {} 135 | mdunparser = {} 136 | for (key, required, parseFcn, unparseFcn) in parsers: 137 | parser[key] = parseFcn 138 | unparser[key] = unparseFcn 139 | for (key, required, parseFcn, unparseFcn) in mdparsers: 140 | mdparser[key] = parseFcn 141 | mdunparser[key] = unparseFcn 142 | del key,required,parseFcn,unparseFcn 143 | 144 | class MediaDescription: 145 | "The MediaDescription encapsulates all of the SDP media descriptions" 146 | def __init__(self, text=None): 147 | self.media = None 148 | self.nettype = 'IN' 149 | self.addrfamily = 'IP4' 150 | self.ipaddr = None 151 | self.port = None 152 | self.transport = None 153 | self.formats = [] 154 | self._d = {} 155 | self._a = {} 156 | self.rtpmap = OrderedDict() 157 | self.media = 'audio' 158 | self.transport = 'RTP/AVP' 159 | self.keyManagement = None 160 | if text: 161 | parse_m(self, 'm', text) 162 | 163 | def setFormats(self, formats): 164 | if self.media in ( 'audio', 'video'): 165 | for pt in formats: 166 | pt = int(pt) 167 | if pt < 97: 168 | try: 169 | PT = RTPDict[pt] 170 | except KeyError: 171 | # We don't know this one - hopefully there's an 172 | # a=rtpmap entry for it. 173 | continue 174 | self.addRtpMap(PT) 175 | # XXX the above line is unbound local variable error if not RTPDict.has_key(pt) --Zooko 2004-09-29 176 | self.formats = formats 177 | 178 | def setMedia(self, media): 179 | self.media = media 180 | def setTransport(self, transport): 181 | self.transport = transport 182 | def setServerIP(self, l): 183 | self.ipaddr = l 184 | def setLocalPort(self, l): 185 | self.port = l 186 | 187 | def setKeyManagement(self, km): 188 | parse_a(self, 'keymgmt', km) 189 | 190 | def clearRtpMap(self): 191 | self.rtpmap = OrderedDict() 192 | 193 | def addRtpMap(self, fmt): 194 | if fmt.pt is None: 195 | pts = self.rtpmap.keys() 196 | pts.sort() 197 | if pts and pts[-1] > 100: 198 | payload = pts[-1] + 1 199 | else: 200 | payload = 101 201 | else: 202 | payload = fmt.pt 203 | rtpmap = "%d %s/%d%s%s"%(payload, fmt.name, fmt.clock, 204 | ((fmt.params and '/') or ""), 205 | fmt.params or "") 206 | self.rtpmap[int(payload)] = (rtpmap, fmt) 207 | self._a.setdefault('rtpmap', OrderedDict())[payload] = rtpmap 208 | self.formats.append(str(payload)) 209 | 210 | def addFMTP(self, streamtype, mode="rtp-midi", profile_level=12, config=0): 211 | """specific for midirtp""" 212 | payload = 96 213 | fmtp = "%d streamtype=%d; mode=%s; profile-level-id=%d; config=%d" % (payload, streamtype, mode, profile_level, config) 214 | self._a.setdefault('fmtp', OrderedDict())[96]= fmtp 215 | 216 | def intersect(self, other): 217 | # See RFC 3264 218 | map1 = self.rtpmap 219 | d1 = {} 220 | for code,(e,fmt) in map1.items(): 221 | d1[rtpmap2canonical(code,e)] = e 222 | map2 = other.rtpmap 223 | outmap = OrderedDict() 224 | # XXX quadratic - make rtpmap an ordereddict 225 | for code, (e, fmt) in map2.items(): 226 | canon = rtpmap2canonical(code,e) 227 | if d1.has_key(canon): 228 | outmap[code] = (e, fmt) 229 | self.rtpmap = outmap 230 | self.formats = [ str(x) for x in self.rtpmap.keys() ] 231 | self._a['rtpmap'] = OrderedDict([ (code,e) for (code, (e, fmt)) in outmap.items() ]) 232 | 233 | class SDP: 234 | def __init__(self, text=None): 235 | from time import time 236 | self._id = None 237 | self._d = {'v': '0', 't': '0 0', 's': 'sropulpof_midi'} 238 | self._a = OrderedDict() 239 | self.mediaDescriptions = [] 240 | # XXX Use the username preference 241 | self._o_username = '-' 242 | self._o_sessid = self._o_version = str(int(time()%1000 * 100)) 243 | self._o_nettype = self.nettype = 'IN' 244 | self._o_addrfamily = self.addrfamily = 'IP4' 245 | self._o_ipaddr = self.ipaddr = None 246 | self.port = None 247 | if text: 248 | self.parse(text) 249 | self.assertSanity() 250 | else: 251 | # new SDP 252 | pass 253 | 254 | def name(self): 255 | return self._sessionName 256 | 257 | def info(self): 258 | return self._sessionInfo 259 | 260 | def version(self): 261 | return self._o_version 262 | 263 | def id(self): 264 | if not self._id: 265 | self._id = (self._o_username, self._o_sessid, self.nettype, 266 | self.addrfamily, self.ipaddr) 267 | return self._id 268 | 269 | def parse(self, text): 270 | lines = text.split('\r\n') 271 | md = None 272 | for line in lines: 273 | elts = line.split('=') 274 | if len(elts) != 2: 275 | continue 276 | (k,v) = elts 277 | if k == 'm': 278 | md = MediaDescription(v) 279 | self.mediaDescriptions.append(md) 280 | elif md: 281 | mdparser[k](md, k, v) 282 | else: 283 | parser[k](self, k, v) 284 | 285 | def get(self, typechar, option=None): 286 | if option is None: 287 | return get(self, typechar) 288 | elif typechar is 'a': 289 | return getA(self, option) 290 | else: 291 | raise ValueError, "only know about suboptions for 'a' so far" 292 | 293 | def setServerIP(self, l): 294 | self._o_ipaddr = self.ipaddr = l 295 | 296 | def addSessionAttribute(self, attrname, attrval): 297 | if not isinstance(attrval, (list, tuple)): 298 | attrval = (attrval,) 299 | self._a[attrname] = attrval 300 | 301 | def addMediaDescription(self, md): 302 | self.mediaDescriptions.append(md) 303 | 304 | def removeMediaDescription(self, md): 305 | self.mediaDescriptions.remove(md) 306 | 307 | def getMediaDescription(self, media): 308 | for md in self.mediaDescriptions: 309 | if md.media == media: 310 | return md 311 | return None 312 | 313 | def hasMediaDescriptions(self): 314 | return bool(len(self.mediaDescriptions)) 315 | 316 | def show(self): 317 | out = [] 318 | for (k, req, p, u) in parsers: 319 | for l in u(self, k): 320 | out.append('%s=%s' % (k, l)) 321 | for md in self.mediaDescriptions: 322 | for (k, req, p, u) in mdparsers: 323 | for l in u(md, k): 324 | out.append('%s=%s' % (k, l)) 325 | out.append('') 326 | s = '\r\n'.join(out) 327 | return s 328 | 329 | def intersect(self, other): 330 | # See RFC 3264 331 | mds = self.mediaDescriptions 332 | self.mediaDescriptions = [] 333 | for md in mds: 334 | omd = None 335 | for o in other.mediaDescriptions: 336 | if md.media == o.media: 337 | omd = o 338 | break 339 | if omd: 340 | md.intersect(omd) 341 | self.mediaDescriptions.append(md) 342 | 343 | def assertSanity(self): 344 | pass 345 | 346 | def ntp2delta(ticks): 347 | return (ticks - 220898800) 348 | 349 | 350 | def rtpmap2canonical(code, entry): 351 | if not isinstance(code, int): 352 | raise ValueError(code) 353 | if code < 96: 354 | return code 355 | else: 356 | ocode,desc = entry.split(' ',1) 357 | desc = desc.split('/') 358 | if len(desc) == 2: 359 | desc.append('1') # default channels 360 | name,rate,channels = desc 361 | return (name.lower(),int(rate),int(channels)) 362 | 363 | 364 | if __name__ == "__main__": 365 | 366 | #SDP Creation 367 | s = SDP() 368 | s.setServerIP("127.0.0.1") 369 | md = MediaDescription() # defaults to type 'audio' 370 | s.addMediaDescription(md) 371 | md.setServerIP("127.0.0.1") 372 | md.setLocalPort(44000) 373 | 374 | #Payload Type 375 | md.addRtpMap(PT_AVP) 376 | md.addFMTP(5) 377 | #FMTP 378 | 379 | #md.addRtpMap(PT_PCMU) 380 | res = s.show() 381 | 382 | #Parsing SDP 383 | tmp_sdp = SDP() 384 | tmp_sdp.parse(res) 385 | app_conf = tmp_sdp.getMediaDescription("audio").rtpmap 386 | 387 | print app_conf.items()[0][1][0] 388 | 389 | #Compare 390 | ss = SDP() 391 | ss.setServerIP("127.0.0.8") 392 | md = MediaDescription() # defaults to type 'audio' 393 | ss.addMediaDescription(md) 394 | md.setServerIP("127.0.0.9") 395 | md.setLocalPort(4420) 396 | md.addFMTP(10) 397 | 398 | 399 | 400 | -------------------------------------------------------------------------------- /rtpmidi/protocols/rtp/utils.py: -------------------------------------------------------------------------------- 1 | class Singleton (object): 2 | instances = {} 3 | def __new__(cls, *args, **kargs): 4 | if Singleton.instances.get(cls) is None: 5 | Singleton.instances[cls] = object.__new__(cls, *args, **kargs) 6 | return Singleton.instances[cls] 7 | 8 | class TestSingleton(Singleton): 9 | def __init__(self, argr): 10 | self.test = 10 11 | self.arg = argr 12 | 13 | def tt(self): 14 | self.o = 69 15 | 16 | """ 17 | class Singleton: 18 | class __OnlyOne: 19 | def __init__(self): 20 | pass 21 | 22 | 23 | instance = {} 24 | def __init__(self): 25 | if not self.__class__ in Singleton.instance: 26 | Singleton.instance[self.__class__] = Singleton.__OnlyOne() 27 | #return True 28 | else : 29 | print 'warning : trying to recreate a singleton' 30 | #return False 31 | 32 | 33 | def __getattr__(self, name): 34 | return getattr(self.instance[self.__class__], name) 35 | 36 | 37 | def __setattr__(self, name, value): 38 | return setattr(self.instance[self.__class__], name, value) 39 | 40 | 41 | 42 | singletons = {} 43 | class Singleton(object): 44 | def __new__(cls, *args, **kwargs): 45 | if cls in singletons: 46 | return singletons[cls] 47 | self = object.__new__(cls) 48 | cls.__init__(self, *args, **kwargs) 49 | singletons[cls] = self 50 | return self 51 | 52 | 53 | class Singleton(object): 54 | _ref = None 55 | 56 | def __new__(cls, *args, **kw): 57 | if cls._ref is None: 58 | cls._ref = super(Singleton, cls).__new__(cls, *args, **kw) 59 | 60 | return cls._ref 61 | 62 | def pitch_cmp(x, y): 63 | if x[0][1] > y[0][1]: 64 | return 1 65 | elif x[0][1] > y[0][1]: 66 | return 0 67 | else: 68 | return -1 69 | 70 | """ 71 | 72 | 73 | 74 | if __name__ == "__main__": 75 | e = TestSingleton(42) 76 | e.tt() 77 | a = TestSingleton(44) 78 | 79 | print e.arg 80 | print e.o 81 | print a.o 82 | 83 | -------------------------------------------------------------------------------- /rtpmidi/runner.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Scenic 5 | # Copyright (C) 2008 Société des arts technologiques (SAT) 6 | # http://www.sat.qc.ca 7 | # All rights reserved. 8 | # 9 | # This file is free software: you can redistribute it and/or modify 10 | # it under the terms of the GNU General Public License as published by 11 | # the Free Software Foundation, either version 2 of the License, or 12 | # (at your option) any later version. 13 | # 14 | # Scenic 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 Scenic. If not, see . 21 | """ 22 | Main runner of the app is the run() function. 23 | """ 24 | import sys 25 | import socket 26 | from twisted.internet import reactor 27 | from twisted.internet import defer 28 | from optparse import OptionParser 29 | 30 | try: 31 | from rtpmidi.engines.midi.midi_session import MidiSession 32 | from rtpmidi.protocols.rtp.rtp_control import RTPControl 33 | from rtpmidi import utils 34 | except ImportError, e: 35 | # print "Import error %s" % (str(e)) 36 | imported_midi = False 37 | print e 38 | else: 39 | imported_midi = True 40 | 41 | class Config(object): 42 | """ 43 | Configuration for the application. 44 | """ 45 | def __init__(self): 46 | # str: 47 | self.peer_address = None 48 | # int: 49 | self.receiving_port = None 50 | self.sending_port = None 51 | self.input_device = None 52 | self.output_device = None 53 | self.latency = 20 # ms 54 | self.jitter_buffer = 10 # ms 55 | # bool: 56 | self.safe_keyboard = False 57 | self.disable_recovery_journal = False 58 | self.follow_standard = False 59 | self.verbose = False 60 | 61 | def list_midi_devices(): 62 | """ 63 | Lists MIDI devices. 64 | 65 | @return: None 66 | """ 67 | rtp_control = RTPControl() 68 | #FIXME: We should not need to instanciated all those session objects to simply list MIDI devices! 69 | # we give it dummy port numbers, just to enable sending and receiving. 70 | midi_session = rtp_control.add_session(MidiSession("127.0.0.1", rport=1, sport=1)) 71 | midi_session = rtp_control.get_session(midi_session) 72 | dev_in, dev_out = midi_session.get_devices() 73 | print "List of MIDI devices:" 74 | print " Input devices:" 75 | print " --------------" 76 | for dev in dev_in: 77 | print " * input %2s %30s" % (dev[0], '"%s"' % (dev[1])), 78 | if dev[2] == 1: 79 | print " [open]" 80 | else: 81 | print " [closed]" 82 | print " Output devices:" 83 | print " ---------------" 84 | for dev in dev_out: 85 | print " * output %2s %30s" % (dev[0], '"%s"' % (dev[1])), 86 | if dev[2] == 1: 87 | print " [open]" 88 | else: 89 | print " [closed]" 90 | 91 | def cleanup_everything(): 92 | print "Shutting down midi stream module." 93 | # FIXME: what does it do, actually? 94 | return RTPControl().stop() 95 | 96 | def before_shutdown(): 97 | """ 98 | @rtype: Deferred 99 | """ 100 | cleanup_everything() # FIXME: does it return a Deferred? 101 | return defer.succeed(None) 102 | 103 | def run(version): 104 | """ 105 | MAIN of the application. 106 | Parses the arguments and runs the app. 107 | @param version: Version of the app. (str) 108 | """ 109 | description = "Creates a MIDI RTP stream between two hosts, uni or bi-directionnal." 110 | details = """Example: midistream -a 10.0.1.29 -r 44000 -s 44000 -i 1 -o 0 111 | 112 | This command creates a bi-directional connection with 10.0.1.29 on port 44000, midi device 1 is the source for sending data, and all received data are sent to midi device 0. 113 | Caution: If the stream is bi-directionnal receiving port and sending port must be equal.""" 114 | parser = OptionParser(usage="%prog", version=version, description=description, epilog=details) 115 | parser.add_option("-a", "--address", type="string", help="Specify the address of the peer (mandatory)") 116 | parser.add_option("-l", "--list-devices", action="store_true", help="Show a complete list of midi devices") 117 | parser.add_option("-s", "--sending-port", type="int", help="Select the sending port. Must be used the --input-device option.") 118 | parser.add_option("-r", "--receiving-port", type="int", help="Select the listening port. Must be used the --output-device option.") 119 | parser.add_option("-o", "--output-device", type="int", help="Select a midi output device. (sink) Must be used with the --receiving-port option.") 120 | parser.add_option("-i", "--input-device", type="int", help="Select a midi input device. (source) Must be used with the --sending-port option.") 121 | parser.add_option("-L", "--latency", type="int", help="Specify the latency (in ms) of the midi out device (default is 20)") 122 | parser.add_option("-b", "--jitter-buffer", type="int", help="Specify the jitter buffer size in ms (default is 10)") 123 | parser.add_option("-k", "--safe-keyboard", action="store_true", help="Take care of note ON/OFF alternating, useful if several notes in a ms (experimental)"), 124 | parser.add_option("-j", "--disable-recovery-journal", action="store_true", help="DISABLE recovery journal (journal provide note recovery when a packet is lost, so at your own risks!)"), 125 | parser.add_option("-f", "--follow-standard", action="store_true", help="Take care of MIDI standard (ex: omni on) in recovery journal (experimental)"), 126 | parser.add_option("-v", "--verbose", action="store_true", help="Enables a verbose output"), 127 | (options, args) = parser.parse_args() 128 | 129 | if not imported_midi: 130 | print "MIDI module cannot run, importing rtp modules was not successful." 131 | print "You should install either python-portmidi on python-pygame." 132 | sys.exit(1) 133 | 134 | config = Config() 135 | 136 | if options.list_devices: 137 | list_midi_devices() 138 | sys.exit(0) 139 | # bool options: 140 | if options.disable_recovery_journal: 141 | config.disable_recovery_journal = True 142 | if options.follow_standard: 143 | config.follow_standard = True 144 | if options.safe_keyboard: 145 | config.safe_keyboard = True 146 | if options.verbose: 147 | config.verbose = True 148 | # int options: 149 | if options.latency is not None: 150 | config.latency = options.latency 151 | if options.input_device is not None: 152 | config.input_device = options.input_device 153 | if options.output_device is not None: 154 | config.output_device = options.output_device 155 | if options.jitter_buffer is not None: 156 | config.jitter_buffer = options.jitter_buffer 157 | if options.receiving_port is not None: 158 | config.receiving_port = options.receiving_port 159 | if not utils.check_port(config.receiving_port): 160 | print "Incorrect receiving port number:", config.receiving_port 161 | parser.print_usage() 162 | sys.exit(2) 163 | if options.sending_port is not None: 164 | config.sending_port = options.sending_port 165 | if not utils.check_port(config.sending_port): 166 | print "Incorrect sending port number:", config.sending_port 167 | parser.print_usage() 168 | sys.exit(2) 169 | # string options: 170 | if options.address is not None: 171 | if utils.check_ip(options.address): 172 | config.peer_address = options.address 173 | else: 174 | try: 175 | config.peer_address = socket.gethostbyname(options.address) 176 | except socket.gaierror, e: 177 | print "socket error: %s" % (str(e)) 178 | print "Wrong ip address format: ", options.address 179 | sys.exit(2) 180 | # validate logic in options : ----------------- 181 | if config.peer_address is None: 182 | print "Error: You must specify a peer address." 183 | parser.print_usage() 184 | sys.exit(2) 185 | if config.sending_port is not None: 186 | if config.input_device is None: 187 | print "Error: You must specify an input device number if in sending mode." 188 | parser.print_usage() 189 | sys.exit(2) 190 | if config.receiving_port is not None: 191 | if config.output_device is None: 192 | print "Error: You must specify an output device number if in receiving mode." 193 | parser.print_usage() 194 | sys.exit(2) 195 | if config.receiving_port is None and config.sending_port is None: 196 | print "Error: You must choose either the sending or receiving mode." 197 | parser.print_usage() 198 | sys.exit(2) 199 | if config.sending_port is not None and config.receiving_port is not None: 200 | if config.sending_port != config.receiving_port: 201 | print "Error: receiving port and sending port must be equal if both specified." 202 | parser.print_usage() 203 | sys.exit(2) 204 | # start the app: 205 | midi_session_c = RTPControl().add_session(MidiSession( 206 | config.peer_address, 207 | sport=config.sending_port, 208 | rport=config.receiving_port, 209 | latency=config.latency, 210 | jitter_buffer_size=config.jitter_buffer, 211 | safe_keyboard=config.safe_keyboard, 212 | recovery=config.disable_recovery_journal, 213 | follow_standard=config.follow_standard, 214 | verbose=config.verbose)) 215 | midi_session = RTPControl().get_session(midi_session_c) 216 | dev_in, dev_out = midi_session.get_devices() #FIXME: I think this might crash if we're either not sending or receiving, since it will return a 1-tuple 217 | if config.input_device is not None: 218 | res = midi_session.set_device_in(config.input_device) 219 | if not res: 220 | print("Could not start session with input device %s" % (config.input_device)) 221 | sys.exit(2) 222 | if config.output_device is not None: 223 | res = midi_session.set_device_out(config.output_device) 224 | if not res: 225 | print("Could not start session with output device %s" % (config.output_device)) 226 | sys.exit(2) 227 | RTPControl().start_session(midi_session_c) 228 | reactor.addSystemEventTrigger("before", "shutdown", before_shutdown) 229 | reactor.run() 230 | -------------------------------------------------------------------------------- /rtpmidi/test/NOMENCLATURE.txt: -------------------------------------------------------------------------------- 1 | We use trial to run the tests. 2 | 3 | trial test/test_common.py 4 | 5 | They are also specified in the makefile.am : 6 | 7 | make check 8 | 9 | Test names prefixes : 10 | 11 | * "test_" : unit tests. Those tests must be in Makefile.am to be 12 | automatically tested with "make check" 13 | * "once_" : work in progress. Not part of official test suites. 14 | For developement purposes. 15 | * "check_" : tests that internally start miville and telnet processes to 16 | test. Most of them use test/lib_miville_telnet.py. 17 | * "dist_" : Official test case testing. Those tests need two computers. 18 | Miville needs to be started manually on both prior to run them. 19 | * "lib_" : Libraries for making tests 20 | 21 | -------------------------------------------------------------------------------- /rtpmidi/test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/avsaj/rtpmidi/622a8cffeb3b4e67b66cbaf63a6432aef9fb6984/rtpmidi/test/__init__.py -------------------------------------------------------------------------------- /rtpmidi/test/test_jitter_buffer.py: -------------------------------------------------------------------------------- 1 | from twisted.trial import unittest 2 | from rtpmidi.protocols.rtp.jitter_buffer import JitterBuffer 3 | from rtpmidi.protocols.rtp.packets import RTPPacket 4 | 5 | 6 | class TestJitterBuffer(unittest.TestCase): 7 | """Testing function from JitterBuffer""" 8 | 9 | def setUp(self): 10 | self.jitter_buffer = JitterBuffer() 11 | 12 | def tearDown(self): 13 | pass 14 | 15 | def test_init(self): 16 | jitter_buffer = JitterBuffer() 17 | assert(len(jitter_buffer.buffer) == 0), self.fail("Wrong size at initialization") 18 | 19 | def test_add(self): 20 | """Test Adding a packet to jitter buffer""" 21 | packet = RTPPacket(0, 42, 0, "", 0, \ 22 | marker=0, \ 23 | xhdrtype=None, xhdrdata='') 24 | time = 0 25 | 26 | self.jitter_buffer.add([packet, time]) 27 | assert(len(self.jitter_buffer.buffer) == 1), \ 28 | self.fail("Wrong size afther adding an element") 29 | assert(self.jitter_buffer.buffer[0][1] == 0), self.fail("Wrong value in the buffer for time") 30 | assert(self.jitter_buffer.buffer[0][0].header.seq == 42), self.fail("Wrong value in the buffer for seq num") 31 | 32 | packet = RTPPacket(0, 41, 0, "", 0, \ 33 | marker=0, \ 34 | xhdrtype=None, xhdrdata='') 35 | time = 2 36 | 37 | self.jitter_buffer.add([packet, time]) 38 | 39 | def test_has_seq(self): 40 | """Testing seq num existenze in the buffer""" 41 | #Adding packets 42 | packet = RTPPacket(0, 42, 0, "", 0, \ 43 | marker=0, \ 44 | xhdrtype=None, xhdrdata='') 45 | time = 0 46 | self.jitter_buffer.add([packet, time]) 47 | 48 | 49 | packet = RTPPacket(0, 43, 0, "", 0, \ 50 | marker=0, \ 51 | xhdrtype=None, xhdrdata='') 52 | time = 5 53 | self.jitter_buffer.add([packet, time]) 54 | 55 | #TEsting values 56 | res = self.jitter_buffer.has_seq(42) 57 | assert(res==True), \ 58 | self.fail("Wrong answer from has_seq, can't find an int that is in the buffer") 59 | 60 | res = self.jitter_buffer.has_seq(54) 61 | assert(res==False), \ 62 | self.fail("Wrong answer from has_seq, can find an int that isn't in the buffer") 63 | 64 | def test_get_packets(self): 65 | """Testing Get Packets from jitter buffer""" 66 | #Adding packets 67 | packet = RTPPacket(0, 42, 0, "", 0, \ 68 | marker=0, \ 69 | xhdrtype=None, xhdrdata='') 70 | time = 0 71 | self.jitter_buffer.add([packet, time]) 72 | 73 | 74 | packet = RTPPacket(0, 43, 0, "", 0, \ 75 | marker=0, \ 76 | xhdrtype=None, xhdrdata='') 77 | time = 5 78 | self.jitter_buffer.add([packet, time]) 79 | 80 | packet = RTPPacket(0, 45, 0, "", 0, \ 81 | marker=0, \ 82 | xhdrtype=None, xhdrdata='') 83 | time = 8 84 | self.jitter_buffer.add([packet, time]) 85 | 86 | packet = RTPPacket(0, 44, 0, "", 0, \ 87 | marker=0, \ 88 | xhdrtype=None, xhdrdata='') 89 | time = 9 90 | self.jitter_buffer.add([packet, time]) 91 | 92 | res = self.jitter_buffer.get_packets(9) 93 | 94 | assert(len(res)==4), self.fail("Wrong size returned") 95 | for i in range(len(res)): 96 | assert(res[i].header.seq==42+i), self.fail("Wrong seq num returned") 97 | 98 | 99 | assert(len(self.jitter_buffer.buffer)==0), self.fail("Packets have not been erase from jitter buffer") 100 | -------------------------------------------------------------------------------- /rtpmidi/test/test_listcirc.py: -------------------------------------------------------------------------------- 1 | from twisted.trial.unittest import TestCase 2 | from rtpmidi.engines.midi.list_circ import ListCirc, PacketCirc 3 | from rtpmidi.engines.midi.midi_object import OldPacket 4 | 5 | 6 | class TestListCirc(TestCase): 7 | def setUp(self): 8 | self.list_to_test = ListCirc(10) 9 | 10 | def tearDown(self): 11 | del self.list_to_test 12 | 13 | def test_to_list(self): 14 | 15 | #Testing type 16 | assert(type(self.list_to_test) == ListCirc), \ 17 | self.fail("Problem with list type") 18 | 19 | #Test add note on first items 20 | for i in range(10): 21 | self.list_to_test.to_list(i) 22 | 23 | #Testing content 24 | for i in range(10): 25 | assert(self.list_to_test[i] == i),\ 26 | self.fail("Problem with content of the listcirc") 27 | 28 | #Test replace note 29 | for i in range(20): 30 | self.list_to_test.to_list(i) 31 | 32 | #Testing size 33 | assert(len(self.list_to_test) == 10),\ 34 | self.fail("Problem of size with the listcirc") 35 | 36 | #Testing content of the list 37 | for i in range(10): 38 | assert(self.list_to_test[i] == 10+i),\ 39 | self.fail("Problem with content of the listcirc") 40 | 41 | 42 | def test_flush(self): 43 | #Test add note on first items 44 | for i in range(10): 45 | self.list_to_test.to_list(i) 46 | 47 | #Flushing list 48 | self.list_to_test.flush() 49 | 50 | #Testing attribute 51 | assert(len(self.list_to_test)==0), \ 52 | self.fail("Problem flushing the list") 53 | 54 | assert(self.list_to_test.round==0), \ 55 | self.fail("Problem with round attributeflushing the list") 56 | 57 | assert(self.list_to_test.index==0), \ 58 | self.fail("Problem with index attribute flushing the list") 59 | 60 | 61 | 62 | class TestPacketCirc(TestCase): 63 | """Testing packet Circ""" 64 | 65 | def setUp(self): 66 | self.packet_circ = PacketCirc(10) 67 | 68 | #list to test the packet list 69 | plist = [[[192, 120, 100],0],[[144, 104, 50],1], [[145, 110, 0],2], \ 70 | [[145, 112, 0],3], [[144, 124, 50],4], \ 71 | [[145, 114, 0],5], [[145, 12, 0],6]] 72 | 73 | #test without wrap around 74 | for i in range(10): 75 | #modify plist for each packet 76 | plist_1 = [ [[plist[j][0][0], plist[j][0][1], plist[j][0][2]], \ 77 | plist[j][1] + i] for j in range(len(plist)) ] 78 | packy = OldPacket(i, plist_1, 0) 79 | #Adding packet to list 80 | self.packet_circ.to_list(packy) 81 | 82 | def test_find_packet(self): 83 | #Trying find a packet 84 | emplacement = self.packet_circ.find_packet(5) 85 | 86 | #Testing type return 87 | assert(type(emplacement)==int), \ 88 | self.fail("Problem with type returned in find packet function") 89 | 90 | packet = self.packet_circ[emplacement] 91 | 92 | #Testing type 93 | assert(type(packet)==OldPacket), \ 94 | self.fail("Problem with type contained in packet list") 95 | 96 | #Testing content 97 | for i in range(len(packet.packet)): 98 | assert(packet.packet[i][1]==i+5), \ 99 | self.fail("Problem getting right element , find packet fun") 100 | 101 | 102 | def test_get_packets_1_2(self): 103 | """Packet Circ Testing get packet (case: checkpoint > act_seq)""" 104 | #TestCase 1.2 105 | checkpoint = 6 106 | act_seq = 4 107 | midi_cmd = self.packet_circ.get_packets(checkpoint, act_seq) 108 | 109 | #Test len, 8 == nb packets to get 110 | length = (10 - 6 + 4) 111 | assert(len(midi_cmd) == length), \ 112 | self.fail("Problem getting the good length from get_packets") 113 | 114 | #Verify content (base on timestamp control) 115 | #iterator modulo 10 116 | iterator = 7 117 | for i in range(len(midi_cmd)): 118 | midi_cmd_notes = midi_cmd[i].packet 119 | 120 | if i != 0: 121 | iterator = (iterator + 1 )% 10 122 | 123 | for j in range(len(midi_cmd_notes)): 124 | assert(midi_cmd_notes[j][1]== iterator + j), \ 125 | self.fail("Problem with midi cmd content") 126 | 127 | def test_get_packets_1_1(self): 128 | """Packet Circ Testing get packet (case: checkpoint > act_seq with wrap around)""" 129 | #TestCase 1.1 130 | checkpoint = 8 131 | act_seq = 1 132 | 133 | #Adapting list 134 | self.packet_circ.flush() 135 | 136 | #listto test the packet list 137 | plist = [[[192, 120, 100],0],[[144, 104, 50],1], [[145, 110, 0],2], \ 138 | [[145, 112, 0],3], [[144, 124, 50],4], \ 139 | [[145, 114, 0],5], [[145, 12, 0],6]] 140 | 141 | #Create the gap 142 | empty_list = [] 143 | for i in range(8): 144 | packy = OldPacket(i, [], 0) 145 | #Adding packet to list 146 | self.packet_circ.to_list(packy) 147 | 148 | #fulling the list 149 | for i in range(10): 150 | #modify plist for each packet 151 | plist_1 = [ [[plist[j][0][0], plist[j][0][1], plist[j][0][2]], \ 152 | plist[j][1] + i] for j in range(len(plist)) ] 153 | 154 | packy = OldPacket(i, plist_1, 0) 155 | #Adding packet to list 156 | self.packet_circ.to_list(packy) 157 | 158 | midi_cmd = self.packet_circ.get_packets(checkpoint, act_seq) 159 | 160 | #Test len (nb packets) 161 | length = 3 162 | assert(len(midi_cmd) == length), \ 163 | self.fail("Problem getting the good length from get_packets") 164 | 165 | #Testing content 166 | #iterator 167 | iterator = 9 168 | for i in range(len(midi_cmd)): 169 | #increment iterator 170 | if i != 0: 171 | iterator = (iterator + 1 )% 10 172 | 173 | midi_cmd_notes = midi_cmd[i].packet 174 | 175 | #Testing content of packet 9, 0, 1 176 | for j in range(len(midi_cmd_notes)): 177 | assert(midi_cmd_notes[j][1]== j + iterator), \ 178 | self.fail("Problem with midi cmd content") 179 | 180 | def test_get_packets_2(self): 181 | """Packet Circ Testing get packet (case: checkpoint < act_seq)""" 182 | #TestCase 2 183 | checkpoint = 2 184 | act_seq = 8 185 | midi_cmd = self.packet_circ.get_packets(checkpoint, act_seq) 186 | 187 | #Test len, of packet list returned 188 | length = (8 - 2) 189 | assert(len(midi_cmd) == length), \ 190 | self.fail("Problem getting the good length from get_packets") 191 | 192 | #Verify content (base on timestamp control) 193 | #iterator modulo 10 194 | iterator = 3 195 | for i in range(len(midi_cmd)): 196 | midi_cmd_notes = midi_cmd[i].packet 197 | for j in range(len(midi_cmd_notes)): 198 | #increment iterator 199 | if not j%7 and i != 0: 200 | iterator = (iterator+1)%10 201 | 202 | assert(midi_cmd_notes[j][1]== iterator + j%7), \ 203 | self.fail("Problem with midi cmd content") 204 | -------------------------------------------------------------------------------- /rtpmidi/test/test_midi_in.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from twisted.trial import unittest 4 | from rtpmidi.engines.midi.midi_in import MidiIn 5 | 6 | class FakeClient(object): 7 | def __init__(self): 8 | pass 9 | 10 | def send_midi_data(self, data, time): 11 | pass 12 | 13 | class TestMidiIn(unittest.TestCase): 14 | def setUp(self): 15 | fake_client = FakeClient 16 | self.midi_in = MidiIn(fake_client) 17 | 18 | def test_start(self): 19 | res = self.midi_in.start() 20 | assert(res == 0), self.fail("Can start without a midi device set") 21 | 22 | #Faking midiDevice 23 | self.midi_in.midi_in = 1 24 | res = self.midi_in.start() 25 | assert(res == 1), self.fail("Can't start with a midi device set") 26 | 27 | def test_stop_1(self): 28 | self.midi_in.midi_in = 1 29 | self.midi_in.start() 30 | self.midi_in.stop() 31 | assert(self.midi_in.end_flag == True), \ 32 | self.fail("Problem stopping Midi in.") 33 | 34 | def test_stop_2(self): 35 | #Set midi device 36 | #Getting list of midi device 37 | self.midi_in.get_devices() 38 | 39 | #Setting and testing midi device 40 | if len(self.midi_in.midi_device_list) > 0: 41 | dev_to_use = self.midi_in.midi_device_list 42 | self.midi_in.midi_in = 1 43 | 44 | else: 45 | self.fail("Problem getting list of midi" \ 46 | + " devices or no midi device available.") 47 | 48 | #Launch midi in 49 | self.midi_in.start() 50 | 51 | #Testing end flag 52 | assert( self.midi_in.end_flag == False ), \ 53 | self.fail("Problem with end flag when midi is in activity") 54 | 55 | 56 | self.midi_in.stop() 57 | assert( self.midi_in.end_flag == True ), \ 58 | self.fail("Problem with end flag when midi out in activity") 59 | 60 | def test_get_devices(self): 61 | self.midi_in.get_devices() 62 | assert(len(self.midi_in.midi_device_list) > 0), self.fail("Problem getting devices") 63 | 64 | def test_set_device(self): 65 | #Getting list of midi device 66 | self.midi_in.get_devices() 67 | 68 | #Setting and testing midi device 69 | if len(self.midi_in.midi_device_list) > 0: 70 | dev_to_use = self.midi_in.midi_device_list 71 | print dev_to_use 72 | #Port midi failed with the following lines ?? 73 | #res = self.midi_in.set_device(1) 74 | #assert( res == True ), self.fail("Problem setting midi device in") 75 | else: 76 | self.fail("Problem getting list of midi" \ 77 | + " devices or no midi device available.") 78 | 79 | 80 | def test_get_device_info(self): 81 | #setting device midi 82 | self.midi_in.get_devices() 83 | self.midi_in.set_device(self.midi_in.midi_device_list[0][0]) 84 | 85 | #Getting device infos 86 | res = self.midi_in.get_device_info() 87 | 88 | #Testing device infos 89 | assert(res[1] == "Midi Through Port-0"), \ 90 | self.fail("Problem getting right info from midi device") 91 | 92 | 93 | def test_get_input(self): 94 | pass 95 | 96 | 97 | def test_polling(self): 98 | pass 99 | 100 | 101 | -------------------------------------------------------------------------------- /rtpmidi/test/test_midi_object.py: -------------------------------------------------------------------------------- 1 | from twisted.trial import unittest 2 | from rtpmidi.engines.midi.midi_object import MidiCommand 3 | from rtpmidi.engines.midi.midi_object import SafeKeyboard 4 | class TestMidiCommand(unittest.TestCase): 5 | """Testing MidiCommand class""" 6 | 7 | def setUp(self): 8 | self.midi_command = MidiCommand() 9 | 10 | def tearDown(self): 11 | del self.midi_command 12 | 13 | def test_header(self): 14 | """Testing header for MIDICommand""" 15 | marker_b, recovery, timestamp, phantom, length = 0, 0, 0, 0, 10 16 | res = self.midi_command.header(marker_b, recovery, timestamp, phantom, length) 17 | 18 | assert(type(res)==str), self.fail("Wrong type returned") 19 | assert(len(res)==1), self.fail("Wrong size returned") 20 | 21 | 22 | def test_parse_header(self): 23 | """Testing parse header for MIDICommand""" 24 | marker_b, recovery, timestamp, phantom, length = 0, 0, 0, 0, 10 25 | res = self.midi_command.header(marker_b, recovery, timestamp, phantom, length) 26 | marker_b, marker_j, marker_z, marker_p, length = self.midi_command.parse_header(res) 27 | 28 | assert(marker_b==0), self.fail("Wrong value returned for marker_b") 29 | assert(marker_j==0), self.fail("Wrong value returned for marker_j") 30 | assert(marker_z==0), self.fail("Wrong value returned for marker_z") 31 | assert(marker_p==0), self.fail("Wrong value returned for marker_p") 32 | assert(length==10), self.fail("Wrong value returned for length") 33 | 34 | def test_encode_midi_commands(self): 35 | 36 | plist = [[[192, 120, 100],1069],[[144, 104, 50],1030], 37 | [[145, 110, 0],10], [[145, 112, 0],1044], 38 | [[144, 124, 50],19],[[145, 114, 0],8], 39 | [[145, 12, 0],999]] 40 | 41 | decorate = [((x[0][0]&15), (x[0][0]&240), x[0][1],x) for x in plist] 42 | decorate.sort() 43 | plist = [x[3] for x in decorate] 44 | 45 | res, nb_notes = self.midi_command.encode_midi_commands(plist) 46 | 47 | assert(nb_notes==7), \ 48 | self.fail("Problem with nb_notes , it's not corresponding" \ 49 | + " to reality") 50 | assert(len(res) == nb_notes*7), \ 51 | self.fail("Problem of size with formated command") 52 | 53 | def test_decode_midi_commands(self): 54 | plist = [[[192, 120, 100],1069],[[144, 104, 50],1069], \ 55 | [[145, 110, 0],1070], [[145, 112, 0],1071], \ 56 | [[144, 124, 50],1071],[[145, 114, 0],1072], \ 57 | [[145, 12, 0],1072]] 58 | 59 | decorate = [((x[0][0]&15), (x[0][0]&240), x[0][1],x) for x in plist] 60 | decorate.sort() 61 | plist = [x[3] for x in decorate] 62 | 63 | res, nb_notes = self.midi_command.encode_midi_commands(plist) 64 | 65 | midi_cmd = self.midi_command.decode_midi_commands(res, nb_notes) 66 | 67 | assert(len(plist)==len(midi_cmd)), \ 68 | self.fail("list haven't got the same size") 69 | 70 | for i in range(len(midi_cmd)): 71 | if midi_cmd[i][0][0] != plist[i][0][0]: 72 | self.fail("Problem with event encoding") 73 | if midi_cmd[i][0][1] != plist[i][0][1]: 74 | self.fail("Problem with note encoding") 75 | if midi_cmd[i][0][2] != plist[i][0][2]: 76 | self.fail("Problem with velocity encoding") 77 | 78 | if midi_cmd[i][1] != plist[i][1] - plist[0][1]: 79 | self.fail("Problem with timestamp encoding") 80 | 81 | 82 | class TestSafeKeyboard(unittest.TestCase): 83 | def setUp(self): 84 | self.key_safe = SafeKeyboard() 85 | 86 | 87 | def test_note_index(self): 88 | pass 89 | 90 | 91 | def test_check_1(self): 92 | """Test SafeKeyboard with only one flow only one chan""" 93 | plist = [[[144, 120, 100], 1069], [[144, 120, 100], 1069], 94 | [[128, 120, 100],1069], [[128, 120, 100], 1069], 95 | [[144, 120, 100], 1069],[[128, 120, 100], 1069], 96 | [[128, 120, 100],1069], [[144, 120, 100], 1069]] 97 | 98 | #Test with only one flow 99 | res = self.key_safe.check(plist) 100 | 101 | #Verify that all note are of afther the pass (nb off == nb on) 102 | for i in range(len(self.key_safe.keyboard)): 103 | assert(self.key_safe.keyboard[0][i] == False), \ 104 | self.fail("Note history is not respected") 105 | 106 | 107 | #Checking alternate 108 | for i in range(len(res)): 109 | if i % 2 == 0: 110 | assert(res[i][0][0]==144), self.fail("Bad alternation") 111 | else: 112 | assert(res[i][0][0]==128), self.fail("Bad alternation") 113 | 114 | 115 | def test_check_2(self): 116 | """Test SafeKeyboard with all channels (doesn't have to delete notes)""" 117 | plist = [[[144, 120, 100], 1069], [[144, 110, 100], 1069], 118 | [[128, 110, 100],1069], [[128, 120, 100], 1069], 119 | [[128, 120, 100], 1069],[[128, 110, 100], 1069], 120 | [[144, 120, 100],1069], [[144, 110, 100], 1069]] 121 | 122 | note_list = [] 123 | for i in range(16): 124 | for j in range(len(plist)): 125 | note_list.append([[plist[j][0][0] + i, 126 | plist[j][0][1], plist[j][0][2]], 127 | plist[j][1]]) 128 | 129 | res = self.key_safe.check(note_list) 130 | 131 | keyboard = [] 132 | 133 | #Building a map of all notes to test the result 134 | for i in range(16): 135 | note_list = [False for i in range(127)] 136 | keyboard.append(note_list) 137 | 138 | 139 | for i in range(len(res)): 140 | #Note on 141 | if res[i][0][0]&240 == 144: 142 | chan = res[i][0][0]&15 143 | pitch = res[i][0][1] 144 | if not keyboard[chan][pitch]: 145 | keyboard[chan][pitch] = True 146 | else: 147 | self.fail("Problem of alternation") 148 | 149 | #Note off 150 | elif res[i][0][0]&240 == 128: 151 | chan = res[i][0][0]&15 152 | pitch = res[i][0][1] 153 | if keyboard[chan][pitch]: 154 | keyboard[chan][pitch] = False 155 | else: 156 | self.fail("Problem of alternation") 157 | 158 | 159 | -------------------------------------------------------------------------------- /rtpmidi/test/test_midi_out.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from twisted.trial import unittest 3 | from rtpmidi.engines.midi.midi_out import MidiOut 4 | 5 | class TestMidiOut(unittest.TestCase): 6 | """Test on MidiOut class""" 7 | 8 | def setUp(self): 9 | self.midi_out = MidiOut(1, 10) 10 | 11 | def tearDown(self): 12 | del self.midi_out 13 | 14 | def test_get_devices(self): 15 | self.midi_out.get_devices() 16 | assert(len(self.midi_out.midi_device_list) > 0), \ 17 | self.fail("Problem getting devices") 18 | 19 | def test_get_device_info(self): 20 | #setting device midi 21 | self.midi_out.get_devices() 22 | self.midi_out.set_device(self.midi_out.midi_device_list[0][0]) 23 | 24 | #Getting device infos 25 | res = self.midi_out.get_device_info() 26 | print res 27 | 28 | #Testing device infos 29 | assert(res[1] == "Midi Through Port-0"), \ 30 | self.fail("Problem getting right info from midi device") 31 | 32 | def test_set_device(self): 33 | #Getting list of midi device 34 | self.midi_out.get_devices() 35 | 36 | #Setting and testing midi device 37 | if len(self.midi_out.midi_device_list) > 0: 38 | dev_to_use = self.midi_out.midi_device_list 39 | res = self.midi_out.set_device(dev_to_use[0][0]) 40 | assert( res == True ), self.fail("Problem setting midi device out") 41 | else: 42 | self.fail("Problem getting list of midi" \ 43 | + " devices or no midi device available.") 44 | 45 | def test_start(self): 46 | #Without device 47 | res = self.midi_out.start() 48 | assert(res == 0), \ 49 | self.fail("Can start publy before setting a midi device") 50 | 51 | #With device 52 | self.midi_out.get_devices() 53 | 54 | dev = self.midi_out.set_device(self.midi_out.midi_device_list[0][0]) 55 | 56 | if dev == 0: 57 | res = self.midi_out.start() 58 | assert(res == 1), \ 59 | self.fail("Can't start publy with a midi device set") 60 | 61 | def test_send_note_off(self): 62 | self.midi_out.get_devices() 63 | self.midi_out.set_device(self.midi_out.midi_device_list[0][0]) 64 | self.midi_out.send_note_off() 65 | #Nothin to test ?? 66 | 67 | def test_play_midi_notes(self): 68 | pass 69 | 70 | def test_publish_midi_notes(self): 71 | pass 72 | 73 | -------------------------------------------------------------------------------- /rtpmidi/test/test_packets.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import time 3 | from twisted.trial import unittest 4 | 5 | from rtpmidi.protocols.rtp.packets import RTCPPacket 6 | from rtpmidi.protocols.rtp.packets import RTCPCompound 7 | from rtpmidi.protocols.rtp.packets import ext_32_out_of_64 8 | from rtpmidi.protocols.rtp.packets import unformat_from_32 9 | from rtpmidi.protocols.rtp.packets import RTPPacket 10 | 11 | ##################RTCP##################### 12 | class TestRTCPPacket(unittest.TestCase): 13 | """Testing RTCPPacket class""" 14 | def setUp(self): 15 | self.member = {'user_name': "name", 'cname': "user@host", 'tool': "none", 16 | 'addr':0, 'rtp_port':0, 'rtcp_port':0, 17 | 'last_rtp_received':0, 'last_rtcp_received':0, 18 | 'total_received_bytes':0, 'total_received_packets':0, 19 | 'last_seq':0, 'lost':0, 'last_ts':0, 'last_time':0, 20 | 'jitter':0, 'lsr':0, 'dlsr': 0, 21 | 'rt_time':0} 22 | 23 | def tearDown(self): 24 | pass 25 | 26 | def test_ext_32_out_of_64(self): 27 | ref = 10 28 | res = ext_32_out_of_64(ref) 29 | ref = 10 << 16 30 | assert(res==ref), self.fail("Wrong encoding") 31 | 32 | 33 | def test_unformat_from_32(self): 34 | ref = 10 35 | res = ext_32_out_of_64(ref) 36 | res_2 = unformat_from_32(res) 37 | assert(res_2==ref), self.fail("Wrong encoding or decoding") 38 | 39 | ref = 10.245 40 | res = ext_32_out_of_64(ref) 41 | res_2 = unformat_from_32(res) 42 | assert(res_2==ref), self.fail("Wrong encoding or decoding") 43 | 44 | def test_encode_SR(self): 45 | """Testing encode functione for SR""" 46 | ttt = time.time() 47 | cont \ 48 | = (ssrc, ntp, rtp_ts, total_packets, total_bytes, ssrc_1, 49 | frac_lost, lost, highest, jitter, lsr, dlsr) \ 50 | = (424242, ttt, 143, 100, 800, 424243, 1, 1, 65535, 15, 51 | int(time.time()-10), 10) 52 | 53 | members = {} 54 | new_member = self.member.copy() 55 | new_member['last_ts'] = rtp_ts 56 | new_member['last_seq'] = highest 57 | new_member['jitter'] = jitter 58 | new_member['lost'] = lost 59 | members_table = {} 60 | members_table[ssrc] = new_member 61 | 62 | arg_list = (ssrc, rtp_ts, 63 | total_packets, 64 | total_bytes, members_table) 65 | 66 | 67 | 68 | rtcp = RTCPPacket("SR", ptcode=200, contents=arg_list) 69 | rtcp_pac = rtcp.encode() 70 | 71 | #Testing result 72 | assert(type(rtcp_pac)==str), self.fail("Wrong type returned for SR" \ 73 | + " RTCP packet") 74 | 75 | assert(len(rtcp_pac) == 28), self.fail("Wrong size returned for SR" \ 76 | + " RTCP packet") 77 | 78 | def test_decode_SR(self): 79 | """Decode SR packet with only one feed""" 80 | ttt = time.time() 81 | (ssrc, ntp, rtp_ts, total_packets, total_bytes, ssrc_1, 82 | frac_lost, lost, highest, jitter, lsr, dlsr) \ 83 | = (424242, ttt, 143, 100, 800, 424243, 1, 1, 65535, 15, 84 | int(time.time()-10), 10) 85 | 86 | frac_lost = int(lost / float(total_packets + lost)) 87 | 88 | members = {} 89 | new_member = self.member.copy() 90 | new_member['last_ts'] = rtp_ts 91 | new_member['last_seq'] = highest 92 | new_member['jitter'] = jitter 93 | new_member['lost'] = lost 94 | new_member['lsr'] = lsr 95 | new_member['dlsr'] = dlsr 96 | 97 | members_table = {} 98 | members_table[ssrc_1] = new_member 99 | 100 | arg_list = (ssrc, rtp_ts, 101 | total_packets, 102 | total_bytes, members_table) 103 | 104 | rtcp = RTCPPacket("SR", ptcode=200, contents=arg_list) 105 | rtcp_pac = rtcp.encode() 106 | 107 | #Unpacking 108 | #Decoding packet 109 | packet = RTCPCompound(rtcp_pac) 110 | 111 | #Getting content of the first block 112 | cont = packet._rtcp[0].getContents() 113 | 114 | lsr = ext_32_out_of_64(lsr) 115 | lsr = unformat_from_32(lsr) 116 | 117 | dlsr = ext_32_out_of_64(dlsr) 118 | dlsr = unformat_from_32(dlsr) 119 | 120 | #Testing content decode 121 | assert(cont[0]== ssrc), self.fail("SSRC is not correctly encode or " \ 122 | + "decode") 123 | 124 | assert(int(cont[1]['ntpTS'])==int(ntp)), \ 125 | self.fail("NTP Timestamp is not correctly encode or decode") 126 | assert(cont[1]['packets']==total_packets), \ 127 | self.fail("Cumulative number of packets is not correctly encode or decode") 128 | assert(cont[1]['octets']==total_bytes), \ 129 | self.fail("Cumulative octets sum is not correctly encode or decode") 130 | assert(cont[1]['rtpTS']==rtp_ts), \ 131 | self.fail("RTP timestamp is not correctly encode or decode") 132 | 133 | assert(cont[2][0]['ssrc']==ssrc_1), \ 134 | self.fail("SSRC_1 is not correctly encode or decode") 135 | assert(cont[2][0]['jitter']==jitter), \ 136 | self.fail("Jitter is not correctly encode or decode") 137 | assert(cont[2][0]['fraclost']==frac_lost), \ 138 | self.fail("Frac lost is not correctly encode or decode") 139 | 140 | assert(cont[2][0]['lsr']==lsr), \ 141 | self.fail("Last received is not correctly encode or decode") 142 | assert(cont[2][0]['highest']==highest), \ 143 | self.fail("Highest seq num is not correctly encode or decode") 144 | 145 | def test_encode_RR(self): 146 | ttt = time.time() 147 | (ssrc, ntp, rtp_ts, total_packets, total_bytes, ssrc_1, \ 148 | frac_lost, lost, highest, jitter, lsr, dlsr) \ 149 | = (424242, ttt, 143, 100, 800, 424243, 1, 1, 65535, 15, \ 150 | int(time.time()-10), 10) 151 | 152 | frac_lost = int(lost / float(total_packets + lost)) 153 | 154 | members = {} 155 | new_member = self.member.copy() 156 | new_member['last_ts'] = rtp_ts 157 | new_member['last_seq'] = highest 158 | new_member['jitter'] = jitter 159 | new_member['lost'] = lost 160 | new_member['lsr'] = lsr 161 | new_member['dlsr'] = dlsr 162 | 163 | members_table = {} 164 | members_table[ssrc_1] = new_member 165 | 166 | arg_list = (ssrc, members_table) 167 | 168 | rtcp = RTCPPacket("RR", ptcode=201, contents=arg_list) 169 | rtcp_pac = rtcp.encode() 170 | 171 | #Testing result 172 | assert(type(rtcp_pac)==str), self.fail("Wrong type returned for RR" \ 173 | + " RTCP packet") 174 | assert(len(rtcp_pac) == 32), self.fail("Wrong size returned for RR" \ 175 | + " RTCP packet") 176 | 177 | def test_decode_RR(self): 178 | ttt = time.time() 179 | (ssrc, ntp, rtp_ts, total_packets, total_bytes, ssrc_1, \ 180 | frac_lost, lost, highest, jitter, lsr, dlsr) \ 181 | = (424242, ttt, 143, 100, 800, 424243, 1, 1, 65535, 15, \ 182 | int(time.time()-10), 10) 183 | 184 | frac_lost = int(lost / float(total_packets + lost)) 185 | 186 | members = {} 187 | new_member = self.member.copy() 188 | new_member['last_ts'] = rtp_ts 189 | new_member['last_seq'] = highest 190 | new_member['jitter'] = jitter 191 | new_member['lost'] = lost 192 | new_member['lsr'] = lsr 193 | new_member['dlsr'] = dlsr 194 | 195 | members_table = {} 196 | members_table[ssrc_1] = new_member 197 | 198 | arg_list = (ssrc, members_table) 199 | 200 | rtcp = RTCPPacket("RR", ptcode=201, contents=arg_list) 201 | rtcp_pac = rtcp.encode() 202 | 203 | #Unpacking 204 | #Decoding packet 205 | packet = RTCPCompound(rtcp_pac) 206 | 207 | #Getting content of the first block 208 | cont = packet._rtcp[0].getContents() 209 | 210 | lsr = ext_32_out_of_64(lsr) 211 | lsr = unformat_from_32(lsr) 212 | 213 | dlsr = ext_32_out_of_64(dlsr) 214 | dlsr = unformat_from_32(dlsr) 215 | 216 | #Testing content decode 217 | assert(cont[0]== ssrc), self.fail("SSRC is not correctly encode or " \ 218 | + "decode") 219 | assert(cont[1][0]['ssrc']==ssrc_1), \ 220 | self.fail("SSRC_1 is not correctly encode or decode") 221 | assert(cont[1][0]['jitter']==jitter), \ 222 | self.fail("Jitter is not correctly encode or decode") 223 | assert(cont[1][0]['fraclost']==frac_lost), \ 224 | self.fail("Frac lost is not correctly encode or decode") 225 | 226 | assert(cont[1][0]['lsr']==lsr), \ 227 | self.fail("Last received is not correctly encode or decode") 228 | assert(cont[1][0]['highest']==highest), \ 229 | self.fail("Highest seq num is not correctly encode or decode") 230 | 231 | def test_encode_BYE(self): 232 | """Test encode BYE packet with a single SSRC""" 233 | cont = (ssrc, reason) = ([4242], "because") 234 | rtcp = RTCPPacket("BYE", ptcode=203, contents=cont) 235 | rtcp_pac = rtcp.encode() 236 | 237 | assert(type(rtcp_pac)==str), \ 238 | self.fail("Wrong type returned by encode RTCP BYE packet") 239 | #MUST be padd ?? 240 | assert(len(rtcp_pac) == 16), \ 241 | self.fail("Wrong size returned by encode RTCP BYE packet") 242 | 243 | def test_decode_BYE(self): 244 | """Test decode BYE packet with a single SSRC""" 245 | cont = (ssrc, reason) = ([4242], "because") 246 | rtcp = RTCPPacket("BYE", ptcode=203, contents=cont) 247 | rtcp_pac = rtcp.encode() 248 | 249 | #Unpacking 250 | #Decoding packet 251 | packet = RTCPCompound(rtcp_pac) 252 | 253 | #Getting content of the first block 254 | cont = packet._rtcp[0].getContents() 255 | 256 | assert(cont[0][0]==ssrc[0]), \ 257 | self.fail("SSRC is not correctly encode or decode") 258 | assert(cont[1]==reason), \ 259 | self.fail("Reason is not correctly encode or decode") 260 | 261 | 262 | def test_encode_SDES(self): 263 | 264 | item = [] 265 | cont = [] 266 | item.append(("CNAME", "me@myself.mine")) 267 | item.append(("NAME", "memyselfandi")) 268 | item.append(("TOOL", "sropulpof_test")) 269 | cont.append((424242, item)) 270 | 271 | rtcp = RTCPPacket("SDES", ptcode=202, contents=cont) 272 | rtcp_pac = rtcp.encode() 273 | 274 | assert(type(rtcp_pac)==str), \ 275 | self.fail("Wrong type returned by encode RTCP SDES packet") 276 | 277 | #MUST be padd 278 | length_wait = 8 + 2 + len("me@myself.mine") + 2 + len("memyselfandi") + 2 + len("sropulpof_test") 279 | pad = 4 - (length_wait%4) 280 | length_wait += pad 281 | 282 | assert(len(rtcp_pac) == length_wait), \ 283 | self.fail("Wrong size returned by encode RTCP SDES packet") 284 | 285 | 286 | def test_decode_SDES(self): 287 | item = [] 288 | cont = [] 289 | item.append(("CNAME", "me@myself.mine")) 290 | item.append(("NAME", "memyselfandi")) 291 | item.append(("TOOL", "sropulpof_test")) 292 | cont.append((424242, item)) 293 | 294 | rtcp = RTCPPacket("SDES", ptcode=202, contents=cont) 295 | rtcp_pac = rtcp.encode() 296 | 297 | packets = RTCPCompound(rtcp_pac) 298 | 299 | for packet in packets: 300 | packet_type = packet.getPT() 301 | cont = packet.getContents() 302 | 303 | assert(packet_type=="SDES"), self.fail("Wrong type select by RTCPCompound") 304 | assert(cont[0][0]==424242), self.fail("Wrong ssrc") 305 | for elt in cont[0][1]: 306 | if elt[0] == "CNAME": 307 | assert(elt[1]=="me@myself.mine"), self.fail("wrong encoded for Cname") 308 | 309 | elif elt[0] == "NAME": 310 | assert(elt[1]=="memyselfandi"), self.fail("wrong encoded for Name") 311 | 312 | elif elt[0] == "TOOL": 313 | assert(elt[1]=="sropulpof_test"), self.fail("wrong encoded for Tool") 314 | 315 | 316 | class TestRTCPCompound(unittest.TestCase): 317 | def setUp(self): 318 | pass 319 | 320 | def tearDown(self): 321 | pass 322 | 323 | 324 | def test_encode(self): 325 | pass 326 | 327 | def test_decode(self): 328 | pass 329 | 330 | 331 | ##################RTP##################### 332 | class TestRTPPacket(unittest.TestCase): 333 | def setUp(self): 334 | pass 335 | 336 | def tearDown(self): 337 | pass 338 | 339 | class TestHeader(unittest.TestCase): 340 | def setUp(self): 341 | pass 342 | 343 | def tearDown(self): 344 | pass 345 | 346 | def test_init(self): 347 | #params: ssrc, pt, ct, seq, ts, marker=0, xhdrtype=None, xhdrdata='' 348 | pass 349 | 350 | def test_netbytes(self): 351 | pass 352 | 353 | 354 | def test_init(self): 355 | #params: ssrc, seq, ts, data, pt=None, ct=None, marker=0, 356 | # authtag='', xhdrtype=None, xhdrdata='' 357 | # 358 | test = "some data" 359 | ssrc, seq, ts, data, marker = (424242, 1, 143, test, 1) 360 | packet = RTPPacket(ssrc, seq, ts, data, marker=marker) 361 | #assert(len(packet)== 362 | 363 | def test_netbytes(self): 364 | pass 365 | 366 | def test_parse_rtppacket(self): 367 | pass 368 | 369 | 370 | 371 | class TestNTE(unittest.TestCase): 372 | def test_init(self): 373 | pass 374 | 375 | def test_getKey(self): 376 | pass 377 | 378 | def test_getPayload(self): 379 | pass 380 | 381 | def test_isDone(self): 382 | pass 383 | 384 | -------------------------------------------------------------------------------- /rtpmidi/test/test_protocol.py: -------------------------------------------------------------------------------- 1 | from twisted.trial import unittest 2 | from rtpmidi.protocols.rtp.protocol import RTPProtocol 3 | from rtpmidi.protocols.rtp.packets import RTPPacket 4 | 5 | class TestRTPProtocol(unittest.TestCase): 6 | def setUp(self): 7 | self.rtp = RTPProtocol(self, "ahahahahaha", 96) 8 | 9 | def tearDown(self): 10 | pass 11 | 12 | def test_checksum(self): 13 | #Test a good packet 14 | packet = RTPPacket(424242, 2, 10, "", pt=96) 15 | bytes = packet.netbytes() 16 | res = self.rtp.checksum(bytes) 17 | assert(res==1), self.fail("Wrong checksum for RTP packet") 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /rtpmidi/test/test_recovery_journal_chapters.py: -------------------------------------------------------------------------------- 1 | from twisted.trial import unittest 2 | 3 | from rtpmidi.engines.midi.recovery_journal_chapters import * 4 | 5 | class TestNote(unittest.TestCase): 6 | def setUp(self): 7 | self.note = Note() 8 | 9 | def test_note_on(self): 10 | #simple 11 | note_to_test = self.note.note_on(100, 90) 12 | 13 | #Testing type 14 | assert(type(note_to_test)==str), self.fail("Wrong type return") 15 | #length test 16 | assert(len(note_to_test)==2), \ 17 | self.fail("len of note On is higher than 2 octet") 18 | 19 | #with all args 20 | note_to_test = self.note.note_on(100, 90, 0, 1) 21 | #length test 22 | assert(len(note_to_test)==2), \ 23 | self.fail("len of note On is higher than 2 octet") 24 | 25 | def test_parse_note_on(self): 26 | #Simple 27 | note_to_test = self.note.note_on(100, 90) 28 | res_n = self.note.parse_note_on(note_to_test) 29 | 30 | #Testing content 31 | assert(res_n[1] == 100), self.fail("Note number is not respected") 32 | assert(res_n[3] == 90), self.fail("Note velocity is not respected") 33 | 34 | #With all args 35 | note_to_test = self.note.note_on(100, 90, 0, 1) 36 | res_n = self.note.parse_note_on(note_to_test) 37 | 38 | #Testing content 39 | assert(res_n[0] == 1), self.fail("S mark is not respected") 40 | assert(res_n[1] == 100), self.fail("Note number is not respected") 41 | assert(res_n[2] == 0), self.fail("Y mark not respected") 42 | assert(res_n[3] == 90), self.fail("Note velocity is not respected") 43 | 44 | def test_note_off(self): 45 | #list of notes to test (note from the same midi channel) 46 | plist = [[[128, 57, 100],1000], [[144, 4, 0],1000], \ 47 | [[144, 110, 0],1000], [[144, 112, 0],1000]] 48 | 49 | #setting low and high like in create_chapter_n 50 | high = 113 / 8 51 | low = 4 / 8 52 | 53 | #selecting note off like in create_chapter_n 54 | note_off_list = [ plist[i][0][1] for i in range(len(plist))\ 55 | if (plist[i][0][0]&240 == 128) or \ 56 | (plist[i][0][2] == 0) ] 57 | res = self.note.note_off(note_off_list, low, high) 58 | 59 | #type test 60 | assert(type(res)==str), self.fail("Wrong type return") 61 | 62 | #checking size 63 | size_wait = high - low + 1 64 | assert(len(res) == size_wait), \ 65 | self.fail("Problem of size with note off creation") 66 | 67 | def test_parse_note_off(self): 68 | """Test parse note off""" 69 | #list of notes to test 70 | #plist = [[[128, 120, 100],1000],[[145, 4, 0],1000],\ 71 | # [[145, 110, 0],1000], [[145, 112, 0],1000]] 72 | 73 | #setting low and high like in create_chapter_n 74 | note_off_test = [12, 57, 112, 114 ] 75 | high = 115 / 8 76 | low = 12 / 8 77 | 78 | res = self.note.note_off(note_off_test, low, high) 79 | 80 | #testing the result of parsing 81 | res_parsed = self.note.parse_note_off(res, low, high) 82 | 83 | #Testing type 84 | assert(type(res_parsed)==list), self.fail("Wrong type returned") 85 | 86 | #res_parsed.sort() 87 | #Testing content 88 | note_off_test = [12, 57, 112, 114 ] 89 | for i in range(len(note_off_test)): 90 | assert(res_parsed[i][1]==note_off_test[i]), \ 91 | self.fail("Problem getting the good value for note off encoded") 92 | 93 | 94 | class TestChapterP(unittest.TestCase): 95 | def setUp(self): 96 | self.chapter_p = ChapterP() 97 | 98 | #program change with msb and lsb 99 | self.plist = [[[176, 0, 75], 1000], [[176, 32, 110], 1000], \ 100 | [[192, 110, 0], 1000]] 101 | 102 | #program change without msb and lsb 103 | self.plist_1 = [[[192, 110, 0], 1000]] 104 | 105 | def test_update(self): 106 | """Testing chapter P creation from a list (with MSB and LSB)""" 107 | self.chapter_p.update(self.plist) 108 | chapter = self.chapter_p.content 109 | 110 | #Testing len 111 | assert(len(chapter)==3), \ 112 | self.fail("Size of chapter p is not 24 bits!!!") 113 | 114 | #Testing type 115 | assert(type(chapter)==str), self.fail("Problem of type") 116 | 117 | #Testing content 118 | size, chapter_parse, marker_s, marker_x, marker_b \ 119 | = self.chapter_p.parse(chapter) 120 | 121 | #Testing content 122 | assert(marker_s==1), \ 123 | self.fail("Problem getting right value of S") 124 | assert(chapter_parse[0][1]==110), \ 125 | self.fail("Problem getting right value of PROGRAM") 126 | assert(marker_b==1), \ 127 | self.fail("Problem getting right value of B") 128 | assert(chapter_parse[1][2]==75), \ 129 | self.fail("Problem getting right value of MSB") 130 | assert(marker_x==0), \ 131 | self.fail("Problem getting right value of X") 132 | assert(chapter_parse[2][2]==110), \ 133 | self.fail("Problem getting right value of LSB") 134 | 135 | def test_update_1(self): 136 | """Testing chapter P creation from a list (without MSB and LSB)""" 137 | self.chapter_p.update(self.plist_1) 138 | chapter = self.chapter_p.content 139 | 140 | #Testing len 141 | assert(len(chapter)==3), \ 142 | self.fail("Size of chapter p is not 24 bits!!!") 143 | 144 | #Testing type 145 | assert(type(chapter)==str), self.fail("Problem of type") 146 | 147 | #Testing content 148 | size, chapter_parse, marker_s, marker_x, marker_b \ 149 | = self.chapter_p.parse(chapter) 150 | 151 | #Testing content 152 | assert(marker_s==1), \ 153 | self.fail("Problem getting right value of S") 154 | assert(chapter_parse[0][1]==110), \ 155 | self.fail("Problem getting right value of PROGRAM") 156 | assert(marker_b==0), \ 157 | self.fail("Problem getting right value of B") 158 | assert(marker_x==0), \ 159 | self.fail("Problem getting right value of X") 160 | 161 | 162 | 163 | class TestChapterC(unittest.TestCase): 164 | def setUp(self): 165 | self.chapter_c = ChapterC() 166 | self.plist = [] 167 | 168 | for i in range(127): 169 | self.plist.append([[176, i, 100],6]) 170 | 171 | 172 | def test_header(self): 173 | """Test header creation ChapterC""" 174 | #Creating header 175 | header = self.chapter_c.header(10, 1) 176 | 177 | #Testing type 178 | assert(type(header)==str), self.fail("Wrong type returned") 179 | #Testing length 180 | assert(len(header)==1), self.fail("Wrong header size") 181 | 182 | def test_parse_header(self): 183 | """Test header parsing ChapterC""" 184 | #Creating header 185 | header = self.chapter_c.header(10, 1) 186 | 187 | #Parsing header 188 | header_parsed = self.chapter_c.parse_header(header) 189 | 190 | #Testing type 191 | assert(type(header_parsed)==tuple), self.fail("Wrong size returned") 192 | 193 | #Testing content 194 | assert(header_parsed[0]==1), self.fail("Wrong marker_s value") 195 | assert(header_parsed[1]==10), self.fail("Wrong length value") 196 | 197 | def test_create_log_c(self): 198 | """Test create log C (individual component from ChapterC""" 199 | res = self.chapter_c.create_log_c(0, 110, 1, 90) 200 | assert(type(res)==str), self.fail("Wrong type returned") 201 | assert(len(res)==2), self.fail("Wrong size returned") 202 | 203 | def test_parse_log_c(self): 204 | """Test parsing individual component from chapterC""" 205 | res = self.chapter_c.create_log_c(0, 110, 1, 90) 206 | res_parsed = self.chapter_c.parse_log_c(res) 207 | 208 | assert(res_parsed[0]==0), self.fail("Wrong value for marker_s") 209 | assert(res_parsed[1]==110), self.fail("Wrong value for number") 210 | assert(res_parsed[2]==1), self.fail("Wrong value for marker_a") 211 | assert(res_parsed[3]==90), self.fail("Wrong value for value") 212 | 213 | 214 | def test_update(self): 215 | """Testing chapter C creation""" 216 | self.chapter_c.update(self.plist) 217 | assert(type(self.chapter_c.content)==str), self.fail("Wrong type returned") 218 | 219 | #length calc header == 1 + 2 * length 220 | length_wait = 1 + 2 * len(self.plist) 221 | assert(len(self.chapter_c.content)==length_wait), self.fail("Wrong length returned") 222 | 223 | 224 | def test_update_1(self): 225 | self.plist.append([[176, 42, 100],6]) 226 | self.chapter_c.update(self.plist) 227 | length_wait = 1 + 2 * 127 228 | assert(len(self.chapter_c.content)==length_wait), self.fail("Wrong length returned") 229 | 230 | 231 | def test_parse(self): 232 | """Test chapter C parsing""" 233 | self.chapter_c.update(self.plist) 234 | 235 | size, parsed_res, marker_s = self.chapter_c.parse(self.chapter_c.content) 236 | assert(len(parsed_res)==len(self.plist)), \ 237 | self.fail("Wrong number of command returned") 238 | 239 | for i in range(len(self.plist)): 240 | assert(parsed_res[i][0]==self.plist[i][0][0]), \ 241 | self.fail("Wrong value returned for cmd") 242 | assert(parsed_res[i][1]==self.plist[i][0][1]), \ 243 | self.fail("Wrong value returned for pitch") 244 | assert(parsed_res[i][2]==self.plist[i][0][2]), \ 245 | self.fail("Wrong value returned for velocity") 246 | 247 | 248 | 249 | def test_trim(self): 250 | plist = [] 251 | plist.append([[176, 42, 100],6]) 252 | plist.append([[176, 43, 100],7]) 253 | plist.append([[176, 44, 100],8]) 254 | self.chapter_c.update(plist) 255 | self.chapter_c.trim(7) 256 | assert(len(self.chapter_c.controllers)==1), self.fail("Problem erasing controllers on trim") 257 | 258 | 259 | def test_update_highest(self): 260 | plist = [] 261 | plist.append([[176, 42, 100],6]) 262 | plist.append([[176, 43, 100],7]) 263 | plist.append([[176, 44, 100],8]) 264 | 265 | self.chapter_c.update(plist) 266 | assert(self.chapter_c.highest==8), \ 267 | self.fail("Problem with highest on update") 268 | 269 | self.chapter_c.trim(7) 270 | assert(self.chapter_c.highest==8), \ 271 | self.fail("Problem with highest on trim(1)") 272 | 273 | self.chapter_c.trim(8) 274 | assert(self.chapter_c.highest==0), \ 275 | self.fail("Problem with highest on trim(2)") 276 | 277 | 278 | class TestChapterW(unittest.TestCase): 279 | def setUp(self): 280 | self.chapter_w = ChapterW() 281 | self.plist = [[[224, 0, 120], 6], [[224, 1, 110], 6]] 282 | 283 | def test_update(self): 284 | """Test create chapter W""" 285 | self.chapter_w.update(self.plist) 286 | 287 | assert(type(self.chapter_w.content)==str), self.fail("Wrong type returned") 288 | assert(len(self.chapter_w.content)==2), \ 289 | self.fail("Wrong size for chapter W part in recovery journal") 290 | 291 | def test_parse(self): 292 | self.chapter_w.update(self.plist) 293 | size, res_2, mark_s = self.chapter_w.parse(self.chapter_w.content) 294 | assert(mark_s == 1), \ 295 | self.fail("Wrong value for S bit in Chapter W") 296 | assert(res_2[0][2]==120), \ 297 | self.fail("Wrong value for wheel_1 in Chapter W") 298 | assert(res_2[1][2]==110), \ 299 | self.fail("Wrong value for wheel_2 in Chapter W") 300 | 301 | def test_trim(self): 302 | self.chapter_w.update(self.plist) 303 | self.chapter_w.trim(6) 304 | 305 | for data in self.chapter_w.data_list: 306 | assert(data[0]==0), self.fail("Problem trimming chapter") 307 | 308 | assert(self.chapter_w.highest==0), self.fail("Wrong update for highest") 309 | 310 | 311 | class TestChapterN(unittest.TestCase): 312 | def setUp(self): 313 | self.chapter_n = ChapterN() 314 | self.plist_on = [] 315 | self.plist_off = [] 316 | 317 | #List of notes to test 318 | #Note on 319 | for i in range(127): 320 | self.plist_on.append([[144, i, 100],6]) 321 | 322 | #Note off 323 | for i in range(127): 324 | self.plist_off.append([[128, i, 100],7]) 325 | 326 | 327 | def test_header(self): 328 | """Test Create header of chapterN """ 329 | #Creating chapter 330 | self.chapter_n.update(self.plist_on) 331 | 332 | res = self.chapter_n.header() 333 | 334 | #length type test 335 | assert(len(res)==2), self.fail("length of header is not good") 336 | assert(type(res)==str), self.fail("Wrong type return") 337 | 338 | def test_parse_header(self): 339 | """Test parse header of ChapterN""" 340 | #Creating chapter 341 | self.chapter_n.update(self.plist_off) 342 | 343 | res = self.chapter_n.header() 344 | 345 | #Parsing 346 | res_parsed = self.chapter_n.parse_header(res) 347 | 348 | #Testing type 349 | assert(type(res_parsed)==tuple), self.fail("Wrong type return") 350 | 351 | #Testing content 352 | assert(res_parsed[1]==0), \ 353 | self.fail("Problem getting good value of LEN") 354 | assert(res_parsed[2]==0), \ 355 | self.fail("Problem getting good value of LOW") 356 | assert(res_parsed[3]==15), \ 357 | self.fail("Problem getting good value of HIGH") 358 | 359 | 360 | def test_update(self): 361 | """Update with 127 note_off""" 362 | self.chapter_n.update(self.plist_off) 363 | 364 | #Test len content 365 | length_wait = 128 / 8 + 2 366 | 367 | assert(len(self.chapter_n.content)==length_wait), \ 368 | self.fail("Wrong size for chapter encoded returned") 369 | 370 | #Test note_on 371 | assert(len(self.chapter_n.note_on)==0), \ 372 | self.fail("Wrong nb of note on recorded") 373 | 374 | #Test note_off 375 | assert(len(self.chapter_n.note_off)==127), \ 376 | self.fail("Wrong nb of note off recorded") 377 | 378 | #Test low 379 | assert(self.chapter_n.low==0), self.fail("Wrong low calculation") 380 | 381 | #Test high 382 | assert(self.chapter_n.high==15), self.fail("Wrong high calculation") 383 | 384 | #TEst highest 385 | assert(self.chapter_n.highest==7), self.fail("Wrong highest saved") 386 | 387 | def test_update_1(self): 388 | """Update with 127 note_on""" 389 | self.chapter_n.update(self.plist_on) 390 | 391 | #Test len content 392 | length_wait = 127 * 2 + 2 393 | 394 | assert(len(self.chapter_n.content)==length_wait), \ 395 | self.fail("Wrong size for chapter encoded returned") 396 | 397 | #Test note_on 398 | assert(len(self.chapter_n.note_on)==127), \ 399 | self.fail("Wrong nb of note on recorded") 400 | 401 | #Test note_off 402 | assert(len(self.chapter_n.note_off)==0), \ 403 | self.fail("Wrong nb of note off recorded") 404 | 405 | #Test low 406 | assert(self.chapter_n.low==0), self.fail("Wrong low calculation") 407 | 408 | #Test high 409 | assert(self.chapter_n.high==0), self.fail("Wrong high calculation") 410 | 411 | #TEst highest 412 | assert(self.chapter_n.highest==6), self.fail("Wrong highest saved") 413 | 414 | def test_update_2(self): 415 | """Update with note_on / off and ...""" 416 | self.plist_on.append([[144, 42, 100],6]) 417 | self.chapter_n.update(self.plist_on) 418 | 419 | #Test len content 420 | length_wait = 127 * 2 + 2 421 | assert(len(self.chapter_n.content)==length_wait), \ 422 | self.fail("Wrong size for chapter encoded returned") 423 | 424 | assert(len(self.chapter_n.note_on)==127), \ 425 | self.fail("Wrong nb of note on recorded") 426 | 427 | self.chapter_n.update(self.plist_off) 428 | 429 | #Test len content 430 | length_wait = 128 / 8 + 2 431 | 432 | assert(len(self.chapter_n.content)==length_wait), \ 433 | self.fail("Wrong size for chapter encoded returned") 434 | 435 | #Test note_on 436 | assert(len(self.chapter_n.note_on)==0), \ 437 | self.fail("Wrong nb of note on recorded") 438 | 439 | #Test note_off 440 | assert(len(self.chapter_n.note_off)==127), \ 441 | self.fail("Wrong nb of note off recorded") 442 | 443 | 444 | def test_parse(self): 445 | """ Test parse chapter N with several notes""" 446 | #creating chapter 447 | self.chapter_n.update(self.plist_off) 448 | 449 | size, notes_parsed = self.chapter_n.parse(self.chapter_n.content) 450 | assert(len(notes_parsed)==127), self.fail("Wrong number of notes returned") 451 | assert(size==18), self.fail("Wrong size of encoded chapter") 452 | 453 | def test_parse_2(self): 454 | off_mont = [[[128, 62, 100],1000]] 455 | self.chapter_n.update(off_mont) 456 | size, notes_parsed = self.chapter_n.parse(self.chapter_n.content) 457 | 458 | 459 | def test_trim(self): 460 | self.chapter_n.update(self.plist_off) 461 | self.chapter_n.trim(6) 462 | 463 | #Test highest 464 | assert(self.chapter_n.highest==7), \ 465 | self.fail("Wrong highest saved") 466 | 467 | #Test note_on 468 | assert(len(self.chapter_n.note_on)==0), \ 469 | self.fail("Wrong nb of note on recorded") 470 | 471 | #Test note_off 472 | assert(len(self.chapter_n.note_off)==127), \ 473 | self.fail("Wrong nb of note off recorded") 474 | 475 | self.chapter_n.trim(7) 476 | assert(len(self.chapter_n.note_off)==0), \ 477 | self.fail("Wrong nb of note off recorded after trim") 478 | 479 | 480 | def test_update_highest(self): 481 | plist = [] 482 | plist.append([[144, 1, 100],6]) 483 | plist.append([[144, 1, 100],7]) 484 | plist.append([[144, 1, 100],8]) 485 | 486 | self.chapter_n.update(plist) 487 | assert(self.chapter_n.highest==8), \ 488 | self.fail("wrong update of highest on update") 489 | 490 | self.chapter_n.trim(7) 491 | assert(self.chapter_n.highest==8), \ 492 | self.fail("wrong update of highest on trim") 493 | 494 | self.chapter_n.trim(8) 495 | assert(self.chapter_n.highest==0), \ 496 | self.fail("wrong update of highest on trim") 497 | 498 | 499 | class TestChapterT(unittest.TestCase): 500 | def setUp(self): 501 | self.chap_t = ChapterT() 502 | 503 | def test_update(self): 504 | """Test Create Chapter T (After Touch)""" 505 | plist = [[[208, 80, 98], 1000]] 506 | self.chap_t.update(plist) 507 | res = self.chap_t.content 508 | assert(type(res)==str), self.fail("Wrong type returned") 509 | assert(len(res) == 1), self.fail("Wrong size returned") 510 | 511 | assert(self.chap_t.highest==1000), self.fail("Problem with highest update") 512 | 513 | def test_parse(self): 514 | """Test parse Chapter T""" 515 | self.chap_t.update( [[[208, 80, 0], 1000]]) 516 | res = self.chap_t.content 517 | 518 | size, midi_cmd = self.chap_t.parse(res) 519 | pressure = midi_cmd[0][1] 520 | assert(size==1), self.fail("Wrong size returned") 521 | assert(pressure==80), self.fail("Wrong value returned for pressure") 522 | 523 | 524 | class TestChapterA(unittest.TestCase): 525 | def setUp(self): 526 | self.chap_a = ChapterA() 527 | 528 | def test_header(self): 529 | """Test header for Chapter A""" 530 | res = self.chap_a.header(1, 127) 531 | assert(type(res)==str), self.fail("Wrong type returned") 532 | assert(len(res)==1), self.fail("Wrong size returned") 533 | 534 | def test_parse_header(self): 535 | """Test parse header Chapter A""" 536 | res = self.chap_a.header(1, 127) 537 | marker_s, length = self.chap_a.parse_header(res) 538 | assert(marker_s==1), self.fail("Wrong value returned for marker S") 539 | assert(length==127), self.fail("Wrong value returned for length") 540 | 541 | def test_create_log_a(self): 542 | """Test Create log A""" 543 | res = self.chap_a.create_log_a(1, 127, 1, 127) 544 | assert(type(res)==str), self.fail("Wrong type returned") 545 | assert(len(res)==2), self.fail("Wrong size returned") 546 | 547 | def test_parse_log_a(self): 548 | """Test Parse log A""" 549 | res = self.chap_a.create_log_a(1, 127, 1, 110) 550 | marker_s, notenum, marker_x, pressure = self.chap_a.parse_log_a(res) 551 | 552 | assert(marker_s==1), self.fail("Wrong value returned for marker S") 553 | assert(notenum==127), self.fail("Wrong value returned for length") 554 | assert(marker_x==1), self.fail("Wrong value returned for marker S") 555 | assert(pressure==110), self.fail("Wrong value returned for length") 556 | 557 | def test_update(self): 558 | """Test create Chapter A""" 559 | midi_cmd = [[[160, 80, 98], 1000], [[160, 82, 90], 1000]] 560 | self.chap_a.update(midi_cmd) 561 | res = self.chap_a.content 562 | len_expected = 1 + 2 * len(midi_cmd) 563 | 564 | assert(type(res)==str), self.fail("Wrong type returned") 565 | assert(len(res)==len_expected), self.fail("Wrong size returned") 566 | 567 | def test_update_1(self): 568 | """Test create Chapter A with a big amount of commands""" 569 | #With 127 notes (max is 127) 570 | midi_cmd = [] 571 | for i in range(127): 572 | midi_cmd.append([[160, i, 98], 1]) 573 | 574 | 575 | self.chap_a.update(midi_cmd) 576 | 577 | #Test content 578 | res = self.chap_a.content 579 | size, marker_s, midi_cmd_parsed = self.chap_a.parse(res) 580 | size_waited = 1 + 2 *127 581 | assert(size==size_waited), self.fail("Wrong size returned for 127 notes(1) !") 582 | 583 | midi_cmd = [] 584 | midi_cmd.append([[160, 42, 98], 2]) 585 | self.chap_a.update(midi_cmd) 586 | 587 | #Test content 588 | res = self.chap_a.content 589 | size, marker_s, midi_cmd_parsed = self.chap_a.parse(res) 590 | assert(size==size_waited), self.fail("Wrong size returned for 127 notes(2) !") 591 | 592 | 593 | def test_update_2(self): 594 | """Test create Chapter A with a big amount of commands 595 | in a lonely function call""" 596 | #With 127 notes (max is 127) 597 | midi_cmd = [] 598 | for i in range(127): 599 | midi_cmd.append([[160, i, 98], 1]) 600 | 601 | for i in range(127): 602 | midi_cmd.append([[160, i, 98], 1]) 603 | 604 | 605 | self.chap_a.update(midi_cmd) 606 | 607 | #Test content 608 | res = self.chap_a.content 609 | size, marker_s, midi_cmd_parsed = self.chap_a.parse(res) 610 | size_waited = 1 + 2 *127 611 | assert(size==size_waited), self.fail("Wrong size returned for 127 notes(1) !") 612 | 613 | 614 | def test_parse(self): 615 | """Test parsing chapterA""" 616 | midi_cmd = [[[160, 80, 98], 1000], [[160, 82, 90], 1000]] 617 | self.chap_a.update(midi_cmd) 618 | res = self.chap_a.content 619 | 620 | size, marker_s, midi_cmd_parsed = self.chap_a.parse(res) 621 | 622 | assert(size==5), self.fail("Wrong value for size returned") 623 | assert(marker_s==1), self.fail("Wrong value for marker_s returned") 624 | assert(len(midi_cmd)==len(midi_cmd)), self.fail("Wrong size returned") 625 | 626 | for i in range(len(midi_cmd)): 627 | assert(midi_cmd[i][0]==midi_cmd_parsed[i]), \ 628 | self.fail("Wrong value returned") 629 | 630 | 631 | 632 | def test_trim(self): 633 | """Test trim without note remplacement""" 634 | #Adding Packet 1000 635 | midi_cmd = [[[160, 80, 98], 1000], [[160, 82, 90], 1000]] 636 | self.chap_a.update(midi_cmd) 637 | 638 | #Adding Packet 1001 639 | midi_cmd = [[[160, 84, 98], 1001], [[160, 86, 90], 1001]] 640 | self.chap_a.update(midi_cmd) 641 | 642 | #Adding Packet 1002 643 | midi_cmd = [[[160, 88, 98], 1002], [[160, 90, 90], 1002]] 644 | self.chap_a.update(midi_cmd) 645 | 646 | self.chap_a.trim(1001) 647 | 648 | res = self.chap_a.parse(self.chap_a.content) 649 | 650 | def test_update_highest(self): 651 | #Adding Packet 1000 652 | midi_cmd = [[[160, 80, 98], 1000], [[160, 82, 90], 1000]] 653 | self.chap_a.update(midi_cmd) 654 | 655 | self.chap_a.update_highest() 656 | assert(self.chap_a.highest==1000), \ 657 | self.fail("Update problem for highest after an update") 658 | 659 | #Adding Packet 1001 660 | midi_cmd = [[[160, 84, 98], 1001], [[160, 86, 90], 1001]] 661 | self.chap_a.update(midi_cmd) 662 | 663 | self.chap_a.update_highest() 664 | assert(self.chap_a.highest==1001), \ 665 | self.fail("Update problem for highest after an update") 666 | 667 | 668 | self.chap_a.trim(1001) 669 | assert(self.chap_a.highest==0), \ 670 | self.fail("Update problem for highest after an trim") 671 | 672 | -------------------------------------------------------------------------------- /rtpmidi/test/test_rtcp.py: -------------------------------------------------------------------------------- 1 | from rtpmidi.protocols.rtp.rtcp import RTCPProtocol 2 | from rtpmidi.protocols.rtp.packets import RTCPCompound 3 | from rtpmidi.protocols.rtp.packets import RTCPPacket 4 | 5 | from twisted.trial import unittest 6 | from time import time 7 | 8 | class FakeRTP(object): 9 | def __init__(self): 10 | self.last_sent_time = 0 11 | self.ssrc = 0 12 | self.rtp_ts = 143 13 | self.total_packets = 100 14 | self.total_bytes = 800 15 | self.session_bw = 1024 16 | 17 | class TestRTCPProtocol(unittest.TestCase): 18 | def setUp(self): 19 | rtp = FakeRTP() 20 | rtp.ssrc = 424242 21 | self.rtcp = RTCPProtocol(rtp, "localhost") 22 | 23 | #Some members to test 24 | new_member = self.rtcp.member.copy() 25 | new_member['cname'] = "joe@host1" 26 | new_member['user_name'] = "joe" 27 | new_member['tool'] = "sropulpof1" 28 | self.rtcp.members_table[424242] = new_member 29 | 30 | new_member_2 = self.rtcp.member.copy() 31 | new_member_2['cname'] = "jack@host1" 32 | new_member_2['user_name'] = "jack" 33 | new_member_2['tool'] = "sropulpof2" 34 | self.rtcp.members_table[434343] = new_member_2 35 | 36 | def tearDown(self): 37 | del self.rtcp 38 | 39 | #Receiving function 40 | def test_receiveSR(self): 41 | pass 42 | 43 | def test_receiveSRRR(self): 44 | pass 45 | 46 | def test_receiveSDES(self): 47 | packet = self.rtcp.send_SDES() 48 | compound = RTCPCompound() 49 | compound.addPacket(packet) 50 | compound_enc = compound.encode() 51 | 52 | new_compound = RTCPCompound(compound_enc) 53 | #packet_type = packet.getPT() 54 | #cont = packet.getContents() 55 | #print cont 56 | #self.rtcp.receiveSDES(cont) 57 | 58 | 59 | def test_receiveBYE(self): 60 | #First test 61 | reason = "Normal quit" 62 | arg_list = ([self.rtcp.rtp.ssrc], reason) 63 | 64 | rtcp = RTCPPacket("BYE", ptcode=203, contents=arg_list) 65 | rtcp_pac = rtcp.encode() 66 | 67 | #Decoding as datagram received 68 | packets = RTCPCompound(rtcp_pac) 69 | packet_type = packets[0].getPT() 70 | cont = packets[0].getContents() 71 | ssrc = cont[0][0] 72 | reas_gave = cont[1] 73 | 74 | #Checking BYE format 75 | assert(packet_type == "BYE"), \ 76 | self.fail("Wrong packet type encoded or decoded for BYE") 77 | assert(ssrc == self.rtcp.rtp.ssrc), \ 78 | self.fail("Wrong SSRC encoded or decoded for BYE") 79 | assert(reas_gave == reason), \ 80 | self.fail("Wrong reason encoded or decoded for BYE") 81 | 82 | 83 | #SEcond TEst 84 | arg_list = ([434343], reason) 85 | rtcp = RTCPPacket("BYE", ptcode=203, contents=arg_list) 86 | rtcp_pac = rtcp.encode() 87 | #Decoding as datagram received 88 | packets = RTCPCompound(rtcp_pac) 89 | packet_type = packets[0].getPT() 90 | cont = packets[0].getContents() 91 | self.rtcp.receiveBYE(cont) 92 | 93 | #Checking members table 94 | assert(len(self.rtcp.members_table)==1), \ 95 | self.fail("Forget to delete member when receiving BYE packet from him!") 96 | for ssrc in self.rtcp.members_table: 97 | assert(ssrc==424242), \ 98 | self.fail("Wrong member erase after receiving BYE packet") 99 | 100 | 101 | #Sending function 102 | def test_send_SDES(self): 103 | 104 | res = self.rtcp.send_SDES() 105 | #print res 106 | 107 | #Testing res 108 | 109 | 110 | 111 | #Check functions 112 | def test_check_ssrc_timeout_1(self): 113 | #Timing_out two members 114 | #Setting date 115 | self.rtcp.members_table[434343]['last_rtcp_received'] = time() - 16 116 | self.rtcp.members_table[424242]['last_rtcp_received'] = time() - 17 117 | self.rtcp.check_ssrc_timeout() 118 | 119 | #Testing res 120 | assert(len(self.rtcp.members_table)==0), \ 121 | self.fail("Problem deleting members after SSRC timeout") 122 | 123 | def test_check_ssrc_timeout_1(self): 124 | #Timing_out one members 125 | #Setting date 126 | self.rtcp.members_table[434343]['last_rtcp_received'] = time() - 16 127 | self.rtcp.members_table[424242]['last_rtcp_received'] = time() 128 | self.rtcp.check_ssrc_timeout() 129 | 130 | #Testing res 131 | assert(len(self.rtcp.members_table)==1), \ 132 | self.fail("Problem deleting members after SSRC timeout") 133 | 134 | for ssrc in self.rtcp.members_table: 135 | assert(ssrc==424242), \ 136 | self.fail("Wrong member erase after SSRC timeout") 137 | 138 | def test_check_ssrc_1(self): 139 | #check_ssrc(ssrc, addr, ptype, cname) 140 | #Use cases: 141 | #First SSRC view 142 | #Confirm member 143 | 144 | #TODO see CNAME 145 | ssrc = 484848 146 | self.rtcp.check_ssrc(ssrc, ("192.168.0.1", 44001), "SR", "bonjour") 147 | 148 | assert(len(self.rtcp.members_table)==3), \ 149 | self.fail("Problem adding a new member") 150 | if not ssrc in self.rtcp.members_table: 151 | self.fail("Problem adding a new member") 152 | 153 | assert(self.rtcp.members_table[ssrc]['addr']=="192.168.0.1"), \ 154 | self.fail("Wrong address added for the new member") 155 | assert(self.rtcp.members_table[ssrc]['rtcp_port']==44001), \ 156 | self.fail("Wrong RTCP port added for the new member") 157 | 158 | self.rtcp.check_ssrc(ssrc, ("192.168.0.1", 44000), "DATA", "bonjour") 159 | assert(self.rtcp.members_table[ssrc]['rtp_port']==44000), \ 160 | self.fail("Wrong RTP port added for the confirm member") 161 | 162 | 163 | def test_check_ssrc_2(self): 164 | #Use cases: 165 | #collision with confirmed guest 166 | #loop 167 | ssrc = 484848 168 | self.rtcp.check_ssrc(ssrc, ("192.168.0.1", 44001), "SR", "bonjour") 169 | self.rtcp.check_ssrc(ssrc, ("192.168.0.1", 44000), "DATA", "bonjour") 170 | 171 | 172 | self.rtcp.check_ssrc(ssrc, ("192.168.0.2", 44000), "SDES", "coucou") 173 | #Test nb_collision var 174 | 175 | self.rtcp.check_ssrc(ssrc, ("192.168.0.2", 44000), "DATA", "coucou") 176 | #Test nb_loop var 177 | 178 | 179 | def test_check_ssrc_3(self): 180 | #Use cases: 181 | #conflicting addr 182 | #SSRC collision 183 | pass 184 | 185 | def test_we_sent_timeout(self): 186 | #We sent timeout is based on transmission interval value 187 | self.rtcp.compute_transmission_interval() 188 | 189 | #Not timing out 190 | self.rtcp.rtp.last_sent_time = time() 191 | self.rtcp.we_sent = True 192 | self.rtcp.we_sent_time_out() 193 | assert(self.rtcp.we_sent==True), self.fail("Wrong update of we_sent") 194 | 195 | 196 | #Timing out (== 2 * 2.5) 197 | self.rtcp.rtp.last_sent_time = time() - 6 198 | self.rtcp.we_sent = True 199 | 200 | self.rtcp.we_sent_time_out() 201 | 202 | assert(self.rtcp.we_sent==False), \ 203 | self.fail("Wrong update of we_sent when timingout") 204 | 205 | 206 | def test_checksum(self): 207 | 208 | #Testing good checksum packet 209 | reason = "because" 210 | arg_list = ([self.rtcp.rtp.ssrc], reason) 211 | 212 | #Testing BYE PAcket 213 | packet_to_test = RTCPPacket("BYE", ptcode=203, contents=arg_list) 214 | bytes = packet_to_test.encode() 215 | res = self.rtcp.checksum(bytes) 216 | assert(res==1), self.fail("Wrong checksum for BYE packet") 217 | 218 | 219 | arg_list = (self.rtcp.rtp.ssrc, self.rtcp.rtp.rtp_ts, \ 220 | self.rtcp.rtp.total_packets, \ 221 | self.rtcp.rtp.total_bytes, self.rtcp.members_table) 222 | 223 | #Test SR packet 224 | packet_to_test = RTCPPacket("SR", ptcode=201, contents=arg_list) 225 | compound = RTCPCompound() 226 | compound.addPacket(packet_to_test) 227 | bytes = compound.encode() 228 | res = self.rtcp.checksum(bytes) 229 | assert(res==1), self.fail("Wrong checksum for SR packet") 230 | 231 | arg_list = (self.rtcp.rtp.ssrc, self.rtcp.members_table) 232 | 233 | #Test RR packet 234 | packet_to_test = RTCPPacket("RR", ptcode=200, contents=arg_list) 235 | compound = RTCPCompound() 236 | compound.addPacket(packet_to_test) 237 | bytes = compound.encode() 238 | res = self.rtcp.checksum(bytes) 239 | assert(res==1), self.fail("Wrong checksum for RR packet") 240 | -------------------------------------------------------------------------------- /rtpmidi/test/test_rtp_control.py: -------------------------------------------------------------------------------- 1 | from twisted.trial import unittest 2 | from rtpmidi.protocols.rtp.rtp_control import * 3 | 4 | class TestRTPMIDIControl(unittest.TestCase): 5 | """Testing RTPMidiControl function""" 6 | #Test rtp_control 7 | def test_start(self): 8 | pass 9 | 10 | def test_create_rtp_socket(self): 11 | pass 12 | 13 | def test_incoming_rtp(self): 14 | pass 15 | 16 | def test_send_silent_packet(self): 17 | pass 18 | 19 | def test_send_midi_data(self): 20 | pass 21 | 22 | def test_stop(self): 23 | pass 24 | 25 | def test_drop_call(self): 26 | pass 27 | 28 | def test_make_cookie(self): 29 | pass 30 | -------------------------------------------------------------------------------- /rtpmidi/utils.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Scenic 5 | # Copyright (C) 2008 Société des arts technologiques (SAT) 6 | # http://www.sat.qc.ca 7 | # All rights reserved. 8 | # 9 | # This file is free software: you can redistribute it and/or modify 10 | # it under the terms of the GNU General Public License as published by 11 | # the Free Software Foundation, either version 2 of the License, or 12 | # (at your option) any later version. 13 | # 14 | # Scenic 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 Scenic. If not, see . 21 | """ 22 | Utilities only used within runner.py 23 | """ 24 | import re 25 | 26 | def check_ip(address): 27 | """ 28 | Check address format 29 | """ 30 | if re.match('^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$', address): 31 | values = [int(i) for i in address.split('.')] 32 | if ip_range(values): 33 | return True 34 | else: 35 | return False 36 | else: 37 | return False 38 | 39 | def ip_range(nums): 40 | for num in nums: 41 | if num < 0 or num > 255: 42 | return False 43 | return True 44 | 45 | def check_port(portnum): 46 | """ 47 | Checks if the port is in a usable range. 48 | """ 49 | #TODO: do not allow ports for which you must be root to use them. 50 | if 0 < portnum < 65535: 51 | return True 52 | else: 53 | return False 54 | -------------------------------------------------------------------------------- /run-midi: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # This file is not included in the distributed package, nor installed 3 | # It is only useful for developers to simplify launching scenic. 4 | cd $(dirname $0) 5 | ./scripts/midistream $@ 6 | -------------------------------------------------------------------------------- /scripts/midistream.in: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Scenic 5 | # Copyright (C) 2008 Société des arts technologiques (SAT) 6 | # http://www.sat.qc.ca 7 | # All rights reserved. 8 | # 9 | # This file is free software: you can redistribute it and/or modify 10 | # it under the terms of the GNU General Public License as published by 11 | # the Free Software Foundation, either version 2 of the License, or 12 | # (at your option) any later version. 13 | # 14 | # Scenic 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 Scenic. If not, see . 21 | """ 22 | Executable script which calls rtpmidi.runner.run() 23 | """ 24 | import os 25 | import sys 26 | 27 | SCRIPTS_DIR = "scripts" 28 | VERSION = """@PACKAGE_VERSION@""" 29 | PYTHON_LIB_PATH = """@PYTHON_LIB_PATH@"""# path to where the module is installed 30 | 31 | def _is_in_devel(): 32 | d = os.path.split(os.path.dirname(os.path.abspath(__file__)))[1] 33 | return d == SCRIPTS_DIR 34 | 35 | if __name__ == "__main__": 36 | if _is_in_devel(): 37 | d = os.path.split(os.path.dirname(os.path.abspath(__file__)))[0] 38 | sys.path.insert(0, d) 39 | os.environ["PATH"] += ":%s" % (os.path.join(d, SCRIPTS_DIR)) 40 | from rtpmidi import runner 41 | 42 | runner.run(VERSION) 43 | 44 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf8 -*- 2 | 3 | from distutils.core import setup 4 | from textwrap import dedent 5 | 6 | setup(name='rtpmidi', 7 | version='0.7.1', 8 | description='Send and receive midi data over RTP', 9 | author=dedent(''' 10 | Tristan Matthews 11 | Alexandre Quessy 12 | Simon Piette 13 | Philippe Chevry 14 | Koya Charles 15 | Antoine Collet 16 | Sylvain Cormier 17 | Étienne Désautels 18 | Hugo Boyer'''), 19 | author_email=dedent(''' 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | '''), 29 | url='http://github.com/avsaj/rtpmidi', 30 | packages=['rtpmidi']) 31 | --------------------------------------------------------------------------------