From 72de3069d1ceb26d22da7456ef5530a51b75e9de Mon Sep 17 00:00:00 2001 From: riban Date: Tue, 20 Feb 2024 07:39:45 +0000 Subject: [PATCH 1/2] Audio Player enhancements and fixes Adds play-all and toggle-play modes. Soft mute when manually stopping (adds one process cycle, e.g. 11ms delay to stop). Fix failure to detect stop (continually playing dc). --- zyngine/zynthian_engine_audioplayer.py | 4 +- zynlibs/zynaudioplayer/audio_player.h | 2 +- zynlibs/zynaudioplayer/player.cpp | 95 +++++++++++++++++--------- zynlibs/zynaudioplayer/player.h | 4 +- 4 files changed, 67 insertions(+), 38 deletions(-) diff --git a/zyngine/zynthian_engine_audioplayer.py b/zyngine/zynthian_engine_audioplayer.py index 6666d7c8d..a3277fe90 100644 --- a/zyngine/zynthian_engine_audioplayer.py +++ b/zyngine/zynthian_engine_audioplayer.py @@ -301,7 +301,7 @@ def set_preset(self, processor, preset, preload=False): self._ctrls = [ ['gain', None, gain, 2.0], ['record', None, record, ['stopped', 'recording']], - ['loop', None, loop, ['one-shot', 'looping']], + ['loop', None, loop, ['one-shot', 'looping', 'play-all', 'toggle']], ['transport', None, transport, ['stopped', 'playing']], ['position', None, 0.0, dur], ['left track', None, default_a, [track_labels, track_values]], @@ -421,7 +421,7 @@ def control_cb(self, handle, id, value): elif id == 3: ctrl_dict['gain'].set_value(value, False) elif id == 4: - ctrl_dict['loop'].set_value(int(value) * 64, False) + ctrl_dict['loop'].set_value(int(value), False) elif id == 5: ctrl_dict['left track'].set_value(int(value), False) elif id == 6: diff --git a/zynlibs/zynaudioplayer/audio_player.h b/zynlibs/zynaudioplayer/audio_player.h index 58bd5420f..dafb881cd 100644 --- a/zynlibs/zynaudioplayer/audio_player.h +++ b/zynlibs/zynaudioplayer/audio_player.h @@ -64,7 +64,7 @@ class AUDIO_PLAYER { uint8_t play_state = STOPPED; // Current playback state (STOPPED|STARTING|PLAYING|STOPPING) sf_count_t file_read_pos = 0; // Current file read position (frames) - uint8_t loop = 0; // 1 to loop at end of song + uint8_t loop = 0; // 1 to loop at end of song, 2 to play once but ignore note-off bool looped = false; // True if started playing a loop (not first time) sf_count_t loop_start = 0; // Start of loop in frames from start of file sf_count_t loop_start_src = -1; // Start of loop in frames from start after SRC diff --git a/zynlibs/zynaudioplayer/player.cpp b/zynlibs/zynaudioplayer/player.cpp index f01da3de5..9f20f3805 100644 --- a/zynlibs/zynaudioplayer/player.cpp +++ b/zynlibs/zynaudioplayer/player.cpp @@ -387,7 +387,7 @@ void* file_thread_fn(void * param) { bool bReverse = (pPlayer->varispeed < 0.0); if(bReverse) { - if(pPlayer->loop) { + if(pPlayer->loop == 1) { // Limit read to loop range if(pPlayer->file_read_pos <= pPlayer->loop_start) nMaxFrames = 0; @@ -398,7 +398,7 @@ void* file_thread_fn(void * param) { nMaxFrames = pPlayer->file_read_pos - pPlayer->crop_start; } } else { - if(pPlayer->loop) { + if(pPlayer->loop == 1) { // Limit read to loop range if(pPlayer->file_read_pos >= pPlayer->loop_end) nMaxFrames = 0; @@ -437,7 +437,7 @@ void* file_thread_fn(void * param) { } } else - pPlayer->file_read_pos += nFramesRead = sf_readf_float(pFile, pBufferOut, nMaxFrames); + pPlayer->file_read_pos += (nFramesRead = sf_readf_float(pFile, pBufferOut, nMaxFrames)); } else { // Populate SRC input buffer before SRC process if(bReverse) { @@ -459,7 +459,7 @@ void* file_thread_fn(void * param) { } } else - pPlayer->file_read_pos += nFramesRead = sf_readf_float(pFile, pBufferIn + nUnusedFrames * pPlayer->sf_info.channels, nMaxFrames); + pPlayer->file_read_pos += (nFramesRead = sf_readf_float(pFile, pBufferIn + nUnusedFrames * pPlayer->sf_info.channels, nMaxFrames)); } getMutex(); @@ -473,15 +473,15 @@ void* file_thread_fn(void * param) { // We need to perform SRC on this block of code srcData.input_frames = nFramesRead; int rc = src_process(pSrcState, &srcData); - nUnusedFrames = nFramesRead - srcData.input_frames_used; - nFramesRead = srcData.output_frames_gen; if(rc) { DPRINTF("SRC failed with error %d, %lu frames generated\n", nFramesRead, srcData.output_frames_gen); } else { DPRINTF("SRC suceeded - %lu frames generated, %lu frames used, %lu frames unused\n", srcData.output_frames_gen, srcData.input_frames_used, nUnusedFrames); + nUnusedFrames = nFramesRead - srcData.input_frames_used; + nFramesRead = srcData.output_frames_gen; + // Shift unused samples to start of buffer + memcpy(pBufferIn, pBufferIn + srcData.input_frames_used * sizeof(float) * pPlayer->sf_info.channels, nUnusedFrames * sizeof(float) * pPlayer->sf_info.channels); } - // Shift unused samples to start of buffer - memcpy(pBufferIn, pBufferIn + srcData.input_frames_used * sizeof(float) * pPlayer->sf_info.channels, nUnusedFrames * sizeof(float) * pPlayer->sf_info.channels); } else { //DPRINTF("No SRC, read %u frames\n", nFramesRead); } @@ -518,7 +518,7 @@ void* file_thread_fn(void * param) { break; } } - } else if(pPlayer->loop) { + } else if(pPlayer->loop == 1) { // Short read - looping so fill from loop start point in file pPlayer->file_read_status = LOOPING; //srcData.end_of_input = 1; @@ -739,12 +739,12 @@ float get_position(AUDIO_PLAYER * pPlayer) { return 0.0; } -void enable_loop(AUDIO_PLAYER * pPlayer, uint8_t bLoop) { +void enable_loop(AUDIO_PLAYER * pPlayer, uint8_t nLoop) { if(!pPlayer) return; getMutex(); - pPlayer->loop = bLoop; - if(bLoop && pPlayer->play_pos_frames > pPlayer->loop_end_src) + pPlayer->loop = nLoop; + if(nLoop && pPlayer->play_pos_frames > pPlayer->loop_end_src) pPlayer->play_pos_frames = pPlayer->loop_start_src; pPlayer->file_read_status = SEEKING; releaseMutex(); @@ -762,7 +762,7 @@ void set_loop_start_time(AUDIO_PLAYER * pPlayer, float time) { getMutex(); pPlayer->loop_start = frames; pPlayer->loop_start_src = pPlayer->loop_start * pPlayer->src_ratio; - if(pPlayer->loop && pPlayer->looped) + if(pPlayer->loop == 1 && pPlayer->looped) pPlayer->file_read_status = SEEKING; releaseMutex(); pPlayer->last_loop_start = -1; @@ -786,7 +786,7 @@ void set_loop_end_time(AUDIO_PLAYER * pPlayer, float time) { getMutex(); pPlayer->loop_end = frames; pPlayer->loop_end_src = pPlayer->loop_end * pPlayer->src_ratio; - if(pPlayer->loop && pPlayer->looped) + if(pPlayer->loop == 1 && pPlayer->looped) pPlayer->file_read_status = SEEKING; releaseMutex(); pPlayer->last_loop_end = -1; @@ -1221,8 +1221,9 @@ int on_jack_process(jack_nframes_t nFrames, void * arg) { float* output_buffers[] = {pOutA, pOutB}; bool bReverse = pPlayer->varispeed < 0.0; - if(pPlayer->play_state == STARTING && pPlayer->file_read_status != SEEKING) + if(pPlayer->play_state == STARTING && pPlayer->file_read_status != SEEKING) { pPlayer->play_state = PLAYING; + } if(pPlayer->play_state == PLAYING || pPlayer->play_state == STOPPING) { if (pPlayer->time_ratio_dirty) { @@ -1237,6 +1238,7 @@ int on_jack_process(jack_nframes_t nFrames, void * arg) { pPlayer->time_ratio_dirty = false; } while(pPlayer->stretcher->available() < nFrames) { + // Process data from fifo until sufficient to populate this frame (first attempt may give -1 but that's okay as we will repeat) size_t sampsReq = min((size_t)256, pPlayer->stretcher->getSamplesRequired()); size_t nBytes = min(jack_ringbuffer_read_space(pPlayer->ringbuffer_a), jack_ringbuffer_read_space(pPlayer->ringbuffer_b)); nBytes = min(nBytes, sampsReq * sizeof(float)); @@ -1247,11 +1249,11 @@ int on_jack_process(jack_nframes_t nFrames, void * arg) { // stretch pPlayer->stretcher->process(stretch_input_buffers, nRead / sizeof(float), nRead != nBytes); if(nRead == 0) - break; + break; // fifo buffers run dry } a_count = min(pPlayer->stretcher->available(), (int)nFrames); if(a_count < 0) - a_count = 0; + a_count = 0; // If stretcher gives fault it will respond with -1 a_count = pPlayer->stretcher->retrieve(output_buffers, a_count); if(pPlayer->held_note != pPlayer->env_gate) set_env_gate(pPlayer, pPlayer->held_note); @@ -1269,6 +1271,7 @@ int on_jack_process(jack_nframes_t nFrames, void * arg) { pOutB[offset] *= pPlayer->gain; } } + // Advance play position based on the raw (SRC'd) frames if(bReverse) pPlayer->play_pos_frames -= r_count; else @@ -1280,17 +1283,18 @@ int on_jack_process(jack_nframes_t nFrames, void * arg) { if(cue_point_play > cue && pPlayer->play_pos_frames > pPlayer->cue_points[cue].offset) { pPlayer->play_pos_frames = pPlayer->cue_points[cue - 1].offset; pPlayer->env_state = ENV_RELEASE; //!@todo This looks wrong - if(pPlayer->loop) + if(pPlayer->loop == 1) pPlayer->file_read_status = SEEKING; - else + else { pPlayer->play_state = STOPPING; + } } else if(a_count < nFrames && pPlayer->file_read_status == IDLE) { // Reached end of file pPlayer->play_pos_frames = pPlayer->crop_start_src; pPlayer->play_state = STOPPING; } } else { - if(pPlayer->loop) { + if(pPlayer->loop == 1) { if(bReverse) { if(pPlayer->play_pos_frames <= pPlayer->loop_start_src) { size_t i = pPlayer->loop_start_src - pPlayer->play_pos_frames; @@ -1304,38 +1308,46 @@ int on_jack_process(jack_nframes_t nFrames, void * arg) { } } } else if(a_count < nFrames && pPlayer->file_read_status == IDLE) { - // Reached end of file + // No more data from file reader, e.g. reached end of file if(bReverse) pPlayer->play_pos_frames = pPlayer->crop_end_src; else pPlayer->play_pos_frames = pPlayer->crop_start_src; pPlayer->play_state = STOPPING; + pPlayer->env_state = ENV_IDLE; + DPRINTF("libzynaudioplayer: Short read (%lu) and IDLE so STOPPING\n", a_count); } } } if(pPlayer->env_state == ENV_END) pPlayer->env_state = ENV_IDLE; - if(pPlayer->play_state == STOPPING && pPlayer->env_state == ENV_IDLE) { - // Soft mute (not perfect for short last period of file but better than nowt) - + if(pPlayer->play_state == STOPPING) { + // Soft mute (not perfect for short last period of file but better than nowt). Adds a few ms of delay. for(size_t offset = 0; offset < a_count; ++offset) { pOutA[offset] *= 1.0 - ((jack_default_audio_sample_t)offset / a_count); pOutB[offset] *= 1.0 - ((jack_default_audio_sample_t)offset / a_count); } - - pPlayer->play_state = STOPPED; - pPlayer->varispeed = 0.0; - pPlayer->file_read_status = SEEKING; - //DPRINTF("libzynaudioplayer: Stopped. Used %u frames from %u in buffer to soft mute (fade). Silencing remaining %u frames (%u bytes)\n", a_count, nFrames, nFrames - a_count, (nFrames - a_count) * sizeof(jack_default_audio_sample_t)); + if(pPlayer->env_state == ENV_IDLE) { + pPlayer->play_state = STOPPED; + pPlayer->varispeed = 0.0; + pPlayer->file_read_status = SEEKING; + + // Reset MIDI triggers, e.g. held notes that are no longer valid + for(uint8_t i = 0; i < 128; ++i) + pPlayer->held_notes[i] = 0; + pPlayer->held_note = 0; + } + + DPRINTF("libzynaudioplayer: Stopped. Used %u frames from %u in buffer to soft mute (fade). Silencing remaining %u frames (%u bytes)\n", a_count, nFrames, nFrames - a_count, (nFrames - a_count) * sizeof(jack_default_audio_sample_t)); } // Silence remainder of frame memset(pOutA + a_count, 0, (nFrames - a_count) * sizeof(jack_default_audio_sample_t)); memset(pOutB + a_count, 0, (nFrames - a_count) * sizeof(jack_default_audio_sample_t)); if(pPlayer->env_state != ENV_IDLE) - for(int i = 0; i < nFrames-a_count; ++i) + for(int i = 0; i < nFrames - a_count; ++i) process_env(pPlayer); } @@ -1357,6 +1369,8 @@ int on_jack_process(jack_nframes_t nFrames, void * arg) { // Note off pPlayer->held_notes[midiEvent.buffer[1]] = 0; if(pPlayer->last_note_played == midiEvent.buffer[1]) { + if(pPlayer->loop == 3) + continue; //!@todo This is bluntly ignoring note-off but maybe we want to include envelope pPlayer->held_note = pPlayer->sustain; for (uint8_t i = 0; i < 128; ++i) { if(pPlayer->held_notes[i]) { @@ -1381,7 +1395,7 @@ int on_jack_process(jack_nframes_t nFrames, void * arg) { } if(pPlayer->held_note) continue; - if(pPlayer->sustain == 0) { + if(pPlayer->loop < 2 && pPlayer->sustain == 0) { stop_playback(pPlayer); } } @@ -1402,8 +1416,23 @@ int on_jack_process(jack_nframes_t nFrames, void * arg) { pPlayer->play_state = STARTING; } pPlayer->last_note_played = midiEvent.buffer[1]; - pPlayer->held_notes[pPlayer->last_note_played] = 1; - pPlayer->held_note = 1; + if(pPlayer->loop == 3) { + if(pPlayer->held_note) { + pPlayer->held_notes[pPlayer->last_note_played] = 0; + pPlayer->held_note = 0; + stop_playback(pPlayer); + DPRINTF("TOGGLE OFF\n"); + } else { + pPlayer->held_notes[pPlayer->last_note_played] = 1; + pPlayer->held_note = 1; + DPRINTF("TOGGLE ON\n"); + } + continue; + } + else { + pPlayer->held_notes[pPlayer->last_note_played] = 1; + pPlayer->held_note = 1; + } pPlayer->stretcher->reset(); pPlayer->varispeed = pPlayer->play_varispeed; if(!cue_point_play){ diff --git a/zynlibs/zynaudioplayer/player.h b/zynlibs/zynaudioplayer/player.h index 164832cc9..400d3de16 100644 --- a/zynlibs/zynaudioplayer/player.h +++ b/zynlibs/zynaudioplayer/player.h @@ -152,9 +152,9 @@ float get_position(AUDIO_PLAYER * pPlayer); /** @brief Set loop mode * @param player_handle Handle of player provided by init_player() -* @param bLoop True to loop at end of audio +* @param nLoop 1 to loop at end of audio, 2 to play to end (ignore MIDI note-off) */ -void enable_loop(AUDIO_PLAYER * pPlayer, uint8_t bLoop); +void enable_loop(AUDIO_PLAYER * pPlayer, uint8_t nLoop); /* @brief Get loop mode * @param player_handle Handle of player provided by init_player() From 26370a82d0b0fc69a2df5c208556449f4889f5df Mon Sep 17 00:00:00 2001 From: riban Date: Tue, 20 Feb 2024 11:16:40 +0000 Subject: [PATCH 2/2] Add global MIDI transpose. Fixes zynthian/zynthian-issue-tracker#935 --- zyngine/zynthian_engine_audioplayer.py | 1 + zyngui/zynthian_gui_admin.py | 16 ++++++++++++++++ zyngui/zynthian_gui_selector.py | 2 ++ 3 files changed, 19 insertions(+) diff --git a/zyngine/zynthian_engine_audioplayer.py b/zyngine/zynthian_engine_audioplayer.py index a3277fe90..1110b0231 100644 --- a/zyngine/zynthian_engine_audioplayer.py +++ b/zyngine/zynthian_engine_audioplayer.py @@ -83,6 +83,7 @@ def __init__(self, state_manager=None, jackname=None): def start(self): self.player = zynaudioplayer.zynaudioplayer(self.jackname) + logging.warning(f"{self.player}") self.jackname = self.player.get_jack_client_name() self.file_exts = self.get_file_exts() zynsigman.register(zynsigman.S_AUDIO_RECORDER, zynthian_audio_recorder.SS_AUDIO_RECORDER_STATE, self.update_rec) diff --git a/zyngui/zynthian_gui_admin.py b/zyngui/zynthian_gui_admin.py index ca9624af2..8f46dd350 100644 --- a/zyngui/zynthian_gui_admin.py +++ b/zyngui/zynthian_gui_admin.py @@ -109,6 +109,12 @@ def fill_list(self): else: self.list_data.append((self.toggle_midi_sys, 0, "\u2610 MIDI System Messages")) + transpose = lib_zyncore.get_global_transpose() + if transpose > 0: + self.list_data.append((self.global_transpose, transpose, f"[+{transpose}] Global Transpose")) + else: + self.list_data.append((self.global_transpose, transpose, f"[{transpose}] Global Transpose")) + self.list_data.append((None, 0, "> AUDIO")) if self.state_manager.allow_rbpi_headphones(): @@ -179,6 +185,7 @@ def select_action(self, i, t='S'): def set_select_path(self): self.select_path.set("Admin") + self.set_title("Admin") #TODO: Should not need to set title and select_path! def execute_commands(self): self.state_manager.start_busy("admin_commands") @@ -351,6 +358,15 @@ def toggle_midi_sys(self): lib_zyncore.set_midi_filter_system_events(zynthian_gui_config.midi_sys_enabled) self.fill_list() + def global_transpose(self): + self.enable_param_editor(self, "Global Transpose", {'value_min':-24, 'value_max':24, 'value':lib_zyncore.get_global_transpose()}) + + def send_controller_value(self, zctrl): + """ Handle param editor""" + if zctrl.symbol == "Global Transpose": + lib_zyncore.set_global_transpose(zctrl.value) + self.fill_list() + def toggle_usbmidi_by_port(self): if os.environ.get("ZYNTHIAN_USB_MIDI_BY_PORT", "0") == "1": os.environ["ZYNTHIAN_USB_MIDI_BY_PORT"] = "0" diff --git a/zyngui/zynthian_gui_selector.py b/zyngui/zynthian_gui_selector.py index af467ebdc..ec60ed6db 100644 --- a/zyngui/zynthian_gui_selector.py +++ b/zyngui/zynthian_gui_selector.py @@ -349,6 +349,8 @@ def select_action(self, index, t='S'): #-------------------------------------------------------------------------- def zynpot_cb(self, i, dval): + if super().zynpot_cb(i, dval): + return if self.shown and self.zselector and self.zselector.index == i: self.zselector.zynpot_cb(dval) if self.index != self.zselector.zctrl.value: