from collections import OrderedDict
from enum import Enum
from io import BytesIO
from itertools import chain
from struct import pack, unpack
from rv.controller import Controller
from rv.modules import Behavior as B, Module
from rv.note import NOTE
from rv.option import Option
[docs]class Sampler(Module):
"""
.. note::
Radiant Voices only supports sampler modules in files that were
saved using newer versions of SunVox.
Files created using older versions of SunVox, such as some of the files
in the ``simple_examples`` included with SunVox, must first be
loaded into the latest version of SunVox and then saved.
"""
name = mtype = 'Sampler'
mgroup = 'Synth'
chnk = 0x0102
options_chnm = 0x0101
behaviors = {B.receives_notes, B.sends_audio}
[docs] class SampleInterpolation(Enum):
off = 0
linear = 1
spline = 2
[docs] class EnvelopeInterpolation(Enum):
off = 0
linear = 1
class VibratoType(Enum):
sin = 0
saw = 1
square = 2
class LoopType(Enum):
off = 0
forward = 1
ping_pong = 2
class Format(Enum):
int8 = 1
int16 = 2
float32 = 4
class Channels(Enum):
mono = 0
stereo = 8
class NoteSampleMap(OrderedDict):
start_note = NOTE.C0
end_note = NOTE.a9
default_sample = 0
def __init__(self):
super(Sampler.NoteSampleMap, self).__init__(
(NOTE(note_value), self.default_sample)
for note_value in range(self.start_note.value,
self.end_note.value + 1)
)
@property
def bytes(self):
return bytes(self.values())
@bytes.setter
def bytes(self, value):
for k, v in zip(self.keys(), value):
self[k] = v
class Envelope(object):
length = 12
range = None
initial_x_values = None
initial_y_values = None
initial_active_points = None
initial_sustain_point = None
initial_loop_start_point = None
initial_loop_end_point = None
initial_enable = None
initial_sustain = None
initial_loop = None
format = '<' + 'H' * length * 2
def __init__(self):
self.x_values = self.initial_x_values.copy()
self.y_values = self.initial_y_values.copy()
self.active_points = self.initial_active_points
self.sustain_point = self.initial_sustain_point
self.loop_start_point = self.initial_loop_start_point
self.loop_end_point = self.initial_loop_end_point
self.enable = self.initial_enable
self.sustain = self.initial_sustain
self.loop = self.initial_loop
@property
def bitmask(self):
return self.enable | self.sustain * 2 | self.loop * 4
@bitmask.setter
def bitmask(self, value):
self.enable = bool(value & 1)
self.sustain = bool(value & 2)
self.loop = bool(value & 4)
@property
def point_bytes(self):
y_points = (y - self.range[0] for y in self.y_values)
values = list(chain.from_iterable(zip(self.x_values, y_points)))
return pack(self.format, *values)
@point_bytes.setter
def point_bytes(self, value):
values = unpack(self.format, value)
for i in range(self.length):
x, y = values[i * 2], values[i * 2 + 1]
y += self.range[0]
self.x_values[i], self.y_values[i] = x, y
class VolumeEnvelope(Envelope):
range = (0, 40)
initial_active_points = 4
initial_enable = True
initial_loop = False
initial_loop_start_point = 0
initial_loop_end_point = 0
initial_sustain = True
initial_sustain_point = 0
initial_x_values = [0, 8, 128, 256, 0, 0, 0, 0, 0, 0, 0, 0]
initial_y_values = [64, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
class PanningEnvelope(Envelope):
range = (-20, 20)
initial_active_points = 4
initial_enable = False
initial_loop = False
initial_loop_start_point = 0
initial_loop_end_point = 0
initial_sustain = False
initial_sustain_point = 0
initial_x_values = [0, 64, 128, 180, 0, 0, 0, 0, 0, 0, 0, 0]
initial_y_values = [12, -4, 28, 12, -20, -20,
-20, -20, -20, -20, -20, -20]
class Sample(object):
def __init__(self):
self.data = b''
self.loop_start = 0
self.loop_end = 0
self.volume = 64
self.finetune = 100
self.format = Sampler.Format.float32
self.channels = Sampler.Channels.stereo
self.rate = 44100
self.loop_type = Sampler.LoopType.off
self.panning = 0
self.relative_note = 16
self.unknown6 = b'\0' * 23
@property
def frame_size(self):
size = {Sampler.Format.int8: 1, Sampler.Format.int16: 2,
Sampler.Format.float32: 4}
multiplier = {Sampler.Channels.mono: 1, Sampler.Channels.stereo: 2}
return size[self.format] * multiplier[self.channels]
@property
def frames(self):
return len(self.data) // self.frame_size
volume = Controller((0, 512), 256)
panning = Controller((-128, 128), 0)
sample_interpolation = Controller(
SampleInterpolation, SampleInterpolation.spline)
envelope_interpolation = Controller(
EnvelopeInterpolation, EnvelopeInterpolation.linear)
polyphony_ch = Controller((1, 32), 8)
rec_threshold = Controller((0, 10000), 4)
vibrato_type = Controller(VibratoType, VibratoType.sin, attached=False)
vibrato_attack = Controller((0, 255), 0, attached=False)
vibrato_depth = Controller((0, 255), 0, attached=False)
vibrato_rate = Controller((0, 63), 0, attached=False)
volume_fadeout = Controller((0, 8192), 0, attached=False)
record_on_play = Option(False)
record_in_mono = Option(False)
record_with_reduced_sample_rate = Option(False)
record_in_16_bit = Option(False)
stop_recording_on_project_stop = Option(False)
def __init__(self, **kwargs):
super(Sampler, self).__init__(**kwargs)
self.volume_envelope = self.VolumeEnvelope()
self.panning_envelope = self.PanningEnvelope()
self.note_samples = self.NoteSampleMap()
self.samples = [None] * 128
self.unknown1 = b'\0' * 28
self.unknown2 = b'\0' * 4
self.unknown3 = b'\x40\x00\x80\x00\x00\x00\x00\x00'
self.unknown4 = b'\x04\x00\x00\x00'
self.unknown5 = b'\0' * 9
def specialized_iff_chunks(self):
for chunk in self.envelope_chunks():
yield chunk
for chunk in super(Sampler, self).specialized_iff_chunks():
yield chunk
for i, sample in enumerate(self.samples):
if sample is not None:
for chunk in self.sample_chunks(i, sample):
yield chunk
def envelope_chunks(self):
f = BytesIO()
w = f.write
def b(v):
return pack('<B', v)
w(self.unknown1)
compacted_samples = self.samples.copy()
while compacted_samples and compacted_samples[-1] is None:
compacted_samples.pop()
w(pack('<I', len(compacted_samples)))
w(self.unknown2)
w(self.note_samples.bytes[:96])
vol = self.volume_envelope
pan = self.panning_envelope
w(vol.point_bytes)
w(pan.point_bytes)
w(b(vol.active_points))
w(b(pan.active_points))
w(b(vol.sustain_point))
w(b(vol.loop_start_point))
w(b(vol.loop_end_point))
w(b(pan.sustain_point))
w(b(pan.loop_start_point))
w(b(pan.loop_end_point))
w(b(vol.bitmask))
w(b(pan.bitmask))
w(b(self.vibrato_type.value))
w(b(self.vibrato_attack))
w(b(self.vibrato_depth))
w(b(self.vibrato_rate))
w(pack('<H', self.volume_fadeout))
w(self.unknown3)
w(b'PMAS')
w(self.unknown4)
w(self.note_samples.bytes)
w(self.unknown5)
yield (b'CHNM', pack('<I', 0))
yield (b'CHDT', f.getvalue())
f.close()
yield (b'CHFF', pack('<I', 0))
yield (b'CHFR', pack('<I', 0))
def sample_chunks(self, i, sample):
f = BytesIO()
w = f.write
w(pack('<I', sample.frames))
w(pack('<I', sample.loop_start))
w(pack('<I', sample.loop_end))
w(pack('<B', sample.volume))
w(pack('<b', sample.finetune))
format_flag = {self.Format.int8: 0x00, self.Format.int16: 0x10,
self.Format.float32: 0x20}[sample.format]
channels_flag = {self.Channels.mono: 0x00,
self.Channels.stereo: 0x40}[sample.channels]
loop_format_flags = \
sample.loop_type.value | format_flag | channels_flag
w(pack('<B', loop_format_flags))
w(pack('<B', sample.panning + 0x80))
w(pack('<b', sample.relative_note))
w(sample.unknown6)
yield (b'CHNM', pack('<I', i * 2 + 1))
yield (b'CHDT', f.getvalue())
f.close()
yield (b'CHFF', pack('<I', 0))
yield (b'CHFR', pack('<I', 0))
yield (b'CHNM', pack('<I', i * 2 + 2))
yield (b'CHDT', sample.data)
yield (b'CHFF', pack(
'<I', sample.format.value | sample.channels.value))
yield (b'CHFR', pack('<I', sample.rate))
def load_chunk(self, chunk):
if chunk.chnm == self.options_chnm:
self.load_options(chunk)
elif chunk.chnm == 0:
self.load_envelopes(chunk)
elif chunk.chnm % 2 == 1:
self.load_sample_meta(chunk)
elif chunk.chnm % 2 == 0:
self.load_sample_data(chunk)
def load_envelopes(self, chunk):
data = chunk.chdt
vol = self.volume_envelope
pan = self.panning_envelope
vol.point_bytes = data[0x84:0xb4]
pan.point_bytes = data[0xb4:0xe4]
vol.active_points = data[0xe4]
pan.active_points = data[0xe5]
vol.sustain_point = data[0xe6]
vol.loop_start_point = data[0xe7]
vol.loop_end_point = data[0xe8]
pan.sustain_point = data[0xe9]
pan.loop_start_point = data[0xea]
pan.loop_end_point = data[0xeb]
vol.bitmask = data[0xec]
pan.bitmask = data[0xed]
self.vibrato_type = self.VibratoType(data[0xee])
self.vibrato_attack = data[0xef]
self.vibrato_depth = data[0xf0]
self.vibrato_rate = data[0xf1]
self.volume_fadeout, = unpack('<H', data[0xf2:0xf4])
self.note_samples.bytes = data[0x104:0x17b]
self.unknown1 = data[0x00:0x1c]
self.unknown2 = data[0x20:0x24]
self.unknown3 = data[0xf4:0xfc]
self.unknown4 = data[0x100:0x104]
self.unknown5 = data[0x17b:0x184]
def load_sample_meta(self, chunk):
index = (chunk.chnm - 1) // 2
sample = self.samples[index] = self.Sample()
data = chunk.chdt
sample.loop_start, = unpack('<I', data[0x04:0x08])
sample.loop_end, = unpack('<I', data[0x08:0x0c])
sample.volume = data[0x0c]
sample.finetune, = unpack('<b', data[0x0d:0x0e])
loop_format_flags = data[0x0e]
loop = loop_format_flags & (0 | 1 | 2)
sample.loop_type = self.LoopType(loop)
format = loop_format_flags & (0x00 | 0x10 | 0x20)
sample.format = {0x00: self.Format.int8, 0x10: self.Format.int16,
0x20: self.Format.float32}[format]
if loop_format_flags & 0x40:
sample.channels = self.Channels.stereo
else:
sample.channels = self.Channels.mono
sample.panning = data[0x0f] - 0x80
sample.relative_note, = unpack('<b', data[0x10:0x11])
sample.unknown6 = data[0x11:0x28]
def load_sample_data(self, chunk):
index = (chunk.chnm - 2) // 2
sample = self.samples[index]
sample.data = chunk.chdt
sample.format = self.Format(chunk.chff & 0x07)
sample.channels = self.Channels(chunk.chff & 0x08)
sample.rate = chunk.chfr
"""
0 1 2 3 4 5 6 7 8 9 a b c d e f
CHNM: 00000000
CHDT: 00000000: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
------------------------------------------------ unknown1
00000010: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
------------------------------------ unknown1
----------- max sample index + 1
(0 for no samples)
00000020: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
----------- unknown2
-- sample number for note C-0 (note 0)
00000030: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00000040: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00000050: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00000060: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00000070: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00000080: 00 00 00 00 00 00 40 00 08 00 00 00 80 00 00 00 ......@.........
-- sample number for note B-8 (note 95)
----- vol envelope: point 1 x position (always 0)
----- vol envelope: point 1 y position (00-40)
----- vol point 2 x
----- vol point 2 y
----- vol point 3 x
----- vol point 3 y
00000090: 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
----- point 4 x
----- vol point 4 y
----- vol point 5 x
----- vol point 5 y
----- vol point 6 x
----- vol point 6 y
----- vol point 7 x
----- vol point 7 y
000000A0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
----- vol point 8 x
----- vol point 8 y
----- vol point 9 x
----- vol point 9 y
----- vol point 10 x
----- vol point 10 y
----- vol point 11 x
----- vol point 11 y
0 1 2 3 4 5 6 7 8 9 a b c d e f
000000B0: 00 00 00 00 00 00 20 00 40 00 10 00 80 00 30 00 ...... .@.....0.
----- vol point 12 x
----- vol point 12 y
----- panning envelope: point 1 x position (always 0)
----- panning envelope: point 1 y position (00-40, 20=center)
----- pan point 2 x
----- pan point 2 y
----- pan point 3 x
----- pan point 3 y
000000C0: B4 00 20 00 00 00 00 00 00 00 00 00 00 00 00 00 .. .............
----- point 4 x
----- pan point 4 y
----- pan point 5 x
----- pan point 5 y
----- pan point 6 x
----- pan point 6 y
----- pan point 7 x
----- pan point 7 y
000000D0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
----- pan point 8 x
----- pan point 8 y
----- pan point 9 x
----- pan point 9 y
----- pan point 10 x
----- pan point 10 y
----- pan point 11 x
----- pan point 11 y
0 1 2 3 4 5 6 7 8 9 a b c d e f
000000E0: 00 00 00 00 04 04 00 00 00 00 00 00 03 00 00 00 ................
----- pan point 12 x
----- pan point 12 y
-- number of active vol envelope points
-- number of active pan envelope points
-- vol sustain point
-- vol loop start point
-- vol loop end point
-- pan sustain point
-- pan loop start point
-- pan loop end point
-- volume envelope bitmask: 1 = enable, 2 = sustain, 4 = loop
-- panning envelope bitmask: 1 = enable, 2 = sustain, 4 = loop
-- vibrato type: 0=sin, 1=saw, 2=square
-- vibrato attack (0-255)
000000F0: 00 00 00 00 40 00 80 00 00 00 00 00 50 4D 41 53 ....@.......PMAS
-- vibrato depth (0-255)
-- vibrato rate (0-63)
----- volume fadeout (0-8192)
------------------------ unknown3
----------- magic (little-endian SAMP)
00000100: 04 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
----------- unknown4
-- sample number for note C-0 (note 0)
00000110: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00000120: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00000130: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00000140: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00000150: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00000160: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00000170: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0 1 2 3 4 5 6 7 8 9 a b c d e f
-- sample number for note b-9 (note 118)
-------------- unknown5
00000180: 00 00 00 00 ....
----------- unknown5
CHNM: (sample number * 2 + 1)
0 1 2 3 4 5 6 7 8 9 a b c d e f
CHDT: 00000000: C7 08 00 00 00 00 00 00 00 00 00 00 40 64 00 80 ............@d..
----------- length (frames)
----------- loop start frame
----------- loop end frame
-- volume (0-64)
-- finetune (-128-127, signed, center=0x00)
-- 0x_0 = no loop
-- 0x_1 = loop
-- 0x_2 = ping-pong loop
-- 0x0_ = 8-bit mono
-- 0x1_ = 16-bit mono
-- 0x2_ = 32-bit mono
-- add 0x40 for stereo
-- panning (-128-127, 0x00-0xFF, unsigned w/ offset, center = 0x80)
00000010: 10 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
-- relative note (-128-127, signed, center=0x00)
--------------------------------------------- unknown6
00000020: 00 00 00 00 00 00 00 00 ........
----------------------- unknown6
CHFF: 0
CHFR: 0
CHNM: (sample number * 2 + 2)
CHDT: <sample data, signed>
CHFF: format:
1 - 8-bit mono
2 - 16-bit mono
4 - 32-bit mono
9 - 8-bit stereo
A - 16-bit stereo
C - 32-bit stereo
CHFR: sample rate
""" # NOQA