2022-04-03 23:38:06 +00:00
/*
* Copyright ( C ) 2016 Jared Boone , ShareBrained Technology , Inc .
* Copyright ( C ) 2016 Furrtek
* Copyleft ( ↄ ) 2022 NotPike
2023-06-17 20:01:46 +00:00
* Copyright ( C ) 2023 Kyle Reed , zxkmm
2022-04-03 23:38:06 +00:00
*
* This file is part of PortaPack .
*
* This program is free software ; you can redistribute it and / or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation ; either version 2 , or ( at your option )
* any later version .
*
* This program is distributed in the hope that it will be useful ,
* but WITHOUT ANY WARRANTY ; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE . See the
* GNU General Public License for more details .
*
* You should have received a copy of the GNU General Public License
* along with this program ; see the file COPYING . If not , write to
* the Free Software Foundation , Inc . , 51 Franklin Street ,
* Boston , MA 02110 - 1301 , USA .
*/
# include "ui_playlist.hpp"
2023-06-25 18:40:19 +00:00
2023-08-02 19:59:26 +00:00
# include "baseband_api.hpp"
2023-06-15 20:15:26 +00:00
# include "convert.hpp"
# include "file_reader.hpp"
2023-06-25 18:40:19 +00:00
# include "io_file.hpp"
2023-07-22 07:20:56 +00:00
# include "io_convert.hpp"
2023-08-02 19:59:26 +00:00
# include "oversample.hpp"
# include "portapack.hpp"
# include "portapack_persistent_memory.hpp"
2022-04-03 23:38:06 +00:00
# include "string_format.hpp"
# include "ui_fileman.hpp"
2023-06-17 20:01:46 +00:00
# include "utility.hpp"
2022-04-03 23:38:06 +00:00
# include <unistd.h>
2023-05-23 04:39:22 +00:00
# include <fstream>
2022-04-03 23:38:06 +00:00
using namespace portapack ;
2023-06-17 20:01:46 +00:00
namespace fs = std : : filesystem ;
2022-04-03 23:38:06 +00:00
2023-06-26 17:44:01 +00:00
/* TODO
* - Should frequency overrides be saved in the playlist ?
2023-06-28 17:02:06 +00:00
* - Start playing from current track ( vs start over ) ?
2023-06-26 17:44:01 +00:00
*/
2023-06-17 20:01:46 +00:00
namespace ui {
2022-04-03 23:38:06 +00:00
2023-06-28 17:02:06 +00:00
// TODO: consolidate extesions into a shared header?
static const fs : : path ppl_ext = u " .PPL " ;
2023-06-17 20:01:46 +00:00
void PlaylistView : : load_file ( const fs : : path & playlist_path ) {
2023-05-18 20:16:05 +00:00
File playlist_file ;
auto error = playlist_file . open ( playlist_path . string ( ) ) ;
2023-06-15 20:15:26 +00:00
2023-06-17 20:01:46 +00:00
if ( error )
2023-05-18 20:16:05 +00:00
return ;
2023-06-17 20:01:46 +00:00
auto reader = FileLineReader ( playlist_file ) ;
for ( const auto & line : reader ) {
if ( line . length ( ) = = 0 | | line [ 0 ] = = ' # ' )
continue ; // Empty or comment line.
2023-06-15 20:15:26 +00:00
2023-06-17 20:01:46 +00:00
auto cols = split_string ( line , ' , ' ) ;
2023-06-25 18:40:19 +00:00
auto entry = load_entry ( trim ( cols [ 0 ] ) ) ;
2023-05-18 20:16:05 +00:00
2023-06-25 18:40:19 +00:00
if ( ! entry )
continue ;
2023-05-18 20:16:05 +00:00
2023-06-25 18:40:19 +00:00
// Read optional delay value.
if ( cols . size ( ) > 1 )
parse_int ( cols [ 1 ] , entry - > ms_delay ) ;
2023-05-18 20:16:05 +00:00
2023-06-25 18:40:19 +00:00
playlist_db_ . emplace_back ( * std : : move ( entry ) ) ;
}
}
2023-06-11 16:22:15 +00:00
2023-06-25 18:40:19 +00:00
Optional < PlaylistView : : playlist_entry > PlaylistView : : load_entry ( fs : : path & & path ) {
File capture_file ;
2023-05-18 20:16:05 +00:00
2023-06-25 18:40:19 +00:00
auto error = capture_file . open ( path ) ;
if ( error )
return { } ;
// Read metafile if it exists.
auto metadata_path = get_metadata_path ( path ) ;
auto metadata = read_metadata_file ( metadata_path ) ;
2023-06-26 17:44:01 +00:00
// If no metadata found, fallback to the TX frequency.
2023-06-25 18:40:19 +00:00
if ( ! metadata )
2023-06-26 17:44:01 +00:00
metadata = { transmitter_model . target_frequency ( ) , 500'000 } ;
2023-06-25 18:40:19 +00:00
return playlist_entry {
std : : move ( path ) ,
* metadata ,
capture_file . size ( ) ,
0u } ;
2022-04-03 23:38:06 +00:00
}
2023-06-17 20:01:46 +00:00
void PlaylistView : : on_file_changed ( const fs : : path & new_file_path ) {
stop ( ) ;
2022-04-03 23:38:06 +00:00
2023-06-25 18:40:19 +00:00
current_index_ = 0 ;
2023-06-17 20:01:46 +00:00
playlist_path_ = new_file_path ;
playlist_db_ . clear ( ) ;
load_file ( playlist_path_ ) ;
update_ui ( ) ;
2023-06-28 19:50:11 +00:00
// TODO: fix in UI framework with 'try_focus()'?
// Hack around focus getting called by ctor before parent is set.
if ( parent ( ) )
button_play . focus ( ) ;
2022-04-03 23:38:06 +00:00
}
2023-06-25 18:40:19 +00:00
void PlaylistView : : open_file ( bool prompt_save ) {
if ( playlist_dirty_ & & prompt_save ) {
nav_ . display_modal (
" Save? " , " Save changes? " , YESNO ,
[ this ] ( bool choice ) {
if ( choice )
save_file ( false ) ;
} ) ;
nav_ . set_on_pop ( [ this ] ( ) { open_file ( false ) ; } ) ;
return ;
}
auto open_view = nav_ . push < FileLoadView > ( " .PPL " ) ;
2023-06-28 17:02:06 +00:00
open_view - > push_dir ( u " PLAYLIST " ) ;
2023-06-25 18:40:19 +00:00
open_view - > on_changed = [ this ] ( fs : : path new_file_path ) {
on_file_changed ( new_file_path ) ;
} ;
}
void PlaylistView : : save_file ( bool show_dialogs ) {
if ( ! playlist_dirty_ | | playlist_path_ . empty ( ) )
return ;
2023-06-26 17:44:01 +00:00
ensure_directory ( playlist_path_ . parent_path ( ) ) ;
2023-06-25 18:40:19 +00:00
File playlist_file ;
auto error = playlist_file . create ( playlist_path_ . string ( ) ) ;
if ( error ) {
if ( show_dialogs )
nav_ . display_modal (
" Save Error " ,
" Could not save file \n " + playlist_path_ . string ( ) ) ;
return ;
}
for ( const auto & entry : playlist_db_ ) {
playlist_file . write_line (
entry . path . string ( ) + " , " +
to_string_dec_uint ( entry . ms_delay ) ) ;
}
playlist_dirty_ = false ;
if ( show_dialogs )
nav_ . display_modal (
" Save " ,
" Saved playlist \n " + playlist_path_ . string ( ) ) ;
}
void PlaylistView : : add_entry ( fs : : path & & path ) {
2023-06-28 19:50:11 +00:00
if ( playlist_path_ . empty ( ) ) {
2023-06-25 18:40:19 +00:00
playlist_path_ = next_filename_matching_pattern ( u " /PLAYLIST/PLAY_????.PPL " ) ;
2023-06-28 19:50:11 +00:00
// Hack around focus getting called by ctor before parent is set.
if ( parent ( ) )
button_play . focus ( ) ;
}
2023-06-25 18:40:19 +00:00
auto entry = load_entry ( std : : move ( path ) ) ;
if ( entry ) {
playlist_db_ . emplace_back ( * std : : move ( entry ) ) ;
current_index_ = playlist_db_ . size ( ) - 1 ;
playlist_dirty_ = true ;
}
update_ui ( ) ;
}
void PlaylistView : : delete_entry ( ) {
if ( playlist_db_ . empty ( ) )
return ;
// Ugh, the STL is gross.
playlist_db_ . erase ( playlist_db_ . begin ( ) + current_index_ ) ;
if ( current_index_ > 0 )
- - current_index_ ;
playlist_dirty_ = true ;
update_ui ( ) ;
2022-04-03 23:38:06 +00:00
}
2023-06-17 20:01:46 +00:00
void PlaylistView : : show_file_error ( const fs : : path & path , const std : : string & message ) {
nav_ . display_modal ( " Error " , " Error opening file \n " + path . string ( ) + " \n " + message ) ;
2023-06-11 16:22:15 +00:00
}
2023-06-26 17:44:01 +00:00
PlaylistView : : playlist_entry * PlaylistView : : current ( ) {
2023-06-25 18:40:19 +00:00
return playlist_db_ . empty ( ) ? nullptr : & playlist_db_ [ current_index_ ] ;
2022-04-03 23:38:06 +00:00
}
2023-06-25 18:40:19 +00:00
bool PlaylistView : : is_active ( ) const {
return replay_thread_ ! = nullptr ;
2023-06-17 20:01:46 +00:00
}
2023-06-25 18:40:19 +00:00
bool PlaylistView : : at_end ( ) const {
return current_index_ > = playlist_db_ . size ( ) ;
2022-04-03 23:38:06 +00:00
}
void PlaylistView : : toggle ( ) {
2023-06-17 20:01:46 +00:00
if ( is_active ( ) )
stop ( ) ;
else
start ( ) ;
2022-04-03 23:38:06 +00:00
}
void PlaylistView : : start ( ) {
2023-06-17 20:01:46 +00:00
if ( playlist_db_ . empty ( ) )
return ; // Nothing to do.
2023-05-18 20:16:05 +00:00
2023-06-25 18:40:19 +00:00
current_index_ = 0u ;
send_current_track ( ) ;
2023-06-17 20:01:46 +00:00
}
2023-05-18 20:16:05 +00:00
2023-06-17 20:01:46 +00:00
/* Advance to the next track in the playlist. */
bool PlaylistView : : next_track ( ) {
2023-06-25 18:40:19 +00:00
+ + current_index_ ;
// Reset to the 0 once at the end to prevent current_index_
// from being outside the bounds of playlist_db_ when painting.
if ( at_end ( ) ) {
current_index_ = 0 ;
return check_loop . value ( ) ; // Keep going if looping.
2023-05-18 20:16:05 +00:00
}
2023-06-25 18:40:19 +00:00
2023-06-17 20:01:46 +00:00
return true ;
}
2023-05-18 20:16:05 +00:00
2023-06-17 20:01:46 +00:00
/* Transmits the current_entry_ */
void PlaylistView : : send_current_track ( ) {
2023-06-19 05:48:29 +00:00
// Prepare to send a file.
2023-06-17 20:01:46 +00:00
replay_thread_ . reset ( ) ;
2023-06-19 05:48:29 +00:00
transmitter_model . disable ( ) ;
2023-06-17 20:01:46 +00:00
ready_signal_ = false ;
2023-06-11 16:22:15 +00:00
2023-06-25 18:40:19 +00:00
if ( ! current ( ) )
2023-06-17 20:01:46 +00:00
return ;
2023-06-25 18:40:19 +00:00
// TODO: Use a timer so the UI isn't frozen.
if ( current ( ) - > ms_delay > 0 )
chThdSleepMilliseconds ( current ( ) - > ms_delay ) ;
2023-06-17 20:01:46 +00:00
// Open the sample file to send.
2023-07-22 07:20:56 +00:00
auto reader = std : : make_unique < FileConvertReader > ( ) ;
2023-06-25 18:40:19 +00:00
auto error = reader - > open ( current ( ) - > path ) ;
2023-06-17 20:01:46 +00:00
if ( error ) {
2023-06-25 18:40:19 +00:00
show_file_error ( current ( ) - > path , " Can't open file to send. " ) ;
2023-06-17 20:01:46 +00:00
return ;
2023-05-18 20:16:05 +00:00
}
2023-08-02 19:59:26 +00:00
// Update the sample rate in proc_replay baseband.
baseband : : set_sample_rate ( current ( ) - > metadata . sample_rate ,
get_oversample_rate ( current ( ) - > metadata . sample_rate ) ) ;
// ReplayThread starts immediately on construction; must be set before creating.
2023-06-25 18:40:19 +00:00
transmitter_model . set_target_frequency ( current ( ) - > metadata . center_frequency ) ;
2023-08-02 19:59:26 +00:00
transmitter_model . set_sampling_rate ( get_actual_sample_rate ( current ( ) - > metadata . sample_rate ) ) ;
2023-10-08 15:50:31 +00:00
transmitter_model . set_baseband_bandwidth ( current ( ) - > metadata . sample_rate < = 500'000 ? 1'750'000 : 2'500'000 ) ; // TX LPF min 1M75 for SR <=500K, and 2M5 (by experimental test) for SR >500K
2023-05-24 04:22:30 +00:00
transmitter_model . enable ( ) ;
2023-06-17 20:01:46 +00:00
2023-06-25 18:40:19 +00:00
// Reset the transmit progress bar.
progressbar_transmit . set_value ( 0 ) ;
2023-06-17 20:01:46 +00:00
// Use the ReplayThread class to send the data.
replay_thread_ = std : : make_unique < ReplayThread > (
std : : move ( reader ) ,
/* read_size */ 0x4000 ,
/* buffer_count */ 3 ,
& ready_signal_ ,
[ ] ( uint32_t return_code ) {
ReplayThreadDoneMessage message { return_code } ;
EventDispatcher : : send_message ( message ) ;
} ) ;
// Now it's sending, update the UI.
update_ui ( ) ;
}
void PlaylistView : : stop ( ) {
// This terminates the underlying chThread.
replay_thread_ . reset ( ) ;
transmitter_model . disable ( ) ;
2023-06-25 18:40:19 +00:00
// Reset the transmit progress bar.
progressbar_transmit . set_value ( 0 ) ;
2023-06-17 20:01:46 +00:00
update_ui ( ) ;
2023-05-18 20:16:05 +00:00
}
2022-04-03 23:38:06 +00:00
2023-06-17 20:01:46 +00:00
void PlaylistView : : update_ui ( ) {
if ( playlist_db_ . empty ( ) ) {
2023-06-26 17:44:01 +00:00
text_filename . set ( " - " ) ;
2023-06-25 18:40:19 +00:00
text_sample_rate . set ( " - " ) ;
text_duration . set ( " - " ) ;
if ( playlist_path_ . empty ( ) )
text_track . set ( " Open playlist or add capture. " ) ;
else
text_track . set ( playlist_path_ . filename ( ) . string ( ) ) ;
progressbar_track . set_value ( 0 ) ;
progressbar_track . set_max ( 0 ) ;
progressbar_transmit . set_max ( 0 ) ;
2023-06-17 20:01:46 +00:00
} else {
2023-06-25 18:40:19 +00:00
chDbgAssert ( ! at_end ( ) , " update_ui #1 " , " current_index_ invalid " ) ;
text_filename . set ( current ( ) - > path . filename ( ) . string ( ) ) ;
2023-07-22 07:20:56 +00:00
text_sample_rate . set ( unit_auto_scale ( current ( ) - > metadata . sample_rate , 3 , ( current ( ) - > metadata . sample_rate > 1000000 ) ? 2 : 0 ) + " Hz " ) ;
2023-06-25 18:40:19 +00:00
2023-07-22 07:20:56 +00:00
uint8_t sample_size = capture_file_sample_size ( current ( ) - > path ) ;
auto duration = ms_duration ( current ( ) - > file_size , current ( ) - > metadata . sample_rate , sample_size ) ;
2023-06-25 18:40:19 +00:00
text_duration . set ( to_string_time_ms ( duration ) ) ;
2023-06-26 17:44:01 +00:00
field_frequency . set_value ( current ( ) - > metadata . center_frequency ) ;
2023-06-25 18:40:19 +00:00
2023-06-17 20:01:46 +00:00
text_track . set (
2023-06-25 18:40:19 +00:00
to_string_dec_uint ( current_index_ + 1 ) + " / " +
2023-06-17 20:01:46 +00:00
to_string_dec_uint ( playlist_db_ . size ( ) ) + " " +
playlist_path_ . filename ( ) . string ( ) ) ;
2023-06-25 18:40:19 +00:00
progressbar_track . set_max ( playlist_db_ . size ( ) - 1 ) ;
progressbar_track . set_value ( current_index_ ) ;
2023-07-22 07:20:56 +00:00
progressbar_transmit . set_max ( current ( ) - > file_size * sizeof ( complex16_t ) / sample_size ) ;
2023-05-18 20:16:05 +00:00
}
2023-05-23 04:39:22 +00:00
2023-06-17 20:01:46 +00:00
button_play . set_bitmap ( is_active ( ) ? & bitmap_stop : & bitmap_play ) ;
}
2023-05-18 20:16:05 +00:00
2023-06-17 20:01:46 +00:00
void PlaylistView : : on_tx_progress ( uint32_t progress ) {
progressbar_transmit . set_value ( progress ) ;
2022-04-03 23:38:06 +00:00
}
2023-06-17 20:01:46 +00:00
void PlaylistView : : handle_replay_thread_done ( uint32_t return_code ) {
2023-05-18 20:16:05 +00:00
if ( return_code = = ReplayThread : : END_OF_FILE ) {
2023-06-17 20:01:46 +00:00
if ( next_track ( ) ) {
send_current_track ( ) ;
return ;
}
2023-05-18 20:16:05 +00:00
}
2023-06-17 20:01:46 +00:00
if ( return_code = = ReplayThread : : READ_ERROR )
2023-06-25 18:40:19 +00:00
show_file_error ( current ( ) - > path , " Replay read failed. " ) ;
2023-06-17 20:01:46 +00:00
stop ( ) ;
2022-04-03 23:38:06 +00:00
}
PlaylistView : : PlaylistView (
2023-05-18 20:16:05 +00:00
NavigationView & nav )
: nav_ ( nav ) {
baseband : : run_image ( portapack : : spi_flash : : image_tag_replay ) ;
add_children ( {
& text_filename ,
& text_sample_rate ,
& text_duration ,
2023-06-17 20:01:46 +00:00
& progressbar_track ,
& progressbar_transmit ,
2023-06-26 17:44:01 +00:00
& field_frequency ,
2023-06-17 20:01:46 +00:00
& tx_view ,
2023-05-18 20:16:05 +00:00
& check_loop ,
& button_play ,
2023-06-15 20:15:26 +00:00
& text_track ,
2023-06-25 18:40:19 +00:00
& button_prev ,
& button_add ,
& button_delete ,
& button_open ,
& button_save ,
& button_next ,
2023-05-18 20:16:05 +00:00
& waterfall ,
} ) ;
2023-09-18 21:22:46 +00:00
ensure_directory ( u " PLAYLIST " ) ;
2023-06-12 05:25:25 +00:00
waterfall . show_audio_spectrum_view ( false ) ;
2023-06-26 17:44:01 +00:00
field_frequency . set_value ( 100'000'000 ) ;
field_frequency . on_change = [ this ] ( rf : : Frequency f ) {
if ( current ( ) )
current ( ) - > metadata . center_frequency = f ;
} ;
field_frequency . on_edit = [ this ] ( ) {
auto freq_view = nav_ . push < FrequencyKeypadView > ( field_frequency . value ( ) ) ;
freq_view - > on_changed = [ this ] ( rf : : Frequency f ) {
field_frequency . set_value ( f ) ;
} ;
} ;
2023-05-18 20:16:05 +00:00
button_play . on_select = [ this ] ( ImageButton & ) {
2023-06-17 20:01:46 +00:00
toggle ( ) ;
2023-05-18 20:16:05 +00:00
} ;
2023-06-25 18:40:19 +00:00
button_add . on_select = [ this , & nav ] ( ) {
2023-06-28 17:02:06 +00:00
if ( is_active ( ) )
return ;
2023-07-22 07:20:56 +00:00
auto open_view = nav_ . push < FileLoadView > ( " .C* " ) ;
2023-06-28 17:02:06 +00:00
open_view - > push_dir ( u " CAPTURES " ) ;
2023-06-25 18:40:19 +00:00
open_view - > on_changed = [ this ] ( fs : : path path ) {
add_entry ( std : : move ( path ) ) ;
2023-05-18 20:16:05 +00:00
} ;
} ;
2023-06-17 20:01:46 +00:00
2023-06-25 18:40:19 +00:00
button_delete . on_select = [ this , & nav ] ( ) {
2023-06-28 17:02:06 +00:00
if ( is_active ( ) )
return ;
2023-06-25 18:40:19 +00:00
delete_entry ( ) ;
} ;
button_open . on_select = [ this , & nav ] ( ) {
2023-06-28 17:02:06 +00:00
if ( is_active ( ) )
stop ( ) ;
2023-06-25 18:40:19 +00:00
open_file ( ) ;
} ;
button_save . on_select = [ this ] ( ) {
2023-06-28 17:02:06 +00:00
if ( is_active ( ) )
stop ( ) ;
2023-06-25 18:40:19 +00:00
save_file ( ) ;
} ;
button_prev . on_select = [ this ] ( ) {
2023-06-28 17:02:06 +00:00
if ( is_active ( ) )
return ;
2023-06-25 18:40:19 +00:00
- - current_index_ ;
if ( at_end ( ) )
current_index_ = playlist_db_ . size ( ) - 1 ;
update_ui ( ) ;
} ;
button_next . on_select = [ this ] ( ) {
2023-06-28 17:02:06 +00:00
if ( is_active ( ) )
return ;
2023-06-25 18:40:19 +00:00
+ + current_index_ ;
if ( at_end ( ) )
current_index_ = 0 ;
update_ui ( ) ;
} ;
2023-06-17 20:01:46 +00:00
update_ui ( ) ;
2022-04-03 23:38:06 +00:00
}
2023-06-28 17:02:06 +00:00
PlaylistView : : PlaylistView (
NavigationView & nav ,
const fs : : path & path )
: PlaylistView ( nav ) {
auto ext = path . extension ( ) ;
if ( path_iequal ( ext , ppl_ext ) )
on_file_changed ( path ) ;
2023-07-22 07:20:56 +00:00
else if ( is_cxx_capture_file ( path ) )
2023-06-28 17:02:06 +00:00
add_entry ( fs : : path { path } ) ;
}
2022-04-03 23:38:06 +00:00
PlaylistView : : ~ PlaylistView ( ) {
2023-05-24 04:22:30 +00:00
transmitter_model . disable ( ) ;
2023-05-18 20:16:05 +00:00
baseband : : shutdown ( ) ;
2022-04-03 23:38:06 +00:00
}
2023-06-17 20:01:46 +00:00
void PlaylistView : : set_parent_rect ( Rect new_parent_rect ) {
View : : set_parent_rect ( new_parent_rect ) ;
2023-06-25 18:40:19 +00:00
ui : : Rect waterfall_rect {
2023-06-17 20:01:46 +00:00
0 , header_height , new_parent_rect . width ( ) ,
new_parent_rect . height ( ) - header_height } ;
waterfall . set_parent_rect ( waterfall_rect ) ;
}
2023-06-28 19:50:11 +00:00
void PlaylistView : : focus ( ) {
2023-06-28 17:02:06 +00:00
if ( playlist_path_ . empty ( ) )
button_add . focus ( ) ;
else
button_play . focus ( ) ;
2022-04-03 23:38:06 +00:00
}
2023-06-28 19:50:11 +00:00
void PlaylistView : : on_hide ( ) {
stop ( ) ;
waterfall . on_hide ( ) ;
View : : on_hide ( ) ;
}
2022-04-03 23:38:06 +00:00
} /* namespace ui */