Source code for rv.modules.sampler

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 flags = 0x008459 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() 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'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