Repository: nrkno/wave-bwf-rf64 (language: python)

Issue:
Support retrieving markers set through r64m chunk



Discussion / hints:

Fix patch:
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..61619a9
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,15 @@
+*~
+*.swp
+*.swo
+*.pyc
+sigmaIni.py
+.ropeproject*
+local_dev--dockerize.sh
+.dockerignore
+.cache
+.coverage
+.idea
+dist/
+*.egg-info/
+/.pytest_cache/
+/venv/
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e09af37..f7b642c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,10 @@
 This documents the (major) changes to the python wave library with support for both
 BWF and RF64 versions of the WAVE / RIFF format.
 
+## [1.0.6] - 2017-12-14
+### Changed
+- Updated to support Python3. We are no longer python2 friendly.
+
 ## [1.0.5] - 2017-01-02
 ### Added
 - Added a new method on the Wave_write class to update an existing BEXT coding history
diff --git a/MANIFEST b/MANIFEST
index 83bee81..e533724 100644
--- a/MANIFEST
+++ b/MANIFEST
@@ -1,6 +1,7 @@
 # file GENERATED by distutils, do NOT edit
+CHANGELOG.md
 COPYING
 README
-chunk_levl_RF64.py
+chunk.py
 setup.py
-wave_bwf_levl_RF64.py
+wave_bwf_rf64.py
diff --git a/MANIFEST.in b/MANIFEST.in
index 6ed3a2a..c6b603e 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -1,1 +1,1 @@
-include README COPYING
+include README COPYING CHANGELOG.md
diff --git a/README b/README
index c3b158c..63f9cc4 100644
--- a/README
+++ b/README
@@ -1,18 +1,23 @@
 Modules:
 
-chunk_levl_RF64.py            Original python module for handling RIFF chunks with extensions for handling RF64
-wave_bext_levl_RF64.py        Extension of original wave.py module with BWF handling added (bext, chna, axml, md5 and levl chunks).
+chunk.py            Original python module for handling RIFF chunks with extensions for handling RF64
+wave_bwf_rf64.py    Extension of original wave.py module with BWF handling added (bext, chna, axml, md5 and levl chunks).
 
 This package will give you the ability to work with both BWF and RF64 compatible wav-files in python.
 
 The addition of the md5 and levl chunks and the RF64 support was implemented by Tormod Værvågen of NRK (Norwegian Broadcasting Corporation).
 
+As of v 1.0.6 we no longer support python version lower than 3.5. Use 1.0.5 for py2 support. We will not backport new features.
+
 To setup distribution:
 > python setup.py sdist
-this will produce a 'dist' directory containing wave_bwf-1.0.tar.gz which can be released.
+this will produce a 'dist' directory containing the gunziped file that can be released or installed.
 
 To install modules:
 In 'dist':
-> tar -xzf wave_bwf-1.0.tar.gz
+> tar -xzf wave_bwf_rf64-1.0.6.tar.gz
 > cd wave_bwf-1.0
 > sudo python setup.py install
+
+Or, which would be better:
+> pip install wave_bwf_rf64-1.0.6.tar.gz
diff --git a/chunk_levl_RF64.py b/chunk.py
similarity index 88%
rename from chunk_levl_RF64.py
rename to chunk.py
index 51b1b72..6bc5f34 100644
--- a/chunk_levl_RF64.py
+++ b/chunk.py
@@ -64,8 +64,10 @@
 default is 1, i.e. aligned.
 """
 
+
 class Chunk:
-    def __init__(self, file, align=True, bigendian=True, inclheader=False, set_data_chunk_size=None):
+    def __init__(self, file, align=True, bigendian=True, inclheader=False,
+                 set_data_chunk_size=None):
         import struct
         self.closed = False
         self.align = align      # whether to align to word (2-byte) boundaries
@@ -77,9 +79,9 @@ def __init__(self, file, align=True, bigendian=True, inclheader=False, set_data_
         self.chunkname = file.read(4)
         if len(self.chunkname) < 4:
             raise EOFError
-        if not set_data_chunk_size is None and self.chunkname == 'data':
+        if set_data_chunk_size is not None and self.chunkname == b'data':
             self.chunksize = set_data_chunk_size
-            #Read dummy value
+            # Read dummy value
             struct.unpack(strflag+'L', file.read(4))
         else:
             try:
@@ -87,7 +89,7 @@ def __init__(self, file, align=True, bigendian=True, inclheader=False, set_data_
             except struct.error:
                 raise EOFError
         if inclheader:
-            self.chunksize = self.chunksize - 8 # subtract header
+            self.chunksize = self.chunksize - 8  # subtract header
         self.size_read = 0
         try:
             self.offset = self.file.tell()
@@ -105,17 +107,19 @@ def getsize(self):
         return self.chunksize
 
     def setsize(self, newsize):
-        #***
+        # ***
         self.chunksize = newsize
 
     def close(self):
         if not self.closed:
-            self.skip()
-            self.closed = True
+            try:
+                self.skip()
+            finally:
+                self.closed = True
 
     def isatty(self):
         if self.closed:
-            raise ValueError, "I/O operation on closed file"
+            raise ValueError('I/O operation on closed file')
         return False
 
     def seek(self, pos, whence=0):
@@ -125,34 +129,35 @@ def seek(self, pos, whence=0):
         """
 
         if self.closed:
-            raise ValueError, "I/O operation on closed file"
+            raise ValueError('I/O operation on closed file')
         if not self.seekable:
-            raise IOError, "cannot seek"
+            raise IOError('cannot seek')
         if whence == 1:
             pos = pos + self.size_read
         elif whence == 2:
             pos = pos + self.chunksize
         if pos < 0 or pos > self.chunksize:
-            print pos, self.chunksize
+            # print(pos, self.chunksize)
             raise RuntimeError
         self.file.seek(self.offset + pos, 0)
         self.size_read = pos
 
     def tell(self):
         if self.closed:
-            raise ValueError, "I/O operation on closed file"
+            raise ValueError('I/O operation on closed file')
         return self.size_read
 
     def read(self, size=-1):
         """Read at most size bytes from the chunk.
+
         If size is omitted or negative, read until the end
         of the chunk.
         """
 
         if self.closed:
-            raise ValueError, "I/O operation on closed file"
+            raise ValueError('I/O operation on closed file')
         if self.size_read >= self.chunksize:
-            return ''
+            return b''
         if size < 0:
             size = self.chunksize - self.size_read
         if size > self.chunksize - self.size_read:
@@ -168,13 +173,14 @@ def read(self, size=-1):
 
     def skip(self):
         """Skip the rest of the chunk.
+
         If you are not interested in the contents of the chunk,
         this method should be called so that the file points to
         the start of the next chunk.
         """
 
         if self.closed:
-            raise ValueError, "I/O operation on closed file"
+            raise ValueError('I/O operation on closed file')
         if self.seekable:
             try:
                 n = self.chunksize - self.size_read
@@ -191,3 +197,4 @@ def skip(self):
             dummy = self.read(n)
             if not dummy:
                 raise EOFError
+
diff --git a/setup.py b/setup.py
index de0b382..cff78ab 100644
--- a/setup.py
+++ b/setup.py
@@ -1,13 +1,19 @@
 #!/usr/bin/env python
 # -*- coding: utf-8 -*-
 from distutils.core import setup
-setup(name='wave_bwf_levl_RF64',
-      version='1.0.5',
-      description=u'Python modules for handling Broadcast Wave Format files. \
-      Updated to also handle the levl and md5 chunk and reading of RF64 files. \
-      This work was done by Tormod Værvågen of NRK (Norsk rikskringkasing).',
+setup(name='wave_bwf_rf64',
+      version='1.1.0',
+      description=u'Python modules for handling the Broadcast Wave Format and RF64 files.',
+      classifiers=[
+          'Development Status :: 3 :: Beta',
+          'Programming Language :: Python :: 3.10',
+          'Programming Language :: Python :: 3.11',
+          'Topic :: Multimedia :: Sound/Audio'
+      ],
+      url='https://github.com/nrkno/wave-bwf-rf64/',
       author='David Marston',
       author_email='david.marston@bbc.co.uk',
-      url='http://data.bbcarp.org.uk/saqas/',
       license="GNU GPLv3",
-      py_modules=['wave_bwf_levl_RF64', 'chunk_levl_RF64'],)
+      py_modules=['wave_bwf_rf64', 'chunk'],
+      python_requires=">=3.10",
+      )
diff --git a/wave_bwf_levl_RF64.py b/wave_bwf_rf64.py
similarity index 74%
rename from wave_bwf_levl_RF64.py
rename to wave_bwf_rf64.py
index 75f7d09..6eca991 100644
--- a/wave_bwf_levl_RF64.py
+++ b/wave_bwf_rf64.py
@@ -122,13 +122,27 @@
 is destroyed.
 """
 
-import __builtin__
+# import __builtin__
+import builtins
+
+__all__ = ["open", "openfp", "Error", "R64mMarker"]
+
+import dataclasses
+import enum
+import typing
+import audioop
+import struct
+import sys
+
+from chunk_levl_RF64 import Chunk
+from collections import namedtuple
+import datetime
 
-__all__ = ["open", "openfp", "Error"]
 
 class Error(Exception):
     pass
 
+
 WAVE_FORMAT_PCM = 0x0001
 # Not supported, as the parsing that existed just blew up with an AttributeError on
 # read/open..
@@ -137,49 +151,50 @@ class Error(Exception):
 
 _array_fmts = None, 'b', 'h', None, 'l'
 
-# Determine endian-ness
-import struct
-if struct.pack("h", 1) == "\000\001":
-    big_endian = 1
-else:
-    big_endian = 0
 
-from chunk_levl_RF64 import Chunk
-from datetime import *
+_wave_params = namedtuple('_wave_params',
+                          'nchannels sampwidth framerate nframes comptype compname')
+
 
 class Bext:
     def __init__(self):
         self._description = None
         self._originator = None
         self._originator_reference = None
-        now = datetime.now()
+        now = datetime.datetime.now()
         self._origination_date = [now.year, now.month, now.day]
         self._origination_time = [now.hour, now.minute, now.second]
         self._time_reference = [0, 0]
         self._version = 1
-        self._umid = ['\0']*64
+        self._umid = [b'\0']*64
         self._loudness_value = 0x7fff
         self._loudness_range = 0x7fff
         self._max_true_peak_level = 0x7fff
         self._max_momentary_loudness = 0x7fff
         self._max_short_term_loudness = 0x7fff
-        self._coding_history = ''
+        self._coding_history = b''
         self._loudness_params_pos = 344
 
     def pack_chunk(self):
         chunk = struct.pack('<0256s', self._description)
         chunk += struct.pack('<032s', self._originator)
         chunk += struct.pack('<032s', self._originator_reference)
-        chunk += struct.pack('<04s', ("{0:04d}".format(self._origination_date[0])))
-        chunk += '-'
-        chunk += struct.pack('<02s', ("{0:02d}".format(self._origination_date[1])))
-        chunk += '-'
-        chunk += struct.pack('<02s', ("{0:02d}".format(self._origination_date[2])))
-        chunk += struct.pack('<02s', ("{0:02d}".format(self._origination_time[0])))
-        chunk += '-'
-        chunk += struct.pack('<02s', ("{0:02d}".format(self._origination_time[1])))
-        chunk += '-'
-        chunk += struct.pack('<02s', ("{0:02d}".format(self._origination_time[2])))
+        chunk += struct.pack('<04s', ('{0:04d}'.format(
+            self._origination_date[0]).encode('utf-8')))
+        chunk += b'-'
+        chunk += struct.pack('<02s', ('{0:02d}'.format(
+            self._origination_date[1]).encode('utf-8')))
+        chunk += b'-'
+        chunk += struct.pack('<02s', ('{0:02d}'.format(
+            self._origination_date[2]).encode('utf-8')))
+        chunk += struct.pack('<02s', ('{0:02d}'.format(
+            self._origination_time[0]).encode('utf-8')))
+        chunk += b'-'
+        chunk += struct.pack('<02s', ('{0:02d}'.format(
+            self._origination_time[1]).encode('utf-8')))
+        chunk += b'-'
+        chunk += struct.pack('<02s', ('{0:02d}'.format(
+            self._origination_time[2]).encode('utf-8')))
         chunk += struct.pack('<ll', self._time_reference[0], self._time_reference[1])
         chunk += struct.pack('<h', self._version)
         for i in range(0, 64):
@@ -193,12 +208,12 @@ def pack_chunk(self):
             chunk += struct.pack('<h', self._max_short_term_loudness)
         else:
             for i in range(0, 10):
-                chunk += '\0'
+                chunk += b'\0'
         for i in range(0, 180):
-            chunk += '\0'
+            chunk += b'\0'
         chunk += self._coding_history
         if (len(self._coding_history) % 2) == 1:
-            chunk += '\0'
+            chunk += b'\0'
         return chunk
 
     def unpack_chunk(self, chunk):
@@ -251,14 +266,15 @@ def scaledown(self, val):
     def generate_coding_history(self, framerate, sampwidth, nchannels):
         """The format of the coding_history is based on EBU R98-1999"""
         if nchannels == 1:
-            chnstr = 'mono'
+            chnstr = b'mono'
         elif nchannels == 2:
-            chnstr = 'stereo'
+            chnstr = b'stereo'
         else:
-            chnstr = "%d" % (nchannels)
-        self._coding_history = "A=PCM,F=%u,W=%hu,M=%s,T=%s-%s\r\n" % (framerate, sampwidth * 8, chnstr, 'wave.py', '2.0')
+            chnstr = b'%d' % (nchannels)
+        self._coding_history = b'A=PCM,F=%u,W=%hu,M=%s,T=%s-%s\r\n' % (
+            framerate, sampwidth * 8, chnstr, b'wave_bwf_levl_RF64.py', b'1.0.6')
         if (len(self._coding_history) % 2) == 1:
-            self._coding_history += '\0'
+            self._coding_history += b'\0'
 
     def loudness_params_pos(self):
         return self._loudness_params_pos
@@ -271,6 +287,7 @@ def rewrite_loudness_parameters(self):
         chunk += struct.pack('<h', self._max_short_term_loudness)
         return chunk
 
+
 class Chna:
     def __init__(self):
         self._num_tracks = 0
@@ -341,6 +358,66 @@ def unpack_chunk(self, chunk):
             self._ch_id.append(ch_id)
 
 
+@dataclasses.dataclass
+class R64mMarker:
+    sample_offset: int
+    time_offset: datetime.timedelta
+    label: str
+
+
+class RawR64mMarkerEntry(typing.NamedTuple):
+    flags: int
+    sample_offset: int
+    byte_offset: int
+    intra_sample_offset: int
+    label_text: bytes
+    label_chunk_identifier: int
+    vendor_and_product: bytes
+    user_data_1: int
+    user_data_2: int
+    user_data_3: int
+    user_data_4: int
+
+
+class MarkerEntryFlags(enum.Flag):
+    ENTRY_IS_VALID = enum.auto()
+    BYTE_OFFSET_IS_VALID = enum.auto()
+    INTRA_SAMPLE_OFFSET_IS_VALID = enum.auto()
+    LABEL_IS_IN_LABL_CHUNK = enum.auto()
+    LABEL_TEXT_IS_UTF_8 = enum.auto()
+
+
+class R64m:
+    def __init__(self):
+        self._markers: list[R64mMarker] = []
+
+    def read_markers(self):
+        return self._markers
+
+    def unpack_chunk(self, chunk: Chunk, sample_rate: int):
+        chunk.seek(0, 0)
+        chunk_size = chunk.getsize()
+        while chunk.tell() < chunk_size:
+            raw_values = struct.unpack("<L Q Q Q 256s L 16s 4L", chunk.read(320))
+            marker_entry = RawR64mMarkerEntry._make(raw_values)
+
+            flags = MarkerEntryFlags(marker_entry.flags)
+            if MarkerEntryFlags.ENTRY_IS_VALID not in flags:
+                continue
+
+            sample_offset = marker_entry.sample_offset
+            seconds_offset = sample_offset / sample_rate
+            time_offset = datetime.timedelta(seconds=seconds_offset)
+
+            if MarkerEntryFlags.LABEL_TEXT_IS_UTF_8 in flags:
+                encoding = "utf-8"
+            else:
+                encoding = "windows-1252"
+            label_until_null = marker_entry.label_text.rstrip(b'\x00')
+            label = label_until_null.decode(encoding)
+            self._markers.append(R64mMarker(sample_offset, time_offset, label))
+
+
 class Wave_read:
     """Variables used in this class:
 
@@ -378,10 +455,10 @@ def initfp(self, file):
         self._file = Chunk(file, bigendian=0)
         # ** Check for RF64 header
         # ** If present, read special chunk
-        if self._file.getname() not in ['RIFF', 'RF64']:#** Support RF64
-            raise Error, 'file does not start with RIFF id'
-        if self._file.read(4) != 'WAVE':
-            raise Error, 'not a WAVE file'
+        if self._file.getname() not in [b'RIFF', b'RF64']:  # ** Support RF64
+            raise Error('file does not start with RIFF id')
+        if self._file.read(4) != b'WAVE':
+            raise Error('not a WAVE file')
         self._fmt_chunk_read = 0
         self._data_chunk = None
         self._bext_chunk = None
@@ -389,11 +466,13 @@ def initfp(self, file):
         self._md5_chunk = None
         self._levl_chunk = None
         self._chna_chunk = None
+        self._r64m_chunk = None
         self._bext = Bext()
         self._chna = Chna()
+        self._r64m = R64m()
 
         # Check for RF64
-        if self._file.getname() == 'RF64':
+        if self._file.getname() == b'RF64':
             self._RF64 = True
             ds64 = Chunk(self._file, bigendian=0)
             self._read_ds64_chunk(ds64)
@@ -413,35 +492,37 @@ def initfp(self, file):
             except EOFError:
                 break
             chunkname = chunk.getname()
-            if chunkname == 'fmt ':
+            if chunkname == b'fmt ':
                 self._read_fmt_chunk(chunk)
                 self._fmt_chunk_read = 1
-            elif chunkname == 'data':
+            elif chunkname == b'data':
                 # ** RF64 check real length
                 if not self._fmt_chunk_read:
-                    raise Error, 'data chunk before fmt chunk'
+                    raise Error('data chunk before fmt chunk')
                 self._data_chunk = chunk
                 self._nframes = chunk.chunksize // self._framesize  # ** Chunksize may be FFFFFFFF read real length from separate chunk
                 self._data_seek_needed = 0
                 # break
-            elif chunkname == "bext":
+            elif chunkname == b'bext':
                 self._bext_chunk = chunk
-            elif chunkname == "axml":
+            elif chunkname == b'axml':
                 self._axml_chunk = chunk
-            elif chunkname == "MD5 ":
+            elif chunkname == b'MD5 ':
                 self._md5_chunk = chunk
-            elif chunkname == "levl":
+            elif chunkname == b'levl':
                 self._levl_chunk = chunk
-            elif chunkname == "chna":
+            elif chunkname == b'chna':
                 self._chna_chunk = chunk
+            elif chunkname == b'r64m':
+                self._r64m_chunk = chunk
             chunk.skip()
         if not self._fmt_chunk_read or not self._data_chunk:
-            raise Error, 'fmt chunk and/or data chunk missing'
+            raise Error('fmt chunk and/or data chunk missing')
 
     def __init__(self, f):
         self._i_opened_the_file = None
-        if isinstance(f, basestring):
-            f = __builtin__.open(f, 'rb')
+        if isinstance(f, str):
+            f = builtins.open(f, 'rb')
             self._i_opened_the_file = f
         # else, assume it is an open file object already
         try:
@@ -499,11 +580,11 @@ def getmarkers(self):
         return None
 
     def getmark(self, id):
-        raise Error, 'no marks'
+        raise Error('no marks')
 
     def setpos(self, pos):
         if pos < 0 or pos > self._nframes:
-            raise Error, 'position not in range'
+            raise Error('position not in range')
         self._soundpos = pos
         self._data_seek_needed = 1
 
@@ -515,27 +596,27 @@ def readframes(self, nframes):
                 self._data_chunk.seek(pos, 0)
             self._data_seek_needed = 0
         if nframes == 0:
-            return ''
-        if self._sampwidth > 1 and big_endian:
-            # unfortunately the fromfile() method does not take
-            # something that only looks like a file object, so
-            # we have to reach into the innards of the chunk object
-            import array
-            chunk = self._data_chunk
-            data = array.array(_array_fmts[self._sampwidth])
-            nitems = nframes * self._nchannels
-            if nitems * self._sampwidth > chunk.chunksize - chunk.size_read:
-                nitems = (chunk.chunksize - chunk.size_read) / self._sampwidth
-            data.fromfile(chunk.file.file, nitems)
-            # "tell" data chunk how much was read
-            chunk.size_read = chunk.size_read + nitems * self._sampwidth
-            # do the same for the outermost chunk
-            chunk = chunk.file
-            chunk.size_read = chunk.size_read + nitems * self._sampwidth
-            data.byteswap()
-            data = data.tostring()
-        else:
-            data = self._data_chunk.read(nframes * self._framesize)
+            return b''
+        data = self._data_chunk.read(nframes * self._framesize)
+        if self._sampwidth != 1 and sys.byteorder == 'big':
+            data = audioop.byteswap(data, self._sampwidth)
+            # # unfortunately the fromfile() method does not take
+            # # something that only looks like a file object, so
+            # # we have to reach into the innards of the chunk object
+            # import array
+            # chunk = self._data_chunk
+            # data = array.array(_array_fmts[self._sampwidth])
+            # nitems = nframes * self._nchannels
+            # if nitems * self._sampwidth > chunk.chunksize - chunk.size_read:
+            #     nitems = (chunk.chunksize - chunk.size_read) / self._sampwidth
+            # data.fromfile(chunk.file.file, nitems)
+            # # "tell" data chunk how much was read
+            # chunk.size_read = chunk.size_read + nitems * self._sampwidth
+            # # do the same for the outermost chunk
+            # chunk = chunk.file
+            # chunk.size_read = chunk.size_read + nitems * self._sampwidth
+            # data.byteswap()
+            # data = data.tostring()
         if self._convert and data:
             data = self._convert(data)
         self._soundpos = self._soundpos + len(data) // (self._nchannels * self._sampwidth)
@@ -594,7 +675,7 @@ def get_bext_chunk(self):
         return self._bext.pack_chunk()
 
     def read_axml(self):
-        if self._axml_chunk != None:
+        if self._axml_chunk is not None:
             self._axml_chunk.seek(0, 0)
         else:
             return False
@@ -602,7 +683,7 @@ def read_axml(self):
         return data
 
     def read_md5(self):
-        if self._md5_chunk != None:
+        if self._md5_chunk is not None:
             self._md5_chunk.seek(0, 0)
         else:
             return False
@@ -610,7 +691,7 @@ def read_md5(self):
         return data
 
     def read_levl(self):
-        if self._levl_chunk != None:
+        if self._levl_chunk is not None:
             self._levl_chunk.seek(0, 0)
         else:
             return False
@@ -632,6 +713,15 @@ def read_chna(self):
         self._chna.unpack_chunk(self._chna_chunk)
         return True
 
+    def read_r64m(self):
+        if not self._r64m_chunk:
+            return False
+        self._r64m.unpack_chunk(self._r64m_chunk, self.getframerate())
+        return True
+
+    def get_r64m_markers(self):
+        return self._r64m.read_markers()
+
     #
     # Internal methods.
     #
@@ -645,11 +735,11 @@ def _read_fmt_chunk(self, chunk):
             sampwidth, cbSize = struct.unpack('<hh', chunk.read(4))
             self._sampwidth = (sampwidth + 7) // 8
             if cbSize != 22:
-                raise Error, 'WAVE_FORMAT_EXTENSIBLE format, wrong cbSize: %r' % (cbSize)
+                raise Error('WAVE_FORMAT_EXTENSIBLE format, wrong cbSize: %r' % (cbSize))
             subFormat = [0, 0, 0, 0]
             wValidBitsPerSample, dwChannelMask, subFormat[0], subFormat[1], subFormat[2], subFormat[3] = struct.unpack('<hLLLLL', chunk.read(22))
         else:
-            raise Error, 'unknown format: %r' % (wFormatTag,)
+            raise Error('unknown format: %r' % (wFormatTag,))
         self._framesize = self._nchannels * self._sampwidth
         self._comptype = 'NONE'
         self._compname = 'not compressed'
@@ -687,8 +777,8 @@ class Wave_write:
 
     def __init__(self, f):
         self._i_opened_the_file = None
-        if isinstance(f, basestring):
-            f = __builtin__.open(f, 'wb')
+        if isinstance(f, str):
+            f = builtins.open(f, 'wb')
             self._i_opened_the_file = f
         try:
             self.initfp(f)
@@ -707,6 +797,7 @@ def initfp(self, file):
         self._nframeswritten = 0
         self._datawritten = 0
         self._datalength = 0
+
         self._bext_chunk_data = None
         self._axml_chunk_data = None
         self._md5_chunk_data = None
@@ -715,51 +806,59 @@ def initfp(self, file):
         self._bext = Bext()
         self._chna = Chna()
 
+        self._headerwritten = False
+
     def __del__(self):
         self.close()
 
+    def __enter__(self):
+        return self
+
+    def __exit__(self, *args):
+        self.close()
+
     #
     # User visible methods.
     #
     def setnchannels(self, nchannels):
         if self._datawritten:
-            raise Error, 'cannot change parameters after starting to write'
+            raise Error('cannot change parameters after starting to write')
         if nchannels < 1:
-            raise Error, 'bad # of channels'
+            raise Error('bad # of channels')
         self._nchannels = nchannels
 
     def getnchannels(self):
         if not self._nchannels:
-            raise Error, 'number of channels not set'
+            raise Error('number of channels not set')
         return self._nchannels
 
     def setsampwidth(self, sampwidth):
         if self._datawritten:
-            raise Error, 'cannot change parameters after starting to write'
+            raise Error('cannot change parameters after starting to write')
         if sampwidth < 1 or sampwidth > 4:
-            raise Error, 'bad sample width'
+            raise Error('bad sample width')
         self._sampwidth = sampwidth
 
     def getsampwidth(self):
         if not self._sampwidth:
-            raise Error, 'sample width not set'
+            raise Error('sample width not set')
         return self._sampwidth
 
     def setframerate(self, framerate):
         if self._datawritten:
-            raise Error, 'cannot change parameters after starting to write'
+            raise Error('cannot change parameters after starting to write')
         if framerate <= 0:
-            raise Error, 'bad frame rate'
-        self._framerate = framerate
+            raise Error('bad frame rate')
+        self._framerate = int(round(framerate))
 
     def getframerate(self):
         if not self._framerate:
-            raise Error, 'frame rate not set'
+            raise Error('frame rate not set')
         return self._framerate
 
     def setnframes(self, nframes):
         if self._datawritten:
-            raise Error, 'cannot change parameters after starting to write'
+            raise Error('cannot change parameters after starting to write')
         self._nframes = nframes
 
     def getnframes(self):
@@ -767,9 +866,9 @@ def getnframes(self):
 
     def setcomptype(self, comptype, compname):
         if self._datawritten:
-            raise Error, 'cannot change parameters after starting to write'
+            raise Error('cannot change parameters after starting to write')
         if comptype not in ('NONE',):
-            raise Error, 'unsupported compression type'
+            raise Error('unsupported compression type')
         self._comptype = comptype
         self._compname = compname
 
@@ -779,9 +878,10 @@ def getcomptype(self):
     def getcompname(self):
         return self._compname
 
-    def setparams(self, (nchannels, sampwidth, framerate, nframes, comptype, compname)):
+    def setparams(self, params):
+        nchannels, sampwidth, framerate, nframes, comptype, compname = params
         if self._datawritten:
-            raise Error, 'cannot change parameters after starting to write'
+            raise Error('cannot change parameters after starting to write')
         self.setnchannels(nchannels)
         self.setsampwidth(sampwidth)
         self.setframerate(framerate)
@@ -790,15 +890,15 @@ def setparams(self, (nchannels, sampwidth, framerate, nframes, comptype, compnam
 
     def getparams(self):
         if not self._nchannels or not self._sampwidth or not self._framerate:
-            raise Error, 'not all parameters set'
-        return self._nchannels, self._sampwidth, self._framerate, \
-              self._nframes, self._comptype, self._compname
+            raise Error('not all parameters set')
+        return _wave_params(self._nchannels, self._sampwidth, self._framerate,
+                            self._nframes, self._comptype, self._compname)
 
     def setmark(self, id, pos, name):
-        raise Error, 'setmark() not supported'
+        raise Error('setmark() not supported')
 
     def getmark(self, id):
-        raise Error, 'no marks'
+        raise Error('no marks')
 
     def getmarkers(self):
         return None
@@ -807,42 +907,48 @@ def tell(self):
         return self._nframeswritten
 
     def writeframesraw(self, data):
+        if not isinstance(data, (bytes, bytearray)):
+            data = memoryview(data).cast('B')
         self._ensure_header_written(len(data))
         nframes = len(data) // (self._sampwidth * self._nchannels)
         if self._convert:
             data = self._convert(data)
-        if self._sampwidth > 1 and big_endian:
-            import array
-            data = array.array(_array_fmts[self._sampwidth], data)
-            data.byteswap()
-            data.tofile(self._file)
-            self._datawritten = self._datawritten + len(data) * self._sampwidth
-        else:
-            self._file.write(data)
-            self._datawritten = self._datawritten + len(data)
+
+        if self._sampwidth != 1 and sys.byteorder == 'big':
+            data = audioop.byteswap(data, self._sampwidth)
+
+        self._file.write(data)
+        self._datawritten += len(data)
         self._nframeswritten = self._nframeswritten + nframes
 
     def writeframes(self, data):
         self.writeframesraw(data)
-        #if self._datalength != self._datawritten:
-        self._patchheader()
+        if self._datalength != self._datawritten:
+            self._patchheader()
 
     def close(self):
-        if self._file:
-            self._ensure_header_written(0)
-            #if self._datalength != self._datawritten:
-            self._patchheader()
-            if self._axml_chunk_data:
-                self._write_axml_chunk()
-            if self._levl_chunk_data:
-                self._write_levl_chunk()
-            if self._md5_chunk_data:
-                self._write_md5_chunk()
-            self._file.flush()
+        try:
+            if self._file:
+                self._ensure_header_written(0)
+                # if self._datalength != self._datawritten:
+                self._patchheader()
+                if self._axml_chunk_data:
+                    self._write_axml_chunk()
+                if self._levl_chunk_data:
+                    self._write_levl_chunk()
+                if self._md5_chunk_data:
+                    self._write_md5_chunk()
+                self._file.flush()
+        finally:
             self._file = None
-        if self._i_opened_the_file:
-            self._i_opened_the_file.close()
-            self._i_opened_the_file = None
+            file = self._i_opened_the_file
+            if file:
+                self._i_opened_the_file = None
+                file.close()
+
+    #
+    # This is the expansion to support BWF/LEVL/RF64
+    #
 
     def set_bext_description(self, val):
         self._bext._description = val
@@ -889,20 +995,22 @@ def set_bext_coding_history(self, val):
         self._bext._coding_history = val
 
     def set_bext(self):
-        if self._bext._coding_history == '':
-            self._bext.generate_coding_history(self._framerate, self._sampwidth, self._nchannels)
+        if self._bext._coding_history == b'':
+            self._bext.generate_coding_history(self._framerate,
+                                               self._sampwidth, self._nchannels)
         self._bext_chunk_data = self._bext.pack_chunk()
 
     def update_bext_coding_history(self):
-        """This will update an existing bext coding history chunk with a new line."""
+        """Update an existing bext coding history chunk with a new line."""
         old_history = self._bext._coding_history
-        self._bext.generate_coding_history(self._framerate, self._sampwidth, self._nchannels)
+        self._bext.generate_coding_history(self._framerate, self._sampwidth,
+                                           self._nchannels)
         history_addition = self._bext._coding_history
         # Only add a newline to the old one if it didn't have one
-        if old_history.endswith('\r\n'):
+        if old_history.endswith(b'\r\n'):
             self._bext._coding_history = old_history + history_addition
         else:
-            self.bext._coding_history = old_history + '\r\n' + history_addition
+            self._bext._coding_history = old_history + b'\r\n' + history_addition
 
     def copy_bext(self, _bext_chunk):
         self._bext_chunk_data = _bext_chunk
@@ -910,17 +1018,17 @@ def copy_bext(self, _bext_chunk):
     def set_axml(self, data):
         self._axml_chunk_data = data
         if (len(self._axml_chunk_data) % 2) == 1:
-            self._axml_chunk_data += ' '
+            self._axml_chunk_data += b' '
 
     def set_md5(self, data):
         self._md5_chunk_data = data
         if (len(self._md5_chunk_data) % 2) == 1:
-            self._md5_chunk_data += ' '
+            self._md5_chunk_data += b' '
 
     def set_levl(self, data):
         self._levl_chunk_data = data
         if (len(self._levl_chunk_data) % 2) == 1:
-            self._levl_chunk_data += ' '#Vet ikke om denne behøves
+            self._levl_chunk_data += b' '  # Vet ikke om denne behøves
 
     def chna_add_new_track(self, track_idx, track_uid, track_ref, pack_ref):
         self._chna.add_new_track(track_idx, track_uid, track_ref, pack_ref)
@@ -936,69 +1044,84 @@ def set_chna(self):
     #
 
     def _ensure_header_written(self, datasize):
-        if not self._datawritten:
+        # if not self._datawritten:
+        if not self._headerwritten:
             if not self._nchannels:
-                raise Error, '# channels not specified'
+                raise Error('# channels not specified')
             if not self._sampwidth:
-                raise Error, 'sample width not specified'
+                raise Error('sample width not specified')
             if not self._framerate:
-                raise Error, 'sampling rate not specified'
+                raise Error('sampling rate not specified')
+
             self._write_header(datasize)
             if self._bext_chunk_data:
                 self._write_bext_chunk()
             if self._chna_chunk_data:
                 self._write_chna_chunk()
+
             self._write_data_header()
 
     def _write_header(self, initlength):
-        #*** MOD
+        # *** MOD
+        assert not self._headerwritten
         if not self._nframes:
             self._nframes = initlength / (self._nchannels * self._sampwidth)
         self._datalength = self._nframes * self._nchannels * self._sampwidth
-        if self._datalength > 2140483647: # eg 2147483647 - some space for other chunks
-            self._type = 'RF64'
-            self._file.write('RF64')
+
+        # RF64 territory
+        if self._datalength > 2140483647:  # eg 2147483647 - some space for other chunks
+            self._type = b'RF64'
+            self._file.write(b'RF64')
             self._form_length_pos = self._file.tell()
             self._file.write(struct.pack('<l', -1))
-            self._file.write('WAVE')
+            self._file.write(b'WAVE')
             self._form_length_pos = self._file.tell()
             self._write_ds64_header()
             self._write_fmt()
         else:
-            self._type = 'RIFF'
-            self._file.write('RIFF')
+            self._type = b'RIFF'
+            self._file.write(b'RIFF')
             self._form_length_pos = self._file.tell()
-            self._file.write(struct.pack('<l4s',
-                36 + self._datalength, 'WAVE'))
+            self._file.write(struct.pack('<L4s',
+                                         36 + self._datalength, b'WAVE'))
             self._write_fmt()
-        #Add
+
+        self._headerwritten = True
+
+    # Add
     def _write_ds64_header(self, new_size=None):
         if not new_size:
-            self._file.write(struct.pack('<4slqqql', 'ds64', 28, self._datalength + 36 + 36, self._datalength, self._nframes, 0))
+            self._file.write(struct.pack(
+                '<4slqqql', b'ds64', 28, self._datalength + 36 + 36,
+                self._datalength, self._nframes, 0))
         else:
-            self._file.write(struct.pack('<4slqqql', 'ds64', 28, new_size + 36, self._datawritten, self._nframes, 0))
-            #Add 36 byte for ds64 header
+            self._file.write(struct.pack(
+                '<4slqqql', b'ds64', 28, new_size + 36,
+                self._datawritten, self._nframes, 0))
+
+            # Add 36 byte for ds64 header
 
     def _write_fmt(self):
         self._file.write(struct.pack('<4slhhllhh',
-                'fmt ', 16,
-                WAVE_FORMAT_PCM, self._nchannels, self._framerate,
-                self._nchannels * self._framerate * self._sampwidth,
-                self._nchannels * self._sampwidth,
-                self._sampwidth * 8))
+                                     b'fmt ', 16,
+                                     WAVE_FORMAT_PCM, self._nchannels, self._framerate,
+                                     self._nchannels * self._framerate * self._sampwidth,
+                                     self._nchannels * self._sampwidth,
+                                     self._sampwidth * 8))
 
     def _write_data_header(self):
-        self._file.write(struct.pack('<4s', 'data'))
+        self._file.write(struct.pack('<4s', b'data'))
         self._data_length_pos = self._file.tell()
-        if self._type == 'RF64':
+        if self._type == b'RF64':
             self._file.write(struct.pack('<l', -1))
         else:
             self._file.write(struct.pack('<l', self._datalength))
 
     def _patchheader(self):
-        #if self._datawritten == self._datalength:
+        # if self._datawritten == self._datalength:
         #    return
-        #Mask values before struct.pack & 0xFFFFFFFF
+        # Mask values before struct.pack & 0xFFFFFFFF
+        assert self._headerwritten
         curpos = self._file.tell()
         self._file.seek(self._form_length_pos, 0)
         new_size = 36                  # fmt chunk size
@@ -1013,7 +1136,7 @@ def _patchheader(self):
             new_size += len(self._md5_chunk_data) + 8
         if self._levl_chunk_data:           # levl chunk size
             new_size += len(self._levl_chunk_data) + 8
-        if self._type == 'RF64':
+        if self._type == b'RF64':
             self._write_ds64_header(new_size)
         else:
             self._file.write(struct.pack('<l', new_size))
@@ -1026,28 +1149,28 @@ def _patchheader(self):
         self._datalength = self._datawritten
 
     def _write_bext_chunk(self):
-        self._file.write(struct.pack('<4s', 'bext'))
+        self._file.write(struct.pack('<4s', b'bext'))
         self._file.write(struct.pack('<l', len(self._bext_chunk_data)))
         self._loudness_params_pos = self._bext.loudness_params_pos() + self._file.tell()
         self._file.write(self._bext_chunk_data)
 
     def _write_axml_chunk(self):
-        self._file.write(struct.pack('<4s', 'axml'))
+        self._file.write(struct.pack('<4s', b'axml'))
         self._file.write(struct.pack('<l', len(self._axml_chunk_data)))
         self._file.write(self._axml_chunk_data)
 
     def _write_md5_chunk(self):
-        self._file.write(struct.pack('<4s', 'MD5 '))
+        self._file.write(struct.pack('<4s', b'MD5 '))
         self._file.write(struct.pack('<l', 16))
         self._file.write(struct.pack('>16s', self._md5_chunk_data[::-1]))
 
     def _write_levl_chunk(self):
-        self._file.write(struct.pack('<4s', 'levl'))
+        self._file.write(struct.pack('<4s', b'levl'))
         self._file.write(struct.pack('<l', len(self._levl_chunk_data)))
         self._file.write(self._levl_chunk_data)
 
     def _write_chna_chunk(self):
-        self._file.write(struct.pack('<4s', 'chna'))
+        self._file.write(struct.pack('<4s', b'chna'))
         self._file.write(struct.pack('<l', len(self._chna_chunk_data)))
         self._file.write(self._chna_chunk_data)
 
@@ -1063,6 +1186,7 @@ def open(f, mode=None):
     elif mode in ('w', 'wb'):
         return Wave_write(f)
     else:
-        raise Error, "mode must be 'r', 'rb', 'w', or 'wb'"
+        raise Error("mode must be 'r', 'rb', 'w', or 'wb'")
+
 
-openfp = open # B/W compatibility
+openfp = open  # B/W compatibility


Test patch:
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/markers.wav b/tests/markers.wav
new file mode 100644
index 0000000..d5137ce
Binary files /dev/null and b/tests/markers.wav differ
diff --git a/tests/test_r64m.py b/tests/test_r64m.py
new file mode 100644
index 0000000..ca182d8
--- /dev/null
+++ b/tests/test_r64m.py
@@ -0,0 +1,37 @@
+import pathlib
+import datetime
+
+import pytest
+
+import wave_bwf_levl_RF64
+
+
+def test_reading_markers(markers_wavefile):
+    has_r64m_chunk = markers_wavefile.read_r64m()
+    assert has_r64m_chunk, "did not find r64m chunk?"
+
+    markers = markers_wavefile.get_r64m_markers()
+    assert markers[0].time_offset == datetime.timedelta(seconds=1, milliseconds=990)
+    assert markers[0].label == "Test of special characters in UTF-8: Ođas ja áigeguovdil. Vuoiŋŋat. Sápmelaččain"
+
+    assert markers[1].time_offset == datetime.timedelta(seconds=8, milliseconds=60)
+    assert markers[1].label == "Test of special characters in Windows-1252: €æøåá"
+
+    assert markers[2].time_offset == datetime.timedelta(seconds=11)
+    assert markers[2].label == "From off to on"
+
+    assert markers[3].time_offset == datetime.timedelta(seconds=12)
+    assert markers[3].label == "From on to off"
+
+    assert len(markers) == 4
+
+
+@pytest.fixture
+def markers_wavefile():
+    this_dir = pathlib.Path(__file__).parent
+    filepath = this_dir / 'markers.wav'
+    wavefile = wave_bwf_levl_RF64.open(str(filepath), "rb")
+    try:
+        yield wavefile
+    finally:
+        wavefile.close()