Class: Wavesync::CueChunk

Inherits:
Object
  • Object
show all
Defined in:
lib/wavesync/cue_chunk.rb

Constant Summary collapse

RIFF_HEADER_SIZE =
12
CHUNK_ID_SIZE =
4
CHUNK_SIZE_FIELD_SIZE =
4
CUE_CHUNK_ID =
'cue '
LIST_CHUNK_ID =
'LIST'
ADTL_LIST_TYPE =
'adtl'
LABL_CHUNK_ID =
'labl'
NOTE_CHUNK_ID =
'note'
DATA_CHUNK_ID =
'data'
CUE_HEADER_SIZE =
4
BYTES_PER_CUE_POINT =
24
LOOP_START_NOTE =
'start'
LOOP_END_NOTE =
'end'
UINT32_LE =
'V'

Class Method Summary collapse

Class Method Details

.append_to_file(filepath, cue_points) ⇒ Object

: (String filepath, Array cue_points) -> void



105
106
107
108
109
110
111
112
113
114
115
116
# File 'lib/wavesync/cue_chunk.rb', line 105

def self.append_to_file(filepath, cue_points)
  return if cue_points.empty?

  Timing.current.measure(:wav_chunks) do
    File.open(filepath, 'ab') do |file|
      write_cue_chunk(file, cue_points)
      write_adtl_chunk(file, cue_points) if cue_points.any? { |cue_point| cue_point[:label] || cue_point[:note] }
    end

    update_riff_size(filepath)
  end
end

.build_adtl_entry(sub_id, identifier, text_content) ⇒ Object

: (String sub_id, Integer identifier, String text_content) -> String, identifier: Integer, text: String, data_size: Integer, pad: Integer



205
206
207
208
209
210
# File 'lib/wavesync/cue_chunk.rb', line 205

def self.build_adtl_entry(sub_id, identifier, text_content)
  text = "#{text_content}\x00"
  data_size = 4 + text.length
  pad = data_size.odd? ? 1 : 0
  { sub_id: sub_id, identifier: identifier, text: text, data_size: data_size, pad: pad }
end

.loop_marker?(cue_point) ⇒ Boolean

: (Integer, sample_offset: Integer, label: String?, note: String? cue_point) -> bool

Returns:

  • (Boolean)


86
87
88
89
90
91
# File 'lib/wavesync/cue_chunk.rb', line 86

def self.loop_marker?(cue_point)
  note = cue_point[:note]
  return false if note.nil?

  [LOOP_START_NOTE, LOOP_END_NOTE].include?(note)
end

.loops(cue_points) ⇒ Object

: (Array cue_points) -> Array



94
95
96
97
98
99
100
101
102
# File 'lib/wavesync/cue_chunk.rb', line 94

def self.loops(cue_points)
  starts = cue_points.select { |cue_point| cue_point[:note] == LOOP_START_NOTE }.sort_by { |cue_point| cue_point[:sample_offset] }
  ends = cue_points.select { |cue_point| cue_point[:note] == LOOP_END_NOTE }.sort_by { |cue_point| cue_point[:sample_offset] }
  starts.zip(ends).filter_map do |loop_start, loop_end|
    next nil unless loop_start && loop_end && loop_end[:sample_offset] > loop_start[:sample_offset]

    { start_sample: loop_start[:sample_offset], end_sample: loop_end[:sample_offset] }
  end
end

.read(filepath) ⇒ Object

: (String filepath) -> Array



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
# File 'lib/wavesync/cue_chunk.rb', line 26

def self.read(filepath)
  Timing.current.measure(:wav_chunks) do
    cue_points = [] #: Array[{identifier: Integer, sample_offset: Integer, label: String?, note: String?}]
    labels = {} #: Hash[Integer, String]
    notes = {} #: Hash[Integer, String]

    File.open(filepath, 'rb') do |file|
      file.seek(RIFF_HEADER_SIZE)

      until file.eof?
        chunk_id = file.read(CHUNK_ID_SIZE)
        break if chunk_id.nil? || chunk_id.length < CHUNK_ID_SIZE

        chunk_size_bytes = file.read(CHUNK_SIZE_FIELD_SIZE)
        break if chunk_size_bytes.nil?

        chunk_size = chunk_size_bytes.unpack1(UINT32_LE).to_i
        chunk_data_start = file.tell

        if chunk_id == CUE_CHUNK_ID
          num_cues = file.read(4)&.unpack1(UINT32_LE).to_i
          num_cues.times do
            identifier = file.read(4)&.unpack1(UINT32_LE)&.to_i
            file.read(16) # skip position, fcc_chunk, chunk_start, block_start
            sample_offset = file.read(4)&.unpack1(UINT32_LE)&.to_i
            cue_points << { identifier: identifier, sample_offset: sample_offset, label: nil, note: nil } if identifier && sample_offset
          end
        elsif chunk_id == LIST_CHUNK_ID && chunk_size >= 4
          list_type = file.read(4)
          read_adtl_entries(file, chunk_data_start + chunk_size, labels, notes) if list_type == ADTL_LIST_TYPE
        end

        chunk_padding = chunk_size.odd? ? 1 : 0
        file.seek(chunk_data_start + chunk_size + chunk_padding)
      end
    end

    cue_points.map do |cue_point|
      {
        identifier: cue_point[:identifier],
        sample_offset: cue_point[:sample_offset],
        label: labels[cue_point[:identifier]],
        note: notes[cue_point[:identifier]]
      }
    end
  end
end

.read_adtl_entries(file, list_end, labels, notes) ⇒ Object

: (untyped file, Integer list_end, Hash[Integer, String] labels, Hash[Integer, String] notes) -> void



213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
# File 'lib/wavesync/cue_chunk.rb', line 213

def self.read_adtl_entries(file, list_end, labels, notes)
  while file.tell < list_end - 8
    sub_id = file.read(CHUNK_ID_SIZE)
    break if sub_id.nil? || sub_id.length < CHUNK_ID_SIZE

    sub_size_bytes = file.read(CHUNK_SIZE_FIELD_SIZE)
    break if sub_size_bytes.nil?

    sub_size = sub_size_bytes.unpack1(UINT32_LE).to_i
    sub_data_start = file.tell

    if [LABL_CHUNK_ID, NOTE_CHUNK_ID].include?(sub_id) && sub_size >= 4
      identifier = file.read(4)&.unpack1(UINT32_LE)&.to_i
      text_size = sub_size - 4
      text = text_size.positive? ? (file.read(text_size) || '') : ''
      target = sub_id == LABL_CHUNK_ID ? labels : notes
      target[identifier] = text.delete("\x00") if identifier
    end

    sub_padding = sub_size.odd? ? 1 : 0
    file.seek(sub_data_start + sub_size + sub_padding)
  end
end

.same?(cue_points_a, cue_points_b) ⇒ Boolean

: (Array cue_points_a, Array cue_points_b) -> bool

Returns:

  • (Boolean)


75
76
77
# File 'lib/wavesync/cue_chunk.rb', line 75

def self.same?(cue_points_a, cue_points_b)
  to_comparable(cue_points_a) == to_comparable(cue_points_b)
end

.to_comparable(cue_points) ⇒ Object

: (Array cue_points) -> Array



80
81
82
83
# File 'lib/wavesync/cue_chunk.rb', line 80

def self.to_comparable(cue_points)
  mapped = cue_points.map { |cp| { sample_offset: cp[:sample_offset], label: cp[:label], note: cp[:note] } } #: Array[{sample_offset: Integer, label: String?, note: String?}]
  mapped.sort_by { |cp| cp[:sample_offset] }
end

.update_riff_size(filepath) ⇒ Object

: (String filepath) -> void



238
239
240
241
242
243
244
245
246
# File 'lib/wavesync/cue_chunk.rb', line 238

def self.update_riff_size(filepath)
  File.open(filepath, 'r+b') do |file|
    file.seek(0, IO::SEEK_END)
    file_size = file.tell
    riff_size = file_size - 8
    file.seek(4)
    file.write([riff_size].pack(UINT32_LE))
  end
end

.write(source_filepath, output_filepath, cue_points) ⇒ Object

: (String source_filepath, String output_filepath, Array cue_points) -> void



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
155
156
157
158
159
160
161
162
163
# File 'lib/wavesync/cue_chunk.rb', line 119

def self.write(source_filepath, output_filepath, cue_points)
  Timing.current.measure(:wav_chunks) do
    File.open(source_filepath, 'rb') do |input|
      File.open(output_filepath, 'wb') do |output|
        output.write(input.read(RIFF_HEADER_SIZE))

        until input.eof?
          chunk_id = input.read(CHUNK_ID_SIZE)
          break if chunk_id.nil? || chunk_id.length < CHUNK_ID_SIZE

          chunk_size_bytes = input.read(CHUNK_SIZE_FIELD_SIZE)
          break if chunk_size_bytes.nil?

          chunk_size = chunk_size_bytes.unpack1(UINT32_LE).to_i
          chunk_padding = chunk_size.odd? ? 1 : 0

          if chunk_id == CUE_CHUNK_ID
            input.read(chunk_size + chunk_padding)
          elsif chunk_id == LIST_CHUNK_ID && chunk_size >= 4
            list_type = input.read(4)
            if list_type == ADTL_LIST_TYPE
              input.read(chunk_size - 4 + chunk_padding)
            else
              output.write(chunk_id)
              output.write(chunk_size_bytes)
              output.write(list_type)
              output.write(input.read(chunk_size - 4 + chunk_padding))
            end
          else
            output.write(chunk_id)
            output.write(chunk_size_bytes)
            output.write(input.read(chunk_size + chunk_padding))
          end
        end

        unless cue_points.empty?
          write_cue_chunk(output, cue_points)
          write_adtl_chunk(output, cue_points) if cue_points.any? { |cue_point| cue_point[:label] || cue_point[:note] }
        end
      end
    end

    update_riff_size(output_filepath)
  end
end

.write_adtl_chunk(output, cue_points) ⇒ Object

: (untyped output, Array cue_points) -> void



183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
# File 'lib/wavesync/cue_chunk.rb', line 183

def self.write_adtl_chunk(output, cue_points)
  entries = [] #: Array[{sub_id: String, identifier: Integer, text: String, data_size: Integer, pad: Integer}]
  cue_points.each do |cue_point|
    entries << build_adtl_entry(LABL_CHUNK_ID, cue_point[:identifier], cue_point[:label]) if cue_point[:label]
    entries << build_adtl_entry(NOTE_CHUNK_ID, cue_point[:identifier], cue_point[:note]) if cue_point[:note]
  end

  adtl_data_size = 4 + entries.sum { |entry| 8 + entry[:data_size] + entry[:pad] }
  output.write(LIST_CHUNK_ID)
  output.write([adtl_data_size].pack(UINT32_LE))
  output.write(ADTL_LIST_TYPE)

  entries.each do |entry|
    output.write(entry[:sub_id])
    output.write([entry[:data_size]].pack(UINT32_LE))
    output.write([entry[:identifier]].pack(UINT32_LE))
    output.write(entry[:text])
    output.write("\x00") if entry[:pad] == 1
  end
end

.write_cue_chunk(output, cue_points) ⇒ Object

: (untyped output, Array cue_points) -> void



166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
# File 'lib/wavesync/cue_chunk.rb', line 166

def self.write_cue_chunk(output, cue_points)
  chunk_size = CUE_HEADER_SIZE + (cue_points.size * BYTES_PER_CUE_POINT)
  output.write(CUE_CHUNK_ID)
  output.write([chunk_size].pack(UINT32_LE))
  output.write([cue_points.size].pack(UINT32_LE))
  cue_points.each_with_index do |cue_point, index|
    identifier = cue_point[:identifier] || (index + 1)
    output.write([identifier].pack(UINT32_LE))
    output.write([0].pack(UINT32_LE))
    output.write(DATA_CHUNK_ID)
    output.write([0].pack(UINT32_LE))
    output.write([0].pack(UINT32_LE))
    output.write([cue_point[:sample_offset]].pack(UINT32_LE))
  end
end