diff --git a/README.md b/README.md index d50b64e4..d17358cd 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ AMY supports * MIDI input support and synthesizer voice management, including voice stealing, controllers and per-channel multi-timbral operation * A strong Juno-6 style analog synthesizer * An operator / algorithm-based frequency modulation (FM) synth, modeled after the DX-7 - * PCM sampler, reading from a baked-in buffer of percussive and misc samples, or by loading samples with looping and base midi note + * PCM sampler, reading from a baked-in buffer of percussive and misc samples, or by loading samples into RAM, or playing from files on disk directly, with loop points and base midi note * karplus-strong string with adjustable feedback * An arbitrary number of band-limited oscillators, each with adjustable frequency, pan, phase, amplitude: * pulse (+ adjustable duty cycle), sine, saw (up and down), triangle, noise @@ -41,6 +41,7 @@ AMY supports * A front end for DX7 and Juno-6 SYSEX patches and conversion setup commands * Built-in event clock and pattern sequencer, using hardware real time timers on microcontrollers * Multi-core (including microcontrollers) for rendering if available + * File transfer to the host The FM synth provides a Python library, [`fm.py`](https://github.com/shorepine/amy/blob/main/amy/fm.py) that can convert any DX7 patch into an AMY patch. diff --git a/amy/__init__.py b/amy/__init__.py index b8526344..cf6809ef 100644 --- a/amy/__init__.py +++ b/amy/__init__.py @@ -127,15 +127,17 @@ def str_of_int(arg): _KW_MAP_LIST = [ # Order matters because patch_string must come last. - ('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', 'gI'), ('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'), - ('eq', 'xL'), ('filter_type', 'GI'), ('ratio', 'IF'), ('latency_ms', 'NI'), ('algo_source', 'OL'), ('load_sample', 'zL'), + ('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', 'gI'), + ('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'), ('eq', 'xL'), ('filter_type', 'GI'), ('ratio', 'IF'), ('latency_ms', 'NI'), + ('algo_source', 'OL'), ('load_sample', 'zL'), ('transfer_file', 'zTL'), ('disk_sample', 'zFL'), ('algorithm', 'oI'), ('chorus', 'kL'), ('reverb', 'hL'), ('echo', 'ML'), ('patch', 'KI'), ('voices', 'rL'), ('external_channel', 'WI'), ('portamento', 'mI'), ('sequence', 'HL'), ('tempo', 'jF'), ('synth', 'iI'), ('pedal', 'ipI'), ('synth_flags', 'ifI'), ('num_voices', 'ivI'), ('to_synth', 'itI'), - ('grab_midi_notes', 'imI'), ('synth_delay', 'idI'), # 'i' is prefix for some two-letter synth-level codes. - ('preset', 'pI'), ('num_partials', 'pI'), # Note alaising. + ('grab_midi_notes', 'imI'), ('synth_delay', 'idI'), ('preset', 'pI'), ('num_partials', 'pI'), # note aliasing + ('start_sample', 'zSL'), ('stop_sample', 'zOI'), ('patch_string', 'uS'), # patch_string MUST be last because we can't identify when it ends except by end-of-message. ] _KW_PRIORITY = {k: i for i, (k, _) in enumerate(_KW_MAP_LIST)} # Maps each key to its index within _KW_MAP_LIST. @@ -305,6 +307,29 @@ def unload_sample(patch=0): send(load_sample=s) print("Patch %d unloaded from RAM" % (patch)) +# For AMYBoard and other AMYs that can get messages over MIDI sysex +# AMYboard is the name of the default AMYboard USB over MIDI device. +# If you're using another MIDI device, set output_name to it +# Use this like amy.override_send = sysex_write +def sysex_write(message, output_name='AMYboard'): + import mido + outputs = mido.get_output_names() + target_name = None + for name in outputs: + if output_name in name: + target_name = name + break + if target_name is None: + print("Could not find %s MIDI") + if isinstance(message, str): + payload = message.encode('ascii') + elif isinstance(message, bytes): + payload = message + # AMY sysex message + data = [0x00, 0x03, 0x45] + list(payload) + with midi.open_output(target_name) as out: + m = mido.Message('sysex', data=data) + out.send(m) try: import base64 @@ -315,6 +340,13 @@ def b64(b): def b64(b): return ubinascii.b2a_base64(b)[:-1] +def start_sample(preset=0, bus=1, max_frames=0, midinote=60, loopstart=0, loopend=0): + s = "%d,%d,%d,%d,%d,%d" % (preset, bus, max_frames, midinote, loopstart, loopend) + send(start_sample=s) + +def stop_sample(): + send(stop_sample=1) + def load_sample_bytes(b, stereo=False, preset=0, midinote=60, loopstart=0, loopend=0, sr=AMY_SAMPLE_RATE): # takes in a python bytes obj instead of filename from math import ceil @@ -330,7 +362,27 @@ def load_sample_bytes(b, stereo=False, preset=0, midinote=60, loopstart=0, loope message = b64(frames_bytes) send_raw(message.decode('ascii')) last_f = last_f + 188 - print("Loaded sample over wire protocol. Preset #%d. %d bytes, %d frames, midinote %d" % (preset, n_frames*2, n_frames, midinote)) + +def disk_sample(wavfilename, preset=0, midinote=60): + s = "%d,%s,%d" % (preset, wavfilename, midinote) + send(disk_sample=s) + +def transfer_file(source_filename, dest_filename=None): + import os + from math import ceil + if(dest_filename is None): + dest_filename = source_filename + file_size = os.path.getsize(source_filename) + s = "%s,%d" % (dest_filename, file_size) + send(transfer_file=s) + # Now generate the base64 encoded segments, 188 bytes at a time + # why 188? that generates 252 bytes of base64 text. amy's max message size is currently 255. + w = open(source_filename, 'rb') + for i in range(ceil(file_size/188)): + file_bytes = w.read(188) + message = b64(file_bytes) + send_raw(message.decode('ascii')) + w.close() def load_sample(wavfilename, preset=0, midinote=0, loopstart=0, loopend=0): from math import ceil diff --git a/amy/constants.py b/amy/constants.py index 06cb0fb4..7a9fcb6f 100644 --- a/amy/constants.py +++ b/amy/constants.py @@ -1,3 +1,4 @@ +MAX_FILENAME_LEN=127 AMY_BLOCK_SIZE=128 BLOCK_SIZE_BITS=7 AMY_BLOCK_SIZE=256 @@ -6,6 +7,16 @@ AMY_SAMPLE_RATE=48000 AMY_SAMPLE_RATE=44100 PCM_AMY_SAMPLE_RATE=22050 +AMY_TRANSFER_TYPE_NONE=0 +AMY_TRANSFER_TYPE_AUDIO=1 +AMY_TRANSFER_TYPE_FILE=2 +AMY_TRANSFER_TYPE_SAMPLE=3 +AMY_PCM_TYPE_ROM=0 +AMY_PCM_TYPE_FILE=1 +AMY_PCM_TYPE_MEMORY=2 +PCM_FILE_BUFFER_MULT=8 +AMY_BUS_OUTPUT=1 +AMY_BUS_AUDIO_IN=2 AMY_MAX_CORES=2 AMY_MAX_CHANNELS=2 AMY_NCHANS=2 @@ -72,8 +83,11 @@ AUDIO_EXT0=14 AUDIO_EXT1=15 AMY_MIDI=16 -CUSTOM=17 -WAVE_OFF=18 +PCM_LEFT=17 +PCM_RIGHT=18 +PCM_MIX=7 +CUSTOM=19 +WAVE_OFF=20 SYNTH_OFF=0 SYNTH_AUDIBLE=1 SYNTH_AUDIBLE_SUSPENDED=2 diff --git a/amy/test.py b/amy/test.py index 4056fb42..4c61aee6 100644 --- a/amy/test.py +++ b/amy/test.py @@ -1,5 +1,8 @@ import sys import os +import random +import string +import tempfile import numpy as np import scipy.io.wavfile as wav @@ -831,6 +834,61 @@ def run(self): amy.send(time=900, osc=0, vel=0) +class TestFileTransfer(AmyTest): + + def test(self): + _amy.stop() + _amy.start_no_default() + payload = ''.join(random.choice(string.ascii_letters + string.digits) for _ in range(2048)) + with tempfile.NamedTemporaryFile(mode='w', delete=True) as f: + with tempfile.NamedTemporaryFile(mode='w+', delete=True) as g: + f.write(payload) + f.flush() + os.fsync(f.fileno()) + amy.transfer_file(f.name, g.name) + amy.render(0.1) + g.seek(0) + transferred = g.read() + if transferred != payload: + raise AssertionError('transfer file contents mismatch') + print('TestFileTransfer: ok') + + +class TestDiskSample(AmyTest): + def run(self): + amy.disk_sample('sounds/partial_sources/CL SHCI A3.wav', preset=1024, midinote=57) + amy.send(time=50, osc=0, preset=1024, wave=amy.PCM_MIX, vel=2, note=57) + +class TestRestartFileSample(AmyTest): + def run(self): + amy.disk_sample('sounds/partial_sources/CL SHCI A3.wav', preset=1024, midinote=60) + amy.send(time=50, osc=0, preset=1024, wave=amy.PCM_MIX, vel=2, note=72) + amy.send(time=500, osc=0, preset=1024, wave=amy.PCM_MIX, vel=2, note=50) + +class TestDiskSampleStereo(AmyTest): + + def run(self): + amy.disk_sample('sounds/220_440_stereo.wav', preset=1024, midinote=60) + amy.disk_sample('sounds/220_440_stereo.wav', preset=1025, midinote=60) + amy.send(time=50, osc=0, preset=1024, wave=amy.PCM_LEFT, pan=0, vel=1, note=60) + amy.send(time=500, osc=1, preset=1025, wave=amy.PCM_RIGHT, pan=1, vel=1, note=60) + +class TestSample(AmyTest): + def run(self): + amy.start_sample(preset=1024,bus=1,max_frames=22050, midinote=60) + amy.send(time=0, synth=1, num_voices=4, patch=20) + amy.send(time=50, synth=1, note=48, vel=1) + amy.send(time=150, synth=1, note=60, vel=1) + amy.send(time=250, synth=1, note=63, vel=1) + # notes off + amy.send(time=400, synth=1, note=48, vel=0) + amy.send(time=400, synth=1, note=60, vel=0) + amy.send(time=400, synth=1, note=63, vel=0) + + # play a pitched up version + amy.send(osc=116, time=400, preset=1024, wave=amy.PCM_MIX, vel=1, note=72) + amy.send(osc=116, time=700, preset=1024, wave=amy.PCM_MIX, vel=2, note=84) + def main(argv): if len(argv) > 1: @@ -864,7 +922,10 @@ def main(argv): #TestVoiceStealing().test() #TestSustainPedal().test() #TestPatchFromEvents().test() - TestVoiceStealDecay().test() + #TestVoiceStealDecay().test() + #TestRestartFileSample().test() + #TestDiskSample().test() + TestFileTransfer().test() amy.send(debug=0) print("tests done.") diff --git a/docs/api.md b/docs/api.md index 329380b7..2351b0c6 100644 --- a/docs/api.md +++ b/docs/api.md @@ -120,6 +120,16 @@ void (*amy_external_midi_input_hook)(uint8_t * bytes, uint16_t len, uint8_t is_s // Called every sequencer tick void (*amy_external_sequencer_hook)(uint32_t) = NULL; + +// Hooks for file reading / writing / opening if your AMY host supports that +// We provide this for POSIX platforms (mac, linux, etc) +uint32_t (*amy_external_fopen_hook)(char * filename, char * mode) = NULL; +uint32_t (*amy_external_fwrite_hook)(uint32_t fptr, uint8_t * bytes, uint32_t len) = NULL; +uint32_t (*amy_external_fread_hook)(uint32_t fptr, uint8_t * bytes, uint32_t len) = NULL; +void (*amy_external_fclose_hook)(uint32_t fptr) = NULL; + +// Called when a file transfer is done (used in Micropython platforms to unpack) +void (*amy_external_file_transfer_done_hook)(const char *filename) = NULL; ``` @@ -153,7 +163,7 @@ Please see [AMY synthesizer details](synth.md) for more explanation on the synth | Wire code | C/JS `amy_event` | Python `amy.send` | Type-range | Notes | | ------ | -------- | ---------- | ---------- | ------------------------------------- | | `v` | `osc` | `osc` | uint 0 to OSCS-1 | Which oscillator to control | -| `w` | `wave` | `wave` | uint 0-16 | Waveform: [0=SINE, PULSE, SAW_DOWN, SAW_UP, TRIANGLE, NOISE, KS, PCM, ALGO, PARTIAL, BYO_PARTIALS, INTERP_PARTIALS, AUDIO_IN0, AUDIO_IN1, CUSTOM, OFF]. default: 0/SINE | +| `w` | `wave` | `wave` | uint 0-20 | Waveform: [0=SINE, PULSE, SAW_DOWN, SAW_UP, TRIANGLE, NOISE, KS, PCM, ALGO, PARTIAL, BYO_PARTIALS, INTERP_PARTIALS, AUDIO_IN0, AUDIO_IN1, AUDIO_EXT0, AUDIO_EXT1, AMY_MIDI, PCM_LEFT, PCM_RIGHT, CUSTOM, OFF]. default: 0/SINE | | `S` | `reset_osc`| `reset` | uint | Resets given oscillator. set to RESET_ALL_OSCS to reset all oscillators, gain and EQ. RESET_TIMEBASE resets the clock (immediately, ignoring `time`). RESET_AMY restarts AMY. RESET_SEQUENCER clears the sequencer.| | `A` | `bp0` | `bp0` | string | Envelope Generator 0's comma-separated breakpoint pairs of time(ms) and level, e.g. `100,0.5,50,0.25,200,0`. The last pair triggers on note off (release) | | `B` | `bp1` | `bp1` | string | Breakpoints for Envelope Generator 1. See bp0 | @@ -185,6 +195,14 @@ These per-oscillator parameters use [CtrlCoefs](synth.md) notation | `f` | `freq_coefs[]` | `freq` | float[,float...] | Frequency of oscillator, set of ControlCoefficients. Default is 0,1,0,0,0,0,1 (from `note` pitch plus `pitch_bend`) | | `F` | `filter_freq_coefs[]` | `filter_freq` | float[,float...] | Center/break frequency for variable filter, set of ControlCoefficients | +### PCM sampling + +| Wire code | C/JS `amy_event` | Python `amy.send` | Type-range | Notes | +| ------ | -------- | ---------- | ---------- | ------------------------------------- | +| `z` | **TODO**| `load_sample` | uint x 6 | Signal to start loading sample. preset number, length(frames), samplerate, channels, midinote, loopstart, loopend. All subsequent messages are base64 encoded WAVE-style frames of audio until `length` is reached. Set `preset` and `length=0` to unload a sample from RAM. | +| `zF` | **TODO**| `disk_sample` | uint,string,uint | Set a PCM preset to play live from a WAV filename on AMY host disk. Params: preset number, filename, midinote. See `hooks` for reading files on host disk. **Only one file sample can be played at once per preset number. Use multiple presets if you want polyphony from a single sample.** | +| `zS` | **TODO**| `start_sample` | uint x 6 | Start sampling to a stereo PCM preset from bus. Params: preset number, bus, max length in frames, midinote, loopstart, loopend. bus = 1 is AMY mixed output. bus = 2 is AUDIO_IN0 + 1. Will sample until max length is reached, `stop_sample` is issued, or a new `start_sample` is issued. | +| `zO` | **TODO**| `stop_sample` | uint | Stop sampling. Does nothing if no sampling active. param ignored. | ### Other @@ -200,7 +218,5 @@ These per-oscillator parameters use [CtrlCoefs](synth.md) notation | `t` | `time` | `time` | uint | Request playback time relative to some fixed start point on your host, in ms. Allows precise future scheduling. | | `V` | `volume`| `volume` | float 0-10 | Volume knob for entire synth, default 1.0 | | `x` | `eq_l, eq_m, eq_h` |`eq` | float,float,float | Equalization in dB low (~800Hz) / med (~2500Hz) / high (~7500Gz) -15 to 15. 0 is off. default 0. | -| `z` | **TODO**| `load_sample` | uint x 6 | Signal to start loading sample. preset number, length(samples), samplerate, midinote, loopstart, loopend. All subsequent messages are base64 encoded WAVE-style frames of audio until `length` is reached. Set `preset` and `length=0` to unload a sample from RAM. | | `D` | **TODO** | `debug` | uint, 2-4 | 2 shows queue sample, 3 shows oscillator data, 4 shows modified oscillator. Will interrupt audio! | - - +| `zT` | **TODO**| `transfer_file` | string,uint | Transfer a file to the host. Params: destination filename, file size. See `hooks` for writing files on host disk. | diff --git a/docs/synth.md b/docs/synth.md index 27d4d0c7..0c029c5c 100644 --- a/docs/synth.md +++ b/docs/synth.md @@ -18,8 +18,7 @@ * [FM & ALGO type](#fm---algo-type) * [Build-your-own Partials](#build-your-own-partials) * [Interpolated partials](#interpolated-partials) - * [PCM](#pcm) - * [Sampler (aka Memory PCM)](#sampler--aka-memory-pcm-) + * [PCM and Sampler](#pcm-and-sampler) ## Oscillators, voices, patches and synths @@ -283,7 +282,7 @@ Note that the default `bp0` amplitude envelope of the `BYO_PARTIALS` osc is a ga Please see our [piano voice documentation](https://shorepine.github.io/amy/piano.html) for more on the `INTERP_PARTIALS` type. -## PCM +## PCM and Sampler AMY comes with a set of 67 drum-like and instrument PCM samples to use as well, as they are normally hard to render with additive, subtractive or FM synthesis. You can use the type `PCM` and preset numbers 0-66 to explore them. Their native pitch is used if you don't give a frequency or note parameter. You can update the baked-in PCM sample bank using `amy_headers.py`. @@ -302,9 +301,9 @@ amy.send(vel=0) # note off amy.send(wave=amy.PCM,vel=1,preset=35,feedback=1) # nice violin ``` -## Sampler (aka Memory PCM) +### Sampler (aka Memory PCM) -You can also load your own samples into AMY at runtime. We support sending PCM data over the wire protocol. Use `load_sample` in `amy.py` as an example: +You can also load your own samples into AMY memory at runtime by sending PCM data over the wire protocol. Use `load_sample` in `amy.py` as an example: ```python amy.load_sample("G1.wav", preset=3) @@ -313,7 +312,7 @@ amy.send(osc=0, wave=amy.PCM, preset=3, vel=1) # plays the sample You can use any preset number. If it overlaps with an existing PCM baked in number, it will play the memory sample instead of the baked in sample until you `unload_sample` the preset. -If the WAV file has sampler metadata like loop points or base MIDI note, we use that in AMY. You can set it directly as well using `loopstart`, `loopend`, `midinote` or `length` in the `load_sample` call. To unload a sample: +If the WAV file has sampler metadata like loop points or base MIDI note, we use that in AMY. You can set it directly as well using `loopstart`, `loopend`, `channels`, `midinote` or `length` in the `load_sample` call. To unload a sample: ```python amy.unload_sample(3) # unloads the RAM for preset 3 @@ -321,4 +320,51 @@ amy.unload_sample(3) # unloads the RAM for preset 3 Under the hood, if AMY receives a `load_sample` message (with preset number and nonzero length), it will then pause all other message parsing until it has received `length` amount of base64 encoded bytes over the wire protocol. Each individual message must be base64 encoded. Since AMY's maximum message length is 255 bytes, there is logic in `load_sample` in `amy.py` to split the sample data into 188 byte chunks, which generates 252 bytes of base64 text. Please see `amy.load_sample` if you wish to load samples on other platforms. +### WAV file playback + +AMY support playing WAV files directly with pitching (but not looping!) if your host of MCU has file support. You can use this when the WAV files are bigger than available memory. We provide file reading hooks for POSIX platforms (Mac, Linux) and see [Hooks](api.md#Hooks) to build your own `fopen`, `fread` etc on other platforms like Arduino or Micropython. You can set an oscillator to play a channel of the file with `disk_sample`, e.g. + +```python +amy.disk_sample("G1.wav", preset=1024, midinote=31) +amy.send(osc=0, wave=amy.PCM_LEFT, preset=1024, pan=0, note=60, vel=1) # plays sample from disk +``` + +Note that you can only play one instance of the file per **preset**. (We keep one file handle open per `disk_sample` preset.) If you want to play multiple copies of a WAV file at once (for instance, a polyphonic sampler), you should make multiple `preset`s: + +```python +amy.disk_sample("G1.wav", preset=1024, midinote=31) +amy.disk_sample("G1.wav", preset=1025, midinote=31) +amy.disk_sample("G1.wav", preset=1026, midinote=31) +amy.send(osc=0, wave=amy.PCM_LEFT, preset=1024, pan=0, note=60, vel=1) # plays sample from disk +amy.send(osc=0, wave=amy.PCM_LEFT, preset=1025, pan=0, note=72, vel=1) +``` + +### Channels + +We support loading 1 or 2 channel WAV for `load_sample` and `disk_sample`. For `disk_sample`, channels are decoded from the WAV file metadata on disk. For `load_sample`, you should set the channels you're sending over. + +Each oscillator in AMY is mono, but you can hint it which channel of PCM to play back with `wave=PCM_LEFT` or `PCM_RIGHT`. `PCM` or `PCM_MIX` will average each channel if it was a two channel source. To play back stereo, set up two channels and use AMY's `pan`: + +```python +amy.disk_sample("G1.wav", preset=1024, midinote=31) +amy.disk_sample("G1.wav", preset=1025, midinote=31) +amy.send(osc=0, wave=amy.PCM_LEFT, preset=1024, pan=0, note=60, vel=1) +amy.send(osc=1, wave=amy.PCM_RIGHT, preset=1025, pan=1, note=60, vel=1) +``` + +### Sampling + +AMY can also sample directly into a PCM memory buffer from a `bus`. [A bus in AMY is a work in progress](https://github.com/shorepine/amy/issues/114) but for now we support two stereo buses: `bus=1` is the final AMY output and `bus=2` is just `AUDIO_IN0` and `AUDIO_IN1`. To start sampling to a PCM preset, use `start_sample`: + +```python +amy.start_sample(preset=1024, bus=0, max_frames=44100) # sample for one second +amy.stop_sample() # stop all sampling, not needed if using max_frames +amy.start_sample(preset=1024, bus=1, max_frames=11025, midinote=60) # set base midi note, looping, too +amy.send(osc=0, wave=amy.PCM_LEFT, preset=1024, pan=0, note=72, vel=1) # play back AUDIO_IN sample an octave higher +amy.send(osc=1, wave=amy.PCM_RIGHT, preset=1024, pan=1, note=72, vel=1) +``` + + + + diff --git a/sounds/.DS_Store b/sounds/.DS_Store deleted file mode 100644 index 1dff16f1..00000000 Binary files a/sounds/.DS_Store and /dev/null differ diff --git a/sounds/220_440_stereo.wav b/sounds/220_440_stereo.wav new file mode 100644 index 00000000..83e34f58 Binary files /dev/null and b/sounds/220_440_stereo.wav differ diff --git a/sounds/sleepwalk.wav b/sounds/sleepwalk.wav new file mode 100644 index 00000000..9ec4426f Binary files /dev/null and b/sounds/sleepwalk.wav differ diff --git a/sounds/sleepwalk_mono.wav b/sounds/sleepwalk_mono.wav new file mode 100644 index 00000000..f1bd1e0f Binary files /dev/null and b/sounds/sleepwalk_mono.wav differ diff --git a/src/amy-example.c b/src/amy-example.c index 96e9751d..8843ca2a 100644 --- a/src/amy-example.c +++ b/src/amy-example.c @@ -56,34 +56,50 @@ int main(int argc, char ** argv) { } amy_external_render_hook = render; - for(int tries = 0; tries < 2; ++tries) { - amy_config_t amy_config = amy_default_config(); - amy_config.audio = AMY_AUDIO_IS_MINIAUDIO; - amy_config.playback_device_id = playback_device_id; - amy_config.capture_device_id = capture_device_id; - amy_config.features.default_synths = 0; - amy_start(amy_config); - - amy_live_start(); - //example_fm(0); - //example_voice_chord(0,0); - example_synth_chord(0, /* patch */ 0); - //example_sustain_pedal(0, /* patch */ 256); - //example_sequencer_drums(0); - //example_patch_from_events(); - - // Check that trying to program a non-user patch doesn't crash - amy_event e = amy_default_event(); - e.patch_number = 25; - e.osc = 0; - e.wave = SINE; - amy_add_event(&e); - - // Now just spin for 15s - uint32_t start = amy_sysclock(); - while(amy_sysclock() - start < 5000) { - usleep(THREAD_USLEEP); - } + amy_config_t amy_config = amy_default_config(); + amy_config.audio = AMY_AUDIO_IS_MINIAUDIO; + amy_config.playback_device_id = playback_device_id; + amy_config.capture_device_id = capture_device_id; + amy_config.features.default_synths = 0; + amy_start(amy_config); + + amy_live_start(); + amy_add_message("zF1024,sounds/sleepwalk.wav,60"); + amy_add_message("zF1025,sounds/sleepwalk.wav,60"); + + + amy_event e = amy_default_event(); + e.wave = PCM_LEFT; + e.preset = 1024; + e.velocity=1; + e.pan_coefs[0] = 0; + e.midi_note = 60; + e.osc = 14; + amy_add_event(&e); + + e.wave = PCM_RIGHT; + e.preset = 1025; + e.velocity=1; + e.pan_coefs[0] = 1; + e.midi_note = 60; + e.osc = 15; + amy_add_event(&e); + + + + //example_fm(0); + //example_voice_chord(0,0); + //example_synth_chord(0, /* patch */ 0); + //example_sustain_pedal(0, /* patch */ 256); + //example_sequencer_drums(0); + //example_patch_from_events(); + + + // Now just spin for 15s + uint32_t start = amy_sysclock(); + while(amy_sysclock() - start < 30000) { + usleep(THREAD_USLEEP); + } //show_debug(99); @@ -93,7 +109,6 @@ int main(int argc, char ** argv) { // Make sure libminiaudio has time to clean up. sleep(2); - } return 0; } diff --git a/src/amy.c b/src/amy.c index 2ad4c87f..2ef97d3a 100644 --- a/src/amy.c +++ b/src/amy.c @@ -368,10 +368,12 @@ int8_t global_init(amy_config_t c) { amy_global.eq[1] = F2S(1.0f); amy_global.eq[2] = F2S(1.0f); amy_global.hpf_state = 0; - amy_global.transfer_flag = 0; + amy_global.transfer_flag = AMY_TRANSFER_TYPE_NONE; amy_global.transfer_storage = NULL; - amy_global.transfer_length = 0; - amy_global.transfer_stored = 0; + amy_global.transfer_length_bytes = 0; + amy_global.transfer_stored_bytes = 0; + amy_global.transfer_file_handle = 0; + amy_global.transfer_filename[0] = '\0'; amy_global.debug_flag = 0; amy_global.sequencer_tick_count = 0; amy_global.next_amy_tick_us = 0; @@ -966,7 +968,11 @@ void osc_note_on(uint16_t osc, float initial_freq) { case SAW_UP: saw_up_note_on(osc, initial_freq); break; case TRIANGLE: triangle_note_on(osc, initial_freq); break; case PULSE: pulse_note_on(osc, initial_freq); break; - case PCM: pcm_note_on(osc); break; + case PCM: + case PCM_LEFT: + case PCM_RIGHT: + pcm_note_on(osc); + break; case ALGO: algo_note_on(osc, initial_freq); break; case NOISE: noise_note_on(osc); break; case AUDIO_IN0: audio_in_note_on(osc, 0); break; @@ -1038,7 +1044,9 @@ void play_delta(struct delta *d) { DELTA_TO_SYNTH_I(NOTE_SOURCE, note_source) DELTA_TO_SYNTH_I(EG0_TYPE, eg_type[0]) DELTA_TO_SYNTH_I(EG1_TYPE, eg_type[1]) - if (d->param == PRESET) synth[d->osc]->preset = (uint16_t)d->data.i; + if (d->param == PRESET) { + synth[d->osc]->preset = (uint16_t)d->data.i; + } if (d->param == PORTAMENTO) synth[d->osc]->portamento_alpha = portamento_ms_to_alpha(d->data.i); if (d->param == PHASE) synth[d->osc]->trigger_phase = F2P(d->data.f); @@ -1199,7 +1207,11 @@ void play_delta(struct delta *d) { case SAW_UP: saw_down_mod_trigger(mod_osc); break; case TRIANGLE: triangle_mod_trigger(mod_osc); break; case PULSE: pulse_mod_trigger(mod_osc); break; - case PCM: pcm_mod_trigger(mod_osc); break; + case PCM: + case PCM_LEFT: + case PCM_RIGHT: + pcm_mod_trigger(mod_osc); + break; case CUSTOM: custom_mod_trigger(mod_osc); break; } } @@ -1214,7 +1226,11 @@ void play_delta(struct delta *d) { switch(synth[osc]->wave) { case KS: ks_note_off(osc); break; case ALGO: algo_note_off(osc); break; - case PCM: pcm_note_off(osc); break; + case PCM: + case PCM_LEFT: + case PCM_RIGHT: + pcm_note_off(osc); + break; case AMY_MIDI: amy_send_midi_note_off(osc); break; case CUSTOM: custom_note_off(osc); break; case BYO_PARTIALS: @@ -1416,7 +1432,7 @@ SAMPLE render_osc_wave(uint16_t osc, uint8_t core, SAMPLE* buf) { } } if(pcm_samples) - if(synth[osc]->wave == PCM) max_val = render_pcm(buf, osc); + if (AMY_WAVE_IS_PCM(synth[osc]->wave)) max_val = render_pcm(buf, osc); if(synth[osc]->wave == ALGO) max_val = render_algo(buf, osc, core); if(AMY_HAS_PARTIALS) { if(synth[osc]->wave == BYO_PARTIALS || synth[osc]->wave == INTERP_PARTIALS) @@ -1473,11 +1489,11 @@ void amy_render(uint16_t start, uint16_t end, uint8_t core) { // check it's not off, just in case. todo, why do i care? // apply filter to osc if set if(synth[osc]->filter_type != FILTER_NONE) { - //fprintf(stderr, "time %.3f osc %d filter_type %d\n", - // (float)amy_global.total_blocks*AMY_BLOCK_SIZE / AMY_SAMPLE_RATE, - // osc, synth[osc]->filter_type); + //fprintf(stderr, "time %.3f osc %d filter_type %d\n", + // (float)amy_global.total_blocks*AMY_BLOCK_SIZE / AMY_SAMPLE_RATE, + // osc, synth[osc]->filter_type); max_val = filter_process(per_osc_fb[core], osc, max_val); - } + } uint8_t handled = 0; if(amy_external_render_hook!=NULL) { handled = amy_external_render_hook(osc, per_osc_fb[core], AMY_BLOCK_SIZE); @@ -1489,7 +1505,7 @@ void amy_render(uint16_t start, uint16_t end, uint8_t core) { // only mix the audio in if the external hook did not handle it if(!handled) mix_with_pan(fbl[core], per_osc_fb[core], msynth[osc]->last_pan, msynth[osc]->pan); if (max_val > max_max) max_max = max_val; - } + } // end if audible } core_max[core] = max_max; @@ -1710,6 +1726,24 @@ int16_t * amy_fill_buffer() { } } } + + // Handle sampling after block is rendered + if(amy_global.transfer_flag==AMY_TRANSFER_TYPE_SAMPLE) { + uint32_t bytes_per_frame = AMY_NCHANS * sizeof(int16_t); + uint32_t byte_offset = amy_global.transfer_stored_bytes; + uint32_t bytes_to_copy = AMY_BLOCK_SIZE * bytes_per_frame; + if(amy_global.transfer_file_handle==AMY_BUS_OUTPUT) { + // copy block[] to amy_global.transfer_storage + memcpy(amy_global.transfer_storage + byte_offset, block, bytes_to_copy); + } else if(amy_global.transfer_file_handle==AMY_BUS_AUDIO_IN) { + // copy audio input buffer to storage + memcpy(amy_global.transfer_storage + byte_offset, amy_in_block, bytes_to_copy); + } + amy_global.transfer_stored_bytes += AMY_BLOCK_SIZE * AMY_NCHANS * sizeof(int16_t); + if(amy_global.transfer_stored_bytes >= amy_global.transfer_length_bytes) { + amy_global.transfer_flag = AMY_TRANSFER_TYPE_NONE; + } + } amy_global.total_blocks++; AMY_PROFILE_STOP(AMY_FILL_BUFFER) diff --git a/src/amy.h b/src/amy.h index b5ff4779..71bd617e 100644 --- a/src/amy.h +++ b/src/amy.h @@ -18,6 +18,7 @@ extern pthread_mutex_t amy_queue_lock; #endif #endif + // This is for baked in samples that come with AMY. The header file written by `amy/headers.py` writes this. typedef struct { uint32_t offset; @@ -37,6 +38,9 @@ extern const uint16_t pcm_samples; #define AMY_MCU #endif +#define MAX_FILENAME_LEN 127 + + // Set block size and SR. We try for 256/44100, but some platforms don't let us: #ifdef AMY_DAISY @@ -57,6 +61,23 @@ extern const uint16_t pcm_samples; #define PCM_AMY_SAMPLE_RATE 22050 +// Transfer types. +#define AMY_TRANSFER_TYPE_NONE 0 +#define AMY_TRANSFER_TYPE_AUDIO 1 +#define AMY_TRANSFER_TYPE_FILE 2 +#define AMY_TRANSFER_TYPE_SAMPLE 3 + +#define AMY_PCM_TYPE_ROM 0 +#define AMY_PCM_TYPE_FILE 1 +#define AMY_PCM_TYPE_MEMORY 2 + +// File-streaming buffer size multiplier (in blocks). +#define PCM_FILE_BUFFER_MULT 8 + + +#define AMY_BUS_OUTPUT 1 +#define AMY_BUS_AUDIO_IN 2 + // Always use fixed point. You can remove this if you want float #define AMY_USE_FIXEDPOINT @@ -205,8 +226,12 @@ enum coefs{ #define AUDIO_EXT0 14 #define AUDIO_EXT1 15 #define AMY_MIDI 16 -#define CUSTOM 17 -#define WAVE_OFF 18 +#define PCM_LEFT 17 +#define PCM_RIGHT 18 +#define PCM_MIX 7 // same as PCM +#define CUSTOM 19 +#define WAVE_OFF 20 +#define AMY_WAVE_IS_PCM(w) ((w) == PCM || (w) == PCM_LEFT || (w) == PCM_RIGHT) // synth[].status values #define SYNTH_OFF 0 @@ -665,8 +690,10 @@ struct state { // Transfer uint8_t transfer_flag; uint8_t * transfer_storage; - uint32_t transfer_length; - uint32_t transfer_stored; + uint32_t transfer_length_bytes; + uint32_t transfer_stored_bytes; + uint32_t transfer_file_handle; + char transfer_filename[MAX_FILENAME_LEN]; // Sequencer uint32_t sequencer_tick_count; @@ -765,6 +792,12 @@ extern float (*amy_external_coef_hook)(uint16_t); extern void (*amy_external_block_done_hook)(void); extern void (*amy_external_midi_input_hook)(uint8_t *, uint16_t, uint8_t); extern void (*amy_external_sequencer_hook)(uint32_t); +// Hooks for file reading / writing / opening if your AMY host supports that +extern uint32_t (*amy_external_fopen_hook)(char * filename, char * mode) ; +extern uint32_t (*amy_external_fwrite_hook)(uint32_t fptr, uint8_t * bytes, uint32_t len); +extern uint32_t (*amy_external_fread_hook)(uint32_t fptr, uint8_t *bytes, uint32_t len); +extern void (*amy_external_fclose_hook)(uint32_t fptr); +extern void (*amy_external_file_transfer_done_hook)(const char *filename); #ifdef __EMSCRIPTEN__ @@ -887,8 +920,9 @@ extern void triangle_mod_trigger(uint16_t osc); extern void pulse_mod_trigger(uint16_t osc); extern void pcm_mod_trigger(uint16_t osc); extern void custom_mod_trigger(uint16_t osc); -extern int16_t * pcm_load(uint16_t patch, uint32_t length, uint32_t samplerate, uint8_t midinote, uint32_t loopstart, uint32_t loopend); -extern void pcm_unload_preset(uint16_t patch_number); +extern int16_t * pcm_load(uint16_t preset_number, uint32_t length, uint32_t samplerate, uint8_t channels, uint8_t midinote, uint32_t loopstart, uint32_t loopend); +extern int pcm_load_file(uint16_t preset_number, const char *filename, uint8_t midinote); +extern void pcm_unload_preset(uint16_t preset_number); extern void pcm_unload_all_presets(); // filters @@ -940,5 +974,3 @@ extern int32_t delta_num_free(); // The size of the remaining pool. extern int peek_stack(char *tag); #endif - - diff --git a/src/amy_midi.c b/src/amy_midi.c index 370f5bc0..5d408a3e 100644 --- a/src/amy_midi.c +++ b/src/amy_midi.c @@ -2,6 +2,9 @@ // i deal with parsing and receiving midi on many platforms #include "amy.h" +#if defined(TULIP) || defined(AMYBOARD) +#include "py/runtime.h" +#endif #ifdef __EMSCRIPTEN__ #include #endif @@ -196,8 +199,9 @@ void midi_clock_received() { */ uint16_t sysex_len = 0; - - +#if defined(TULIP) || defined(AMYBOARD) +extern const mp_obj_fun_builtin_var_t tulip_amy_send_sysex_obj; +#endif uint8_t * sysex_buffer; void parse_sysex() { uint32_t time = AMY_UNSET_VALUE(time); @@ -205,7 +209,14 @@ void parse_sysex() { // let's use 0x00 0x03 0x45 for SPSS if(sysex_buffer[0] == 0x00 && sysex_buffer[1] == 0x03 && sysex_buffer[2] == 0x45) { sysex_buffer[sysex_len] = 0; - amy_add_message((char*)(sysex_buffer+3)); + // For Micropython hosted systems, we run MIDI on a separate "thread" (task) + // than MP, so just calling amy_send_message here can fail if it needs to access + // underlying MP resources. So we schedule it to run in the MP main loop instead. + #if defined(TULIP) || defined(AMYBOARD) + mp_sched_schedule(MP_OBJ_FROM_PTR(&tulip_amy_send_sysex_obj), mp_const_none); + #else + amy_add_message((char*)sysex_buffer+3); + #endif sysex_len = 0; // handled } else { amy_event_midi_message_received(sysex_buffer, sysex_len, 1, time); @@ -239,7 +250,7 @@ void convert_midi_bytes_to_messages(uint8_t * data, size_t len, uint8_t usb) { current_midi_message[0] = byte; if(byte == 0xF4 || byte == 0xF5 || byte == 0xF6 || byte == 0xF9 || byte == 0xFA || byte == 0xFB || byte == 0xFC || byte == 0xFD || byte == 0xFE || byte == 0xFF) { - amy_event_midi_message_received(current_midi_message, 1, 0, time); + amy_event_midi_message_received(current_midi_message, 1, 0, time); if(usb) i = len+1; // exit the loop if usb } else if(byte == 0xF0) { // sysex start // if that's there we then assume everything is an AMY message until 0xF7 @@ -381,9 +392,7 @@ void run_midi_task() { void run_midi() { sysex_buffer = malloc_caps(MAX_SYSEX_BYTES, amy_global.config.ram_caps_sysex); - if(amy_global.config.midi & AMY_MIDI_IS_UART) { - xTaskCreatePinnedToCore(run_midi_task, MIDI_TASK_NAME, (MIDI_TASK_STACK_SIZE) / sizeof(StackType_t), NULL, MIDI_TASK_PRIORITY, &midi_handle, MIDI_TASK_COREID); - } + xTaskCreatePinnedToCore(run_midi_task, MIDI_TASK_NAME, (MIDI_TASK_STACK_SIZE) / sizeof(StackType_t), NULL, MIDI_TASK_PRIORITY, &midi_handle, MIDI_TASK_COREID); } diff --git a/src/api.c b/src/api.c index 360ecbbe..45890efc 100644 --- a/src/api.c +++ b/src/api.c @@ -6,7 +6,8 @@ ////////////////////// // Hooks -// Optional render hook that's called per oscillator during rendering, used (now) for CV output from oscillators. return 1 if this oscillator should be silent +// Optional render hook that's called per oscillator during rendering, used (now) for CV output from oscillators. +// return 1 if this oscillator should be silent uint8_t (*amy_external_render_hook)(uint16_t osc, SAMPLE*, uint16_t len ) = NULL; // Optional external coef setter (meant for CV control of AMY via CtrlCoefs) @@ -21,6 +22,14 @@ void (*amy_external_midi_input_hook)(uint8_t * bytes, uint16_t len, uint8_t is_s // Called every sequencer tick void (*amy_external_sequencer_hook)(uint32_t) = NULL; +// Hooks for file reading / writing / opening if your AMY host supports that +uint32_t (*amy_external_fopen_hook)(char * filename, char * mode) = NULL; +uint32_t (*amy_external_fwrite_hook)(uint32_t fptr, uint8_t * bytes, uint32_t len) = NULL; +uint32_t (*amy_external_fread_hook)(uint32_t fptr, uint8_t * bytes, uint32_t len) = NULL; +void (*amy_external_fclose_hook)(uint32_t fptr) = NULL; +void (*amy_external_file_transfer_done_hook)(const char *filename) = NULL; + + amy_config_t amy_default_config() { amy_config_t c; @@ -48,7 +57,7 @@ amy_config_t amy_default_config() { c.max_memory_patches = 32; // caps -#if defined(TULIP) || defined(AMYBOARD) // || defined(ESP_PLATFORM) + #if defined(TULIP) || defined(AMYBOARD) // || defined(ESP_PLATFORM) c.ram_caps_events = MALLOC_CAP_SPIRAM; c.ram_caps_synth = MALLOC_CAP_SPIRAM; c.ram_caps_block = MALLOC_CAP_DEFAULT; @@ -310,6 +319,7 @@ void amy_start(amy_config_t c) { global_init(c); run_midi(); amy_profiles_init(); + transfer_init(); oscs_init(); if(AMY_HAS_DEFAULT_SYNTHS) amy_default_synths(); if(AMY_HAS_STARTUP_BLEEP) { diff --git a/src/i2s.c b/src/i2s.c index a9fd5dd4..f7637cfd 100644 --- a/src/i2s.c +++ b/src/i2s.c @@ -86,7 +86,6 @@ amy_err_t esp32_setup_i2s(void) { #else // AMYBOARD i2s setup, which is weird -#warning AMYBOARD amy_err_t esp32_setup_i2s(void) { i2s_chan_config_t chan_cfg = I2S_CHANNEL_DEFAULT_CONFIG(I2S_NUM_AUTO, I2S_ROLE_SLAVE); // ************* I2S_ROLE_SLAVE - needs external I2S clock input. i2s_new_channel(&chan_cfg, &tx_handle, &rx_handle); diff --git a/src/parse.c b/src/parse.c index ea0e988a..8e754bd7 100644 --- a/src/parse.c +++ b/src/parse.c @@ -97,6 +97,70 @@ PARSE_LIST(int32_t) PARSE_LIST(int16_t) +char *copy_with_trim(char *dest, size_t dest_len, const char *src, size_t src_len) { + // Copy a string while trimming leading and trailing spaces. + const char *s = src; + char *d = dest; + size_t d_writ = 0; + size_t s_read = 0; + size_t trimmed_src_len = src_len; + // scan for spaces at end + while (trimmed_src_len > 0 && isspace((unsigned char)src[trimmed_src_len - 1])) { + --trimmed_src_len; + } + // skip over leading spaces + while (s_read < trimmed_src_len && isspace((unsigned char)*s)) { + ++s; + ++s_read; + } + while(s_read < trimmed_src_len && d_writ < (dest_len - 1)) { + *d++ = *s++; + ++s_read; + ++d_writ; + } + *d = '\0'; // terminator. + return (char*) (src + src_len); +} + +static const char *strchrnul_local(const char *s, int c) { + const char *found = strchr(s, c); + if (found != NULL) { + return found; + } + return s + strlen(s); +} + +uint16_t parse_list_file_params(char *message, uint32_t *preset, char *filename, size_t filename_len, uint32_t *midinote) { + // Returns number of characters of message that are consumed. + if (filename_len > 0) { + filename[0] = '\0'; + } + char *m = message; + *preset = strtol(m, &m, 0); + if (*m != ',') return m - message; + ++m; + m = copy_with_trim(filename, filename_len, m, strchrnul_local(m, ',') - m); + if (*m != ',') return m - message; + ++m; + *midinote = strtol(m, &m, 0); + return m - message; +} + + +uint16_t parse_list_file_transfer_params(char *message, char *filename, size_t filename_len, + uint32_t *file_size) { + *file_size = 0; + if (filename_len > 0) { + filename[0] = '\0'; + } + char *m = message; + m = copy_with_trim(filename, filename_len, m, strchrnul_local(m, ',') - m); + if (*m != ',') return m - message; + ++m; + *file_size = strtol(m, &m, 0); + return m-message; +} + void copy_param_list_substring(char *dest, const char *src) { // Copy wire command string up to next parameter char. @@ -238,6 +302,67 @@ void amy_parse_synth_layer_message(char *message, amy_event *e) { else fprintf(stderr, "Unrecognized synth-level command '%s'\n", message - 1); } +// Parser for transfer-layer ('z') prefix. Returns how much of a message to skip +uint16_t amy_parse_transfer_layer_message(char *message) { + + if (message[0] >= '0' && message[0] <= '9') { + // z: Signal to start loading sample. + // Params: preset number, length(frames), samplerate, midinote, loopstart, loopend. + uint32_t sm[6]; // preset, length, SR, midinote, loop_start, loopend + parse_list_uint32_t(message, sm, 6, 0); + if(sm[1]==0) { // remove preset + pcm_unload_preset(sm[0]); + } else { + int16_t * ram = pcm_load(sm[0], sm[1], sm[2], 1, sm[3], sm[4], sm[5]); + start_receiving_transfer(sm[1]*2, (uint8_t*)ram); + } + return 0; + } + char cmd = message[0]; + message++; + if (cmd == 'T') { + // zT: Signal to start loading file. + //Params: Destination name, file size. + uint32_t file_size = 0; + char filename[MAX_FILENAME_LEN]; + uint16_t len = parse_list_file_transfer_params(message, filename, sizeof(filename), &file_size); + if (filename[0] != '\0') { + start_receiving_file_transfer(file_size, filename); + } + return len; + } + else if (cmd == 'F') { + // zF: setup PCM preset from WAV filename on disk. + // Params: Preset number, filename, midi note + uint32_t preset = 0; + uint32_t midinote = 0; + char filename[MAX_FILENAME_LEN]; + uint16_t len = parse_list_file_params(message, &preset, filename, sizeof(filename), + &midinote); + if (filename[0] != '\0') { + pcm_load_file(preset, filename, (uint8_t)midinote); + } + return len; + } + else if (cmd == 'S') { + // zS: sample from BUS[1] to a memorypcm patch. + // Params: Preset number, bus, max length in frames,midinote,loopstart,loopend + uint32_t sm[6]; // preset, bus, max frames, midinote, loop_start, loopend + parse_list_uint32_t(message, sm, 6, 0); + int16_t * ram = pcm_load(sm[0], sm[2], AMY_SAMPLE_RATE, 2, sm[3], sm[4], sm[5]); + start_receiving_sample(sm[2], sm[1], ram); + return 1; + } + else if (cmd == 'O') { + //zO: stop sampling from any bus + stop_receiving_sample(); + return 1; + } + else fprintf(stderr, "Unrecognized transfer-level command '%s'\n", message - 1); + return 0; +} + + int _next_alpha(char *s) { // Return how many chars to skip to get to the next alphabet (command prefix) (or EOS). int p = 0; @@ -256,9 +381,9 @@ void amy_parse_message(char * message, int length, amy_event *e) { peek_stack("parse_message"); char cmd = '\0'; uint16_t pos = 0; - + // Check if we're in a transfer block, if so, parse it and leave this loop - if(amy_global.transfer_flag) { + if (amy_global.transfer_flag == AMY_TRANSFER_TYPE_FILE || amy_global.transfer_flag == AMY_TRANSFER_TYPE_AUDIO) { parse_transfer_message(message, length); e->status = EVENT_TRANSFER_DATA; return; @@ -377,14 +502,7 @@ void amy_parse_message(char * message, int length, amy_event *e) { } break; case 'z': { - uint32_t sm[6]; // preset, length, SR, midinote, loop_start, loopend - parse_list_uint32_t(arg, sm, 6, 0); - if(sm[1]==0) { // remove preset - pcm_unload_preset(sm[0]); - } else { - 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); - } + pos += amy_parse_transfer_layer_message(arg); break; } /* Y,y available */ diff --git a/src/pcm.c b/src/pcm.c index deae9f5e..36b3ba93 100644 --- a/src/pcm.c +++ b/src/pcm.c @@ -1,6 +1,7 @@ // pcm.c #include "amy.h" +#include "transfer.h" #ifdef AMY_DAISY #define malloc_caps(a, b) qspi_malloc(a) @@ -10,6 +11,11 @@ // This is for any in-memory PCM samples. typedef struct { + uint8_t type; + char filename[MAX_FILENAME_LEN]; + uint8_t channels; + uint32_t file_handle; + uint32_t file_bytes_remaining; int16_t * sample_ram; uint32_t length; uint32_t loopstart; @@ -32,20 +38,37 @@ memorypcm_ll_t * memorypcm_ll_start; #define PCM_AMY_LOG2_SAMPLE_RATE log2f(PCM_AMY_SAMPLE_RATE / ZERO_LOGFREQ_IN_HZ) -memorypcm_preset_t * memorypreset_for_preset_number(uint16_t preset_number) { - // run through the LL looking for the preset +// Get either memory preset, file preset or baked in preset for preset number +memorypcm_preset_t * get_preset_for_preset_number(uint16_t preset_number) { + // Get the memory preset. If we can't find it, it could be a ROM preset. So copy params in from ROM preset memorypcm_ll_t *preset = memorypcm_ll_start; while(preset != NULL) { if(preset->preset_number == preset_number) { - if(preset->preset->sample_ram != NULL) { + if(preset->preset->sample_ram != NULL || preset->preset->file_handle > 0) { return preset->preset; } } preset = preset->next; } - return NULL; + + // No memory preset found, so try ROM preset. default to 0 if out of range + if (preset_number >= pcm_samples) preset_number = 0; + static memorypcm_preset_t rompreset; + memset(&rompreset, 0, sizeof(rompreset)); + const pcm_map_t cpreset = pcm_map[preset_number]; + rompreset.sample_ram = (int16_t*)pcm + cpreset.offset; + rompreset.length = cpreset.length; + rompreset.loopstart = cpreset.loopstart; + rompreset.loopend = cpreset.loopend; + rompreset.midinote = cpreset.midinote; + rompreset.samplerate = PCM_AMY_SAMPLE_RATE; + rompreset.log2sr = PCM_AMY_LOG2_SAMPLE_RATE; + rompreset.type = AMY_PCM_TYPE_ROM; + rompreset.channels = 1; + return &rompreset; } + void pcm_init() { memorypcm_ll_start = NULL; } @@ -58,17 +81,48 @@ void pcm_deinit() { // The number of bits used to hold the table index. #define PCM_INDEX_BITS (31 - PCM_INDEX_FRAC_BITS) +static void fclose_if_file(memorypcm_preset_t *preset) { + if (preset == NULL) { + return; + } + if (preset->type == AMY_PCM_TYPE_FILE && + preset->file_handle != 0 && + amy_external_fclose_hook != NULL) { + amy_external_fclose_hook(preset->file_handle); + preset->file_handle = 0; + } +} void pcm_note_on(uint16_t osc) { if(AMY_IS_SET(synth[osc]->preset)) { - memorypcm_preset_t *preset = memorypreset_for_preset_number(synth[osc]->preset); - if(preset == NULL) { + memorypcm_preset_t *preset = get_preset_for_preset_number(synth[osc]->preset); + if (preset->type == AMY_PCM_TYPE_FILE) { + fclose_if_file(preset); + if (amy_external_fopen_hook != NULL && amy_external_fclose_hook != NULL) { + uint32_t handle = amy_external_fopen_hook(preset->filename, "rb"); + if (handle != 0) { + wave_info_t info = {0}; + uint32_t data_bytes = 0; + if (wave_parse_header(handle, &info, &data_bytes)) { + preset->file_handle = handle; + preset->channels = info.channels; + preset->samplerate = info.sample_rate; + preset->log2sr = log2f((float)info.sample_rate / ZERO_LOGFREQ_IN_HZ); + preset->file_bytes_remaining = data_bytes; + } else { + amy_external_fclose_hook(handle); + } + } + } + } else if (preset->type == AMY_PCM_TYPE_ROM) { // baked-in PCM - don't overrun. if(synth[osc]->preset >= pcm_samples) synth[osc]->preset = 0; } + synth[osc]->phase = 0; // s16.15 index into the table; as if a PHASOR into a 16 bit sample table. // Special case: We use the msynth feedback flag to indicate note-off for looping PCM. As a result, it's explicitly NOT set in amy:hold_and_modify for PCM voices. Set it here. msynth[osc]->feedback = synth[osc]->feedback; + } } @@ -80,13 +134,10 @@ void pcm_mod_trigger(uint16_t osc) { void pcm_note_off(uint16_t osc) { if(AMY_IS_SET(synth[osc]->preset)) { uint32_t length = 0; - memorypcm_preset_t * preset = memorypreset_for_preset_number(synth[osc]->preset); + memorypcm_preset_t *preset = get_preset_for_preset_number(synth[osc]->preset); if(preset != NULL) { length = preset->length; - } else { - length = pcm_map[synth[osc]->preset].length; } - if(msynth[osc]->feedback == 0) { // Non-looping note: Set phase to the end to cause immediate stop. synth[osc]->phase = F2P(length / (float)(1 << PCM_INDEX_BITS)); @@ -98,76 +149,111 @@ void pcm_note_off(uint16_t osc) { } } -// Get either memory preset or baked in preset for preset number -memorypcm_preset_t get_preset_for_preset_number(uint16_t preset_number) { - // Get the memory preset. If it's null, copy params in from ROM preset - memorypcm_preset_t preset; - memorypcm_preset_t * preset_p = memorypreset_for_preset_number(preset_number); - if(preset_p == NULL) { - const pcm_map_t cpreset = pcm_map[preset_number]; - preset.sample_ram = (int16_t*)pcm + cpreset.offset; - preset.length = cpreset.length; - preset.loopstart = cpreset.loopstart; - preset.loopend = cpreset.loopend; - preset.midinote = cpreset.midinote; - preset.samplerate = PCM_AMY_SAMPLE_RATE; - } else { - preset.sample_ram = preset_p->sample_ram; - preset.length = preset_p->length; - preset.loopstart = preset_p->loopstart; - preset.loopend = preset_p->loopend; - preset.midinote = preset_p->midinote; - preset.samplerate = preset_p->samplerate; + +uint32_t fill_sample_from_file(memorypcm_preset_t *preset_p, uint32_t frames_needed) { + uint32_t bytes_per_frame = preset_p->channels * 2; + uint32_t frames_available = 0; + if (bytes_per_frame > 0) { + frames_available = preset_p->file_bytes_remaining / bytes_per_frame; + } + if (frames_available > 0 && frames_needed > frames_available) { + frames_needed = frames_available; } - return preset; + uint32_t frames_read = wave_read_pcm_frames_s16( + preset_p->file_handle, + preset_p->channels, + &preset_p->file_bytes_remaining, + preset_p->sample_ram, + frames_needed); + return frames_read; } SAMPLE render_pcm(SAMPLE* buf, uint16_t osc) { if(AMY_IS_SET(synth[osc]->preset)) { - memorypcm_preset_t preset = get_preset_for_preset_number(synth[osc]->preset); - // Presets can be > 32768 samples long. - // We need s16.15 fixed-point indexing. + memorypcm_preset_t * preset = get_preset_for_preset_number(synth[osc]->preset); float logfreq = msynth[osc]->logfreq; // If osc[midi_note] is set, shift the freq by the preset's default base_note. - if (AMY_IS_SET(synth[osc]->midi_note)) logfreq -= logfreq_for_midi_note(preset.midinote); - float playback_freq = freq_of_logfreq(PCM_AMY_LOG2_SAMPLE_RATE + logfreq); + if (AMY_IS_SET(synth[osc]->midi_note)) { + logfreq -= logfreq_for_midi_note(preset->midinote); + } + float playback_freq = freq_of_logfreq(preset->log2sr + logfreq); + uint32_t sample_length = preset->length; + if (preset->type == AMY_PCM_TYPE_FILE) { + float frames_per_output = playback_freq / (float)AMY_SAMPLE_RATE; + uint32_t frames_needed = (uint32_t)ceilf(frames_per_output * AMY_BLOCK_SIZE) + 1; + uint32_t max_frames = AMY_BLOCK_SIZE * PCM_FILE_BUFFER_MULT; + if (frames_needed > max_frames) { + frames_needed = max_frames; + } + sample_length = fill_sample_from_file(preset, frames_needed); + if(sample_length != frames_needed) { + // reached end of file + fclose_if_file(preset); + synth[osc]->status = SYNTH_OFF; + } + synth[osc]->phase = 0; + } - SAMPLE max_value = 0; SAMPLE amp = F2S(msynth[osc]->amp); PHASOR step = F2P((playback_freq / (float)AMY_SAMPLE_RATE) / (float)(1 << PCM_INDEX_BITS)); - const LUTSAMPLE* table = preset.sample_ram; + const LUTSAMPLE* table = preset->sample_ram; uint32_t base_index = INT_OF_P(synth[osc]->phase, PCM_INDEX_BITS); - //fprintf(stderr, "render_pcm: time=%.3f preset=%d base_index=%d length=%d loopstart=%d loopend=%d fb=%f is_unset_note_off %d\n", amy_global.total_blocks*AMY_BLOCK_SIZE / (float)AMY_SAMPLE_RATE, synth[osc]->preset, base_index, preset->length, preset->loopstart, preset->loopend, msynth[osc]->feedback, AMY_IS_UNSET(synth[osc]->note_off_clock)); for(uint16_t i=0; i < AMY_BLOCK_SIZE; i++) { SAMPLE frac = S_FRAC_OF_P(synth[osc]->phase, PCM_INDEX_BITS); - LUTSAMPLE b = table[base_index]; - LUTSAMPLE c = b; - if (base_index < preset.length) c = table[base_index + 1]; + LUTSAMPLE b = 0; + LUTSAMPLE c = 0; + uint32_t next_index = base_index + 1; + if (preset->channels == 2) { + uint32_t base_offset = base_index * 2; + uint32_t next_offset = next_index * 2; + if (synth[osc]->wave == PCM_LEFT) { + b = table[base_offset]; + c = (next_index < sample_length) ? table[next_offset] : b; + } else if (synth[osc]->wave == PCM_RIGHT) { + b = table[base_offset + 1]; + c = (next_index < sample_length) ? table[next_offset + 1] : b; + } else { // PCM or PCM_MIX + LUTSAMPLE bl = table[base_offset]; + LUTSAMPLE br = table[base_offset + 1]; + b = (LUTSAMPLE)(((int32_t)bl + (int32_t)br) / 2); + if (next_index < sample_length) { + LUTSAMPLE cl = table[next_offset]; + LUTSAMPLE cr = table[next_offset + 1]; + c = (LUTSAMPLE)(((int32_t)cl + (int32_t)cr) / 2); + } else { + c = b; + } + } + } else { + b = table[base_index]; + c = (next_index < sample_length) ? table[next_index] : b; + } SAMPLE sample = L2S(b) + MUL0_SS(L2S(c - b), frac); synth[osc]->phase = P_WRAPPED_SUM(synth[osc]->phase, step); base_index = INT_OF_P(synth[osc]->phase, PCM_INDEX_BITS); - if(base_index >= preset.length) { // end - synth[osc]->status = SYNTH_OFF;// is this right? - sample = 0; - } else { - if(msynth[osc]->feedback > 0) { // still looping. The feedback flag is cleared by pcm_note_off. - if(base_index >= preset.loopend) { // loopend - // back to loopstart - int32_t loop_len = preset.loopend - preset.loopstart; - synth[osc]->phase -= F2P(loop_len / (float)(1 << PCM_INDEX_BITS)); - base_index -= loop_len; + + if(preset->type != AMY_PCM_TYPE_FILE) { + // For non-file samples, we have to check for end of sample/looping. + if(base_index >= sample_length) { // end + synth[osc]->status = SYNTH_OFF;// is this right? + sample = 0; + } else { + if(msynth[osc]->feedback > 0) { // still looping. The feedback flag is cleared by pcm_note_off. + if(base_index >= preset->loopend) { // loopend + // back to loopstart + int32_t loop_len = preset->loopend - preset->loopstart; + synth[osc]->phase -= F2P(loop_len / (float)(1 << PCM_INDEX_BITS)); + base_index -= loop_len; + } } } } SAMPLE value = buf[i] + MUL4_SS(amp, sample); - buf[i] = value; - if (value < 0) value = -value; - if (value > max_value) max_value = value; + buf[i] = value; } //printf("render_pcm: osc %d preset %d len %d base_ix %d phase %f step %f tablestep %f amp %f\n", // osc, synth[osc]->preset, preset->length, base_index, P2F(synth[osc]->phase), P2F(step), (1 << PCM_INDEX_BITS) * P2F(step), S2F(msynth[osc]->amp)); - - return max_value; + return 1; // i don't believe we ever need to detect silence in a sample. it will shut itself off at the end. } return 0; } @@ -175,34 +261,83 @@ SAMPLE render_pcm(SAMPLE* buf, uint16_t osc) { SAMPLE compute_mod_pcm(uint16_t osc) { if(AMY_IS_SET(synth[osc]->preset)) { - memorypcm_preset_t preset = get_preset_for_preset_number(synth[osc]->preset); - float mod_sr = (float)AMY_SAMPLE_RATE / (float)AMY_BLOCK_SIZE; - PHASOR step = F2P(((float)preset.samplerate / mod_sr) / (1 << PCM_INDEX_BITS)); - const LUTSAMPLE* table = preset.sample_ram; - uint32_t base_index = INT_OF_P(synth[osc]->phase, PCM_INDEX_BITS); - SAMPLE sample; - if(base_index >= preset.length) { // end - synth[osc]->status = SYNTH_OFF;// is this right? - sample = 0; - } else { - sample = L2S(table[base_index]); - synth[osc]->phase = P_WRAPPED_SUM(synth[osc]->phase, step); - } - return MUL4_SS(F2S(msynth[osc]->amp), sample); + SAMPLE buf[AMY_BLOCK_SIZE]; + memset(buf, 0, sizeof(buf)); + render_pcm(buf, osc); + return buf[0]; } return 0; } +int pcm_load_file(uint16_t preset_number, const char *filename, uint8_t midinote) { + pcm_unload_preset(preset_number); + if (filename == NULL || filename[0] == '\0') { + return 0; + } + if (amy_external_fopen_hook == NULL || amy_external_fclose_hook == NULL) { + fprintf(stderr, "fopen hook not enabled on platform\n"); + return 0; + } + uint32_t handle = amy_external_fopen_hook((char *)filename, "rb"); + if (handle == 0) { + fprintf(stderr, "Could not open file %s\n", filename); + return 0; + } + wave_info_t info = {0}; + uint32_t data_bytes = 0; + if (!wave_parse_header(handle, &info, &data_bytes)) { + fprintf(stderr, "Could not parse WAVE file %s\n", filename); + amy_external_fclose_hook(handle); + return 0; + } + uint32_t total_frames = 0; + if (info.channels > 0) { + total_frames = data_bytes / (info.channels * 2); + } + uint32_t buffer_frames = AMY_BLOCK_SIZE * PCM_FILE_BUFFER_MULT; + memorypcm_ll_t *new_preset_pointer = malloc_caps( + sizeof(memorypcm_ll_t) + sizeof(memorypcm_preset_t) + buffer_frames * sizeof(int16_t), + amy_global.config.ram_caps_sample); + if (new_preset_pointer == NULL) { + fprintf(stderr, "No RAM left for sample load\n"); + return 0; + } + new_preset_pointer->next = memorypcm_ll_start; + memorypcm_ll_start = new_preset_pointer; + new_preset_pointer->preset_number = preset_number; + memorypcm_preset_t *memory_preset = + (memorypcm_preset_t *)(((uint8_t *)new_preset_pointer) + sizeof(memorypcm_ll_t)); + strncpy(memory_preset->filename, filename, MAX_FILENAME_LEN - 1); + memory_preset->filename[MAX_FILENAME_LEN - 1] = '\0'; + memory_preset->channels = info.channels; + memory_preset->samplerate = info.sample_rate; + memory_preset->log2sr = log2f((float)info.sample_rate / ZERO_LOGFREQ_IN_HZ); + memory_preset->midinote = midinote; + memory_preset->length = total_frames; + memory_preset->type = AMY_PCM_TYPE_FILE; + memory_preset->file_bytes_remaining = total_frames * info.channels * 2; + memory_preset->file_handle = handle; + memory_preset->sample_ram = malloc_caps(buffer_frames * info.channels * sizeof(int16_t), + amy_global.config.ram_caps_sample); + new_preset_pointer->preset = memory_preset; + if (amy_external_fclose_hook != NULL) { + amy_external_fclose_hook(handle); + memory_preset->file_handle = 0; + } + //fprintf(stderr, "read file %s frames %d channels %d preset %d handle %d\n", filename, total_frames, info.channels, preset_number, handle); + return 1; +} + // load mono samples (let python parse wave files) into preset # // set loopstart, loopend, midinote, samplerate (and log2sr) // return the allocated sample ram that AMY will fill in. -int16_t * pcm_load(uint16_t preset_number, uint32_t length, uint32_t samplerate, uint8_t midinote, uint32_t loopstart, uint32_t loopend) { +int16_t * pcm_load(uint16_t preset_number, uint32_t length, uint32_t samplerate, uint8_t channels, uint8_t midinote, uint32_t loopstart, uint32_t loopend) { // if preset was already a memorypcm, we need to unload it pcm_unload_preset(preset_number); // this is a no-op if preset doesn't exist or is a const pcm // now alloc a new LL entry and preset (the old LL entry is removed with pcm_unload_preset) - memorypcm_ll_t *new_preset_pointer = malloc_caps(sizeof(memorypcm_ll_t) + sizeof(memorypcm_preset_t) + length * sizeof(int16_t), + memorypcm_ll_t *new_preset_pointer = malloc_caps(sizeof(memorypcm_ll_t) + sizeof(memorypcm_preset_t) + length * channels * sizeof(int16_t), amy_global.config.ram_caps_sample); if(new_preset_pointer == NULL) { fprintf(stderr, "No RAM left for sample load\n"); @@ -217,6 +352,11 @@ int16_t * pcm_load(uint16_t preset_number, uint32_t length, uint32_t samplerate, memory_preset->midinote = midinote; memory_preset->loopstart = loopstart; memory_preset->length = length; + memory_preset->channels = channels; + memory_preset->filename[0] = '\0'; + memory_preset->file_bytes_remaining = 0; + memory_preset->file_handle = 0; + memory_preset->type = AMY_PCM_TYPE_MEMORY; memory_preset->sample_ram = (int16_t *)(((uint8_t *)memory_preset) + sizeof(memorypcm_preset_t)); if(loopend == 0) { // loop whole sample memory_preset->loopend = memory_preset->length-1; @@ -232,13 +372,13 @@ void pcm_unload_preset(uint16_t preset_number) { memorypcm_ll_t **preset_pointer = &memorypcm_ll_start; while(*preset_pointer != NULL) { if((*preset_pointer)->preset_number == preset_number) { - memorypcm_ll_t *next = (*preset_pointer)->next; - //fprintf(stderr, "unload_preset: unloading %d\n", (*preset_pointer)->preset_number); + memorypcm_ll_t *next = (*preset_pointer)->next; + fclose_if_file((*preset_pointer)->preset); // free the memory we allocated free((*preset_pointer)); - // close up the list - *preset_pointer = next; - return; + // close up the list + *preset_pointer = next; + return; } else { preset_pointer = &(*preset_pointer)->next; } @@ -250,7 +390,7 @@ void pcm_unload_all_presets() { memorypcm_ll_t *preset_pointer = memorypcm_ll_start; while(preset_pointer != NULL) { memorypcm_ll_t *next_pointer = preset_pointer->next; - //fprintf(stderr, "unload_all_presets: unloading %d\n", preset_pointer->preset_number); + fclose_if_file(preset_pointer->preset); free(preset_pointer); // Go to the next one preset_pointer = next_pointer; diff --git a/src/sequencer.c b/src/sequencer.c index d81a4082..c86d5bd9 100644 --- a/src/sequencer.c +++ b/src/sequencer.c @@ -178,7 +178,7 @@ void _sequencer_start() { void _sequencer_stop() { if (periodic_timer) { - ESP_ERRIR_CHECK(esp_timer_stop(periodic_timer)); + ESP_ERROR_CHECK(esp_timer_stop(periodic_timer)); ESP_ERROR_CHECK(esp_timer_delete(periodic_timer)); periodic_timer = NULL; } diff --git a/src/transfer.c b/src/transfer.c index 8ffdeeff..aa7b611a 100644 --- a/src/transfer.c +++ b/src/transfer.c @@ -6,6 +6,94 @@ #include "transfer.h" #include #include +#include + + + +// per platform file reading / writing +// posix (linux, mac) +// micropython / littlefs - tulip & amybard +// (later) arduino / SPIFFS / SD +// if yours isnt' here, you just write your own amy_external_fopen_hook etc + + +// mac / linux / generic posix + +#if defined(__unix__) || defined(__APPLE__) || defined(_POSIX_VERSION) + +// Map from FILE * to a uint32_t handle to pass to AMY + +static FILE *g_files[MAX_OPEN_FILES]; // index 1..MAX_OPEN_FILES-1 used + +static uint32_t alloc_handle(FILE *f) { + for (uint32_t i = 1; i < MAX_OPEN_FILES; i++) { + if (g_files[i] == NULL) { + g_files[i] = f; + return i; + } + } + return HANDLE_INVALID; // table full +} + +static FILE *lookup_handle(uint32_t h) { + if (h == 0 || h >= MAX_OPEN_FILES) return NULL; + return g_files[h]; +} + +static void free_handle(uint32_t h) { + if (h == 0 || h >= MAX_OPEN_FILES) return; + g_files[h] = NULL; +} + + +// These should map to +// uint32_t (*amy_external_fopen_hook)(char * filename, char * mode) = NULL; +// uint32_t (*amy_external_fwrite_hook)(uint32_t fptr, uint8_t * bytes, uint32_t len) = NULL; +// uint32_t (*amy_external_fread_hook)(uint32_t fptr, uint8_t * bytes, uint32_t len) = NULL; + +// void (*amy_external_fclose_hook)(uint32_t fptr) = NULL; + +uint32_t posix_external_fopen_hook(char * filename, char *mode) { + FILE *f = fopen(filename, mode); + if (!f) { + return HANDLE_INVALID; + } + uint32_t h = alloc_handle(f); + if (h == HANDLE_INVALID) { + fclose(f); + return HANDLE_INVALID; + } + return h; +} + +uint32_t posix_external_fread_hook(uint32_t h, uint8_t *buf, uint32_t len) { + FILE *f = lookup_handle(h); + if (!f) { + return 0; + } + uint32_t r = fread(buf, 1, len, f); + return r; +} + +uint32_t posix_external_fwrite_hook(uint32_t h, uint8_t *buf, uint32_t n) { + FILE *f = lookup_handle(h); + if (!f) { + return 0; + } + uint32_t w = fwrite(buf, 1, n, f); + return w; +} + +void posix_external_fclose_hook(uint32_t h) { + FILE *f = lookup_handle(h); + if (f) { + fclose(f); + free_handle(h); + } +} + +#endif + // We keep one decbuf around and reuse it during transfer @@ -13,10 +101,54 @@ b64_buffer_t decbuf; // 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_global.transfer_flag = 1; + amy_global.transfer_flag = AMY_TRANSFER_TYPE_AUDIO; amy_global.transfer_storage = storage; - amy_global.transfer_length = length; - amy_global.transfer_stored = 0; + amy_global.transfer_length_bytes = length; + amy_global.transfer_stored_bytes = 0; + amy_global.transfer_file_handle = 0; + amy_global.transfer_filename[0] = '\0'; + b64_buf_malloc(&decbuf); +} + +void start_receiving_sample(uint32_t frames, uint8_t bus, int16_t *storage) { + amy_global.transfer_flag = AMY_TRANSFER_TYPE_SAMPLE; + amy_global.transfer_storage = (uint8_t *)storage; + amy_global.transfer_length_bytes = frames*sizeof(int16_t)*AMY_NCHANS; + amy_global.transfer_stored_bytes = 0; + amy_global.transfer_file_handle = bus; // use file handle to store bus number + amy_global.transfer_filename[0] = '\0'; +} + +void stop_receiving_sample() { + amy_global.transfer_file_handle = 0; + amy_global.transfer_flag = AMY_TRANSFER_TYPE_NONE; + amy_global.transfer_stored_bytes = 0; + amy_global.transfer_length_bytes = 0; +} + +// signals to AMY that i'm now receiving a file transfer of length (bytes!) to filename +void start_receiving_file_transfer(uint32_t length, const char *filename) { + + if (filename == NULL || filename[0] == '\0') { + return; + } + if (amy_external_fopen_hook == NULL || amy_external_fwrite_hook == NULL || amy_external_fclose_hook == NULL) { + + fprintf(stderr, "file transfer hooks not enabled on platform\n"); + return; + } + uint32_t handle = amy_external_fopen_hook((char *)filename, "wb"); + if (handle == HANDLE_INVALID) { + fprintf(stderr, "could not open file for transfer: %s\n", filename); + return; + } + amy_global.transfer_flag = AMY_TRANSFER_TYPE_FILE; + amy_global.transfer_storage = NULL; + amy_global.transfer_length_bytes = length; + amy_global.transfer_stored_bytes = 0; + amy_global.transfer_file_handle = handle; + strncpy(amy_global.transfer_filename, filename, sizeof(amy_global.transfer_filename) - 1); + amy_global.transfer_filename[sizeof(amy_global.transfer_filename) - 1] = '\0'; b64_buf_malloc(&decbuf); } @@ -24,11 +156,28 @@ void start_receiving_transfer(uint32_t length, uint8_t * storage) { void parse_transfer_message(char * message, uint16_t len) { size_t decoded = 0; uint8_t * block = b64_decode_ex (message, len, &decbuf, &decoded); - for(uint16_t i=0;i=amy_global.transfer_length) { // we're done - amy_global.transfer_flag = 0; + if (amy_global.transfer_stored_bytes >= amy_global.transfer_length_bytes) { // we're done + if (amy_global.transfer_flag == AMY_TRANSFER_TYPE_FILE) { + if (amy_external_fclose_hook != NULL && amy_global.transfer_file_handle != HANDLE_INVALID) { + amy_external_fclose_hook(amy_global.transfer_file_handle); + } + if (amy_external_file_transfer_done_hook != NULL) { + amy_external_file_transfer_done_hook(amy_global.transfer_filename); + } + amy_global.transfer_file_handle = 0; + amy_global.transfer_filename[0] = '\0'; + } + amy_global.transfer_flag = AMY_TRANSFER_TYPE_NONE; free(decbuf.ptr); } } @@ -36,7 +185,7 @@ void parse_transfer_message(char * message, uint16_t len) { int b64_buf_malloc(b64_buffer_t * buf) { buf->ptr = malloc(B64_BUFFER_SIZE); - if(!buf->ptr) return -1; + if (!buf->ptr) return -1; buf->bufc = 1; @@ -46,160 +195,314 @@ int b64_buf_malloc(b64_buffer_t * buf) // We don't do encoding in AMY, we rely on python for that, but we'll leave it here if you want it in C // Just like decode you pass it an allocated encbuf. char * b64_encode (const unsigned char *src, b64_buffer_t * encbuf, size_t len) { - int i = 0; - int j = 0; - size_t size = 0; - unsigned char buf[4]; - unsigned char tmp[3]; + int i = 0; + int j = 0; + size_t size = 0; + unsigned char buf[4]; + unsigned char tmp[3]; + + + // 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 + + 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'; + } - // parse until end of source - while (len--) { - // read up to 3 bytes at a time into `tmp' - tmp[i++] = *(src++); + // 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; - // 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; + // perform same write to `encbuf->ptr` with new allocation + for (j = 0; (j < i + 1); ++j) { + encbuf->ptr[size++] = b64_table[buf[j]]; + } - // 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 + // while there is still a remainder + // append `=' to `encbuf.ptr' + while ((i++ < 3)) { + encbuf->ptr[size++] = '='; + } + } - for (i = 0; i < 4; ++i) { - encbuf->ptr[size++] = b64_table[buf[i]]; - } + // Make sure we have enough space to add '\0' character at end. + encbuf->ptr[size] = '\0'; - // reset index - i = 0; - } - } + return encbuf->ptr; +} - // remainder - if (i > 0) { - // fill `tmp' with `\0' at most 3 times - for (j = i; j < 3; ++j) { - tmp[j] = '\0'; + + +unsigned char * +b64_decode_ex (const char *src, size_t len, b64_buffer_t * decbuf, size_t *decsize) { + int i = 0; + int j = 0; + int l = 0; + size_t size = 0; + 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((unsigned char)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; + } } - // 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; + // 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]; - // perform same write to `encbuf->ptr` with new allocation - for (j = 0; (j < i + 1); ++j) { - encbuf->ptr[size++] = b64_table[buf[j]]; + // 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]; + } } - // while there is still a remainder - // append `=' to `encbuf.ptr' - while ((i++ < 3)) { - encbuf->ptr[size++] = '='; + // 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; } - } - // Make sure we have enough space to add '\0' character at end. - encbuf->ptr[size] = '\0'; + return (unsigned char*) decbuf->ptr; +} + - return encbuf->ptr; +void transfer_init() { +#if defined(_POSIX_VERSION) + amy_external_fopen_hook = posix_external_fopen_hook; + amy_external_fread_hook = posix_external_fread_hook; + amy_external_fwrite_hook = posix_external_fwrite_hook; + amy_external_fclose_hook = posix_external_fclose_hook; +#endif } +static uint16_t read_u16_le(const uint8_t *buf) { + return (uint16_t)buf[0] | ((uint16_t)buf[1] << 8); +} +static uint32_t read_u32_le(const uint8_t *buf) { + return (uint32_t)buf[0] | ((uint32_t)buf[1] << 8) | + ((uint32_t)buf[2] << 16) | ((uint32_t)buf[3] << 24); +} -unsigned char * -b64_decode_ex (const char *src, size_t len, b64_buffer_t * decbuf, size_t *decsize) { - int i = 0; - int j = 0; - int l = 0; - size_t size = 0; - 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((unsigned char)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 +static int read_exact(uint32_t handle, uint8_t *buf, uint32_t len) { + if (amy_external_fread_hook == NULL) { + return 0; + } + uint32_t offset = 0; + while (offset < len) { + uint32_t got = amy_external_fread_hook(handle, buf + offset, len - offset); + if (got == 0) { + return 0; + } + offset += got; + } + return 1; +} + +static int skip_bytes(uint32_t handle, uint32_t len) { + uint8_t scratch[64]; + while (len > 0) { + uint32_t chunk = len > sizeof(scratch) ? sizeof(scratch) : len; + if (!read_exact(handle, scratch, chunk)) { + return 0; + } + len -= chunk; + } + return 1; +} + +int wave_parse_header(uint32_t handle, wave_info_t *info, uint32_t *data_bytes) { + if (info == NULL || data_bytes == NULL) { + return 0; + } + uint8_t riff_header[12]; + if (!read_exact(handle, riff_header, sizeof(riff_header))) { + return 0; + } + if (memcmp(riff_header, "RIFF", 4) != 0 || memcmp(riff_header + 8, "WAVE", 4) != 0) { + return 0; + } + uint8_t fmt_found = 0; + wave_info_t tmp_info = {0}; + while (1) { + uint8_t chunk_header[8]; + if (!read_exact(handle, chunk_header, sizeof(chunk_header))) { + return 0; + } + uint32_t chunk_size = read_u32_le(chunk_header + 4); + if (memcmp(chunk_header, "fmt ", 4) == 0) { + if (chunk_size < 16) { + return 0; + } + uint8_t fmt_chunk[16]; + if (!read_exact(handle, fmt_chunk, sizeof(fmt_chunk))) { + return 0; + } + uint16_t audio_format = read_u16_le(fmt_chunk); + tmp_info.channels = read_u16_le(fmt_chunk + 2); + tmp_info.sample_rate = read_u32_le(fmt_chunk + 4); + if (audio_format != 1 || tmp_info.channels == 0 || tmp_info.channels > 2) { + return 0; + } + if (!skip_bytes(handle, chunk_size - sizeof(fmt_chunk))) { + return 0; + } + if (chunk_size & 1) { + if (!skip_bytes(handle, 1)) { + return 0; + } + } + fmt_found = 1; + continue; + } + if (memcmp(chunk_header, "data", 4) == 0) { + if (!fmt_found) { + return 0; + } + *info = tmp_info; + *data_bytes = chunk_size; + return 1; + } + if (!skip_bytes(handle, chunk_size)) { + return 0; + } + if (chunk_size & 1) { + if (!skip_bytes(handle, 1)) { + return 0; + } + } + } + return 0; +} +uint32_t wave_read_pcm_frames_s16(uint32_t handle, uint16_t channels, + uint32_t *bytes_remaining, int16_t *dest, uint32_t max_frames) { + if (dest == NULL || bytes_remaining == 0 || channels == 0 || channels > 2) { + return 0; + } + uint32_t bytes_per_frame = channels * 2; + if (*bytes_remaining < bytes_per_frame) { + return 0; + } + uint32_t max_bytes = max_frames * bytes_per_frame; + if (max_bytes > *bytes_remaining) { + max_bytes = *bytes_remaining - (*bytes_remaining % bytes_per_frame); + } + uint32_t frames_to_read = max_bytes / bytes_per_frame; + if (frames_to_read == 0) { + return 0; + } + uint8_t raw_buf[(AMY_BLOCK_SIZE * PCM_FILE_BUFFER_MULT + 1) * 4]; + if (max_bytes > sizeof(raw_buf)) { + max_bytes = sizeof(raw_buf) - (sizeof(raw_buf) % bytes_per_frame); + frames_to_read = max_bytes / bytes_per_frame; + } + if (amy_external_fread_hook == NULL) { + return 0; + } + uint32_t got = amy_external_fread_hook(handle, raw_buf, max_bytes); + got -= got % bytes_per_frame; + if (got == 0) { + return 0; + } + frames_to_read = got / bytes_per_frame; + *bytes_remaining -= got; + if (channels == 1) { + for (uint32_t i = 0; i < frames_to_read; i++) { + dest[i] = (int16_t)read_u16_le(raw_buf + i * 2); + } + } else { + for (uint32_t i = 0; i < frames_to_read; i++) { + dest[i * 2] = (int16_t)read_u16_le(raw_buf + i * 4); + dest[i * 2 + 1] = (int16_t)read_u16_le(raw_buf + i * 4 + 2); + } + } + return frames_to_read; +} diff --git a/src/transfer.h b/src/transfer.h index f671f168..96e9c3fd 100644 --- a/src/transfer.h +++ b/src/transfer.h @@ -12,11 +12,26 @@ #include #include "amy.h" + +#define MAX_OPEN_FILES 64 +#define HANDLE_INVALID 0 + typedef struct b64_buffer { char * ptr; int bufc; } b64_buffer_t; +void transfer_init(); + +typedef struct { + uint16_t channels; + uint32_t sample_rate; +} wave_info_t; + +int wave_parse_header(uint32_t handle, wave_info_t *info, uint32_t *data_bytes); +uint32_t wave_read_pcm_frames_s16(uint32_t handle, uint16_t channels, + uint32_t *bytes_remaining, int16_t *dest, uint32_t max_frames); + // How much memory to allocate per buffer #define B64_BUFFER_SIZE (256) @@ -40,7 +55,12 @@ static const char b64_table[] = { }; void start_receiving_transfer(uint32_t length, uint8_t * storage); +void start_receiving_file_transfer(uint32_t length, const char *filename); void parse_transfer_message(char * message, uint16_t len) ; +void start_receiving_sample(uint32_t frames, uint8_t bus, int16_t *storage); +void stop_receiving_sample(); + + /** * Encode `unsigned char *' source with `size_t' size. @@ -57,4 +77,4 @@ char * b64_encode (const unsigned char *src, b64_buffer_t * encbuf, size_t len); unsigned char * b64_decode_ex (const char *, size_t, b64_buffer_t * decbuf, size_t *); -#endif \ No newline at end of file +#endif diff --git a/tests/ref/TestDiskSample.wav b/tests/ref/TestDiskSample.wav new file mode 100644 index 00000000..0a4906e8 Binary files /dev/null and b/tests/ref/TestDiskSample.wav differ diff --git a/tests/ref/TestDiskSampleStereo.wav b/tests/ref/TestDiskSampleStereo.wav new file mode 100644 index 00000000..c44f2468 Binary files /dev/null and b/tests/ref/TestDiskSampleStereo.wav differ diff --git a/tests/ref/TestRestartFileSample.wav b/tests/ref/TestRestartFileSample.wav new file mode 100644 index 00000000..39460c86 Binary files /dev/null and b/tests/ref/TestRestartFileSample.wav differ diff --git a/tests/ref/TestSample.wav b/tests/ref/TestSample.wav new file mode 100644 index 00000000..097879ef Binary files /dev/null and b/tests/ref/TestSample.wav differ