Hi Rik,
> (maybe I should ask Wolf for advice)
You're welcome. The "image rejecting up/down converter" is
written in plain C. It should be easily portable. Uses a
37-tap FIR filter for the Hilbert transformer that rejects the
"unwanted" mixing product.
Not sure if the sourcecode (indentation) survives the emailing
process, but fyi it's attached below.
If you think it's usable for your project, I can send
everything (including the data type definitions and the filter
coefficient table).
Cheers,
Wolf .
Below: Core of the "universal frequency converter / mixer" as
currently used in Spectrum Lab (this is the simple one,
without forward and reverse FFT).
void SOUND_RunThroughMixer(
T_SOUND_FREQ_CONVERTER *pcnv, // contains all
parameters for a converter
T_Float *input_samples, T_Float *input_samples_q,
T_Float *output_samples, T_Float *output_samples_q,
int number_of_samples )
/* Frequency mixer. May transform the frequency for some kind
of RX.
* Can optionally cancel one sideband using a Hilbert
transformer,
* I/Q-mixer etc.
*/
{
int sample_i, j;
T_Float d, y, dbl_i, dbl_q;
const T_Float* coeff_ptr;
T_Float* queue_ptr;
T_Float* input_queue_end;
double dblPhzInc;
int iCosTableIndex, iSineTableOffset = 0;
T_Float dblNcoI, dblNcoQ;
double dblNcoPhase = pcnv->dblNcoPhase; // use local var
for speed
// Prepare the NCO for the "mixer" frequency ...
// Note: SoundTab_fltCosTable[SOUND_COS_TABLE_LEN] contains
exactly ONE PERIOD.
// "Negative" frequencies are possible by inverting
the Q-channel .
if(pcnv->dblFSample!=0)
dblPhzInc = (double)SOUND_COS_TABLE_LEN *
pcnv->dblOscillatorFreq / pcnv->dblFSample;
else
dblPhzInc = 0; // mixer frequency is ZERO, so noo
phase increment
if(dblPhzInc > 0)
{ // array index offset to read a SINE WAVE from a COSINE
TABLE :
iSineTableOffset = (int) (0.499 + 3.0 *
(T_Float)SOUND_COS_TABLE_LEN / 4.0);
}
else // phase increment negative, "negative" mixer
frequency ....
{
dblPhzInc = -dblPhzInc; // Make sure the table reading
index INCREMENTS,
// because the wrap test will not
look for negative indices.
// Get array index offset to read a NEGATIVE SINE WAVE
from a COSINE TABLE :
iSineTableOffset = (int)(0.499 +
(T_Float)SOUND_COS_TABLE_LEN / 4.0);
} // end if <produce NEGATIVE frequencies ... possible
only with I/Q mult. >
if(dblNcoPhase<0) dblNcoPhase = 0;
if(pcnv->fDcReject)
{ // Accumulate & remove a bit DC offset from the new
chunk:
for(sample_i=0; sample_i<number_of_samples;
++sample_i)
{
pcnv->dblMixerDcOffset =
pcnv->dblMixerDcOffset * 0.9999
+ input_samples[sample_i] * 0.0001;
// bad style to modifying the INPUT samples
// but this won't hurt anyone :>
input_samples[sample_i] -= pcnv->dblMixerDcOffset;
}
}
if(pcnv->iMixerScheme==CFG_FREQ_MIX_DSB)
{ // No sideband rejection:
// This is a simple and very fast mixer without
sideband-rejection.
// No complicated broadband 90° phase shifter required.
for(sample_i=0; sample_i<number_of_samples; ++sample_i)
{
// Run the NCO (numerical controlled oscillator)
dblNcoPhase += dblPhzInc;
while(dblNcoPhase >=(T_Float)SOUND_COS_TABLE_LEN) //
"while", not "if" !!
dblNcoPhase -=(T_Float)SOUND_COS_TABLE_LEN; //
table index wrap
d = SoundTab_fltCosTable[(int)dblNcoPhase];
// DSB mixer: simply multiply the input signal with the
NCO output :
*output_samples++ = (*input_samples++) * d;
}
} // end if <frequency converter WITHOUT sideband
rejection>
else
if(pcnv->iMixerScheme==CFG_FREQ_MIX_COMPLEX)
{ // Complex multiplication of complex input :
// (a + j*b ) * ( c + j*d ) = a*c - b*d + j * ( a*d +
b*c )
// To do this, SOUND_RunThroughMixer() must be called
with
// TWO source- and TWO destination blocks.
// (real and imaginary parts are in sepearate blocks)
if( (input_samples!=NULL) &&
(input_samples_q!=NULL)
&&(output_samples!=NULL) &&
(output_samples_q!=NULL) )
{
T_Float inp_i, inp_q, outp_i, outp_q;
for(sample_i=0; sample_i<number_of_samples;
++sample_i)
{
// Run the NCO (numerical controlled oscillator;
here with quadrature output)
dblNcoPhase += dblPhzInc;
while(dblNcoPhase >=(T_Float)SOUND_COS_TABLE_LEN)
// "while", not "if" !!
dblNcoPhase -=(T_Float)SOUND_COS_TABLE_LEN; //
table index wrap
iCosTableIndex = (int)dblNcoPhase;
dblNcoI = SoundTab_fltCosTable[iCosTableIndex];
dblNcoQ =
SoundTab_fltCosTable[(iCosTableIndex+iSineTableOffset) %
SOUND_COS_TABLE_LEN];
// Get complex input sample, multiply with complex
oscillator, and put into output buffers:
inp_i = *(input_samples++);
inp_q = *(input_samples_q++);
outp_i = inp_i * dblNcoI - inp_q * dblNcoQ;
outp_q = inp_i * dblNcoQ + inp_q * dblNcoI;
*output_samples++ = outp_i;
*output_samples_q++ = outp_q;
}
}
} // end if <frequency converter WITHOUT sideband
rejection>
else
if( (pcnv->iMixerScheme ==
CFG_FREQ_MIX_LSB_DOWNCONVERTER )
|| (pcnv->iMixerScheme ==
CFG_FREQ_MIX_USB_DOWNCONVERTER ) )
{ // Optimized mixer for "downconversion".
// Based on a schematic diagram from the ARRL handbook
1996,
// page 17.73: "The R2: An image-rejecting D-C
Receiver"
// Principle: Splitter, TWO Mixers (with I and Q
output),
// audio phase shift network, summer,
filter.
// Note: 90° phase shifting is done AFTER mixing (in
contrast to the
// schemes CFG_FREQ_MIX_LSB_UP and
CFG_FREQ_MIX_USB_UP.
for(sample_i=0; sample_i<number_of_samples; ++sample_i)
{
// Let the NCO (numerical controlled oscillator) produce
quadrature-phase signals :
dblNcoPhase += dblPhzInc;
while(dblNcoPhase >=(T_Float)SOUND_COS_TABLE_LEN) //
"while", not "if" !!
dblNcoPhase -=(T_Float)SOUND_COS_TABLE_LEN; //
table index wrap
iCosTableIndex = (int)dblNcoPhase;
dblNcoI = SoundTab_fltCosTable[iCosTableIndex];
dblNcoQ =
SoundTab_fltCosTable[(iCosTableIndex+iSineTableOffset) %
SOUND_COS_TABLE_LEN];
// Now mix the incoming (higher-frequency) signal
..input_samples[]..
// with a sine and a cosine signal from the NCO
("LO").
dbl_i = input_samples[sample_i]; // this is the INPUT
signal
if(pcnv->iMixerScheme ==
CFG_FREQ_MIX_LSB_DOWNCONVERTER)
{
dbl_q = dbl_i * dblNcoI; // LSB (below NCO freq)
moved DOWN
dbl_i = dbl_i * dblNcoQ;
}
else // the "other" sideband with reversed oscillator
phases:
{
dbl_q = dbl_i * dblNcoQ; // USB (above NCO freq)
moved DOWN
dbl_i = dbl_i * dblNcoI;
}
// arrived here: dbl_i is the output of the "I"-mixer,
// dbl_q is the output of the "Q"-mixer.
// Let the I-channel run through a delay line [ length
(N-1)/2 ]
// to compensate the delay from the hilbert trafo (see
below).
if( (pcnv->iDelayQIndex < 0)
||(pcnv->iDelayQIndex >=
((SOUND_Hilbert_filter_length-1)/2)) )
pcnv->iDelayQIndex = 0;
y = pcnv->dblDelayQueue[pcnv->iDelayQIndex];
pcnv->dblDelayQueue[pcnv->iDelayQIndex++] = dbl_i;
dbl_i = y;
// Let the Q-channel run through a broadband 90° phase
shifter
// (hilbert transformer, implemented as FIR filter
here).
// keep the circular buffer pointer 'valid' all the
time :
input_queue_end =
&pcnv->dblHilbertQueue[SOUND_Hilbert_filter_length-1];
if( (pcnv->pdblHilbertQPointer <
&pcnv->dblHilbertQueue[0])
||(pcnv->pdblHilbertQPointer > input_queue_end)
)
pcnv->pdblHilbertQPointer =
&pcnv->dblHilbertQueue[0];
if( --pcnv->pdblHilbertQPointer <
&pcnv->dblHilbertQueue[0] )
pcnv->pdblHilbertQPointer = input_queue_end; //
deal with wraparound
*pcnv->pdblHilbertQPointer = dbl_q; // hilbert
trafo input
queue_ptr = pcnv->pdblHilbertQPointer; // pointer to
read from input queue
coeff_ptr = &SOUND_HilbertCoeffs[0]; // pointer to
read from coeff table
y = 0.0; // clear the 'global adder'
j = (SOUND_Hilbert_filter_length+1)/2 -1; // j=12 for
25th-order filter
while( j-- ) // do the MAC's (with every
2nd coeff ZERO)
{
// coeffs[0], [2], [4].. are (almost) zero and are
not calculated
// so just skip the 'queue'
pointer
++queue_ptr; ++coeff_ptr;
if( queue_ptr > input_queue_end ) // deal with
wraparound
queue_ptr = &pcnv->dblHilbertQueue[0];
// odd coefficients, non-zero, do a MAC-operation :
y += ( (*queue_ptr++) * (*coeff_ptr++) );
if( queue_ptr > input_queue_end ) // deal with
wraparound
queue_ptr = &pcnv->dblHilbertQueue[0];
}
dbl_q = y; // filter output = sum from all 'taps'
// finally add the output of the "audio phase-shift
network"...
output_samples[sample_i] = dbl_i + dbl_q;
} // end for (sample_i ... )
} // end if <..mixer_scheme ==
CFG_FREQ_MIX_USB_DOWNCONVERTER >
else // neither "double side band" nor "USB
DOWNCONVERTER"....
{
for(sample_i=0; sample_i<number_of_samples; ++sample_i)
{
// Let the I-value run through a delay line [ length
(N-1)/2 ]
// to compensate the delay from the hilbert trafo (see
below).
if( (pcnv->iDelayQIndex < 0)
||(pcnv->iDelayQIndex >=
((SOUND_Hilbert_filter_length-1)/2)) )
pcnv->iDelayQIndex = 0;
dbl_i = pcnv->dblDelayQueue[pcnv->iDelayQIndex];
pcnv->dblDelayQueue[pcnv->iDelayQIndex++]
= input_samples[sample_i];
// Generate the Q-value by passing the real input
through a
// hilbert transformer (which is implemented as an FIR
filter here).
// keep the circular buffer pointer 'valid' all the
time :
input_queue_end =
&pcnv->dblHilbertQueue[SOUND_Hilbert_filter_length-1];
if( (pcnv->pdblHilbertQPointer <
&pcnv->dblHilbertQueue[0])
||(pcnv->pdblHilbertQPointer > input_queue_end)
)
pcnv->pdblHilbertQPointer =
&pcnv->dblHilbertQueue[0];
if( --pcnv->pdblHilbertQPointer <
&pcnv->dblHilbertQueue[0] )
pcnv->pdblHilbertQPointer = input_queue_end; //
deal with wraparound
*pcnv->pdblHilbertQPointer =
input_samples[sample_i]; // filter input = "X"
queue_ptr = pcnv->pdblHilbertQPointer; // pointer to
read from input queue
coeff_ptr = &SOUND_HilbertCoeffs[0]; // pointer to
read from coeff table
y = 0.0; // clear the 'global adder'
j = (SOUND_Hilbert_filter_length+1)/2 -1; // j=12 for
25th-order filter
while( j-- ) // do the MAC's (with every
2nd coeff ZERO)
{
// coeffs[0], [2], [4].. are (almost) zero and are
not calculated
// so just skip the 'queue'
pointer
#if(0) // not optimized:
y += ( (*queue_ptr++) * (*coeff_ptr++) ); // here's
the MAC
#else // optimized:
++queue_ptr; ++coeff_ptr;
#endif
if( queue_ptr > input_queue_end ) // deal with
wraparound
queue_ptr = &pcnv->dblHilbertQueue[0];
// odd coefficients, non-zero, do a MAC-operation :
y += ( (*queue_ptr++) * (*coeff_ptr++) );
if( queue_ptr > input_queue_end ) // deal with
wraparound
queue_ptr = &pcnv->dblHilbertQueue[0];
}
dbl_q = y; // filter output = sum from all 'taps'
// Let the NCO (numerical controlled oscillator) produce
quadrature-phase signals :
dblNcoPhase += dblPhzInc;
while(dblNcoPhase >=(T_Float)SOUND_COS_TABLE_LEN) //
"while", not "if" !!
dblNcoPhase -=(T_Float)SOUND_COS_TABLE_LEN; //
table index wrap
iCosTableIndex = (int)dblNcoPhase;
dblNcoI = SoundTab_fltCosTable[iCosTableIndex];
dblNcoQ = SoundTab_fltCosTable[(iCosTableIndex +
iSineTableOffset) % SOUND_COS_TABLE_LEN];
if(pcnv->iMixerScheme==CFG_FREQ_MIX_LSB_UP)
{
// multiply the I- and Q- samples with the sin- and
cos- output
// of the local oscillator.
output_samples[sample_i] =
dbl_q * dblNcoI // multiply with "cosine"
- dbl_i * dblNcoQ; // multiply with "sine"
}
else // not LSB but USB:
{
output_samples[sample_i] =
dbl_i * dblNcoI // multiply with "cosine"
- dbl_q * dblNcoQ; // multiply with "sine"
}
} // end for (sample_i ... )
} // end if <frequency UP converter with sideband
rejection>
pcnv->dblNcoPhase = dblNcoPhase; // save the NCO phase
for next block
} // end SOUND_RunThroughMixer()
On 29.12.2018 10:47, Rik Strobbe
wrote:
Hello Markus,
nice copy on N1BUG, this might be a new distance record
for Paul.
Thanks for your help with the JT9-2 and JT9-5 resampling.
Lifting the frequency restriction on TX is "peanuts", but
on RX I would need to implement frequency conversion in
SpecLab style.
I will have a look to that (maybe I should ask Wolf for
advice).
73, Rik ON7YD - OR7T
Thanks to Rik for the software, and to Spiros and
Dionysios for their reports. This is what I picked
up using JT9-2
18-12-28 21:38 2038 -25 0.1 648 2 CQ SV8CS
KM07
18-12-28 21:40 2040 -25 0.1 1200 2 LA3EQ
JO28XJ
18-12-28 21:42 2042 -26 0.1 648 2 CQ SV8CS
KM07
18-12-28 21:44 2044 -26 0.2 1200 2 LA3EQ
JO28XJ
18-12-28 21:48 2048 -25 0.1 1250 2 LA3EQ
JO28XJ
18-12-28 21:52 2052 -25 0.2 1250 2 LA3EQ
JO28XJ
18-12-28 21:56 2056 -25 0.2 1250 2 LA3EQ
JO28XJ
18-12-28 22:00 2100 -28 0.3 1250 2 LA3EQ
JO28XJ
and later JT9-5:
18-12-29 02:30 0130 -34 -0.7 615 5 VVV N1BUG
5
18-12-29 02:40 0140 -34 -0.6 615 5 VVV N1BUG
5
18-12-29 03:30 0230 -35 -0.6 615 5 VVV N1BUG
5
As my LF transceiver needs to be parked on 135.5 kHz
I am always struggling with audio
frequency restrictions in WSJT, WSPR and JT-9
software. My workaround are frequency conversions in
two separate SpecLab instances for RX and TX,
connected to the left and right channels of a
single VB-Audio virtual cable instance. However
there is a noticable latency in this process.
Best 73,
Markus (DF6NM)
-----Ursprüngliche Mitteilung-----
Von: Dionysios Vlachiotis
<[email protected]>
An: rsgb_lf_group
<[email protected]>;
rsgb_lf_group
<[email protected]>
Verschickt: Sa, 29. Dez 2018 8:39
Betreff: LF: JT9-5 decodes in km07ks
245 -28 -0.4 650 5 CQ DF6NM
JN59
2255 -27 -0.4 650 5 CQ DF6NM
JN59
2315 -27 -0.5 650 5 CQ DF6NM
JN59
2325 -24 -0.3 650 5 CQ DF6NM
JN59
0035 -23 -0.2 650 5 CQ DF6NM
JN59
0045 -21 -0.3 650 5 CQ DF6NM JN59
73 de SV8RV
Zakynthos isl. GREECE (km07ks)
-----Ursprüngliche Mitteilung-----
Von: SV8CS-Spiros Chimarios
<[email protected]>
An: rsgb_lf_group
<[email protected]>
Verschickt: Sa, 29. Dez 2018 5:56
Betreff: LF: SV8CS SlowJT9
Good morning.
Decoded only DF6NM during the
night (JT9-5) – 136 KHz.
2245 -30 -0.4 650 5 CQ DF6NM
JN59
2255 -29 -0.4 650 5 CQ DF6NM
JN59
2315 -27 -0.4 650 5 CQ DF6NM
JN59
2325 -26 -0.4 650 5 CQ DF6NM
JN59
0035 -21 -0.6 650 5 CQ DF6NM
JN59
0045 -20 -0.4 650 5 CQ DF6NM
JN59
73, Spiros/SV8CS