Source code for ndspy.soundWave

# Copyright 2019 RoadrunnerWMC
#
# This file is part of ndspy.
#
# ndspy is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# ndspy is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with ndspy.  If not, see <https://www.gnu.org/licenses/>.
"""
Support for SWAV sound waves.
"""

import enum
import struct

from . import WaveType
from . import _common


[docs]class SWAV: """ An SWAV streamed audio file. """ def __init__(self, file=None): self.waveType = WaveType.PCM8 self.isLooped = False self.sampleRate = 8000 self.time = 0 self.loopOffset = 0 self.totalLength = 0 self.data = b'' if file is not None: if not file.startswith(b'SWAV'): raise ValueError("Wrong magic (should be b'SWAV', instead " f'found {file[:4]})') self._initFromData(file) def _initFromData(self, file): """ Initialize the SWAV from file data. """ magic, bom, version, filesize, headersize, numblocks = \ _common.NDS_STD_FILE_HEADER.unpack_from(file, 0) if version != 0x100: raise ValueError(f'Unsupported SWAV version: {version}') assert magic == b'SWAV', f'Incorrect SWAV magic ({magic})' dataMagic, dataSize = struct.unpack_from('<4sI', file, 0x10) assert dataMagic == b'DATA', f'Incorrect SWAV DATA magic ({dataMagic})' (waveType, self.isLooped, self.sampleRate, self.time, self.loopOffset, loopLength) = struct.unpack_from('<B?3HI', file, 0x18) self.waveType = WaveType(waveType) self.totalLength = self.loopOffset + loopLength self.data = file[0x24:filesize]
[docs] @classmethod def fromData(cls, data=b'', *, waveType=WaveType.PCM8, isLooped=False, sampleRate=8000, time=0, loopOffset=0, totalLength=0): """ Create an SWAV from raw waveform data. """ self = cls() self.waveType = waveType self.isLooped = isLooped self.sampleRate = sampleRate self.time = time self.loopOffset = loopOffset self.totalLength = totalLength self.data = data return self
[docs] @classmethod def fromFile(cls, filePath): """ Load an SWAV from a filesystem file. """ with open(filePath, 'rb') as f: return cls(f.read())
[docs] def save(self, *, updateTime=False, updateTotalLength=False): """ Generate file data representing this SWAV. """ if updateTime: self.time = int(16756991 / self.sampleRate) if updateTotalLength: self.totalLength = len(self.data) // 4 if self.loopOffset > self.totalLength: raise ValueError(f'Loop offset ({self.loopOffset}) > total length ({self.totalLength})!') data = bytearray() data.extend(_common.NDS_STD_FILE_HEADER.pack( b'SWAV', 0xFEFF, 0x100, 0x24 + len(self.data), 0x10, 1)) data.extend(struct.pack( '<4sI', b'DATA', 0x14 + len(self.data))) data.extend(struct.pack( '<B?3HI', self.waveType, self.isLooped, self.sampleRate, self.time, self.loopOffset, self.totalLength - self.loopOffset)) data.extend(self.data) return bytes(data)
[docs] def saveToFile(self, filePath, *, updateTime=False, updateTotalLength=False): """ Generate file data representing this SWAV, and save it to a filesystem file. """ d = self.save(updateTime=updateTime, updateTotalLength=updateTotalLength) with open(filePath, 'wb') as f: f.write(d)
def __str__(self): try: waveType = WaveType(self.waveType).name except Exception: waveType = f'waveType={hex(self.waveType)}' looped = ' looped' if self.isLooped else '' return f'<swav {waveType} {self.sampleRate}Hz{looped}>' def __repr__(self): args = [] args.append(_common.shortBytesRepr(self.data)) args.append(f'waveType={self.waveType!r}') args.append(f'isLooped={self.isLooped!r}') args.append(f'sampleRate={self.sampleRate!r}') args.append(f'time={self.time!r}') args.append(f'loopOffset={self.loopOffset!r}') args.append(f'totalLength={self.totalLength!r}') return f'{type(self).__name__}.fromData({", ".join(args)})'