File: oscencode.rb

package info (click to toggle)
sonic-pi 3.2.2~repack-8
  • links: PTS, VCS
  • area: main
  • in suites: bookworm
  • size: 71,872 kB
  • sloc: ruby: 30,548; cpp: 8,490; sh: 957; ansic: 461; erlang: 360; lisp: 141; makefile: 44
file content (154 lines) | stat: -rw-r--r-- 4,955 bytes parent folder | download | duplicates (4)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
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
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
#--
# This file is part of Sonic Pi: http://sonic-pi.net
# Full project source: https://github.com/samaaron/sonic-pi
# License: https://github.com/samaaron/sonic-pi/blob/master/LICENSE.md
#
# Copyright 2013, 2014, 2015, 2016 by Sam Aaron (http://sam.aaron.name).
# All rights reserved.
#
# Permission is granted for use, copying, modification, and
# distribution of modified versions of this work as long as this
# notice is included.
#++

module SonicPi
  module OSC
    class OscEncode
      # Apologies for the density of this code - I've inlined a lot of the
      # code to reduce method dispatch overhead and to increase efficiency.
      # See http://opensoundcontrol.org for spec.

      def initialize(use_cache = false, cache_size=1000)
        @literal_binary_str = "BINARY".freeze
        @literal_cap_n = 'N'.freeze
        @literal_cap_n2 = 'N2'.freeze
        @literal_low_f = 'f'.freeze
        @literal_low_i = 'i'.freeze
        @literal_low_g = 'g'.freeze
        @literal_low_s = 's'.freeze
        @literal_empty_str = ''.freeze
        @literal_str_encode_regexp = /\000.*\z/
        @literal_str_pad = "\000".freeze
        @literal_two_to_pow_2 = 2 ** 32
        @literal_magic_time_offset = 2208988800

        @use_cache = use_cache
        @integer_cache = {}
        @string_cache = {}
        @float_cache = {}
        @cache_size = cache_size

        @num_cached_integers = 0
        @num_cached_floats = 0
        @num_cached_strings = 0

        @bundle_header = get_from_or_add_to_string_cache("#bundle")
      end

      def encode_single_message(address, args=[])
        args_encoded, tags = String.new(""), String.new(",")

        # inlining this method was not faster surprisingly
        address = get_from_or_add_to_string_cache(address)

        args.each do |arg|
          case arg
          when Integer
            tags << @literal_low_i

            if @use_cache
              if cached = @integer_cache[arg]
                args_encoded << cached
              else
                res = [arg].pack(@literal_cap_n)
                if @num_cached_integers < @cache_size
                  @integer_cache[arg] = res
                  @num_cached_integers += 1
                  # log "caching integer #{arg}"
                end
                args_encoded << res
              end
            else
              args_encoded << [arg].pack(@literal_cap_n)
            end
          when Float, Rational
            arg = arg.to_f
            tags << @literal_low_f

            if @use_cache
              if cached = @float_cache[arg]
                args_encoded << cached
              else
                res = [arg].pack(@literal_low_g)
                if @num_cached_floats < @cache_size
                  @float_cache[arg] = res
                  @num_cached_floats += 1
                  # log "caching float #{arg}"
                end
                args_encoded << res
              end
            else
              args_encoded << [arg].pack(@literal_low_g)
            end
          when String, Symbol
            arg = arg.to_s
            tags << @literal_low_s

            args_encoded << get_from_or_add_to_string_cache(arg)
          else
            raise "Unknown arg type to encode: #{arg.inspect}"
          end
        end

        tags_encoded = get_from_or_add_to_string_cache(tags)
        # Address here needs to be a new string, not sure why
        "#{address}#{tags_encoded}#{args_encoded}"
      end

      def encode_single_bundle(ts, address, args=[])
        message = encode_single_message(address, args)
        message_encoded = [message.size].pack(@literal_cap_n) << message
        "#{@bundle_header}#{time_encoded(ts)}#{message_encoded}"
      end

      private
      def get_from_or_add_to_string_cache(s)
        if cached = @string_cache[s]
          return cached
        else
          # This makes a null padded string rounded up to the nearest
          # multiple of four
          size = s.bytesize
          res = [s].pack("Z#{size + 4 - (size % 4)}")
          if @num_cached_strings < @cache_size
            # only cache the first @cache_size strings to avoid a memory
            # memory leak.
            @string_cache[s] = res
            @num_cached_strings += 1
            # log "caching string #{s}"
          end
          return  res
        end
      end

      def time_encoded(time)
        t1, fr = (time.to_f + @literal_magic_time_offset).divmod(1)

        t2 = (fr * @literal_two_to_pow_2).to_i
        [t1, t2].pack(@literal_cap_n2)
      end
    end

    class StreamOscEncode < OscEncode
      def encode_single_message(address, args=[])
        message = super
        ([message.length].pack(@literal_cap_n) << message).force_encoding(@literal_binary_str)
      end

      def encode_single_bundle(ts, address, args=[])
        message = super
        message.count.pack(@literal_cap_n) << message
      end
    end
  end
end