diff --git a/Makefile b/Makefile index c6daf7b..66eed75 100644 --- a/Makefile +++ b/Makefile @@ -43,13 +43,13 @@ all: default SOURCES = src/algorithms.c src/amy.c src/envelope.c src/examples.c \ src/filters.c src/oscillators.c src/pcm.c src/partials.c src/custom.c \ - src/delay.c src/log2_exp2.c src/patches.c + src/delay.c src/log2_exp2.c src/patches.c src/transfer.c OBJECTS = $(patsubst %.c, %.o, src/algorithms.c src/amy.c src/envelope.c \ src/delay.c src/partials.c src/custom.c src/patches.c \ src/examples.c src/filters.c src/oscillators.c src/pcm.c src/log2_exp2.c \ - src/libminiaudio-audio.c) - + src/libminiaudio-audio.c src/transfer.c) + HEADERS = $(wildcard src/*.h) src/amy_config.h HEADERS_BUILD := $(filter-out src/patches.h,$(HEADERS)) diff --git a/README.md b/README.md index 2a751b5..5d1056e 100644 --- a/README.md +++ b/README.md @@ -224,6 +224,7 @@ Here's the full list: | `w` | `wave` | uint 0-11 | Waveform: [0=SINE, PULSE, SAW_DOWN, SAW_UP, TRIANGLE, NOISE, KS, PCM, ALGO, PARTIAL, PARTIALS, BYO_PARTIALS, OFF]. default: 0/SINE | | `x` | `eq` | float,float,float | Equalization in dB low (~800Hz) / med (~2500Hz) / high (~7500Gz) -15 to 15. 0 is off. default 0. | | `X` | `eg1_type` | uint 0-3 | Type for Envelope Generator 1 - 0: Normal (RC-like) / 1: Linear / 2: DX7-style / 3: True exponential. | +| `z` | `load_sample` | uint x 6 | Signal to start loading sample. patch, length(samples), samplerate, midinote, loopstart, loopend. All subsequent messages are base64 encoded WAVE-style frames of audio until `length` is reached. | diff --git a/amy.py b/amy.py index e497c6e..d389a25 100644 --- a/amy.py +++ b/amy.py @@ -158,7 +158,7 @@ def message(**kwargs): kw_map = {'osc': 'vI', 'wave': 'wI', 'note': 'nF', 'vel': 'lF', 'amp': 'aC', 'freq': 'fC', 'duty': 'dC', 'feedback': 'bF', 'time': 'tI', 'reset': 'SI', 'phase': 'PF', 'pan': 'QC', 'client': 'cI', 'volume': 'vF', 'pitch_bend': 'sF', 'filter_freq': 'FC', 'resonance': 'RF', 'bp0': 'AL', 'bp1': 'BL', 'eg0_type': 'TI', 'eg1_type': 'XI', 'debug': 'DI', 'chained_osc': 'cI', 'mod_source': 'LI', 'clone_osc': 'CI', - 'eq': 'xL', 'filter_type': 'GI', 'algorithm': 'oI', 'ratio': 'IF', 'latency_ms': 'NI', 'algo_source': 'OL', + 'eq': 'xL', 'filter_type': 'GI', 'algorithm': 'oI', 'ratio': 'IF', 'latency_ms': 'NI', 'algo_source': 'OL', 'load_sample': 'zL', 'chorus': 'kL', 'reverb': 'hL', 'echo': 'ML', 'load_patch': 'KI', 'store_patch': 'uS', 'voices': 'rL', 'external_channel': 'WI', 'portamento': 'mI', 'patch': 'pI', 'num_partials': 'pI', # Note alaising. @@ -309,7 +309,48 @@ def stop(): def restart(): import libamy libamy.restart() + +def transfer_wav(wavfilename, patch=1024, midinote=0, loopstart=0, loopend=0): + from math import ceil + import amy_wave # our version of a wave file reader that looks for sampler metadata + # tulip has ubinascii, normal has base64 + try: + import base64 + def b64(b): + return base64.b64encode(b) + except ImportError: + import ubinascii + def b64(b): + return ubinascii.b2a_base64(b)[:-1] + + w = amy_wave.open(wavfilename, 'r') + if(w.getnchannels()>1): + # de-interleave and just choose the first channel + f = bytes([f[j] for i in range(0,len(f),4) for j in (i,i+1)]) + if(loopstart==0): + if(hasattr(w,'_loopstart')): + loopstart = w._loopstart + if(loopend==0): + if(hasattr(w,'_loopend')): + loopend = w._loopend + if(midinote==0): + if(hasattr(w,'_midinote')): + midinote = w._midinote + else: + midinote=60 + + # Tell AMY we're sending over a sample + s = "%d,%d,%d,%d,%d,%d" % (patch, w.getnframes(), w.getframerate(), midinote, loopstart, loopend) + send(load_sample=s) + # Now generate the base64 encoded segments, 188 bytes / 94 frames at a time + # why 188? that generates 252 bytes of base64 text. amy's max message size is currently 255. + for i in range(ceil(w.getnframes()/94)): + message = b64(w.readframes(94)) + send_raw(message.decode('ascii')) + print("Loaded sample over wire protocol. Patch #%d. %d bytes, %d frames, midinote %d" % (patch, w.getnframes()*2, w.getnframes(), midinote)) + + """ Convenience functions """ diff --git a/amy_wave.py b/amy_wave.py new file mode 100644 index 0000000..6f0ff84 --- /dev/null +++ b/amy_wave.py @@ -0,0 +1,509 @@ +"""Stuff to parse WAVE files. + +Usage. + +Reading WAVE files: + f = wave.open(file, 'r') +where file is either the name of a file or an open file pointer. +The open file pointer must have methods read(), seek(), and close(). +When the setpos() and rewind() methods are not used, the seek() +method is not necessary. + +This returns an instance of a class with the following public methods: + getnchannels() -- returns number of audio channels (1 for + mono, 2 for stereo) + getsampwidth() -- returns sample width in bytes + getframerate() -- returns sampling frequency + getnframes() -- returns number of audio frames + getcomptype() -- returns compression type ('NONE' for linear samples) + getcompname() -- returns human-readable version of + compression type ('not compressed' linear samples) + getparams() -- returns a namedtuple consisting of all of the + above in the above order + getmarkers() -- returns None (for compatibility with the + aifc module) + getmark(id) -- raises an error since the mark does not + exist (for compatibility with the aifc module) + readframes(n) -- returns at most n frames of audio + rewind() -- rewind to the beginning of the audio stream + setpos(pos) -- seek to the specified position + tell() -- return the current position + close() -- close the instance (make it unusable) +The position returned by tell() and the position given to setpos() +are compatible and have nothing to do with the actual position in the +file. +The close() method is called automatically when the class instance +is destroyed. + +Writing WAVE files: + f = wave.open(file, 'w') +where file is either the name of a file or an open file pointer. +The open file pointer must have methods write(), tell(), seek(), and +close(). + +This returns an instance of a class with the following public methods: + setnchannels(n) -- set the number of channels + setsampwidth(n) -- set the sample width + setframerate(n) -- set the frame rate + setnframes(n) -- set the number of frames + setcomptype(type, name) + -- set the compression type and the + human-readable compression type + setparams(tuple) + -- set all parameters at once + tell() -- return current position in output file + writeframesraw(data) + -- write audio frames without pathing up the + file header + writeframes(data) + -- write audio frames and patch up the file header + close() -- patch up the file header and close the + output file +You should set the parameters before the first writeframesraw or +writeframes. The total number of frames does not need to be set, +but when it is set to the correct value, the header does not have to +be patched up. +It is best to first set all parameters, perhaps possibly the +compression type, and then write audio frames using writeframesraw. +When all frames have been written, either call writeframes('') or +close() to patch up the sizes in the header. +The close() method is called automatically when the class instance +is destroyed. +""" + +import builtins + +__all__ = ["open", "openfp", "Error"] + +class Error(Exception): + pass + +WAVE_FORMAT_PCM = 0x0001 + +_array_fmts = None, 'b', 'h', None, 'i' + +#import audioop +import struct +import sys +from chunk import Chunk +from collections import namedtuple + +_wave_params = namedtuple('_wave_params', + 'nchannels sampwidth framerate nframes comptype compname') + +class Wave_read: + """Variables used in this class: + + These variables are available to the user though appropriate + methods of this class: + _file -- the open file with methods read(), close(), and seek() + set through the __init__() method + _nchannels -- the number of audio channels + available through the getnchannels() method + _nframes -- the number of audio frames + available through the getnframes() method + _sampwidth -- the number of bytes per audio sample + available through the getsampwidth() method + _framerate -- the sampling frequency + available through the getframerate() method + _comptype -- the AIFF-C compression type ('NONE' if AIFF) + available through the getcomptype() method + _compname -- the human-readable AIFF-C compression type + available through the getcomptype() method + _soundpos -- the position in the audio stream + available through the tell() method, set through the + setpos() method + + These variables are used internally only: + _fmt_chunk_read -- 1 iff the FMT chunk has been read + _data_seek_needed -- 1 iff positioned correctly in audio + file for readframes() + _data_chunk -- instantiation of a chunk class for the DATA chunk + _framesize -- size of one frame in the file + """ + + def initfp(self, file): + self._convert = None + self._soundpos = 0 + self._file = Chunk(file, bigendian = 0) + if self._file.getname() != b'RIFF': + 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 + while 1: + self._data_seek_needed = 1 + try: + chunk = Chunk(self._file, bigendian = 0) + except EOFError: + break + chunkname = chunk.getname() + if chunkname == b'fmt ': + self._read_fmt_chunk(chunk) + self._fmt_chunk_read = 1 + elif chunkname == b'data': + if not self._fmt_chunk_read: + raise Error('data chunk before fmt chunk') + self._data_chunk = chunk + self._nframes = chunk.chunksize // self._framesize + self._data_seek_needed = 0 + break + elif chunkname == b'smpl': + self._read_smpl_chunk(chunk) + chunk.skip() + if not self._fmt_chunk_read or not self._data_chunk: + raise Error('fmt chunk and/or data chunk missing') + + def __init__(self, f): + self._i_opened_the_file = None + 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: + self.initfp(f) + except: + if self._i_opened_the_file: + f.close() + raise + + def __del__(self): + self.close() + + def __enter__(self): + return self + + def __exit__(self, *args): + self.close() + + # + # User visible methods. + # + def getfp(self): + return self._file + + def rewind(self): + self._data_seek_needed = 1 + self._soundpos = 0 + + def close(self): + if self._i_opened_the_file: + self._i_opened_the_file.close() + self._i_opened_the_file = None + self._file = None + + def tell(self): + return self._soundpos + + def getnchannels(self): + return self._nchannels + + def getnframes(self): + return self._nframes + + def getsampwidth(self): + return self._sampwidth + + def getframerate(self): + return self._framerate + + def getcomptype(self): + return self._comptype + + def getcompname(self): + return self._compname + + def getparams(self): + return _wave_params(self.getnchannels(), self.getsampwidth(), + self.getframerate(), self.getnframes(), + self.getcomptype(), self.getcompname()) + + def getmarkers(self): + return None + + def getmark(self, id): + raise Error('no marks') + + def setpos(self, pos): + if pos < 0 or pos > self._nframes: + raise Error('position not in range') + self._soundpos = pos + self._data_seek_needed = 1 + + def readframes(self, nframes): + if self._data_seek_needed: + self._data_chunk.seek(0, 0) + pos = self._soundpos * self._framesize + if pos: + self._data_chunk.seek(pos, 0) + self._data_seek_needed = 0 + if nframes == 0: + return b'' + data = self._data_chunk.read(nframes * self._framesize) + if self._sampwidth != 1 and sys.byteorder == 'big': + data = audioop.byteswap(data, self._sampwidth) + if self._convert and data: + data = self._convert(data) + self._soundpos = self._soundpos + len(data) // (self._nchannels * self._sampwidth) + return data + + # + # Internal methods. + # + def _read_smpl_chunk(self, chunk): + _, _,_, self._midinote, _, _, _, self._loops, _ = struct.unpack('0): # read first loop + _, _, self._loopstart, self._loopend = struct.unpack(' 4: + raise Error('bad sample width') + self._sampwidth = sampwidth + + def getsampwidth(self): + if not self._sampwidth: + 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') + if framerate <= 0: + raise Error('bad frame rate') + self._framerate = int(round(framerate)) + + def getframerate(self): + if not self._framerate: + 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') + self._nframes = nframes + + def getnframes(self): + return self._nframeswritten + + def setcomptype(self, comptype, compname): + if self._datawritten: + raise Error('cannot change parameters after starting to write') + if comptype not in ('NONE',): + raise Error('unsupported compression type') + self._comptype = comptype + self._compname = compname + + def getcomptype(self): + return self._comptype + + def getcompname(self): + return self._compname + + def setparams(self, params): + nchannels, sampwidth, framerate, nframes, comptype, compname = params + if self._datawritten: + raise Error('cannot change parameters after starting to write') + self.setnchannels(nchannels) + self.setsampwidth(sampwidth) + self.setframerate(framerate) + self.setnframes(nframes) + self.setcomptype(comptype, compname) + + def getparams(self): + if not self._nchannels or not self._sampwidth or not self._framerate: + 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') + + def getmark(self, id): + raise Error('no marks') + + def getmarkers(self): + return None + + 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 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() + + def close(self): + if self._file: + try: + self._ensure_header_written(0) + if self._datalength != self._datawritten: + self._patchheader() + 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 + + # + # Internal methods. + # + + def _ensure_header_written(self, datasize): + if not self._headerwritten: + if not self._nchannels: + raise Error('# channels not specified') + if not self._sampwidth: + raise Error('sample width not specified') + if not self._framerate: + raise Error('sampling rate not specified') + self._write_header(datasize) + + def _write_header(self, initlength): + assert not self._headerwritten + self._file.write(b'RIFF') + if not self._nframes: + self._nframes = initlength // (self._nchannels * self._sampwidth) + self._datalength = self._nframes * self._nchannels * self._sampwidth + try: + self._form_length_pos = self._file.tell() + except (AttributeError, OSError): + self._form_length_pos = None + self._file.write(struct.pack('0): - metadata["midi_note"] = w.midiUnityNote - if(w.loopstart >= 0 and w.loopend >= 0): - metadata["sustain_ms"] = int(((w.loopstart + ((w.loopend-w.loopstart)/2.0)) / amy.AMY_SAMPLE_RATE) * 1000.0) - except AttributeError: - pass # No wav metadata + import amy_wave # Forked version + w = amy_wave.open(filename,'r') + if(hasattr(w,'_midinote')): + metadata["midi_note"] = w._midinote + if(hasattr(w,'_loopstart') and hasattr(w,'_loopend')): + if(w._loopstart >= 0 and w._loopend >= 0): + metadata["sustain_ms"] = int(((w._loopstart + ((w._loopend-w._loopstart)/2.0)) / amy.AMY_SAMPLE_RATE) * 1000.0) # Do the loris analyze analyzer = loris.Analyzer(freq_res, analysis_window) diff --git a/src/amy.c b/src/amy.c index 8e4c46f..6876a7a 100644 --- a/src/amy.c +++ b/src/amy.c @@ -89,6 +89,8 @@ void amy_profiles_print() { for(uint8_t i=0;i pthread_mutex_t amy_queue_lock; @@ -1627,7 +1629,34 @@ int parse_float_list_message(char *message, float *vals, int max_num_vals, float return num_vals_received; } -int parse_int_list_message(char *message, int16_t *vals, int max_num_vals, int16_t skipped_val) { +int parse_int_list_message32(char *message, int32_t *vals, int max_num_vals, int32_t skipped_val) { + // Return the number of values extracted from message. + uint16_t c = 0, last_c; + uint16_t stop = strspn(message, " -0123456789,"); // Space, no period. + int num_vals_received = 0; + while(c < stop && num_vals_received < max_num_vals) { + *vals = atoi(message + c); + // Skip spaces in front of number. + while (message[c] == ' ') ++c; + // Figure length of number string. + last_c = c; + c += strspn(message + c, "-0123456789"); // No space, just minus and digits. + if (last_c == c) // Zero-length number. + *vals = skipped_val; // Rewrite with special value for skips. + // Step through (spaces?) to next comma, or end of string or region. + while (message[c] != ',' && message[c] != 0 && c < MAX_MESSAGE_LEN) c++; + ++c; // Step over the comma (if that's where we landed). + ++vals; // Move to next output. + ++num_vals_received; + } + if (c < stop) { + fprintf(stderr, "WARNING: parse_int_list: More than %d values in \"%s\"\n", + max_num_vals, message); + } + return num_vals_received; +} + +int parse_int_list_message16(char *message, int16_t *vals, int max_num_vals, int16_t skipped_val) { // Return the number of values extracted from message. uint16_t c = 0, last_c; uint16_t stop = strspn(message, " -0123456789,"); // Space, no period. @@ -1667,7 +1696,7 @@ void copy_param_list_substring(char *dest, const char *src) { // helper to parse the list of source voices for an algorithm void parse_algorithm_source(struct synthinfo * t, char *message) { - int num_parsed = parse_int_list_message(message, t->algo_source, MAX_ALGO_OPS, + int num_parsed = parse_int_list_message16(message, t->algo_source, MAX_ALGO_OPS, AMY_UNSET_VALUE(t->algo_source[0])); // Clear unspecified values. for (int i = num_parsed; i < MAX_ALGO_OPS; ++i) { @@ -1717,6 +1746,14 @@ struct event amy_parse_message(char * message) { uint16_t start = 0; uint16_t c = 0; int16_t length = strlen(message); + + // Check if we're in a transfer block, if so, parse it and leave this loop + if(amy_transfer_flag) { + parse_transfer_message(message, length); + AMY_PROFILE_STOP(AMY_PARSE_MESSAGE) + return amy_default_event(); + } + struct event e = amy_default_event(); uint32_t sysclock = amy_sysclock(); @@ -1831,6 +1868,14 @@ struct event amy_parse_message(char * message) { e.eq_h = eq[2]; } break; + case 'z': { + int32_t sm[6]; // patch, length, SR, midinote, loopstart, loopend + parse_int_list_message32(message+start, sm, 6, 0); + int16_t * ram = pcm_load(sm[0], sm[1], sm[2], sm[3],sm[4], sm[5]); + start_receiving_transfer(sm[1]*2, (uint8_t*)ram); + break; + + } /* Y,y,z available */ /* Z used for end of message */ default: diff --git a/src/amy.h b/src/amy.h index f3d33e5..32faba5 100644 --- a/src/amy.h +++ b/src/amy.h @@ -380,6 +380,9 @@ extern struct profile profiles[NO_TAG]; extern int64_t amy_get_us(); #endif +extern uint8_t amy_transfer_flag ; + + // Chorus gets is modulator from a special osc one beyond the normal range. #define CHORUS_MOD_SOURCE AMY_OSCS @@ -470,7 +473,8 @@ void amy_print_devices(); void amy_set_custom(struct custom_oscillator* custom); void amy_reset_sysclock(); -extern int parse_int_list_message(char *message, int16_t *vals, int max_num_vals, int16_t skipped_val); +extern int parse_int_list_message32(char *message, int32_t *vals, int max_num_vals, int32_t skipped_val); +extern int parse_int_list_message16(char *message, int16_t *vals, int max_num_vals, int16_t skipped_val); extern void reset_osc(uint16_t i ); @@ -539,6 +543,7 @@ extern void custom_mod_trigger(uint16_t osc); extern SAMPLE amy_get_random(); //extern void algo_custom_setup_patch(uint16_t osc, uint16_t * target_oscs); +extern int16_t * pcm_load(uint16_t patch, uint32_t length, uint32_t samplerate, uint8_t midinote, uint32_t loopstart, uint32_t loopend); // filters extern void filters_init(); diff --git a/src/patches.c b/src/patches.c index b394fa9..fb88f00 100644 --- a/src/patches.c +++ b/src/patches.c @@ -100,7 +100,7 @@ void patches_store_patch(char * message) { // So i know that the patch / voice alloc already exists and the patch has already been set! void patches_event_has_voices(struct event e) { int16_t voices[MAX_VOICES]; - uint8_t num_voices = parse_int_list_message(e.voices, voices, MAX_VOICES, 0); + uint8_t num_voices = parse_int_list_message16(e.voices, voices, MAX_VOICES, 0); // clear out the voices and patch now from the event. If we didn't, we'd keep calling this over and over e.voices[0] = 0; AMY_UNSET(e.load_patch); @@ -120,7 +120,7 @@ void patches_load_patch(struct event e) { char sub_message[255]; int16_t voices[MAX_VOICES]; - uint8_t num_voices = parse_int_list_message(e.voices, voices, MAX_VOICES, 0); + uint8_t num_voices = parse_int_list_message16(e.voices, voices, MAX_VOICES, 0); char *message; uint16_t patch_osc = 0; if(e.load_patch > 1023) { diff --git a/src/pcm.c b/src/pcm.c index 7189ff3..e6ba981 100644 --- a/src/pcm.c +++ b/src/pcm.c @@ -181,50 +181,39 @@ SAMPLE compute_mod_pcm(uint16_t osc) { // load mono samples (let python parse wave files) into patch # // set loopstart, loopend, midinote, samplerate (and log2sr) -int8_t pcm_load(int16_t * samples, uint32_t length, uint32_t samplerate, uint8_t midinote, uint32_t loopstart, uint32_t loopend) { - // find the next free patch # - int8_t patch = -1; - for(uint8_t i=0;isamplerate = samplerate; - memorypcm_map[patch]->log2sr = log2f((float)samplerate / ZERO_LOGFREQ_IN_HZ); - memorypcm_map[patch]->midinote = midinote; - memorypcm_map[patch]->loopstart = loopstart; - memorypcm_map[patch]->length = length; - memorypcm_map[patch]->sample_ram = malloc_caps(length*2, SAMPLE_RAM_CAPS); - if(memorypcm_map[patch]->sample_ram == NULL) { - free(memorypcm_map[patch]); - return -1; // no ram for sample + uint16_t mp = patch - MEMORYPCM_PATCHES_START_AT; + memorypcm_map[mp] = malloc_caps(sizeof(memorypcm_map_t), SAMPLE_RAM_CAPS); + memorypcm_map[mp]->samplerate = samplerate; + memorypcm_map[mp]->log2sr = log2f((float)samplerate / ZERO_LOGFREQ_IN_HZ); + memorypcm_map[mp]->midinote = midinote; + memorypcm_map[mp]->loopstart = loopstart; + memorypcm_map[mp]->length = length; + memorypcm_map[mp]->sample_ram = malloc_caps(length*2, SAMPLE_RAM_CAPS); + if(memorypcm_map[mp]->sample_ram == NULL) { + free(memorypcm_map[mp]); + return NULL; // no ram for sample } if(loopend == 0) { // loop whole sample - memorypcm_map[patch]->loopend = memorypcm_map[patch]->length-1; + memorypcm_map[mp]->loopend = memorypcm_map[mp]->length-1; } else { - memorypcm_map[patch]->loopend = loopend; + memorypcm_map[mp]->loopend = loopend; } - memcpy(memorypcm_map[patch]->sample_ram, samples, length*2); - return patch; // patch number + return memorypcm_map[mp]->sample_ram; } void pcm_unload_patch(uint8_t patch) { - if(memorypcm_map[patch] == NULL) return; - free(memorypcm_map[patch]->sample_ram); - free(memorypcm_map[patch]); - memorypcm_map[patch] = NULL; + if(memorypcm_map[patch-MEMORYPCM_PATCHES_START_AT] == NULL) return; + free(memorypcm_map[patch-MEMORYPCM_PATCHES_START_AT]->sample_ram); + free(memorypcm_map[patch-MEMORYPCM_PATCHES_START_AT]); + memorypcm_map[patch-MEMORYPCM_PATCHES_START_AT] = NULL; } //free all patches void pcm_unload_all() { - for(uint8_t i=0;i +#include +#include "transfer.h" +#include + +uint8_t amy_transfer_flag = 0; +uint8_t * amy_transfer_storage = NULL; +uint32_t amy_transfer_length = 0; +uint32_t amy_transfer_stored =0; + +// signals to AMY that i'm now receiving a transfer of length (bytes!) into allocated storage +void start_receiving_transfer(uint32_t length, uint8_t * storage) { + amy_transfer_flag = 1; + amy_transfer_storage = storage; + amy_transfer_length = length; + amy_transfer_stored = 0; +} + +// takes a wire message and adds it to storage after decoding it. stops transfer when it's done +void parse_transfer_message(char * message, uint16_t len) { + size_t decoded = 0; + uint8_t * block = b64_decode_ex (message, len, &decoded); + for(uint16_t i=0;i=amy_transfer_length) { // we're done + amy_transfer_flag = 0; + } +} + +int b64_buf_malloc(b64_buffer_t * buf) +{ + buf->ptr = malloc(B64_BUFFER_SIZE); + if(!buf->ptr) return -1; + + buf->bufc = 1; + + return 0; +} + +int b64_buf_realloc(b64_buffer_t* buf, size_t size) +{ + if (size > (size_t) (buf->bufc * B64_BUFFER_SIZE)) + { + while (size > (size_t)(buf->bufc * B64_BUFFER_SIZE)) buf->bufc++; + buf->ptr = realloc(buf->ptr, B64_BUFFER_SIZE * buf->bufc); + if (!buf->ptr) return -1; + } + + return 0; +} + + +char * +b64_encode (const unsigned char *src, size_t len) { + int i = 0; + int j = 0; + b64_buffer_t encbuf; + size_t size = 0; + unsigned char buf[4]; + unsigned char tmp[3]; + + // alloc + if(b64_buf_malloc(&encbuf) == -1) { return NULL; } + + // parse until end of source + while (len--) { + // read up to 3 bytes at a time into `tmp' + tmp[i++] = *(src++); + + // if 3 bytes read then encode into `buf' + if (3 == i) { + buf[0] = (tmp[0] & 0xfc) >> 2; + buf[1] = ((tmp[0] & 0x03) << 4) + ((tmp[1] & 0xf0) >> 4); + buf[2] = ((tmp[1] & 0x0f) << 2) + ((tmp[2] & 0xc0) >> 6); + buf[3] = tmp[2] & 0x3f; + + // allocate 4 new byts for `enc` and + // then translate each encoded buffer + // part by index from the base 64 index table + // into `encbuf.ptr' unsigned char array + if (b64_buf_realloc(&encbuf, size + 4) == -1) return NULL; + + for (i = 0; i < 4; ++i) { + encbuf.ptr[size++] = b64_table[buf[i]]; + } + + // reset index + i = 0; + } + } + + // remainder + if (i > 0) { + // fill `tmp' with `\0' at most 3 times + for (j = i; j < 3; ++j) { + tmp[j] = '\0'; + } + + // perform same codec as above + buf[0] = (tmp[0] & 0xfc) >> 2; + buf[1] = ((tmp[0] & 0x03) << 4) + ((tmp[1] & 0xf0) >> 4); + buf[2] = ((tmp[1] & 0x0f) << 2) + ((tmp[2] & 0xc0) >> 6); + buf[3] = tmp[2] & 0x3f; + + // perform same write to `encbuf->ptr` with new allocation + for (j = 0; (j < i + 1); ++j) { + if (b64_buf_realloc(&encbuf, size + 1) == -1) return NULL; + + encbuf.ptr[size++] = b64_table[buf[j]]; + } + + // while there is still a remainder + // append `=' to `encbuf.ptr' + while ((i++ < 3)) { + if (b64_buf_realloc(&encbuf, size + 1) == -1) return NULL; + + encbuf.ptr[size++] = '='; + } + } + + // Make sure we have enough space to add '\0' character at end. + if (b64_buf_realloc(&encbuf, size + 1) == -1) return NULL; + encbuf.ptr[size] = '\0'; + + return encbuf.ptr; +} + + +unsigned char * +b64_decode (const char *src, size_t len) { + return b64_decode_ex(src, len, NULL); +} + +unsigned char * +b64_decode_ex (const char *src, size_t len, size_t *decsize) { + int i = 0; + int j = 0; + int l = 0; + size_t size = 0; + b64_buffer_t decbuf; + unsigned char buf[3]; + unsigned char tmp[4]; + + // alloc + if (b64_buf_malloc(&decbuf) == -1) { return NULL; } + + // parse until end of source + while (len--) { + // break if char is `=' or not base64 char + if ('=' == src[j]) { break; } + if (!(isalnum(src[j]) || '+' == src[j] || '/' == src[j])) { break; } + + // read up to 4 bytes at a time into `tmp' + tmp[i++] = src[j++]; + + // if 4 bytes read then decode into `buf' + if (4 == i) { + // translate values in `tmp' from table + for (i = 0; i < 4; ++i) { + // find translation char in `b64_table' + for (l = 0; l < 64; ++l) { + if (tmp[i] == b64_table[l]) { + tmp[i] = l; + break; + } + } + } + + // decode + buf[0] = (tmp[0] << 2) + ((tmp[1] & 0x30) >> 4); + buf[1] = ((tmp[1] & 0xf) << 4) + ((tmp[2] & 0x3c) >> 2); + buf[2] = ((tmp[2] & 0x3) << 6) + tmp[3]; + + // write decoded buffer to `decbuf.ptr' + if (b64_buf_realloc(&decbuf, size + 3) == -1) return NULL; + for (i = 0; i < 3; ++i) { + ((unsigned char*)decbuf.ptr)[size++] = buf[i]; + } + + // reset + i = 0; + } + } + + // remainder + if (i > 0) { + // fill `tmp' with `\0' at most 4 times + for (j = i; j < 4; ++j) { + tmp[j] = '\0'; + } + + // translate remainder + for (j = 0; j < 4; ++j) { + // find translation char in `b64_table' + for (l = 0; l < 64; ++l) { + if (tmp[j] == b64_table[l]) { + tmp[j] = l; + break; + } + } + } + + // decode remainder + buf[0] = (tmp[0] << 2) + ((tmp[1] & 0x30) >> 4); + buf[1] = ((tmp[1] & 0xf) << 4) + ((tmp[2] & 0x3c) >> 2); + buf[2] = ((tmp[2] & 0x3) << 6) + tmp[3]; + + // write remainer decoded buffer to `decbuf.ptr' + if (b64_buf_realloc(&decbuf, size + (i - 1)) == -1) return NULL; + for (j = 0; (j < i - 1); ++j) { + ((unsigned char*)decbuf.ptr)[size++] = buf[j]; + } + } + + // Make sure we have enough space to add '\0' character at end. + if (b64_buf_realloc(&decbuf, size + 1) == -1) return NULL; + ((unsigned char*)decbuf.ptr)[size] = '\0'; + + // Return back the size of decoded string if demanded. + if (decsize != NULL) { + *decsize = size; + } + + return (unsigned char*) decbuf.ptr; +} \ No newline at end of file diff --git a/src/transfer.h b/src/transfer.h new file mode 100644 index 0000000..8c6a9f4 --- /dev/null +++ b/src/transfer.h @@ -0,0 +1,69 @@ +// transfer.h +// data transfer over AMY messages +// b64 stuff from https://github.com/jwerle/b64.c + +// bytes -> b64 length: 4 * n / 3 gives unpadded length. +// ((4 * n / 3) + 3) & ~3 gives padded length + +// so if max AMY message is 255 chars, 252 bytes of b64 is max = 189 bytes of pcm + +#ifndef TRANSFER_H +#define TRANSFER_H 1 + +typedef struct b64_buffer { + char * ptr; + int bufc; +} b64_buffer_t; + + // How much memory to allocate per buffer +#define B64_BUFFER_SIZE (256) + + // Start buffered memory +int b64_buf_malloc(b64_buffer_t * buffer); + +// Update memory size. Returns the same pointer if we +// have enough space in the buffer. Otherwise, we add +// additional buffers. +int b64_buf_realloc(b64_buffer_t * buffer, size_t size); + +/** + * Base64 index table. + */ + +static const char b64_table[] = { + 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', + 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', + 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', + 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', + 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', + 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', + 'w', 'x', 'y', 'z', '0', '1', '2', '3', + '4', '5', '6', '7', '8', '9', '+', '/' +}; + +void start_receiving_transfer(uint32_t length, uint8_t * storage); +void parse_transfer_message(char * message, uint16_t len) ; + +/** + * Encode `unsigned char *' source with `size_t' size. + * Returns a `char *' base64 encoded string. + */ + +char * +b64_encode (const unsigned char *, size_t); + +/** + * Decode `char *' source with `size_t' size. + * Returns a `unsigned char *' base64 decoded string. + */ +unsigned char * +b64_decode (const char *, size_t); + +/** + * Decode `char *' source with `size_t' size. + * Returns a `unsigned char *' base64 decoded string + size of decoded string. + */ +unsigned char * +b64_decode_ex (const char *, size_t, size_t *); + +#endif \ No newline at end of file diff --git a/wavdumper.py b/wavdumper.py deleted file mode 100644 index 85b5a6c..0000000 --- a/wavdumper.py +++ /dev/null @@ -1,451 +0,0 @@ -import glob -import math -import os -import struct -import sys - -""" -Print detailed information on the headers and structure of a WAV file. - -Author: Kristian Ovaska -WWW: http://www.cs.helsinki.fi/u/hkovaska/wavdumper/ -License: GNU General Public License -Version: 0.5 (2003-11-13) - -See readme.html for details. -""" - -NOTES = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'] - -TEXT_CHUNKS = { - 'iarl': 'Archival location', - 'iart': 'Artist', - 'icms': 'Commissioned', - 'icmt': 'Comments', - 'icop': 'Copyright', - 'icrd': 'Creation date', - 'ieng': 'Engineer', - 'ignr': 'Genre', - 'ikey': 'Keywords', - 'inam': 'Name', - 'imed': 'Medium', - 'iprd': 'Product', - 'isbj': 'Subject', - 'isft': 'Software', - 'isrc': 'Source', - 'isrf': 'Original medium', - 'itch': 'Technician', -} - -# Only reasonably-common formats here; see RFC 2361 for the rest -WAVE_FORMAT = { - 0x01: 'Uncompressed PCM', - 0x02: 'Microsoft ADPCM', - 0x03: 'IEEE Float', - 0x06: 'ITU G.711 a-law', - 0x07: 'ITU G.711 u-law', - 0x11: 'IMA ADPCM', - 0x16: 'ITU G.723 ADPCM', - 0x31: 'GSM 6.10', - 0x40: 'ITU G.721 ADPCM', - 0x50: 'MPEG', - 0x55: 'MPEG Layer 3', -} - -def stripNull(string): - return string.strip('\0') - - -class Wav: - """WAV parser and printer. Only printInfo is used outside the class. - Technically, the class is similar to LL(1) parsers. There are read_XYZ - methods that correspond to WAV chunk types. They are defined in - alphabetical order, with assistant methods right after the main method. - A few methods, like read_junk, are used for more than one type of chunk - (junk and pad, in this case). - """ - - def __init__(self, filename): - self.filename = filename - - def printInfo(self, outfile=None): - """Read file and print info. Outfile is a file-like object; default - is sys.stdout.""" - - self.reset() - if outfile: - self.outfile = outfile - - if (not os.path.exists(self.filename)) or (not os.path.isfile(self.filename)): - self.p('Could not open %s' % self.filename) - return - - try: - # We use open instead of file for backwards compability - self.infile = open(self.filename, 'rb') - except IOError: - self.p('Could not open %s' % self.filename) - return - - self.filesize = os.path.getsize(self.filename) - self.p('File: %s (%s bytes)' % (self.filename, self.filesize)) - - # First, find out if we have a valid WAV file. - if self.filesize<12: - self.p('Not a WAV file') - self.infile.close() - return - id, = struct.unpack('4s', self.read(4)) - if id not in [b'RIFF', b'RIFX']: - self.p('Not a WAV file') - self.infile.close() - return - if id=='RIFX': - self.p('File is in big-endian format') - self.endian = '>' - - length, rifftype = struct.unpack(self.endian+'i 4s', self.read(8)) - if rifftype!=b'WAVE': - self.p('Not a WAV file') - self.infile.close() - return - if length!=self.filesize-8: - self.p('Warning: incorrect RIFF header length (%s)' % length) - - # Now, parse the file. - while self.pos()' is big-endian - self.indent = 0 # indentation level - self.fmtChunks = 0 # number of fmt chunks - self.dataChunks = 0 # number of data chunks - self.waveFormat = 0 - self.sampleRate = 0 - self.bytesPerSample = 0 - - def read(self, bytes): - return self.infile.read(bytes) - - def skip(self, bytes): - self.infile.seek(bytes, 1) - - def pos(self): - return self.infile.tell() - - def p(self, *args): - """Print args with indentation.""" - if self.indent: - self.outfile.write(' ' * self.indent) - for arg in args: - print (arg, file=self.outfile) - print ('\n', file=self.outfile) - - def read_chunk(self): - """Determine chunk type by reading a few bytes and launch a specialized - read_ function. Return bytes read: this is needed by read_list.""" - - id, length = struct.unpack(self.endian+'4s i', self.read(8)) - self.p('Chunk at pos %s: id = "%s", length = %s bytes' % (self.pos(), id, length)) - - id = id.lower() - - methodmap = { - b'cue ': self.read_cue, - b'data': self.read_data, - b'disp': self.read_disp, - b'fact': self.read_fact, - b'fmt ': self.read_fmt, - b'junk': self.read_junk, - b'labl': self.read_labl, - b'ltxt': self.read_ltxt, - b'list': self.read_list, - b'note': self.read_labl, - b'pad ': self.read_junk, - b'plst': self.read_plst, - b'smpl': self.read_smpl, - } - - self.indent += 1 - if id in methodmap: - f = methodmap[id] - f(length) - elif id in TEXT_CHUNKS: # short iXYZ text chunks - s = stripNull(self.read(length)) - self.p('%s: %s' % (TEXT_CHUNKS[id], s)) - else: - self.p('Unknown chunk') - self.skip(length) - self.indent -= 1 - - if length%2 != 0: - self.skip(1) # chunks are word-aligned, but the pad byte isn't counted in length - length += 1 - - return length+8 - -### read_ methods for each chunk. ### - - def read_cue(self, length): - """Cue point list chunk.""" - points, = struct.unpack(self.endian+'i', self.read(4)) - count = 4 - - self.p('Cue List Chunk') - self.p('Points:', points) - - for i in range(points): - self.indent += 1 - count += 24 - self.read_cuepoint() - self.indent -= 1 - - if length>count: - self.skip(length-count) - - def read_cuepoint(self): - """Cue point inside cue point list chunk.""" - id, position, fccChunk, chunkStart, blockStart, sampleOffset = \ - struct.unpack(self.endian+'2i 4s 3i', self.read(24)) - self.p('ID: %s, playlist pos: %s, sample offset: %s' % (id, position, sampleOffset)) - - def read_data(self, length): - """Sample data chunk.""" - if self.dataChunks>0: - self.p('ERROR: too many Data chunks!') - self.dataChunks += 1 - - self.p('%s bytes sample data' % length) - if self.waveFormat==1: - self.samples = length/self.bytesPerSample - - self.p('%s samples' % int(self.samples)) - self.p('%.3f seconds' % (float(self.samples)/self.sampleRate)) - self.skip(length) - - def read_disp(self, length): - """Some Microsoft(?) thing, couldn't find specs.""" - self.p('Disp Chunk') - if length==0: - return - - dispType, = struct.unpack(self.endian+'i', self.read(4)) - count = 4 - - if dispType==1: - s = stripNull(self.read(length-count)) - count += length-count - self.p('Type: %s, text: %s' % (dispType, s)) - elif dispType==8: - self.p('Type: %s, bitmap' % dispType) - else: - self.p('Type:', dispType) - - if length>count: - self.skip(length-count) - - def read_fact(self, length): - self.p('Fact Chunk') - count = 0 - if length>=4: - numberOfSamples, = struct.unpack(self.endian+'i', self.read(4)) - self.p('Number of samples in file:', numberOfSamples) - count += 4 - if length>count: - self.skip(length-count) - - def read_fmt(self, length): - """Format chunk, basic info about the wave data.""" - if self.fmtChunks>0: - self.p('ERROR: too many Format chunks!') - self.fmtChunks += 1 - - formatTag, self.channels, samplesPerSec, avgBytesPerSec, blockAlign = \ - struct.unpack(self.endian+'h H I I H', self.read(14)) - count = 14 - - self.sampleRate = float(samplesPerSec) - self.waveFormat = formatTag - - self.p('Format Chunk') - if formatTag in WAVE_FORMAT: - self.p('Data format:', WAVE_FORMAT[formatTag]) - else: - self.p('Data format: unknown (%s)' % formatTag) - self.p('Channels:', self.channels) - self.p('Sample rate: %s Hz' % samplesPerSec) - self.p('Average bytes per sec:', avgBytesPerSec) - self.p('Block align (bytes):', blockAlign) - - if length>=16: - # bitsPerSample wasn't part of the original WAV specs... - bitsPerSample, = struct.unpack(self.endian+'H', self.read(2)) - count += 2 - self.p('Bits per sample:', bitsPerSample) - self.bytesPerSample = float(self.channels)*(math.ceil(bitsPerSample/8.0)) - else: - # A hack, but should be very rare anyway - self.bytesPerSample = float(blockAlign) - - if length>=18: - additionalSize, = struct.unpack(self.endian+'H', self.read(2)) - count += 2 - if additionalSize>0: - self.p('%s bytes additional space reported' % additionalSize) - - if length>count: - self.p('%s bytes additional skipped' % (length-count)) - self.skip(length-count) - - def read_junk(self, length): - """Junk chunk, can be used for aligning.""" - self.p('Junk/Pad Chunk') - self.skip(length) - - def read_labl(self, length): - """labl or note chunk""" - id, = struct.unpack(self.endian+'i', self.read(4)) - self.p('Label/note chunk, id = %s' % id) - if length>4: - s = stripNull(self.read(length-4)) - self.p('Text:', s) - else: - self.p('(No text)') - - def read_ltxt(self, length): - self.p('Labeled Text Chunk') - - id, sampleLength, purporse, country, languange, dialect, codepage = \ - struct.unpack(self.endian+'3i 4h', self.read(20)) - - if length>20: - s = stripNull(self.read(length-20)) - self.p('Text:', s) - else: - self.p('(No text)') - - def read_list(self, length): - """List chunk, contains other chunks.""" - listType, = struct.unpack(self.endian+'4s', self.read(4)) - count = 4 - self.p('List Chunk, id = "%s"' % listType) - - while countcount: - self.skip(length-count) - - def read_plst(self, length): - """Playlist chunk.""" - segments, = struct.unpack(self.endian+'i', self.read(4)) - count = 4 - self.p('Playlist Chunk') - self.p('Segments:', segments) - - totalLength = 0 - for i in range(segments): - self.indent += 1 - id, segLength, repeats = struct.unpack('3i', self.read(3*4)) - count += 3*4 - self.p('ID: %s, length: %s, repeats: %s' % (id, segLength, repeats)) - totalLength += segLength*repeats - self.indent -= 1 - self.p('Total playing time: %.1f seconds' % (float(totalLength)/self.sampleRate)) - - if length>count: - self.skip(length-count) - - def read_smpl(self, length): - """Sampler info chunk.""" - manufacturer, product, samplePeriod, self.midiUnityNote, midiPitchFraction, smpteFormat, \ - smpteOffset, self.sampleLoops, samplerData = struct.unpack(self.endian+'9i', self.read(9*4)) - count = 9*4 # read bytes - - self.p('Sampler Chunk') - self.p('Manufacturer: %x' % manufacturer) - self.p('Product: %x' % product) - noteStr = '%s-%s' % (NOTES[self.midiUnityNote % 12], self.midiUnityNote/12) - self.p('MIDI note (60=middle-C): %s (%s) +- %.1f%%' % \ - (self.midiUnityNote, noteStr, float(midiPitchFraction)/0x80000000)) - - for i in range(self.sampleLoops): - self.p('Sample loop') - self.indent += 1 - self.read_sampleloop(samplerData) - count += 6*4 + samplerData - self.indent -= 1 - - if length>count: - self.skip(length-count) - - def read_sampleloop(self, additional): - """Sampleloop inside sampler info. additional is the length of - vendor-specific data, which is skipped.""" - - id, loopType, self.loopstart, self.loopend, fraction, playCount = struct.unpack(self.endian+'6i', self.read(6*4)) - - self.p('Loop ID:', id) - if loopType==0: - s = 'forward' - elif loopType==1: - s = 'ping pong' - elif loopType==2: - s = 'backward' - elif loopType>=3 and loopType<=31: - s = 'unknown, WAV standard (%s)' % loopType - else: - s = 'device specific (%s)' % loopType - self.p('Type:', s) - self.p('Range: %s - %s' % (self.loopstart, self.loopend)) - if playCount==0: - self.p('Repeat: infinity') - else: - self.p('Repeat: %s times' % playCount) - - if additional: - self.p('%s bytes additional space skipped' % additional) - self.skip(additional) - - -######################################################################## - -def argfiles(args, onlyFiles=False): - """Expand arguments (globs and non-globs). Return a flat list.""" - files = [] - for arg in args: - if ('*' in arg) or ('?' in arg): - files.extend(glob.glob(arg)) - else: - files.append(arg) - if onlyFiles: - files = filter(os.path.isfile, files) - return files - -def main(args): - if not args: - print('Usage: wavdump ') - return - - files = argfiles(args) - if not files: - print('No matching files found.') - return - - wav = Wav(files[0]) - wav.printInfo() - - for filename in files[1:]: - print() - wav = Wav(filename) - wav.printInfo() - -if __name__=='__main__': - main(sys.argv[1:])