Real to IQ in software, image reject mixer.

Hi, I experimented with some code to do the conversion from real (analog signal) to IQ for digital processing in Octave to implement an SSB receiver.
The reason is to extend the tuning range of simple micro based SDR as most rely on a Tayloe mixer which requires phase shifted LO and SI5351 cannot seem to do it up to 200mhz and phase locking 2 AD4351's is difficult.
The resulting code works but when I discussed it on another forum I was told it could not work without them even having tested the code. But I can hear the signals quite clearly and have used the same technique on an ESP32 in fixed point and it works also.
For anyone who is interested here is the Octave code.
clc;
pkg load signal;
clear all;
close all;
file='40m22kmono.wav';
[M, fs] = audioread(file);
fs;
mm=M';
PIx2 = 2*pi;
fs = 22050;
fcdm = 950; %950 %5500; %3000; %900;% 6200; %8950; %6200; % fc;
t = (0 : 1 / fs :100 );
m = resize(mm,size(t));
fr = dlmread("65 Tap Phase Added BPF.csv");
hcplus45 = fr';
fr = dlmread("65 Tap Phase minus BPF.csv");
hcminus45 = fr';
% Demodulation
% split into iq at carrier by time increment.
mi = m .* cos(PIx2 * fcdm * t);
mq = m .* -sin(PIx2 * fcdm * t);
% hilbert filter to 90deg phase shift
xr = filter(hcplus45,1,mq);
xi = filter(hcminus45,1,mi);
% ssb demod. + for lower, - for upper
ssbdemod = xr + xi;
% filter audio
bands=[250, 300, 2700, 3000]; mag=[0, 1, 0]; dev=0.1;
[n, w, beta, ftype] = kaiserord(bands, mag, dev, fs);
d=max(1,fix(n/10));
hc = fir1(n-d,w,ftype,kaiser(n-d+1,beta));
xf = filter(hc,1,ssbdemod);
xf = 0 - xf; % make the audio AC
xf = xf .*75; % make the audio louder
filename = 'ssbdemodtest.wav';
audiowrite(filename,xf,fs);
I am not a math expert but I followed the formulae on Rick Campbells description of his R2 receiver.
The file is one side of an IQ sample of 40m resampled to 22050 and the alternates for fcdm allow tuning to different conversations at various frequencies in the file.
The Hilbert coefficients were created with Iowa Hills Hilbert software using the phase add option for a 300-11khz BPF.
It would be interesting to know if it works for anyone else and if so why or if not what I am doing wrong.
I put the sample signal and the coefficients on Google Drive for anyone interested to test the code. My demodulated result is there as well.
https://drive.google.com/drive/folders/1EuX6iqEkGc...
PS. I borrowed a lot of the code sections from various posts on this site and elsewhere so if you see a line that looks like your code, thanks.

Inspecting the code it looks like this should.
My thought process is that you have a real signal and complex multiply that with a complex LO (cos wt - j sin wt) which is e^{j wt}. This left shifts the entire spectrum (both upper and lower sideband) to complex IQ baseband (as well as moves the negative spectrum further into negative frequency in the complex IQ output of the complex multiplier but your final low pass filter gets rid of that. The “Hilbert Transform” operation is essentially computing the “analytic signal” which removes all negative frequencies and then taking the real part of that.
Here it is in math showing how that works:
Given \(I_x[n]\) as the real input and \(I_{LO}[n]+jQ_{LO[n]}\) as the complex LO (with \(I_{LO}[n] = \cos(\omega_c [n])\) and \(Q_{LO}[n] = -\sin(\omega_c [n])\)) the LO is therefore \(e^{-j\omega_c [n]}\) and we see from that product that it left shifts the entire spectrum. If \(\omega_c\) is the carrier then the resulting spectrum is shifted to baseband with another copy at twice the carrier in the negative frequency side to be filtered out after.
Thus we get the output after the multiplier as \(x[n]=(I_x[n])(I_{LO}[n]+jQ_{LO}[n]\).
The analytic signal is one sided in frequency (positive frequencies only) and is given as \(x_a[n] = x[n] + j\hat{x}[n]\), where \(\hat{x}[n]\) is the Hilbert Transform of \( x[n]\).
We would typically see this with \(x[n]\) real, but \(x[n]\) can be complex just as the application here. The result is a one sided spectrum with no negative frequencies. At baseband at the multiplier output before doing the Hilbert we have a two-sided spectrum at baseband with the upper sideband in the positive frequencies and the lower sideband in the negative frequencies. By computing the analytic signal as formulated above, we can select just the upper sideband as \(x_a[n]\ but this is still a complex signal (with no negative frequencies). Selecting the real part of that results in our final real upper sideband output!
So we get the following for the Hilbert Transform of x in this case:
$$\hat{x}[n] = \mathscr{H}\{I_x[n]I_{LO}[n]\} + j\mathscr{H}\{Q_x[n]Q_{LO}[n]\}$$
Add the real part of this to the real part of x to get the real part of the analytic signal as the desired uppersideband output!

I haven't run this and don't plan to, so I'm just inspecting the code.
This does a complex mix by fcdm, so mi and mq are the real and imaginary (in-phase and quadrature) parts of the complex signal. So you already have a complex signal at this point:
% Demodulation% split into iq at carrier by time increment.
mi = m .* cos(PIx2 * fcdm * t);
mq = m .* -sin(PIx2 * fcdm * t);
If it was a dual-sideband signal mixed to baseband and you only want one sideband you can apply Hilbert transforms. I'm guessing that's why you do this next step. If it wasn't DSB you probably don't really need this:
% hilbert filter to 90deg phase shift
xr = filter(hcplus45,1,mq);
xi = filter(hcminus45,1,mi);
I'm not completely sure about the comment here, since the Hilbert transform was presumably to make it SSB. In any case, if you're doing AM demodulation this next step only an approximation of magnitude detection. Using sqrt((xr*xr)+(xi*xi)) will provide a more linear output.
% ssb demod. + for lower, - for upperssbdemod = xr + xi;
The bottom line is that if it is working for you, then good job. If you want it efficient, there's probably more work to do.

I changed the coefficients of the Hilbert to have a gain of 1 as per the reccommendations on https://www.g0kla.com/sdr/tutorials/sdr_tutorial7.... and it has made the recovered audio and the sideband rejection even better.
The filter characteristics are 3khz bandwidth and gain of 1 with 45deg add and subtract on Iowa Hills Hilbert designer.
The new filters are named 65TapPhase3kG1.zip on my Google drive
https://drive.google.com/drive/folders/1EuX6iqEkGc...
I also uploaded another 22050 fs sample IQ file named "ssb80M.wav" which if tuned to 1800 by fcdm the conversation is louder and clearer now. You can hear the code passes the mustard test.
If the sideband is changed the conversation disappears.
I will receive sone STM32G474's soon which have ligtning fast trig functions and will try the code on them.