Creative Voice File: Ruby parsing library

Creative Voice File is a container file format for digital audio wave data. Initial revisions were able to support only unsigned 8-bit PCM and ADPCM data, later versions were revised to add support for 16-bit PCM and a-law / u-law formats.

This format was actively used in 1990s, around the advent of Creative's sound cards (Sound Blaster family). It was a popular choice for a digital sound container in lots of games and multimedia software due to simplicity and availability of Creative's recording / editing tools.

File extension

voc

KS implementation details

License: CC0-1.0

References

This page hosts a formal specification of Creative Voice File using Kaitai Struct. This specification can be automatically translated into a variety of programming languages to get a parsing library.

Usage

Runtime library

All parsing code for Ruby generated by Kaitai Struct depends on the Ruby runtime library. You have to install it before you can parse data.

The Ruby runtime library can be installed from RubyGems:

gem install kaitai-struct

Code

Parse a local file and get structure in memory:

data = CreativeVoiceFile.from_file("path/to/local/file.voc")

Or parse structure from a string of bytes:

bytes = "\x00\x01\x02..."
data = CreativeVoiceFile.new(Kaitai::Struct::Stream.new(bytes))

After that, one can get various attributes from the structure by invoking getter methods like:

data.header_size # => Total size of this main header (usually 0x001A)

Ruby source code to parse Creative Voice File

creative_voice_file.rb

# This is a generated file! Please edit source .ksy file and use kaitai-struct-compiler to rebuild

require 'kaitai/struct/struct'

unless Gem::Version.new(Kaitai::Struct::VERSION) >= Gem::Version.new('0.9')
  raise "Incompatible Kaitai Struct Ruby API: 0.9 or later is required, but you have #{Kaitai::Struct::VERSION}"
end


##
# Creative Voice File is a container file format for digital audio
# wave data. Initial revisions were able to support only unsigned
# 8-bit PCM and ADPCM data, later versions were revised to add support
# for 16-bit PCM and a-law / u-law formats.
# 
# This format was actively used in 1990s, around the advent of
# Creative's sound cards (Sound Blaster family). It was a popular
# choice for a digital sound container in lots of games and multimedia
# software due to simplicity and availability of Creative's recording
# / editing tools.
# @see https://wiki.multimedia.cx/index.php?title=Creative_Voice Source
class CreativeVoiceFile < Kaitai::Struct::Struct

  BLOCK_TYPES = {
    0 => :block_types_terminator,
    1 => :block_types_sound_data,
    2 => :block_types_sound_data_cont,
    3 => :block_types_silence,
    4 => :block_types_marker,
    5 => :block_types_text,
    6 => :block_types_repeat_start,
    7 => :block_types_repeat_end,
    8 => :block_types_extra_info,
    9 => :block_types_sound_data_new,
  }
  I__BLOCK_TYPES = BLOCK_TYPES.invert

  CODECS = {
    0 => :codecs_pcm_8bit_unsigned,
    1 => :codecs_adpcm_4bit,
    2 => :codecs_adpcm_2_6bit,
    3 => :codecs_adpcm_2_bit,
    4 => :codecs_pcm_16bit_signed,
    6 => :codecs_alaw,
    7 => :codecs_ulaw,
    512 => :codecs_adpcm_4_to_16bit,
  }
  I__CODECS = CODECS.invert
  def initialize(_io, _parent = nil, _root = self)
    super(_io, _parent, _root)
    _read
  end

  def _read
    @magic = @_io.read_bytes(20)
    raise Kaitai::Struct::ValidationNotEqualError.new([67, 114, 101, 97, 116, 105, 118, 101, 32, 86, 111, 105, 99, 101, 32, 70, 105, 108, 101, 26].pack('C*'), magic, _io, "/seq/0") if not magic == [67, 114, 101, 97, 116, 105, 118, 101, 32, 86, 111, 105, 99, 101, 32, 70, 105, 108, 101, 26].pack('C*')
    @header_size = @_io.read_u2le
    @version = @_io.read_u2le
    @checksum = @_io.read_u2le
    @blocks = []
    i = 0
    while not @_io.eof?
      @blocks << Block.new(@_io, self, @_root)
      i += 1
    end
    self
  end

  ##
  # @see https://wiki.multimedia.cx/index.php?title=Creative_Voice#Block_type_0x04:_Marker Source
  class BlockMarker < Kaitai::Struct::Struct
    def initialize(_io, _parent = nil, _root = self)
      super(_io, _parent, _root)
      _read
    end

    def _read
      @marker_id = @_io.read_u2le
      self
    end

    ##
    # Marker ID
    attr_reader :marker_id
  end

  ##
  # @see https://wiki.multimedia.cx/index.php?title=Creative_Voice#Block_type_0x03:_Silence Source
  class BlockSilence < Kaitai::Struct::Struct
    def initialize(_io, _parent = nil, _root = self)
      super(_io, _parent, _root)
      _read
    end

    def _read
      @duration_samples = @_io.read_u2le
      @freq_div = @_io.read_u1
      self
    end
    def sample_rate
      return @sample_rate unless @sample_rate.nil?
      @sample_rate = (1000000.0 / (256 - freq_div))
      @sample_rate
    end

    ##
    # Duration of silence, in seconds
    def duration_sec
      return @duration_sec unless @duration_sec.nil?
      @duration_sec = (duration_samples / sample_rate)
      @duration_sec
    end

    ##
    # Duration of silence, in samples
    attr_reader :duration_samples

    ##
    # Frequency divisor, used to determine sample rate
    attr_reader :freq_div
  end

  ##
  # @see https://wiki.multimedia.cx/index.php?title=Creative_Voice#Block_type_0x09:_Sound_data_.28New_format.29 Source
  class BlockSoundDataNew < Kaitai::Struct::Struct
    def initialize(_io, _parent = nil, _root = self)
      super(_io, _parent, _root)
      _read
    end

    def _read
      @sample_rate = @_io.read_u4le
      @bits_per_sample = @_io.read_u1
      @num_channels = @_io.read_u1
      @codec = Kaitai::Struct::Stream::resolve_enum(CreativeVoiceFile::CODECS, @_io.read_u2le)
      @reserved = @_io.read_bytes(4)
      @wave = @_io.read_bytes_full
      self
    end
    attr_reader :sample_rate
    attr_reader :bits_per_sample
    attr_reader :num_channels
    attr_reader :codec
    attr_reader :reserved
    attr_reader :wave
  end
  class Block < Kaitai::Struct::Struct
    def initialize(_io, _parent = nil, _root = self)
      super(_io, _parent, _root)
      _read
    end

    def _read
      @block_type = Kaitai::Struct::Stream::resolve_enum(CreativeVoiceFile::BLOCK_TYPES, @_io.read_u1)
      if block_type != :block_types_terminator
        @body_size1 = @_io.read_u2le
      end
      if block_type != :block_types_terminator
        @body_size2 = @_io.read_u1
      end
      if block_type != :block_types_terminator
        case block_type
        when :block_types_sound_data_new
          @_raw_body = @_io.read_bytes(body_size)
          _io__raw_body = Kaitai::Struct::Stream.new(@_raw_body)
          @body = BlockSoundDataNew.new(_io__raw_body, self, @_root)
        when :block_types_repeat_start
          @_raw_body = @_io.read_bytes(body_size)
          _io__raw_body = Kaitai::Struct::Stream.new(@_raw_body)
          @body = BlockRepeatStart.new(_io__raw_body, self, @_root)
        when :block_types_marker
          @_raw_body = @_io.read_bytes(body_size)
          _io__raw_body = Kaitai::Struct::Stream.new(@_raw_body)
          @body = BlockMarker.new(_io__raw_body, self, @_root)
        when :block_types_sound_data
          @_raw_body = @_io.read_bytes(body_size)
          _io__raw_body = Kaitai::Struct::Stream.new(@_raw_body)
          @body = BlockSoundData.new(_io__raw_body, self, @_root)
        when :block_types_extra_info
          @_raw_body = @_io.read_bytes(body_size)
          _io__raw_body = Kaitai::Struct::Stream.new(@_raw_body)
          @body = BlockExtraInfo.new(_io__raw_body, self, @_root)
        when :block_types_silence
          @_raw_body = @_io.read_bytes(body_size)
          _io__raw_body = Kaitai::Struct::Stream.new(@_raw_body)
          @body = BlockSilence.new(_io__raw_body, self, @_root)
        else
          @body = @_io.read_bytes(body_size)
        end
      end
      self
    end

    ##
    # body_size is a 24-bit little-endian integer, so we're
    # emulating that by adding two standard-sized integers
    # (body_size1 and body_size2).
    def body_size
      return @body_size unless @body_size.nil?
      if block_type != :block_types_terminator
        @body_size = (body_size1 + (body_size2 << 16))
      end
      @body_size
    end

    ##
    # Byte that determines type of block content
    attr_reader :block_type
    attr_reader :body_size1
    attr_reader :body_size2

    ##
    # Block body, type depends on block type byte
    attr_reader :body
    attr_reader :_raw_body
  end

  ##
  # @see https://wiki.multimedia.cx/index.php?title=Creative_Voice#Block_type_0x06:_Repeat_start Source
  class BlockRepeatStart < Kaitai::Struct::Struct
    def initialize(_io, _parent = nil, _root = self)
      super(_io, _parent, _root)
      _read
    end

    def _read
      @repeat_count_1 = @_io.read_u2le
      self
    end

    ##
    # Number of repetitions minus 1; 0xffff means infinite repetitions
    attr_reader :repeat_count_1
  end

  ##
  # @see https://wiki.multimedia.cx/index.php?title=Creative_Voice#Block_type_0x01:_Sound_data Source
  class BlockSoundData < Kaitai::Struct::Struct
    def initialize(_io, _parent = nil, _root = self)
      super(_io, _parent, _root)
      _read
    end

    def _read
      @freq_div = @_io.read_u1
      @codec = Kaitai::Struct::Stream::resolve_enum(CreativeVoiceFile::CODECS, @_io.read_u1)
      @wave = @_io.read_bytes_full
      self
    end
    def sample_rate
      return @sample_rate unless @sample_rate.nil?
      @sample_rate = (1000000.0 / (256 - freq_div))
      @sample_rate
    end

    ##
    # Frequency divisor, used to determine sample rate
    attr_reader :freq_div
    attr_reader :codec
    attr_reader :wave
  end

  ##
  # @see https://wiki.multimedia.cx/index.php?title=Creative_Voice#Block_type_0x08:_Extra_info Source
  class BlockExtraInfo < Kaitai::Struct::Struct
    def initialize(_io, _parent = nil, _root = self)
      super(_io, _parent, _root)
      _read
    end

    def _read
      @freq_div = @_io.read_u2le
      @codec = Kaitai::Struct::Stream::resolve_enum(CreativeVoiceFile::CODECS, @_io.read_u1)
      @num_channels_1 = @_io.read_u1
      self
    end

    ##
    # Number of channels (1 = mono, 2 = stereo)
    def num_channels
      return @num_channels unless @num_channels.nil?
      @num_channels = (num_channels_1 + 1)
      @num_channels
    end
    def sample_rate
      return @sample_rate unless @sample_rate.nil?
      @sample_rate = (256000000.0 / (num_channels * (65536 - freq_div)))
      @sample_rate
    end

    ##
    # Frequency divisor
    attr_reader :freq_div
    attr_reader :codec

    ##
    # Number of channels minus 1 (0 = mono, 1 = stereo)
    attr_reader :num_channels_1
  end
  attr_reader :magic

  ##
  # Total size of this main header (usually 0x001A)
  attr_reader :header_size
  attr_reader :version

  ##
  # Checksum: this must be equal to ~version + 0x1234
  attr_reader :checksum

  ##
  # Series of blocks that contain the actual audio data
  attr_reader :blocks
end