Class: Webmidi::Network::RTP::ControlPacket

Inherits:
Object
  • Object
show all
Defined in:
lib/webmidi/network/rtp.rb

Constant Summary collapse

SIGNATURE =
0xFFFF
PROTOCOL_VERSION =
2
COMMANDS =
{
  invitation: "IN",
  accepted: "OK",
  rejected: "NO",
  synchronization: "CK",
  receiver_feedback: "RS",
  end_session: "BY"
}.freeze
COMMAND_BY_CODE =
COMMANDS.invert.freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(command:, version: PROTOCOL_VERSION, token: 0, ssrc: 0, name: "", count: 0, timestamps: [], sequence_number: 0) ⇒ ControlPacket

Returns a new instance of ControlPacket.



28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
# File 'lib/webmidi/network/rtp.rb', line 28

def initialize(command:, version: PROTOCOL_VERSION, token: 0, ssrc: 0, name: "", count: 0,
  timestamps: [], sequence_number: 0)
  raise InvalidMessageError, "Unknown AppleMIDI command: #{command.inspect}" unless COMMANDS.key?(command)
  self.class.validate_range!(version, "Protocol version", 0, 0xFFFF_FFFF)
  self.class.validate_range!(token, "Initiator token", 0, 0xFFFF_FFFF)
  self.class.validate_range!(ssrc, "SSRC", 0, 0xFFFF_FFFF)
  self.class.validate_range!(count, "Synchronization count", 0, 3)
  self.class.validate_range!(sequence_number, "Sequence number", 0, 0xFFFF)

  @command = command
  @version = version
  @token = token
  @ssrc = ssrc
  @name = name.to_s
  @count = count
  @timestamps = timestamps.dup.freeze
  @sequence_number = sequence_number
end

Instance Attribute Details

#commandObject (readonly)

Returns the value of attribute command.



26
27
28
# File 'lib/webmidi/network/rtp.rb', line 26

def command
  @command
end

#countObject (readonly)

Returns the value of attribute count.



26
27
28
# File 'lib/webmidi/network/rtp.rb', line 26

def count
  @count
end

#nameObject (readonly)

Returns the value of attribute name.



26
27
28
# File 'lib/webmidi/network/rtp.rb', line 26

def name
  @name
end

#sequence_numberObject (readonly)

Returns the value of attribute sequence_number.



26
27
28
# File 'lib/webmidi/network/rtp.rb', line 26

def sequence_number
  @sequence_number
end

#ssrcObject (readonly)

Returns the value of attribute ssrc.



26
27
28
# File 'lib/webmidi/network/rtp.rb', line 26

def ssrc
  @ssrc
end

#timestampsObject (readonly)

Returns the value of attribute timestamps.



26
27
28
# File 'lib/webmidi/network/rtp.rb', line 26

def timestamps
  @timestamps
end

#tokenObject (readonly)

Returns the value of attribute token.



26
27
28
# File 'lib/webmidi/network/rtp.rb', line 26

def token
  @token
end

#versionObject (readonly)

Returns the value of attribute version.



26
27
28
# File 'lib/webmidi/network/rtp.rb', line 26

def version
  @version
end

Class Method Details

.accepted(token:, ssrc:, name:, version: PROTOCOL_VERSION) ⇒ Object



51
52
53
# File 'lib/webmidi/network/rtp.rb', line 51

def self.accepted(token:, ssrc:, name:, version: PROTOCOL_VERSION)
  new(command: :accepted, version: version, token: token, ssrc: ssrc, name: name)
end

.end_session(ssrc:) ⇒ Object



67
68
69
# File 'lib/webmidi/network/rtp.rb', line 67

def self.end_session(ssrc:)
  new(command: :end_session, ssrc: ssrc)
end

.invitation(token:, ssrc:, name:, version: PROTOCOL_VERSION) ⇒ Object



47
48
49
# File 'lib/webmidi/network/rtp.rb', line 47

def self.invitation(token:, ssrc:, name:, version: PROTOCOL_VERSION)
  new(command: :invitation, version: version, token: token, ssrc: ssrc, name: name)
end

.parse(bytes) ⇒ Object



76
77
78
79
80
81
82
83
84
# File 'lib/webmidi/network/rtp.rb', line 76

def self.parse(bytes)
  return nil if bytes.bytesize < 4

  signature = bytes[0, 2].unpack1("n")
  command = COMMAND_BY_CODE[bytes[2, 2]]
  return nil unless signature == SIGNATURE && command

  parse_payload(command, bytes[4..] || "")
end

.parse_payload(command, payload) ⇒ Object



86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
# File 'lib/webmidi/network/rtp.rb', line 86

def self.parse_payload(command, payload)
  case command
  when :invitation, :accepted, :rejected
    return nil if payload.bytesize < 12

    version, token, ssrc = payload[0, 12].unpack("NNN")
    name = (payload[12..] || "").split("\0", 2).first
    new(command: command, version: version, token: token, ssrc: ssrc, name: name)
  when :synchronization
    return nil if payload.bytesize < 28

    ssrc = payload[0, 4].unpack1("N")
    count = payload.getbyte(4)
    timestamps = payload[8, 24].unpack("Q>Q>Q>")
    new(command: command, ssrc: ssrc, count: count, timestamps: timestamps)
  when :receiver_feedback
    return nil if payload.bytesize < 6

    ssrc, sequence_number = payload[0, 6].unpack("Nn")
    new(command: command, ssrc: ssrc, sequence_number: sequence_number)
  when :end_session
    return nil if payload.bytesize < 4

    new(command: command, ssrc: payload[0, 4].unpack1("N"))
  end
end

.receiver_feedback(ssrc:, sequence_number:) ⇒ Object



63
64
65
# File 'lib/webmidi/network/rtp.rb', line 63

def self.receiver_feedback(ssrc:, sequence_number:)
  new(command: :receiver_feedback, ssrc: ssrc, sequence_number: sequence_number)
end

.rejected(token:, ssrc:, name:, version: PROTOCOL_VERSION) ⇒ Object



55
56
57
# File 'lib/webmidi/network/rtp.rb', line 55

def self.rejected(token:, ssrc:, name:, version: PROTOCOL_VERSION)
  new(command: :rejected, version: version, token: token, ssrc: ssrc, name: name)
end

.synchronization(ssrc:, count:, timestamps:) ⇒ Object



59
60
61
# File 'lib/webmidi/network/rtp.rb', line 59

def self.synchronization(ssrc:, count:, timestamps:)
  new(command: :synchronization, ssrc: ssrc, count: count, timestamps: timestamps)
end

.timestampObject



113
114
115
# File 'lib/webmidi/network/rtp.rb', line 113

def self.timestamp
  (Process.clock_gettime(Process::CLOCK_MONOTONIC) * 1_000_000).to_i
end

.validate_range!(value, name, min, max) ⇒ Object



117
118
119
120
121
# File 'lib/webmidi/network/rtp.rb', line 117

def self.validate_range!(value, name, min, max)
  return if value.is_a?(Integer) && value.between?(min, max)

  raise InvalidMessageError, "#{name} must be between #{min} and #{max}, got #{value.inspect}"
end

Instance Method Details

#to_bytesObject



71
72
73
74
# File 'lib/webmidi/network/rtp.rb', line 71

def to_bytes
  header = [SIGNATURE].pack("n") + COMMANDS.fetch(@command)
  header + payload_bytes
end