Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

implement a wav play into pcap #98

Open
wants to merge 10 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 2 additions & 2 deletions CHANGELOG.md
Expand Up @@ -11,8 +11,8 @@
* Feature: Permit `To` domain to be different from the destination. This permits testing multi-tenant systems more easily.

# [0.6.0](https://github.com/mojolingo/sippy_cup/compare/v0.5.0...v0.6.0)
* Change: Call limits (`number_of_calls`, `concurrent_max` and `calls_per_second`) no longer have default values for simplicity of UAS scenarios. The value of `to_user` now defaults to the SIPp default of `s`.
* Feature: Support for setting rate scaling independently of reporting frequency via the new `calls_per_second_interval` option. See also https://github.com/SIPp/sipp/pull/107 and https://github.com/SIPp/sipp/pull/126.
* Change: Call limits (`number_of_calls`, `concurrent_max` and `call_rate`) no longer have default values for simplicity of UAS scenarios. The value of `to_user` now defaults to the SIPp default of `s`.
* Feature: Support for setting rate scaling independently of reporting frequency via the new `call_rate_interval` option. See also https://github.com/SIPp/sipp/pull/107 and https://github.com/SIPp/sipp/pull/126.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are you changing old Changelog entries? It looks like you are changing calls_per_second to call_rate to match the other variables as part of this PR. That's fine, but the change note belongs in the current release.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sorry for late reply. I changed the command for my own need, but didn't rollback when make PR. sorry for that, and will change it soon. Thanks for review!


# [0.5.0](https://github.com/mojolingo/sippy_cup/compare/v0.4.1...v0.5.0)
SYNTAX CHANGES!
Expand Down
29 changes: 19 additions & 10 deletions README.markdown
Expand Up @@ -6,6 +6,15 @@

# Sippy Cup

## NOTES for this fork

* New instruction is added to insert wav file into pcap
- `- play_audio "<wav file path>"`
- need spandsp library for encoidng a-law and u-law
* Debug the DTMF packet generation (end of event)
- reduce the duration to 200 milliseconds
- change obsolete rfc2833 to rfc4733
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To merge this into the main branch, would you move these notes into appropriate areas of the README?


## Overview

### The Problem
Expand Down Expand Up @@ -78,7 +87,7 @@ Using `bundle` will then install the gem dependencies and allow you to run `sipp
source: 192.0.2.15
destination: 192.0.2.200
max_concurrent: 10
calls_per_second: 5
call_rate: 5
number_of_calls: 20
steps:
- invite
Expand Down Expand Up @@ -223,7 +232,7 @@ Each parameter has an impact on the test, and may either be changed once the XML
<dd>By default, SIPp assigns RTP ports dynamically. However, if there is a need for a static RTP port (say, for data collection purposes), it can be done by supplying a port number here. Default: SIPp's default of 6000</dd>

<dt>dtmf_mode</dt>
<dd>Specify the mechanism by which DTMF is signaled. Valid options are `rfc2833` for within the RTP media, or `info` for SIP INFO. Default: rfc2833</dd>
<dd>Specify the mechanism by which DTMF is signaled. Valid options are `rfc4733` for within the RTP media, or `info` for SIP INFO. Default: rfc4733</dd>

<dt>scenario_variables</dt>
<dd>If you're using sippy_cup to run a SIPp XML file, there may be CSV fields in the scenario ([field0], [field1], etc.). Specify a path to a CSV file containing the required information using this option. (File is semicolon delimeted, information can be found [here](http://sipp.sourceforge.net/doc/reference.html#inffile).) Default: unused</dd>
Expand All @@ -235,19 +244,19 @@ Each parameter has an impact on the test, and may either be changed once the XML
<dd>The total number of calls permitted for the entire test. When this limit is reached, the test is over. Defaults to nil.</dd>

<dt>concurrent_max</dt>
<dd>The maximum number of calls permitted to be active at any given time. When this limit is reached, SIPp will slow down or stop sending new calls until there it falls below the limit. Defaults to SIPp's default: (3 * call_duration (seconds) * calls_per_second)</dd>
<dd>The maximum number of calls permitted to be active at any given time. When this limit is reached, SIPp will slow down or stop sending new calls until there it falls below the limit. Defaults to SIPp's default: (3 * call_duration (seconds) * call_rate)</dd>

<dt>calls_per_second</dt>
<dt>call_rate</dt>
<dd>The rate at which new calls should be created. Note that SIPp will automatically adjust this downward to stay at or beneath the maximum number of concurrent calls (`concurrent_max`). Defaults to SIP's default of 10</dt>

<dt>calls_per_second_incr</dt>
<dd>When used with `calls_per_second_max`, tells SIPp the amount by which `calls_per_second` should be incremented. CPS rate is adjusted each `calls_per_second_interval`. Default: 1.</dd>
<dt>call_rate_incr</dt>
<dd>When used with `call_rate_max`, tells SIPp the amount by which `call_rate` should be incremented. CPS rate is adjusted each `call_rate_interval`. Default: 1.</dd>

<dt>calls_per_second_interval</dt>
<dd>When used with `calls_per_second_max`, tells SIPp the time interval (in seconds) by which calls-per-second should be incremented. Default: Unset; SIPp's default (60s). NOTE: Requires a development build of SIPp; see https://github.com/SIPp/sipp/pull/107</dd>
<dt>call_rate_interval</dt>
<dd>When used with `call_rate_max`, tells SIPp the time interval (in seconds) by which calls-per-second should be incremented. Default: Unset; SIPp's default (60s). NOTE: Requires a development build of SIPp; see https://github.com/SIPp/sipp/pull/107</dd>

<dt>calls_per_second_max</dt>
<dd>The maximum rate of calls-per-second. Default: unused (`calls_per_second` will not change)</dd>
<dt>call_rate_max</dt>
<dd>The maximum rate of calls-per-second. Default: unused (`call_rate` will not change)</dd>

<dt>advertise_address</dt>
<dd>The IP address to advertise in SIP and SDP if different from the bind IP. Default: `source` IP address</dd>
Expand Down
2 changes: 1 addition & 1 deletion examples/navigate_ivr.yml
@@ -1,7 +1,7 @@
source: 192.0.2.15
destination: 192.0.2.200
max_concurrent: 10
calls_per_second: 5
call_rate: 5
number_of_calls: 20
steps:
- invite
Expand Down
2 changes: 1 addition & 1 deletion examples/simple_call.yml
@@ -1,7 +1,7 @@
source: 192.0.2.15
destination: 192.0.2.200
max_concurrent: 10
calls_per_second: 5
call_rate: 5
number_of_calls: 20
steps:
- invite
Expand Down
2 changes: 1 addition & 1 deletion examples/wait_for_call.yml
Expand Up @@ -2,7 +2,7 @@
source: 10.3.18.108
destination: 10.3.18.134
max_concurrent: 1
calls_per_second: 1
call_rate: 1
number_of_calls: 1
steps:
- wait_for_call
Expand Down
36 changes: 36 additions & 0 deletions lib/sippy_cup/g711.rb
@@ -0,0 +1,36 @@
# ffi interface to freeswitch's g711
require 'ffi'

module SippyCup
module G711
extend FFI::Library
ffi_lib 'spandsp'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This new dependency on spandsp needs a couple of things:

  1. Document how and where a person can get spandsp, if they don't already have it
  2. Ensure sippy_cup continues to work the way it did before if spandsp is not available


enum :EncodeState, [
:G711_ALAW, 0,
:G711_ULAW, 1,
]

class G711State < FFI::Struct
layout :mode, :EncodeState
end

attach_function :g711_encode, [ G711State, :pointer, :pointer, :int ], :int

def encode(samples)
state = G711State.new
state[:mode] = :G711_ULAW # u-law only

iptr = FFI::MemoryPointer.new(:int16, samples.size)
optr = FFI::MemoryPointer.new(:uint8, samples.size)

#puts samples.join(' ')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dead code should be removed

iptr.write_array_of_type(:int16, :write_int16, samples)
g711_encode(state, optr, iptr, samples.size)
output = optr.read_array_of_type(:uint8, :read_uint8, samples.size)
#puts output.join(' ')
output
end
module_function :encode
end
end
49 changes: 39 additions & 10 deletions lib/sippy_cup/media.rb
@@ -1,11 +1,14 @@
# encoding: utf-8
require 'ipaddr'
require 'wavefile'
require 'ffi'
require 'sippy_cup/media/pcmu_payload'
require 'sippy_cup/media/dtmf_payload'
require 'sippy_cup/g711'

module SippyCup
class Media
VALID_STEPS = %w{silence dtmf}.freeze
VALID_STEPS = %w{silence dtmf play}.freeze
USEC = 1_000_000
MSEC = 1_000
attr_accessor :sequence
Expand Down Expand Up @@ -48,13 +51,11 @@ def compile!
(value.to_i / @generator::PTIME).times do
packet = new_packet
rtp_frame = @generator.new

# The first RTP audio packet should have the marker bit set
if first_audio
rtp_frame.rtp_marker = 1
first_audio = false
end

rtp_frame.rtp_timestamp = timestamp += rtp_frame.timestamp_interval
elapsed += rtp_frame.ptime
rtp_frame.rtp_sequence_num = sequence_number += 1
Expand All @@ -65,24 +66,52 @@ def compile!
end
when 'dtmf'
# value is the DTMF digit to send
# append that RFC2833 digit
# Assume 0.25 second duration for now
count = 250 / DTMFPayload::PTIME
# append that RFC4733 digit
# Assume 0.2 second duration for now
count = 200 / DTMFPayload::PTIME
count.times do |i|
packet = new_packet
dtmf_frame = DTMFPayload.new value
dtmf_frame.rtp_marker = 1 if i == 0
dtmf_frame.rtp_timestamp = timestamp # Is this correct? This is what Blink does...
#dtmf_frame.rtp_timestamp = timestamp += dtmf_frame.timestamp_interval
# The first RTP audio packet should have the marker bit set
if first_audio
rtp_frame.rtp_marker = 1
first_audio = false
end
dtmf_frame.rtp_timestamp = timestamp += dtmf_frame.timestamp_interval
elapsed += dtmf_frame.ptime
dtmf_frame.rtp_sequence_num = sequence_number += 1
dtmf_frame.rtp_ssrc_id = ssrc_id
dtmf_frame.end_of_event = (count == i) # Last packet?
dtmf_frame.end_of_event = (i == count-1) # Last packet
packet.headers.last.body = dtmf_frame.to_bytes
packet.recalc
@pcap_file.body << get_pcap_packet(packet, next_ts(start_time, elapsed))
end
# Now bump up the timestamp to cover the gap
timestamp += count * DTMFPayload::TIMESTAMP_INTERVAL
when 'play'
# value is wav file path
wav = WaveFile::Reader.new(value, WaveFile::Format.new(:mono, :pcm_16, 8000))
duration = wav.total_sample_frames * 1000 / wav.native_format.sample_rate # in milliseconds
(duration / @generator::PTIME).times do |i|
packet = new_packet
rtp_frame = @generator.new
# The first RTP audio packet should have the marker bit set
if first_audio
rtp_frame.rtp_marker = 1
first_audio = false
end
rtp_frame.rtp_timestamp = timestamp += rtp_frame.timestamp_interval
elapsed += rtp_frame.ptime
rtp_frame.rtp_sequence_num = sequence_number += 1
rtp_frame.rtp_ssrc_id = ssrc_id
len = wav.native_format.sample_rate * rtp_frame.ptime / 1000
lin_data = wav.read(len).samples
enc_data = G711::encode(lin_data)
packet.headers.last.body = rtp_frame.header.to_s << enc_data.flatten.pack('c*')
packet.recalc
@pcap_file.body << get_pcap_packet(packet, next_ts(start_time, elapsed))
end
wav.close
else
end
end
Expand Down
12 changes: 7 additions & 5 deletions lib/sippy_cup/runner.rb
Expand Up @@ -105,17 +105,19 @@ def command_options
max_concurrent = @scenario_options[:concurrent_max] || @scenario_options[:max_concurrent]
options[:l] = max_concurrent if max_concurrent
options[:m] = @scenario_options[:number_of_calls] if @scenario_options[:number_of_calls]
options[:r] = @scenario_options[:calls_per_second] if @scenario_options[:calls_per_second]
options[:r] = @scenario_options[:call_rate] if @scenario_options[:call_rate]
options[:rp] = @scenario_options[:call_period] if @scenario_options[:call_period]
options[:s] = @scenario_options[:to].to_s.split('@').first if @scenario_options[:to]

options[:i] = @scenario_options[:source] if @scenario_options[:source]
options[:mp] = @scenario_options[:media_port] if @scenario_options[:media_port]
options[:trace_logs] = nil

if @scenario_options[:calls_per_second_max]
if @scenario_options[:call_rate_max]
options[:no_rate_quit] = nil
options[:rate_max] = @scenario_options[:calls_per_second_max]
options[:rate_increase] = @scenario_options[:calls_per_second_incr] || 1
options[:rate_interval] = @scenario_options[:calls_per_second_interval] if @scenario_options[:calls_per_second_interval]
options[:rate_max] = @scenario_options[:call_rate_max]
options[:rate_increase] = @scenario_options[:call_rate_incr] || 1
options[:rate_interval] = @scenario_options[:call_rate_interval] if @scenario_options[:call_rate_interval]
end

if @scenario_options[:stats_file]
Expand Down
26 changes: 18 additions & 8 deletions lib/sippy_cup/scenario.rb
Expand Up @@ -88,11 +88,11 @@ def self.from_manifest(manifest, options = {})
# @option options [Integer] :media_port The RTCP (media) port to bind to locally.
# @option options [String, Numeric] :max_concurrent The maximum number of concurrent calls to execute.
# @option options [String, Numeric] :number_of_calls The maximum number of calls to execute in the test run.
# @option options [String, Numeric] :calls_per_second The rate at which to initiate calls.
# @option options [String, Numeric] :call_rate The rate at which to initiate calls.
# @option options [String] :stats_file The path at which to dump statistics.
# @option options [String, Numeric] :stats_interval The interval (in seconds) at which to dump statistics (defaults to 1s).
# @option options [String] :transport_mode The transport mode over which to direct SIP traffic.
# @option options [String] :dtmf_mode The output DTMF mode, either rfc2833 (default) or info.
# @option options [String] :dtmf_mode The output DTMF mode, either rfc4733 (default) or info.
# @option options [String] :scenario_variables A path to a CSV file of variables to be interpolated with the scenario at runtime.
# @option options [Hash] :options A collection of options to pass through to SIPp, as key-value pairs. In cases of value-less options (eg -trace_err), specify a nil value.
# @option options [Array<String>] :steps A collection of steps
Expand Down Expand Up @@ -175,7 +175,7 @@ def invite(opts = {})
s=-
c=IN IP[media_ip_type] [media_ip]
t=0 0
m=audio [media_port] RTP/AVP 0 101
m=audio [auto_media_port] RTP/AVP 0 101
a=rtpmap:0 PCMU/8000
a=rtpmap:101 telephone-event/8000
a=fmtp:101 0-15
Expand Down Expand Up @@ -472,6 +472,16 @@ def sleep(seconds)
@media << "silence:#{milliseconds}" if @media
end

#
# add audio to pcap from wav file
#
# @param [String] path of wav file
#
def play_audio(wav_file)
raise "Media not started" unless @media
@media << "play:#{wav_file}"
end

#
# Send DTMF digits
#
Expand All @@ -485,12 +495,12 @@ def sleep(seconds)
#
def send_digits(digits)
raise "Media not started" unless @media
delay = (0.250 * MSEC).to_i # FIXME: Need to pass this down to the media layer
delay = (0.2 * MSEC).to_i # FIXME: Need to pass this down to the media layer
digits.split('').each do |digit|
raise ArgumentError, "Invalid DTMF digit requested: #{digit}" unless VALID_DTMF.include? digit

case @dtmf_mode
when :rfc2833
when :rfc4733
@media << "dtmf:#{digit}"
@media << "silence:#{delay}"
when :info
Expand Down Expand Up @@ -518,7 +528,7 @@ def send_digits(digits)
end
end

if @dtmf_mode == :rfc2833
if @dtmf_mode == :rfc4733
pause delay * 2 * digits.size
end
end
Expand Down Expand Up @@ -773,9 +783,9 @@ def scenario_node
def parse_args(args)
if args[:dtmf_mode]
@dtmf_mode = args[:dtmf_mode].to_sym
raise ArgumentError, "dtmf_mode must be rfc2833 or info" unless [:rfc2833, :info].include?(@dtmf_mode)
raise ArgumentError, "dtmf_mode must be rfc4733 or info" unless [:rfc4733, :info].include?(@dtmf_mode)
else
@dtmf_mode = :rfc2833
@dtmf_mode = :rfc4733
end

@from_user = args[:from_user] || "sipp"
Expand Down
4 changes: 3 additions & 1 deletion sippy_cup.gemspec
Expand Up @@ -19,9 +19,11 @@ Gem::Specification.new do |s|
s.require_paths = ["lib"]

s.add_runtime_dependency 'packetfu', ["= 1.1.11"] # 1.1.12 introduces a breaking change, removing PacketFu::UDPPacket
s.add_runtime_dependency 'nokogiri', ["~> 1.6.0"]
s.add_runtime_dependency 'nokogiri', ["~> 1.7.0"]
s.add_runtime_dependency 'activesupport', [">= 3.0"]
s.add_runtime_dependency 'psych', ["~> 2.0.1"] unless RUBY_PLATFORM == 'java'
s.add_runtime_dependency 'wavefile', [">= 0.8.0"]
s.add_runtime_dependency 'ffi'

s.add_development_dependency 'guard-rspec'
s.add_development_dependency 'rspec', ["~> 3.4"]
Expand Down
2 changes: 1 addition & 1 deletion spec/sippy_cup/fixtures/test.yml
Expand Up @@ -3,7 +3,7 @@ name: My test scenario
source: 192.0.2.15
destination: 192.0.2.200
max_concurrent: 10
calls_per_second: 5
call_rate: 5
number_of_calls: 20
steps:
- invite
Expand Down