2020-08-01 15:59:34 +00:00
|
|
|
|
|
|
|
from base import rObject
|
|
|
|
import audioop
|
|
|
|
import struct
|
|
|
|
import re
|
|
|
|
import sys
|
2020-08-02 21:28:59 +00:00
|
|
|
import os
|
2020-08-01 15:59:34 +00:00
|
|
|
from math import log2
|
|
|
|
|
|
|
|
__all__ = ["rSoundSample"]
|
|
|
|
|
|
|
|
# See: IIgs TechNote #76 Miscellaneous Resource Formats
|
|
|
|
# See: HCGS TechNote #3 Pitching Sampled Sounds
|
|
|
|
|
|
|
|
# HyperCard assumes a sample rate of 26.32 KHz (DOC rate w/ 32 oscillators)
|
|
|
|
# and a pitch of 261.63 Hz (Middle C, C4)
|
|
|
|
# See HyperCard IIgs Tech Note #3: Pitching Sampled Sounds
|
|
|
|
def relative_pitch(fS, fW = None):
|
|
|
|
# fW = frequency of sample
|
|
|
|
# fS = sampling rate
|
|
|
|
|
|
|
|
r = 0
|
|
|
|
if fW: r = (261.63 * fS) / (26320 * fW)
|
|
|
|
else: r = fS / 26320
|
|
|
|
|
|
|
|
|
|
|
|
offset = round(3072 * log2(r))
|
|
|
|
if (offset < -32767) or (offset > 32767):
|
|
|
|
raise Exception("Audio error: offset too big")
|
|
|
|
if offset < 0: offset = 0x8000 | abs(offset)
|
|
|
|
return offset
|
|
|
|
|
|
|
|
|
|
|
|
def pitch_to_hz(p):
|
|
|
|
if p == None: return 261.63
|
|
|
|
if type(p) in (int, float): return float(p)
|
|
|
|
if type(p) == str:
|
2020-08-02 21:28:59 +00:00
|
|
|
m = re.match("^([A-Ga-g])([#b])?([0-8])$", p)
|
2020-08-01 15:59:34 +00:00
|
|
|
if not m: return None
|
|
|
|
note = m1[1].upper(); accidental = m[2]; octave = int(m[3])
|
|
|
|
|
|
|
|
a = "CxDxEFxGxAxB".index(note)-9
|
|
|
|
if accidental == "#": a += 1
|
|
|
|
if accidental == "b": a -= 1
|
|
|
|
|
|
|
|
f = 440.0 * (2 ** (a/12))
|
|
|
|
f *= 2 ** (octave-4)
|
|
|
|
return f
|
|
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
def open_audio(file):
|
|
|
|
|
|
|
|
_, ext = os.path.splitext(os.path.basename(file))
|
|
|
|
ext = ext.lower()
|
|
|
|
|
|
|
|
# if ext in (".wav", ".wave"):
|
|
|
|
# import wave
|
|
|
|
# return wave.open(file, "rb"), 'little', 128
|
|
|
|
|
|
|
|
|
|
|
|
if ext in (".aiff", ".aifc", ".aif"):
|
|
|
|
import aifc
|
|
|
|
return aifc.open(file, "rb"), 'big', 'AIFF'
|
|
|
|
|
|
|
|
if ext in (".au", ".snd"):
|
|
|
|
import sunau
|
|
|
|
return sunau.open(file, "rb"), 'big', 'SUN'
|
|
|
|
|
|
|
|
# default
|
|
|
|
import wave
|
|
|
|
return wave.open(file, "rb"), 'little', 'WAVE'
|
|
|
|
|
|
|
|
|
|
|
|
class rSoundSample(rObject):
|
2020-08-01 16:47:48 +00:00
|
|
|
"""
|
|
|
|
filename: input file to read. format is .wav, .au, .aiff, or .aifc
|
|
|
|
pitch: audio pitch, if this is a note. specify hz (eg 261.63) or name (eg c4)
|
|
|
|
rate: down/upsample audio to this rate (eg 26320)
|
|
|
|
channel: stereo channel
|
|
|
|
|
|
|
|
Native samples are 26320 khz, c4 (261.63 hz)
|
|
|
|
"""
|
2020-08-01 15:59:34 +00:00
|
|
|
|
|
|
|
rName = "rSoundSample"
|
|
|
|
rType = 0x8024
|
|
|
|
|
2020-08-02 21:28:59 +00:00
|
|
|
def __init__(self, filename, pitch=None, rate=None, channel=0, **kwargs):
|
2020-08-01 15:59:34 +00:00
|
|
|
|
2020-08-02 00:39:02 +00:00
|
|
|
super().__init__(**kwargs)
|
2020-08-01 15:59:34 +00:00
|
|
|
|
|
|
|
new_rate = rate
|
|
|
|
freq = pitch_to_hz(pitch)
|
2020-08-02 21:28:59 +00:00
|
|
|
if not freq: raise ValueError("Invalid pitch: {}".format(pitch))
|
2020-08-01 15:59:34 +00:00
|
|
|
|
|
|
|
# audio conversion
|
|
|
|
|
|
|
|
verbose = False
|
|
|
|
# if verbose: print("Input File: {}".format(filename))
|
|
|
|
|
|
|
|
|
|
|
|
rv = bytearray()
|
|
|
|
tr = b"\x01" + bytes(range(1,256)) # remap 0 -> 1
|
|
|
|
|
|
|
|
|
|
|
|
rv += struct.pack("<10x") # header filled in later
|
|
|
|
|
|
|
|
src, byteorder, fmt = open_audio(filename)
|
|
|
|
|
|
|
|
width = src.getsampwidth()
|
|
|
|
channels = src.getnchannels()
|
|
|
|
rate = src.getframerate()
|
|
|
|
bias = 128
|
|
|
|
swap = width > 1 and sys.byteorder != byteorder
|
|
|
|
if width == 1 and fmt == 'wave': bias = 0
|
|
|
|
|
|
|
|
if verbose:
|
|
|
|
print("Input: {} ch, {} Hz, {}-bit, {} ({} frames)".format(
|
|
|
|
channels,
|
|
|
|
rate,
|
|
|
|
width*8,
|
|
|
|
fmt,
|
|
|
|
src.getnframes()
|
|
|
|
))
|
|
|
|
|
|
|
|
if channels > 2:
|
|
|
|
raise Exception("{}: Too many channels ({})".format(filename, channels))
|
|
|
|
|
|
|
|
|
|
|
|
cookie = None
|
|
|
|
while True:
|
|
|
|
frames = src.readframes(32)
|
|
|
|
if not frames: break
|
|
|
|
|
|
|
|
if swap:
|
|
|
|
frames = audioop.byteswap(frames, width)
|
|
|
|
|
|
|
|
if channels > 1:
|
|
|
|
frames = audioop.tomono(frames, width, 0.5, 0.5)
|
|
|
|
|
|
|
|
if new_rate:
|
|
|
|
frames, cookie = audioop.ratecv(frames, width, 1, rate, new_rate, cookie)
|
|
|
|
|
|
|
|
if width != 1:
|
|
|
|
frames = audioop.lin2lin(frames, width, 1)
|
|
|
|
if bias:
|
|
|
|
frames = audioop.bias(frames, 1, bias)
|
|
|
|
|
|
|
|
frames = frames.translate(tr)
|
|
|
|
rv += frames
|
|
|
|
src.close()
|
|
|
|
|
|
|
|
# based on system 6 samples, pages rounds down....
|
|
|
|
# probably a bug.
|
|
|
|
pages = (len(rv)-10+255) >> 8
|
|
|
|
hz = new_rate or rate
|
|
|
|
rp = relative_pitch(hz, freq)
|
|
|
|
|
|
|
|
struct.pack_into("<HHHHH", rv, 0,
|
|
|
|
0, # format
|
|
|
|
pages, # wave size in pages
|
|
|
|
rp,
|
|
|
|
channel, # stereo
|
|
|
|
hz # hz
|
|
|
|
)
|
|
|
|
|
2020-08-02 21:28:59 +00:00
|
|
|
self.data = bytes(rv)
|
2020-08-01 15:59:34 +00:00
|
|
|
self.pages = pages
|
|
|
|
self.channel = channel
|
|
|
|
self.relative_pitch = rp
|
|
|
|
self.sample_rate = hz
|
|
|
|
|
|
|
|
if verbose:
|
|
|
|
print("Output: 1 ch, {} Hz, 8-bit, rSoundSample ({} frames, {:.02f} Hz)".format(
|
|
|
|
hz, len(rv)-10, freq or 261.63))
|
|
|
|
print()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def __bytes__(self):
|
|
|
|
return self.data
|
|
|
|
|
|
|
|
def _rez_string(self):
|
|
|
|
return (
|
|
|
|
"\t0, /* format */\n"
|
|
|
|
"\t{}, /* size (pages) */\n"
|
2020-08-01 19:31:50 +00:00
|
|
|
"\t0x{:04x}, /* relative pitch */\n"
|
|
|
|
"\t0x{:04x}, /* stereo channel */\n"
|
2020-08-02 21:28:59 +00:00
|
|
|
"\t{:d}, /* sample rate */\n"
|
2020-08-01 15:59:34 +00:00
|
|
|
"\t..."
|
|
|
|
).format(
|
|
|
|
self.pages,
|
|
|
|
self.relative_pitch,
|
|
|
|
self.channel,
|
|
|
|
self.sample_rate
|
|
|
|
)
|
|
|
|
|
|
|
|
|