2025-04-25 21:35:35 +02:00
# include "module_sstv_decoder.h"
2025-04-26 21:56:53 +02:00
# include "common/dsp/filter/firdes.h"
2025-04-25 21:35:35 +02:00
# include "common/utils.h"
# include "common/wav.h"
# include "core/exception.h"
2025-05-31 20:26:47 +02:00
# include "core/resources.h"
2025-04-26 21:56:53 +02:00
# include "dsp/filter/fir.h"
# include "dsp/sstv_linesync.h"
# include "dsp/utils/hilbert.h"
# include "dsp/utils/quadrature_demod.h"
2025-05-31 20:26:47 +02:00
# include "image/image.h"
# include "image/io.h"
2025-04-25 21:35:35 +02:00
# include "imgui/imgui.h"
2025-04-26 21:56:53 +02:00
# include "imgui/imgui_image.h"
2025-04-25 21:35:35 +02:00
# include "logger.h"
2025-04-26 21:56:53 +02:00
# include "nlohmann/json_utils.h"
2025-04-25 21:35:35 +02:00
2025-04-26 21:56:53 +02:00
# include "lineproc.h"
2025-04-25 21:35:35 +02:00
namespace sstv
{
SSTVDecoderModule : : SSTVDecoderModule ( std : : string input_file , std : : string output_file_hint , nlohmann : : json parameters ) : ProcessingModule ( input_file , output_file_hint , parameters )
{
if ( parameters . count ( " audio_samplerate " ) > 0 )
d_audio_samplerate = parameters [ " audio_samplerate " ] . get < long > ( ) ;
else
throw satdump_exception ( " Audio samplerate parameter must be present! " ) ;
2025-04-26 21:56:53 +02:00
if ( parameters . count ( " sstv_mode " ) > 0 )
d_sstv_mode = parameters [ " sstv_mode " ] . get < std : : string > ( ) ;
else
throw satdump_exception ( " SSTV Mode parameter must be present! " ) ;
2025-04-25 21:35:35 +02:00
}
SSTVDecoderModule : : ~ SSTVDecoderModule ( ) { }
void SSTVDecoderModule : : process ( )
{
2025-05-31 20:26:47 +02:00
if ( input_data_type = = satdump : : pipeline : : DATA_FILE )
2025-04-25 21:35:35 +02:00
filesize = getFilesize ( d_input_file ) ;
else
filesize = 0 ;
std : : string main_dir = d_output_file_hint . substr ( 0 , d_output_file_hint . rfind ( ' / ' ) ) ;
bool is_stereo = false ;
std : : ifstream data_in ;
2025-05-31 20:26:47 +02:00
if ( input_data_type = = satdump : : pipeline : : DATA_FILE )
2025-04-25 21:35:35 +02:00
{
wav : : WavHeader hdr = wav : : parseHeaderFromFileWav ( d_input_file ) ;
if ( ! wav : : isValidWav ( hdr ) )
throw satdump_exception ( " File is not WAV! " ) ;
if ( hdr . bits_per_sample ! = 16 )
throw satdump_exception ( " Only 16-bits WAV are supported! " ) ;
d_audio_samplerate = hdr . samplerate ;
is_stereo = hdr . channel_cnt = = 2 ;
if ( is_stereo )
logger - > info ( " File is stereo. Ignoring second channel! " ) ;
wav : : FileMetadata md = wav : : tryParseFilenameMetadata ( d_input_file , true ) ;
if ( md . timestamp ! = 0 & & ( d_parameters . contains ( " start_timestamp " ) ? d_parameters [ " start_timestamp " ] = = - 1 : 1 ) )
{
d_parameters [ " start_timestamp " ] = md . timestamp ;
logger - > trace ( " Has timestamp %d " , md . timestamp ) ;
}
else
{
logger - > warn ( " Couldn't automatically determine the timestamp, in case of unexpected results, please verify you have specified the correct timestamp manually " ) ;
}
data_in = std : : ifstream ( d_input_file , std : : ios : : binary ) ;
}
logger - > info ( " Using input wav " + d_input_file ) ;
2025-04-26 21:56:53 +02:00
// SSTV Blocks
satdump : : ndsp : : FIRBlock < float > bpf ;
satdump : : ndsp : : HilbertBlock hilb ;
satdump : : ndsp : : QuadratureDemodBlock quad ;
satdump : : ndsp : : SSTVLineSyncBlock lsync ;
// Configure blocks
auto sstv_modes = loadJsonFile ( resources : : getResourcePath ( " sstv.json " ) ) ;
if ( ! sstv_modes . contains ( d_sstv_mode ) )
throw satdump_exception ( " Invalid SSTV Mode : " + d_sstv_mode ) ;
nlohmann : : json sstv_cfg = sstv_modes [ d_sstv_mode ] ;
double sstv_samplerate = sstv_cfg [ " samplerate " ] ; // TODO resample!
double freq_sync = getValueOrDefault ( sstv_cfg [ " sync_freq " ] , 1200 ) ;
2025-05-24 20:17:10 +02:00
double freq_img_black = getValueOrDefault ( sstv_cfg [ " freq_img_black " ] , 1488 ) ; // 1500); should be 1500?
2025-04-26 21:56:53 +02:00
double freq_img_white = getValueOrDefault ( sstv_cfg [ " freq_img_white " ] , 2300 ) ;
bpf . set_cfg ( " taps " , dsp : : firdes : : band_pass ( 1 , sstv_samplerate , freq_sync , freq_img_white , 500 , dsp : : fft : : window : : WIN_KAISER ) ) ;
2025-04-28 19:43:01 +02:00
hilb . set_cfg ( " ntaps " , 651 ) ;
2025-04-26 21:56:53 +02:00
lsync . set_cfg ( " sync_time " , sstv_cfg [ " sync_time " ] ) ;
lsync . set_cfg ( " line_time " , sstv_cfg [ " line_time " ] ) ;
// Link them
satdump : : ndsp : : BlockIO io_in ;
io_in . name = " in " ;
io_in . type = satdump : : ndsp : : DSP_SAMPLE_TYPE_F32 ;
2025-08-29 15:04:58 +02:00
io_in . fifo = std : : make_shared < satdump : : ndsp : : DSPStream > ( 2 ) ;
2025-04-26 21:56:53 +02:00
bpf . set_input ( io_in , 0 ) ;
hilb . link ( & bpf , 0 , 0 , 2 ) ;
quad . link ( & hilb , 0 , 0 , 2 ) ;
lsync . link ( & quad , 0 , 0 , 2 ) ;
auto io_out = lsync . get_output ( 0 , 4 ) ;
// Start everything
auto feeder_func = [ this , & io_in , & data_in , is_stereo ] ( )
{
const int buffer_size = 1024 ;
int16_t * s16_buf = new int16_t [ buffer_size ] ;
time_t lastTime = 0 ;
2025-05-31 20:26:47 +02:00
while ( input_data_type = = satdump : : pipeline : : DATA_FILE ? ! data_in . eof ( ) : input_active . load ( ) )
2025-04-26 21:56:53 +02:00
{
2025-05-31 20:26:47 +02:00
if ( input_data_type = = satdump : : pipeline : : DATA_FILE )
2025-04-26 21:56:53 +02:00
data_in . read ( ( char * ) s16_buf , buffer_size * sizeof ( int16_t ) ) ;
else
input_fifo - > read ( ( uint8_t * ) s16_buf , buffer_size * sizeof ( int16_t ) ) ;
2025-09-25 17:25:13 +01:00
auto nblk = io_in . fifo - > newBufferSamples ( buffer_size , sizeof ( float ) ) ;
2025-04-26 21:56:53 +02:00
if ( is_stereo )
{
for ( int i = 0 ; i < buffer_size / 2 ; i + + )
nblk . getSamples < float > ( ) [ i ] = s16_buf [ i * 2 ] / 32767.0f ;
nblk . size = buffer_size / 2 ;
io_in . fifo - > wait_enqueue ( nblk ) ;
}
else
{
2025-09-25 17:25:13 +01:00
volk_16i_s32f_convert_32f_u ( ( & nblk ) - > getSamples < float > ( ) , ( const int16_t * ) s16_buf , 32767 , buffer_size ) ;
2025-04-26 21:56:53 +02:00
nblk . size = buffer_size ;
io_in . fifo - > wait_enqueue ( nblk ) ;
}
2025-05-31 20:26:47 +02:00
if ( input_data_type = = satdump : : pipeline : : DATA_FILE )
2025-04-26 21:56:53 +02:00
progress = data_in . tellg ( ) ;
if ( time ( NULL ) % 10 = = 0 & & lastTime ! = time ( NULL ) )
{
lastTime = time ( NULL ) ;
logger - > info ( " Progress " + std : : to_string ( round ( ( ( double ) progress / ( double ) filesize ) * 1000.0 ) / 10.0 ) + " %% " ) ;
}
}
delete [ ] s16_buf ;
2025-08-29 15:04:58 +02:00
io_in . fifo - > wait_enqueue ( io_in . fifo - > newBufferTerminator ( ) ) ;
2025-04-26 21:56:53 +02:00
} ;
std : : thread feeder_thread ( feeder_func ) ;
bpf . start ( ) ;
hilb . start ( ) ;
quad . start ( ) ;
lsync . start ( ) ;
std : : vector < uint32_t > all_lines ;
SSTVLineProc lproc ;
lproc . setCfg ( sstv_cfg ) ;
auto reader_func = [ & ] ( )
{
while ( true )
{
2025-08-29 15:04:58 +02:00
satdump : : ndsp : : DSPBuffer iblk = io_out . fifo - > wait_dequeue ( ) ;
2025-04-26 21:56:53 +02:00
if ( iblk . isTerminator ( ) )
{
2025-08-29 15:04:58 +02:00
io_out . fifo - > free ( iblk ) ;
2025-04-26 21:56:53 +02:00
break ;
}
2025-04-27 00:47:52 +02:00
// logger->info("Got Something!");
2025-04-26 21:56:53 +02:00
auto ll = lproc . lineProc ( std : : vector < float > ( iblk . getSamples < float > ( ) , iblk . getSamples < float > ( ) + iblk . size ) ) ;
all_lines . insert ( all_lines . end ( ) , ll . begin ( ) , ll . end ( ) ) ;
if ( textureID ! = 0 & & all_lines . size ( ) > 0 )
{
auto img = rgbaToImg ( all_lines , lproc . output_width ) ;
img . resize_bilinear ( 512 , img . height ( ) * ( 512.0 / img . width ( ) ) ) ;
image : : Image preview ( 8 , 512 , 512 , 3 ) ;
int offset = img . height ( ) > 512 ? ( img . height ( ) - 512 ) : 0 ;
for ( int ch = 0 ; ch < img . channels ( ) ; ch + + )
for ( int i = 0 ; i < std : : min < int > ( 512 , img . height ( ) ) ; i + + )
image : : imemcpy ( preview , ch * preview . height ( ) * preview . width ( ) + i * 512 , img , ch * img . height ( ) * img . width ( ) + ( i + offset ) * 512 , 512 ) ;
image : : image_to_rgba ( preview , textureBuffer ) ;
has_to_update = true ;
}
2025-08-29 15:04:58 +02:00
io_out . fifo - > free ( iblk ) ;
2025-04-26 21:56:53 +02:00
}
logger - > info ( " BYE! " ) ;
} ;
std : : thread read_thread ( reader_func ) ;
// Stop everything
if ( feeder_thread . joinable ( ) )
feeder_thread . join ( ) ;
bpf . stop ( ) ;
hilb . stop ( ) ;
quad . stop ( ) ;
lsync . stop ( ) ;
if ( read_thread . joinable ( ) )
read_thread . join ( ) ;
auto img = rgbaToImg ( all_lines , lproc . output_width ) ;
2025-04-26 22:23:09 +02:00
image : : save_img ( img , d_output_file_hint + " .png " ) ;
2025-04-25 21:35:35 +02:00
}
void SSTVDecoderModule : : drawUI ( bool window )
{
ImGui : : Begin ( " SSTV Decoder " , NULL , window ? 0 : NOWINDOW_FLAGS ) ;
2025-04-26 21:56:53 +02:00
ImGui : : BeginGroup ( ) ;
{
if ( textureID = = 0 )
{
textureID = makeImageTexture ( ) ;
textureBuffer = new uint32_t [ 262144 ] ; // 512x512
memset ( textureBuffer , 0 , sizeof ( uint32_t ) * 262144 ) ;
has_to_update = true ;
}
if ( has_to_update )
{
updateImageTexture ( textureID , textureBuffer , 512 , 512 ) ;
has_to_update = false ;
}
ImGui : : Image ( ( void * ) ( intptr_t ) textureID , { 200 * ui_scale , 200 * ui_scale } ) ;
}
ImGui : : EndGroup ( ) ;
ImGui : : SameLine ( ) ;
ImGui : : BeginGroup ( ) ;
{
ImGui : : Button ( " Status " , { 200 * ui_scale , 20 * ui_scale } ) ;
drawStatus ( sstv_status ) ;
}
ImGui : : EndGroup ( ) ;
2025-04-25 21:35:35 +02:00
2025-05-31 20:26:47 +02:00
if ( input_data_type = = satdump : : pipeline : : DATA_FILE )
2025-04-25 21:35:35 +02:00
ImGui : : ProgressBar ( ( double ) progress / ( double ) filesize , ImVec2 ( ImGui : : GetContentRegionAvail ( ) . x , 20 * ui_scale ) ) ;
ImGui : : End ( ) ;
}
std : : string SSTVDecoderModule : : getID ( ) { return " sstv_decoder " ; }
2025-05-31 20:26:47 +02:00
std : : shared_ptr < satdump : : pipeline : : ProcessingModule > SSTVDecoderModule : : getInstance ( std : : string input_file , std : : string output_file_hint , nlohmann : : json parameters )
2025-04-25 21:35:35 +02:00
{
return std : : make_shared < SSTVDecoderModule > ( input_file , output_file_hint , parameters ) ;
}
} // namespace sstv